/******************************************************************************* * Copyright (c) 2016 comtel inc. * * 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.jfxvnc.net.rfb.codec; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.jfxvnc.net.rfb.codec.decoder.FrameDecoderHandler; import org.jfxvnc.net.rfb.codec.decoder.ServerDecoderEvent; import org.jfxvnc.net.rfb.codec.encoder.ClientCutTextEncoder; import org.jfxvnc.net.rfb.codec.encoder.KeyButtonEventEncoder; import org.jfxvnc.net.rfb.codec.encoder.PixelFormatEncoder; import org.jfxvnc.net.rfb.codec.encoder.PointerEventEncoder; import org.jfxvnc.net.rfb.codec.encoder.PreferedEncoding; import org.jfxvnc.net.rfb.codec.encoder.PreferedEncodingEncoder; import org.jfxvnc.net.rfb.codec.handshaker.event.ServerInitEvent; import org.jfxvnc.net.rfb.exception.ProtocolException; import org.jfxvnc.net.rfb.render.ConnectInfoEvent; import org.jfxvnc.net.rfb.render.ProtocolConfiguration; import org.jfxvnc.net.rfb.render.RenderProtocol; import org.jfxvnc.net.rfb.render.rect.ImageRect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; public class ProtocolHandler extends MessageToMessageDecoder<Object> { private static Logger logger = LoggerFactory.getLogger(ProtocolHandler.class); private final ProtocolConfiguration config; private ServerInitEvent serverInit; private RenderProtocol render; private final AtomicReference<ProtocolState> state = new AtomicReference<ProtocolState>(ProtocolState.HANDSHAKE_STARTED); private SslContext sslContext; public ProtocolHandler(RenderProtocol render, ProtocolConfiguration config) { this.config = Objects.requireNonNull(config, "configuration must not be empty"); this.render = Objects.requireNonNull(render, "render must not be empty"); } @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { if (config.sslProperty().get()) { if (sslContext == null) { sslContext = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build(); } ctx.pipeline().addFirst("ssl-handler", sslContext.newHandler(ctx.channel().alloc())); } super.channelRegistered(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { logger.debug("connection closed"); if (state.get() == ProtocolState.SECURITY_STARTED) { ProtocolException e = new ProtocolException("connection closed without error message"); render.exceptionCaught(e); } userEventTriggered(ctx, ProtocolState.CLOSED); super.channelInactive(ctx); } @Override protected void decode(final ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception { if (msg instanceof ImageRect) { final ImageRect rect = (ImageRect) msg; render.render(rect); return; } if (msg instanceof ServerDecoderEvent) { final ServerDecoderEvent event = (ServerDecoderEvent) msg; render.eventReceived(event); return; } if (!(msg instanceof ServerInitEvent)) { logger.error("unknown message: {}", msg); ctx.fireChannelRead(msg); return; } serverInit = (ServerInitEvent) msg; logger.debug("handshake completed with {}", serverInit); FrameDecoderHandler frameHandler = new FrameDecoderHandler(serverInit.getPixelFormat()); if (!frameHandler.isPixelFormatSupported()) { ProtocolException e = new ProtocolException(String.format("pixelformat: (%s bpp) not supported yet", serverInit.getPixelFormat().getBitPerPixel())); exceptionCaught(ctx, e); return; } ChannelPipeline cp = ctx.pipeline(); cp.addBefore(ctx.name(), "rfb-encoding-encoder", new PreferedEncodingEncoder()); PreferedEncoding prefEncodings = getPreferedEncodings(frameHandler.getSupportedEncodings()); ctx.write(prefEncodings); cp.addBefore(ctx.name(), "rfb-pixelformat-encoder", new PixelFormatEncoder()); ctx.write(serverInit.getPixelFormat()); ctx.flush(); cp.addBefore(ctx.name(), "rfb-frame-handler", frameHandler); cp.addBefore(ctx.name(), "rfb-keyevent-encoder", new KeyButtonEventEncoder()); cp.addBefore(ctx.name(), "rfb-pointerevent-encoder", new PointerEventEncoder()); cp.addBefore(ctx.name(), "rfb-cuttext-encoder", new ClientCutTextEncoder()); render.eventReceived(getConnectInfoEvent(ctx, prefEncodings)); render.registerInputEventListener(event -> ctx.writeAndFlush(event, ctx.voidPromise())); logger.debug("request full framebuffer update"); sendFramebufferUpdateRequest(ctx, false, 0, 0, serverInit.getFrameBufferWidth(), serverInit.getFrameBufferHeight()); logger.trace("channel pipeline: {}", cp.toMap().keySet()); } private ConnectInfoEvent getConnectInfoEvent(ChannelHandlerContext ctx, PreferedEncoding enc) { ConnectInfoEvent details = new ConnectInfoEvent(); details.setRemoteAddress(ctx.channel().remoteAddress().toString().substring(1)); details.setServerName(serverInit.getServerName()); details.setFrameWidth(serverInit.getFrameBufferWidth()); details.setFrameHeight(serverInit.getFrameBufferHeight()); details.setRfbProtocol(config.versionProperty().get()); details.setSecurity(config.securityProperty().get()); details.setServerPF(serverInit.getPixelFormat()); details.setClientPF(config.clientPixelFormatProperty().get()); details.setSupportedEncodings(enc.getEncodings()); details.setConnectionType(config.sslProperty().get() ? "SSL" : "TCP (standard)"); return details; } public PreferedEncoding getPreferedEncodings(Encoding[] supported) { Encoding[] enc = Arrays.stream(supported).filter(value -> { switch (value) { case COPY_RECT: return config.copyRectEncProperty().get(); case HEXTILE: return config.hextileEncProperty().get(); case RAW: return config.rawEncProperty().get(); case CURSOR: return config.clientCursorProperty().get(); case DESKTOP_SIZE: return config.desktopSizeProperty().get(); case ZLIB: return config.zlibEncProperty().get(); default: return true; } }).toArray(Encoding[]::new); logger.info("encodings: {}", Arrays.toString(enc)); return new PreferedEncoding(enc); } public void sendFramebufferUpdateRequest(ChannelHandlerContext ctx, boolean incremental, int x, int y, int w, int h) { ByteBuf buf = ctx.alloc().buffer(10, 10); buf.writeByte(ClientEventType.FRAMEBUFFER_UPDATE_REQUEST); buf.writeByte(incremental ? 1 : 0); buf.writeShort(x); buf.writeShort(y); buf.writeShort(w); buf.writeShort(h); ctx.writeAndFlush(buf, ctx.voidPromise()); } @Override public void handlerAdded(ChannelHandlerContext ctx) { ChannelPipeline cp = ctx.pipeline(); if (cp.get(ProtocolHandshakeHandler.class) == null) { cp.addBefore(ctx.name(), "rfb-handshake-handler", new ProtocolHandshakeHandler(config)); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { logger.error(cause.getMessage(), cause); render.exceptionCaught(cause); ctx.close(); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { logger.trace("user event: {}", evt); if (evt instanceof ProtocolState) { ProtocolState uvent = (ProtocolState) evt; state.set(uvent); if (uvent == ProtocolState.FBU_REQUEST) { render.renderComplete((rect) -> { if (rect != null){ sendFramebufferUpdateRequest(ctx, true, rect.getX(), rect.getY(), rect.getWidth(), rect.getHeight()); }else{ sendFramebufferUpdateRequest(ctx, true, 0, 0, serverInit.getFrameBufferWidth(), serverInit.getFrameBufferHeight()); } }); } render.stateChanged(uvent); } } }