/*
* Copyright 2012 Jason Miller
*
* 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 jj.repl.telnet;
import static jj.repl.telnet.TelnetProtocol.*;
import static io.netty.buffer.Unpooled.*;
import javax.inject.Inject;
import jj.execution.ServerTask;
import jj.execution.TaskRunner;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* accepts a connection to the repl server, assumes it's telnet and tries to
* negotiate a session using UTF-8 with no client echo in character mode.
*
* depending on what it negotiates here, it further configures the pipeline
*
* assumption: once negotiation is complete, no more control messages will
* happen! so any IACs will simply be read as unknown characters
*
* @author jason
*
*/
class TelnetConnectionHandler extends SimpleChannelInboundHandler<ByteBuf> {
static final ByteBuf START_CHARSET_NEGOTIATION = makeMessage(IAC, WILL, CHARSET);
static final ByteBuf CHARSET_NEGOTIATION_ACCEPTED = makeMessage(IAC, DO, CHARSET);
static final ByteBuf CHARSET_NEGOTIATION_REJECTED = makeMessage(IAC, DONT, CHARSET);
static final ByteBuf TURN_ECHO_OFF = makeMessage(IAC, WILL, ECHO);
static final ByteBuf ECHO_OFF_ACCEPTED = makeMessage(IAC, DO, ECHO);
static final ByteBuf ECHO_OFF_REJECTED = makeMessage(IAC, DONT, ECHO);
static final ByteBuf START_LINEMODE_NEGOTIATION = makeMessage(IAC, WILL, LINEMODE);
static final ByteBuf LINEMODE_NEGOTIATION_ACCEPTED = makeMessage(IAC, DO, LINEMODE);
static final ByteBuf LINEMODE_NEGOTIATION_REJECTED = makeMessage(IAC, DONT, LINEMODE);
private static ByteBuf makeMessage(int...bytes) {
ByteBuf result = buffer(bytes.length, bytes.length);
for (int b : bytes) {
result.writeByte(b);
}
return result;
}
private enum State {
AwaitingClientNegotiation,
AwaitingWillTransmitBinary,
AwaitingWillNegotiateCharset,
Done // technically not a state
}
private final TaskRunner taskRunner;
private State state;
@Inject
TelnetConnectionHandler(final TaskRunner taskRunner) {
this.taskRunner = taskRunner;
}
/**
* this is a timer that runs in 1/4 second after connection. if we didn't receive an opening salvo
* of telnet negotiation commands, it's a dumb client and we just move on with line-mode REPL
*
* otherwise, try to negotiate the terminal we want for niceness
*/
private final ServerTask awaitingInitialNegotiation = new ServerTask("awaiting opening telnet negotiation") {
@Override
protected void run() throws Exception {
System.out.println("negotiation not received. assuming dumb client");
state = State.Done;
}
@Override
protected long delay() {
return 250;
}
};
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// sanity check
assert state == null : "this handler cannot be reused";
// start a timer, if we receive nothing in x millis, try to go into binary mode
// otherwise, we may get an opening negotiation
state = State.AwaitingClientNegotiation;
taskRunner.execute(awaitingInitialNegotiation);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
dumpBuffer("received", msg);
// sanity check
assert state != null : "got into read without expecting anything next";
switch (state) {
case AwaitingClientNegotiation:
// cancel the awaiting timer
awaitingInitialNegotiation.cancelKey().cancel();
// try to read it
ctx.writeAndFlush(makeMessage(IAC, DO, TRANSMIT_BINARY));
//ctx.writeAndFlush(makeMessage(IAC, SB, TERMINAL_TYPE, TerminalType.SEND, IAC, SE));
state = State.AwaitingWillTransmitBinary;
break;
case AwaitingWillTransmitBinary:
if (msg.equals(makeMessage(IAC, WILL, TRANSMIT_BINARY))) {
ctx.writeAndFlush(makeMessage(IAC, WILL, CHARSET));
state = State.AwaitingWillNegotiateCharset;
} else {
state = State.Done;
}
break;
case AwaitingWillNegotiateCharset:
dumpBuffer("charset!", msg);
state = State.Done;
break;
default:
ctx.fireChannelRead(msg);
}
}
private void dumpBuffer(String message, ByteBuf buffer) {
System.out.println(message);
buffer.forEachByte(value -> {
System.out.print(Integer.toHexString(((int)value) & 255));
System.out.print(" ");
return true;
});
System.out.println(); // and linebreak
}
}