All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jfxvnc.net.rfb.codec.ProtocolHandler Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * 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.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.RenderCallback;
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.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
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 {

  private static Logger logger = LoggerFactory.getLogger(ProtocolHandler.class);

  private final ProtocolConfiguration config;

  private ServerInitEvent serverInit;

  private RenderProtocol render;
  
  private final RenderCallback voidCallback = () -> {};
  
  private final AtomicReference state = new AtomicReference(ProtocolState.HANDSHAKE_STARTED);

  private SslContext sslContext;

  public ProtocolHandler(RenderProtocol render, ProtocolConfiguration config) {
    if (config == null) {
      throw new IllegalArgumentException("configuration must not be empty");
    }
    this.config = config;
    this.render = render;
  }

  @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 out) throws Exception {

    if (msg instanceof ImageRect) {
      render.render((ImageRect) msg, voidCallback);
      return;
    }
    if (msg instanceof ServerDecoderEvent) {
      render.eventReceived((ServerDecoderEvent) msg);
      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) {
        sendFramebufferUpdateRequest(ctx, true, 0, 0, serverInit.getFrameBufferWidth(), serverInit.getFrameBufferHeight());
      }

      render.stateChanged(uvent);
    }
  }
}