/*
* Copyright 2009 NCHOVY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.krakenapps.console;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.krakenapps.api.FunctionKeyEvent;
import org.krakenapps.api.ScriptContext;
import org.krakenapps.api.ScriptOutputStream;
import org.krakenapps.api.TelnetCommand;
import org.krakenapps.api.FunctionKeyEvent.KeyCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TelnetStateMachine {
private final Logger logger = LoggerFactory
.getLogger(TelnetStateMachine.class.getName());
private CharsetDecoder charsetDecoder;
private IoBuffer temp;
private int trailingByteCount = 0;
private byte commandCode;
private State state;
private ProtocolDecoderOutput out;
private byte lastByte;
private byte ansiPriorChar;
private ScriptContext context;
private ByteBuffer negoBuffer;
private State negoState;
private enum State {
Command, Option, Data, AnsiEscape, SubNegotiation
}
public TelnetStateMachine(ProtocolDecoderOutput out, ScriptContext context) {
this.out = out;
this.context = context;
state = State.Data;
charsetDecoder = Charset.forName("utf-8").newDecoder();
temp = IoBuffer.allocate(8);
negoBuffer = ByteBuffer.allocate(16);
}
public void feed(byte b) throws CharacterCodingException {
switch (state) {
case Data:
handleData(b);
break;
case Command:
handleCommand(b);
break;
case Option:
handleOption(b);
break;
case AnsiEscape:
handleAnsiEscape(b);
break;
case SubNegotiation:
handleSubNegotiation(b);
break;
}
}
private void handleSubNegotiation(byte b) {
if (negoState == State.Option && isInterpreatAsControl(b)) {
negoState = State.Command;
return;
}
// subnegotiation end (SE)
if (negoState == State.Command && b == TelnetCommand.SE) {
negoBuffer.flip();
// window size
byte c = negoBuffer.get();
if (c == 0x1f) {
short width = negoBuffer.getShort();
short height = negoBuffer.getShort();
context.setWindowSize(width, height);
logger.trace(
"kraken-core: changed negotiate about window size: [{}, {}]",
width, height);
}
negoBuffer.clear();
state = State.Data;
return;
}
negoBuffer.put(b);
}
private void handleAnsiEscape(byte b) {
switch (b) {
case 0x5b:
state = State.AnsiEscape;
break;
case 'A':
out.write(new FunctionKeyEvent(KeyCode.UP));
state = State.Data;
break;
case 'B':
out.write(new FunctionKeyEvent(KeyCode.DOWN));
state = State.Data;
break;
case 'C':
out.write(new FunctionKeyEvent(KeyCode.RIGHT));
state = State.Data;
break;
case 'D':
out.write(new FunctionKeyEvent(KeyCode.LEFT));
state = State.Data;
break;
case '3':
case '1':
case '4':
ansiPriorChar = b;
state = State.AnsiEscape;
break;
case '~':
switch (ansiPriorChar) {
case '3':
out.write(new FunctionKeyEvent(KeyCode.DELETE));
break;
case '1':
out.write(new FunctionKeyEvent(KeyCode.HOME));
break;
case '4':
out.write(new FunctionKeyEvent(KeyCode.END));
break;
}
ansiPriorChar = '\0';
state = State.Data;
break;
default:
state = State.Data;
}
}
private void handleOption(byte b) {
TelnetCommandHandler handler = TelnetCommandHandlerFactory
.create(commandCode);
handler.execute(new byte[] { b });
if (b == 0x1f) {
// send "do negotiate about window size"
ScriptOutputStream os = context.getOutputStream();
os.print(new TelnetCommand() {
@Override
public byte[] toByteArray() {
return new byte[] { (byte) 0xfd, 0x1f };
}
});
}
state = State.Data;
}
private void handleCommand(byte b) throws CharacterCodingException {
if (isEscapeCode(b)) {
putData(b);
state = State.Data;
} else {
commandCode = b;
if (isOptionCommand(commandCode)) {
if (commandCode == TelnetCommand.SB) {
state = State.SubNegotiation;
negoState = State.Option;
} else
state = State.Option;
} else {
TelnetCommandHandler handler = TelnetCommandHandlerFactory
.create(commandCode);
handler.execute(new byte[] {});
state = State.Data;
}
}
}
private void handleData(byte b) throws CharacterCodingException {
if (isInterpreatAsControl(b))
state = State.Command;
else if (isEscape(b))
state = State.AnsiEscape;
else
putData(b);
}
private boolean isEscape(byte b) {
return b == (byte) 0x1b;
}
private void putData(byte b) throws CharacterCodingException {
// exceptional case: CR NUL to CR LF
if (lastByte == 13 && b == 0)
b = 10;
lastByte = b;
temp.put(b);
if (trailingByteCount == 0) {
trailingByteCount = trailingBytes(b);
} else {
trailingByteCount--;
}
if (trailingByteCount == 0) {
consume();
}
}
private void consume() throws CharacterCodingException {
temp.flip();
String character = temp.getString(charsetDecoder);
char ch = character.charAt(0);
if (ch == (char) 1)
out.write(new FunctionKeyEvent(KeyCode.CTRL_A));
else if (ch == (char) 2)
out.write(new FunctionKeyEvent(KeyCode.CTRL_B));
else if (ch == (char) 3)
out.write(new FunctionKeyEvent(KeyCode.CTRL_C));
else if (ch == (char) 4)
out.write(new FunctionKeyEvent(KeyCode.CTRL_D));
else if (ch == (char) 5)
out.write(new FunctionKeyEvent(KeyCode.CTRL_E));
else if (ch == (char) 6)
out.write(new FunctionKeyEvent(KeyCode.CTRL_F));
else if (ch == (char) 7)
out.write(new FunctionKeyEvent(KeyCode.CTRL_G));
else if (ch == (char) 12)
out.write(new FunctionKeyEvent(KeyCode.CTRL_L));
else if (ch == (char) 14)
out.write(new FunctionKeyEvent(KeyCode.CTRL_N));
else if (ch == (char) 15)
out.write(new FunctionKeyEvent(KeyCode.CTRL_O));
else if (ch == (char) 16)
out.write(new FunctionKeyEvent(KeyCode.CTRL_P));
else if (ch == (char) 17)
out.write(new FunctionKeyEvent(KeyCode.CTRL_Q));
else if (ch == (char) 18)
out.write(new FunctionKeyEvent(KeyCode.CTRL_R));
else if (ch == (char) 19)
out.write(new FunctionKeyEvent(KeyCode.CTRL_S));
else if (ch == (char) 20)
out.write(new FunctionKeyEvent(KeyCode.CTRL_T));
else if (ch == (char) 21)
out.write(new FunctionKeyEvent(KeyCode.CTRL_U));
else if (ch == (char) 22)
out.write(new FunctionKeyEvent(KeyCode.CTRL_V));
else if (ch == (char) 23)
out.write(new FunctionKeyEvent(KeyCode.CTRL_W));
else if (ch == (char) 24)
out.write(new FunctionKeyEvent(KeyCode.CTRL_X));
else if (ch == (char) 25)
out.write(new FunctionKeyEvent(KeyCode.CTRL_Y));
else if (ch == (char) 26)
out.write(new FunctionKeyEvent(KeyCode.CTRL_Z));
else if (ch == (char) 127 || ch == (char) 8)
out.write(new FunctionKeyEvent(KeyCode.BACKSPACE));
else
out.write(character);
temp.clear();
}
private int trailingBytes(byte leadingByte) {
int decimal = (int) leadingByte;
if ((decimal & 0xF0) == 0xF0) {
return 3;
} else if ((decimal & 0xE0) == 0xE0) {
return 2;
} else if ((decimal & 0xD0) == 0xC0) {
return 1;
} else {
return 0; // ASCII
}
}
private boolean isInterpreatAsControl(byte commandCode) {
return commandCode == TelnetCommand.InterpretAsControl;
}
private boolean isEscapeCode(byte commandCode) {
return isInterpreatAsControl(commandCode);
}
private boolean isOptionCommand(byte commandCode) {
return commandCode == TelnetCommand.SB
|| commandCode == TelnetCommand.Will
|| commandCode == TelnetCommand.Wont
|| commandCode == TelnetCommand.Do
|| commandCode == TelnetCommand.Dont;
}
}