blob: 5914cb30f7ad8f2e996f972f044b3c804e1bc15b [file] [log] [blame]
// 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);
}
}