| // 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.Element.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); | |
| } | |
| } |