// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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 vncclient.vnc; import streamer.BaseElement; import streamer.ByteBuffer; import streamer.Element; import streamer.Link; import streamer.Pipeline; import streamer.PipelineImpl; import streamer.debug.MockSink; import streamer.debug.MockSource; import common.BitmapOrder; import common.BitmapRectangle; import common.CopyRectOrder; import common.ScreenDescription; import common.adapter.AwtClipboardAdapter; public class VncMessageHandler extends BaseElement { protected ScreenDescription screen = null; // Pad names public static final String SERVER_BELL_ADAPTER_PAD = "bell"; public static final String SERVER_CLIPBOARD_ADAPTER_PAD = "clipboard"; public static final String PIXEL_ADAPTER_PAD = "pixels"; public static final String FRAME_BUFFER_UPDATE_REQUEST_ADAPTER_PAD = "fbur"; // Keys for metadata public static final String TARGET_X = "x"; public static final String TARGET_Y = "y"; public static final String WIDTH = "width"; public static final String HEIGHT = "height"; public static final String SOURCE_X = "srcX"; public static final String SOURCE_Y = "srcY"; public static final String PIXEL_FORMAT = "pixel_format"; private static final String NUM_OF_PROCESSED_RECTANGLES = "rects"; private static final String SAVED_CURSOR_POSITION = "cursor"; // Pixel format: RGB888 LE 32 public static final String RGB888LE32 = "RGB888LE32"; public VncMessageHandler(String id, ScreenDescription screen) { super(id); this.screen = screen; declarePads(); } private void declarePads() { outputPads.put(SERVER_BELL_ADAPTER_PAD, null); outputPads.put(SERVER_BELL_ADAPTER_PAD, null); outputPads.put(SERVER_CLIPBOARD_ADAPTER_PAD, null); outputPads.put(PIXEL_ADAPTER_PAD, null); outputPads.put(FRAME_BUFFER_UPDATE_REQUEST_ADAPTER_PAD, null); inputPads.put("stdin", null); } @Override public void handleData(ByteBuffer buf, Link link) { if (buf == null) return; try { if (verbose) System.out.println("[" + this + "] INFO: Data received: " + buf + "."); if (!cap(buf, 1, UNLIMITED, link, false)) return; // Read server message type int messageType = buf.readUnsignedByte(); // Invoke packet handler by packet type. switch (messageType) { case RfbConstants.SERVER_FRAMEBUFFER_UPDATE: { // Handle frame buffer update if (!handleFBU(buf, link)) return; // Frame buffer update is received and fully processed, send request for // another frame buffer update to server. sendFBUR(); break; } case RfbConstants.SERVER_BELL: { if (!handleBell(buf, link)) return; break; } case RfbConstants.SERVER_CUT_TEXT: { if (!handleClipboard(buf, link)) return; break; } default: // TODO: allow to extend functionality throw new RuntimeException("Unknown server packet type: " + messageType + "."); } // Cut tail, if any cap(buf, 0, 0, link, true); } finally { // Return processed buffer back to pool buf.unref(); } } private boolean handleClipboard(ByteBuffer buf, Link link) { if (!cap(buf, 3 + 4, UNLIMITED, link, true)) return false; // Skip padding buf.skipBytes(3); // Read text length int length = buf.readSignedInt(); // We need full string to parse it if (!cap(buf, length, UNLIMITED, link, true)) return false; String content = buf.readString(length, RfbConstants.US_ASCII_CHARSET); // Send content in metadata ByteBuffer outBuf = new ByteBuffer(0); outBuf.putMetadata(AwtClipboardAdapter.CLIPBOARD_CONTENT, content); pushDataToPad(SERVER_CLIPBOARD_ADAPTER_PAD, outBuf); return true; } private boolean handleBell(ByteBuffer buf, Link link) { // Send empty packet to bell adapter to produce bell pushDataToPad(SERVER_BELL_ADAPTER_PAD, new ByteBuffer(0)); return true; } // FIXME: this method is too complex private boolean handleFBU(ByteBuffer buf, Link link) { // We need at least 3 bytes here, 1 - padding, 2 - number of rectangles if (!cap(buf, 3, UNLIMITED, link, true)) return false; buf.skipBytes(1);// Skip padding // Read number of rectangles int numberOfRectangles = buf.readUnsignedShort(); if (verbose) System.out.println("[" + this + "] INFO: Frame buffer update. Number of rectangles: " + numberOfRectangles + "."); // Each rectangle must have header at least, header length is 12 bytes. if (!cap(buf, 12 * numberOfRectangles, UNLIMITED, link, true)) return false; // For all rectangles // Restore saved point, to avoid flickering and performance problems when // frame buffer update is split between few incoming packets. int numberOfProcessedRectangles = (buf.getMetadata(NUM_OF_PROCESSED_RECTANGLES) != null) ? (Integer)buf.getMetadata(NUM_OF_PROCESSED_RECTANGLES) : 0; if (buf.getMetadata(SAVED_CURSOR_POSITION) != null) buf.cursor = (Integer)buf.getMetadata(SAVED_CURSOR_POSITION); if (verbose && numberOfProcessedRectangles > 0) System.out.println("[" + this + "] INFO: Restarting from saved point. Number of already processed rectangles: " + numberOfRectangles + ", cursor: " + buf.cursor + "."); // For all new rectangles for (int i = numberOfProcessedRectangles; i < numberOfRectangles; i++) { // We need coordinates of rectangle (2x4 bytes) and encoding type (4 // bytes) if (!cap(buf, 12, UNLIMITED, link, true)) return false; // Read coordinates of rectangle int x = buf.readUnsignedShort(); int y = buf.readUnsignedShort(); int width = buf.readUnsignedShort(); int height = buf.readUnsignedShort(); // Read rectangle encoding int encodingType = buf.readSignedInt(); // Process rectangle switch (encodingType) { case RfbConstants.ENCODING_RAW: { if (!handleRawRectangle(buf, link, x, y, width, height)) return false; break; } case RfbConstants.ENCODING_COPY_RECT: { if (!handleCopyRect(buf, link, x, y, width, height)) return false; break; } case RfbConstants.ENCODING_DESKTOP_SIZE: { if (!handleScreenSizeChangeRect(buf, link, x, y, width, height)) return false; break; } default: // TODO: allow to extend functionality throw new RuntimeException("Unsupported ecnoding: " + encodingType + "."); } // Update information about processed rectangles to avoid handling of same // rectangle multiple times. // TODO: push back partial rectangle only instead buf.putMetadata(NUM_OF_PROCESSED_RECTANGLES, ++numberOfProcessedRectangles); buf.putMetadata(SAVED_CURSOR_POSITION, buf.cursor); } return true; } private boolean handleScreenSizeChangeRect(ByteBuffer buf, Link link, int x, int y, int width, int height) { // Remote screen size is changed if (verbose) System.out.println("[" + this + "] INFO: Screen size rect. Width: " + width + ", height: " + height + "."); screen.setFramebufferSize(width, height); return true; } private boolean handleCopyRect(ByteBuffer buf, Link link, int x, int y, int width, int height) { // Copy rectangle from one part of screen to another. // Areas may overlap. Antialiasing may cause visible artifacts. // We need 4 bytes with coordinates of source rectangle if (!cap(buf, 4, UNLIMITED, link, true)) return false; CopyRectOrder order = new CopyRectOrder(); order.srcX = buf.readUnsignedShort(); order.srcY = buf.readUnsignedShort(); order.x = x; order.y = y; order.width = width; order.height = height; if (verbose) System.out.println("[" + this + "] INFO: Copy rect. X: " + x + ", y: " + y + ", width: " + width + ", height: " + height + ", srcX: " + order.srcX + ", srcY: " + order.srcY + "."); pushDataToPad(PIXEL_ADAPTER_PAD, new ByteBuffer(order)); return true; } private boolean handleRawRectangle(ByteBuffer buf, Link link, int x, int y, int width, int height) { // Raw rectangle is just array of pixels to draw on screen. int rectDataLength = width * height * screen.getBytesPerPixel(); // We need at least rectDataLength bytes. Extra bytes may contain other // rectangles. if (!cap(buf, rectDataLength, UNLIMITED, link, true)) return false; if (verbose) System.out.println("[" + this + "] INFO: Raw rect. X: " + x + ", y: " + y + ", width: " + width + ", height: " + height + ", data length: " + rectDataLength + "."); BitmapRectangle rectangle = new BitmapRectangle(); rectangle.x = x; rectangle.y = y; rectangle.width = width; rectangle.height = height; rectangle.bufferWidth = width; rectangle.bufferHeight = height; rectangle.bitmapDataStream = buf.readBytes(rectDataLength); rectangle.colorDepth = screen.getColorDeph(); BitmapOrder order = new BitmapOrder(); order.rectangles = new BitmapRectangle[] {rectangle}; pushDataToPad(PIXEL_ADAPTER_PAD, new ByteBuffer(order)); return true; } @Override public void onStart() { // Send Frame Buffer Update request sendFBUR(); } private void sendFBUR() { ByteBuffer buf = new ByteBuffer(0); buf.putMetadata("incremental", true); pushDataToPad(FRAME_BUFFER_UPDATE_REQUEST_ADAPTER_PAD, buf); } @Override public String toString() { return "VNCMessageHandler(" + id + ")"; } /** * Example. */ public static void main(String[] args) { // System.setProperty("streamer.Link.debug", "true"); System.setProperty("streamer.Element.debug", "true"); // System.setProperty("streamer.Pipeline.debug", "true"); Element source = new MockSource("source") { { // Split messages at random boundaries to check "pushback" logic bufs = ByteBuffer.convertByteArraysToByteBuffers(new byte[] { // Message type: server bell RfbConstants.SERVER_BELL, // Message type: clipboard text RfbConstants.SERVER_CUT_TEXT, // Padding 0, 0, 0, // Length (test) 0, 0, 0, 4, }, new byte[] { // Clipboard text 't', 'e', 's', 't', // Message type: frame buffer update RfbConstants.SERVER_FRAMEBUFFER_UPDATE, // Padding 0, // Number of rectangles 0, 3,}, new byte[] { // x, y, width, height: 0x0@4x4 0, 0, 0, 0, 0, 4, 0, 4, // Encoding: desktop size (byte)((RfbConstants.ENCODING_DESKTOP_SIZE >> 24) & 0xff), (byte)((RfbConstants.ENCODING_DESKTOP_SIZE >> 16) & 0xff), (byte)((RfbConstants.ENCODING_DESKTOP_SIZE >> 8) & 0xff), (byte)((RfbConstants.ENCODING_DESKTOP_SIZE >> 0) & 0xff),}, new byte[] { // x, y, width, height: 0x0@4x4 0, 0, 0, 0, 0, 4, 0, 4, // Encoding: raw rect (byte)((RfbConstants.ENCODING_RAW >> 24) & 0xff), (byte)((RfbConstants.ENCODING_RAW >> 16) & 0xff), (byte)((RfbConstants.ENCODING_RAW >> 8) & 0xff), (byte)((RfbConstants.ENCODING_RAW >> 0) & 0xff), // Raw pixel data 4x4x1 bpp 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,}, new byte[] {11, 12, 13, 14, 15, 16, // x, y, width, height: 0x0@2x2 0, 0, 0, 0, 0, 2, 0, 2, // Encoding: copy rect (byte)((RfbConstants.ENCODING_COPY_RECT >> 24) & 0xff), (byte)((RfbConstants.ENCODING_COPY_RECT >> 16) & 0xff), (byte)((RfbConstants.ENCODING_COPY_RECT >> 8) & 0xff), (byte)((RfbConstants.ENCODING_COPY_RECT >> 0) & 0xff), // srcX, srcY: 2x2 0, 2, 0, 2,}); } }; ScreenDescription screen = new ScreenDescription() { { bytesPerPixel = 1; } }; final Element handler = new VncMessageHandler("handler", screen); ByteBuffer[] emptyBuf = ByteBuffer.convertByteArraysToByteBuffers(new byte[] {}); Element fburSink = new MockSink("fbur", ByteBuffer.convertByteArraysToByteBuffers(new byte[] {}, new byte[] {})); Element bellSink = new MockSink("bell", emptyBuf); Element clipboardSink = new MockSink("clipboard", emptyBuf); Element desktopSizeChangeSink = new MockSink("desktop_size", emptyBuf); Element pixelsSink = new MockSink("pixels", ByteBuffer.convertByteArraysToByteBuffers(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,})); Element copyRectSink = new MockSink("copy_rect", emptyBuf); Pipeline pipeline = new PipelineImpl("test"); pipeline.addAndLink(source, handler); pipeline.add(fburSink, bellSink, clipboardSink, desktopSizeChangeSink, pixelsSink, copyRectSink); pipeline.link("handler >" + FRAME_BUFFER_UPDATE_REQUEST_ADAPTER_PAD, "fbur"); pipeline.link("handler >" + SERVER_BELL_ADAPTER_PAD, "bell"); pipeline.link("handler >" + SERVER_CLIPBOARD_ADAPTER_PAD, "clipboard"); pipeline.link("handler >" + PIXEL_ADAPTER_PAD, "pixels"); pipeline.runMainLoop("source", STDOUT, false, false); } }