io.termd.core.telnet.TelnetConnection Maven / Gradle / Ivy
Show all versions of termd-core Show documentation
/*
* Copyright 2015 Julien Viet
*
* 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 io.termd.core.telnet;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @author Julien Viet
*/
public abstract class TelnetConnection {
public static final byte BYTE_IAC = (byte) 0xFF;
public static final byte BYTE_DONT = (byte) 0xFE;
public static final byte BYTE_DO = (byte) 0xFD;
public static final byte BYTE_WONT = (byte) 0xFC;
public static final byte BYTE_WILL = (byte) 0xFB;
public static final byte BYTE_SB = (byte) 0xFA;
public static final byte BYTE_SE = (byte) 0xF0;
private byte[] pendingBuffer = new byte[256];
private int pendingLength = 0;
Status status;
Byte paramsOptionCode;
byte[] paramsBuffer;
int paramsLength;
boolean paramsIac;
boolean sendBinary;
boolean receiveBinary;
final TelnetHandler handler;
public TelnetConnection(TelnetHandler handler) {
this.status = Status.DATA;
this.paramsOptionCode = null;
this.paramsBuffer = null;
this.paramsIac = false;
this.sendBinary = false;
this.receiveBinary = false;
this.handler = handler;
}
private void appendToParams(byte b) {
while (paramsLength >= paramsBuffer.length) {
paramsBuffer = Arrays.copyOf(paramsBuffer, paramsBuffer.length + 100);
}
paramsBuffer[paramsLength++] = b;
}
public void onInit() {
handler.onOpen(this);
}
public abstract void close();
/**
* Write a do option request to the client.
*
* @param option the option to send
*/
public final void writeDoOption(Option option) {
send(new byte[]{BYTE_IAC, BYTE_DO, option.code});
}
/**
* Write a do will request to the client.
*
* @param option the option to send
*/
public final void writeWillOption(Option option) {
send(new byte[]{BYTE_IAC, BYTE_WILL, option.code});
}
private void rawWrite(byte[] data, int offset, int length) {
if (length > 0) {
if (offset == 0 && length == data.length) {
send(data);
} else {
byte[] chunk = new byte[length];
System.arraycopy(data, offset, chunk, 0, chunk.length);
send(chunk);
}
}
}
protected abstract void execute(Runnable task);
protected abstract void schedule(Runnable task, long delay, TimeUnit unit);
protected abstract void send(byte[] data);
public void receive(byte[] data) {
for (byte b : data) {
status.handle(this, b);
}
flushDataIfNecessary();
}
/**
* Write data to the client, escaping data if necessary or truncating it. The original buffer can
* be mutated if incorrect data is provided.
*
* @param data the data to write
*/
public final void write(byte[] data) {
if (sendBinary) {
// actually the logic here never get executed.
int prev = 0;
for (int i = 0;i < data.length;i++) {
if (data[i] == -1) {
rawWrite(data, prev, i - prev);
send(new byte[]{-1, -1});
prev = i + 1;
}
}
rawWrite(data, prev, data.length - prev);
} else {
// Not fully understand the logic below, but
// Chinese characters will be truncated by the following logic.
// So currently these logic is commented out.
// see middleware-container/arthas/issues/246 for more details
//
// for (int i = 0;i < data.length;i++) {
// data[i] = (byte)(data[i] & 0x7F);
// }
send(data);
}
}
protected void onClose() {
handler.onClose();
}
/**
* Handle option WILL
call back. The implementation will try to find a matching option
* via the {@code Option#values()} and invoke it's {@link Option#handleWill(TelnetConnection)} method
* otherwise a DON'T
will be sent to the client.
*
* This method can be subclassed to handle an option.
*
* @param optionCode the option code
*/
protected void onOptionWill(byte optionCode) {
for (Option option : Option.values()) {
if (option.code == optionCode) {
option.handleWill(this);
return;
}
}
send(new byte[]{BYTE_IAC, BYTE_DONT, optionCode});
}
/**
* Handle option WON'T
call back. The implementation will try to find a matching option
* via the {@code Option#values()} and invoke it's {@link Option#handleWont(TelnetConnection)} method.
*
* This method can be subclassed to handle an option.
*
* @param optionCode the option code
*/
protected void onOptionWont(byte optionCode) {
for (Option option : Option.values()) {
if (option.code == optionCode) {
option.handleWont(this);
return;
}
}
}
/**
* Handle option DO
call back. The implementation will try to find a matching option
* via the {@code Option#values()} and invoke it's {@link Option#handleDo(TelnetConnection)} method
* otherwise a WON'T
will be sent to the client.
*
* This method can be subclassed to handle an option.
*
* @param optionCode the option code
*/
protected void onOptionDo(byte optionCode) {
for (Option option : Option.values()) {
if (option.code == optionCode) {
option.handleDo(this);
return;
}
}
send(new byte[]{BYTE_IAC, BYTE_WONT, optionCode});
}
/**
* Handle option DON'T
call back. The implementation will try to find a matching option
* via the {@code Option#values()} and invoke it's {@link Option#handleDont(TelnetConnection)} method.
*
* This method can be subclassed to handle an option.
*
* @param optionCode the option code
*/
protected void onOptionDont(byte optionCode) {
for (Option option : Option.values()) {
if (option.code == optionCode) {
option.handleDont(this);
return;
}
}
}
/**
* Handle option parameters call back. The implementation will try to find a matching option
* via the {@code Option#values()} and invoke it's {@link Option#handleParameters(TelnetConnection, byte[])} method.
*
* This method can be subclassed to handle an option.
*
* @param optionCode the option code
*/
protected void onOptionParameters(byte optionCode, byte[] parameters) {
for (Option option : Option.values()) {
if (option.code == optionCode) {
option.handleParameters(this, parameters);
return;
}
}
}
/**
* Append a byte in the {@link #pendingBuffer} buffer. When the {@link #pendingBuffer} buffer is full, data
* is flushed.
*
* @param b the byte
* @see #flushData()
*/
private void appendData(byte b) {
if (pendingLength >= pendingBuffer.length) {
flushData();
}
pendingBuffer[pendingLength++] = b;
}
/**
* Flush the {@link #pendingBuffer} buffer when it is not empty.
*
* @see #flushData()
*/
private void flushDataIfNecessary() {
if (pendingLength > 0) {
flushData();
}
}
/**
* Flush the {@link #pendingBuffer} buffer to {@link TelnetHandler#onData(byte[])}.
*/
private void flushData() {
byte[] data = Arrays.copyOf(pendingBuffer, pendingLength);
pendingLength = 0;
handler.onData(data);
}
enum Status {
DATA() {
@Override
void handle(TelnetConnection session, byte b) {
if (b == BYTE_IAC) {
if (session.receiveBinary) {
session.status = ESC;
} else {
session.flushDataIfNecessary();
session.status = IAC;
}
} else {
session.appendData(b);
}
}
},
ESC() {
@Override
void handle(TelnetConnection session, byte b) {
if (b == BYTE_IAC) {
session.appendData((byte)-1);
} else {
session.flushDataIfNecessary();
IAC.handle(session, b);
}
}
},
IAC() {
@Override
void handle(TelnetConnection session, byte b) {
if (b == BYTE_DO) {
session.status = DO;
} else if (b == BYTE_DONT) {
session.status = DONT;
} else if (b == BYTE_WILL) {
session.status = WILL;
} else if (b == BYTE_WONT) {
session.status = WONT;
} else if (b == BYTE_SB) {
session.paramsBuffer = new byte[100];
session.paramsLength = 0;
session.status = SB;
} else {
session.handler.onCommand(b);
session.status = DATA;
}
}
},
SB() {
@Override
void handle(TelnetConnection session, byte b) {
if (session.paramsOptionCode == null) {
session.paramsOptionCode = b;
} else {
if (session.paramsIac) {
session.paramsIac = false;
if (b == BYTE_SE) {
try {
session.onOptionParameters(session.paramsOptionCode, Arrays.copyOf(session.paramsBuffer, session.paramsLength));
} finally {
session.paramsOptionCode = null;
session.paramsBuffer = null;
session.status = DATA;
}
} else if (b == BYTE_IAC) {
session.appendToParams((byte) -1);
}
} else {
if (b == BYTE_IAC) {
session.paramsIac = true;
} else {
session.appendToParams(b);
}
}
}
}
},
DO() {
@Override
void handle(TelnetConnection session, byte b) {
try {
session.onOptionDo(b);
} finally {
session.status = DATA;
}
}
},
DONT() {
@Override
void handle(TelnetConnection session, byte b) {
try {
session.onOptionDont(b);
} finally {
session.status = DATA;
}
}
},
WILL() {
@Override
void handle(TelnetConnection session, byte b) {
try {
session.onOptionWill(b);
} finally {
session.status = DATA;
}
}
},
WONT() {
@Override
void handle(TelnetConnection session, byte b) {
try {
session.onOptionWont(b);
} finally {
session.status = DATA;
}
}
},
;
abstract void handle(TelnetConnection session, byte b);
}
}