Add native LAN VNC streaming backend
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
public class DesCipherDesktop {
|
||||||
|
|
||||||
|
private final Cipher encryptCipher;
|
||||||
|
|
||||||
|
public DesCipherDesktop(byte[] keyBytes) throws Exception {
|
||||||
|
byte[] material = new byte[8];
|
||||||
|
System.arraycopy(keyBytes, 0, material, 0, Math.min(keyBytes.length, 8));
|
||||||
|
|
||||||
|
for (int i = 0; i < material.length; i++) {
|
||||||
|
material[i] = reverseBits(material[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
SecretKey key = new SecretKeySpec(material, "DES");
|
||||||
|
encryptCipher = Cipher.getInstance("DES/ECB/NoPadding");
|
||||||
|
encryptCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void encrypt(byte[] src, int srcOff, byte[] dst, int dstOff) throws Exception {
|
||||||
|
byte[] out = encryptCipher.doFinal(src, srcOff, 8);
|
||||||
|
System.arraycopy(out, 0, dst, dstOff, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte reverseBits(byte b) {
|
||||||
|
int v = b & 0xFF;
|
||||||
|
int r = 0;
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
r = (r << 1) | (v & 1);
|
||||||
|
v >>= 1;
|
||||||
|
}
|
||||||
|
return (byte) r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class DesktopBitmapData {
|
||||||
|
|
||||||
|
private int framebufferWidth;
|
||||||
|
private int framebufferHeight;
|
||||||
|
|
||||||
|
private byte[] pixelBuffer;
|
||||||
|
private byte[] copyBuffer;
|
||||||
|
private byte[] rowTemplateBuffer;
|
||||||
|
|
||||||
|
private static final int MAX_DIRTY_RECTS = 64;
|
||||||
|
|
||||||
|
private boolean dirty = true;
|
||||||
|
private int dirtyRectCount = 0;
|
||||||
|
|
||||||
|
private final int[] dirtyXs = new int[MAX_DIRTY_RECTS];
|
||||||
|
private final int[] dirtyYs = new int[MAX_DIRTY_RECTS];
|
||||||
|
private final int[] dirtyWs = new int[MAX_DIRTY_RECTS];
|
||||||
|
private final int[] dirtyHs = new int[MAX_DIRTY_RECTS];
|
||||||
|
|
||||||
|
private int dirtyMinX;
|
||||||
|
private int dirtyMinY;
|
||||||
|
private int dirtyMaxX;
|
||||||
|
private int dirtyMaxY;
|
||||||
|
|
||||||
|
public DesktopBitmapData(int width, int height) {
|
||||||
|
resize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resize(int width, int height) {
|
||||||
|
this.framebufferWidth = Math.max(1, width);
|
||||||
|
this.framebufferHeight = Math.max(1, height);
|
||||||
|
|
||||||
|
this.pixelBuffer = new byte[framebufferWidth * framebufferHeight * 4];
|
||||||
|
this.copyBuffer = null;
|
||||||
|
this.rowTemplateBuffer = null;
|
||||||
|
|
||||||
|
for (int i = 3; i < pixelBuffer.length; i += 4) {
|
||||||
|
pixelBuffer[i] = (byte) 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dirty = false;
|
||||||
|
markDirty(0, 0, framebufferWidth, framebufferHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFramebufferWidth() {
|
||||||
|
return framebufferWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFramebufferHeight() {
|
||||||
|
return framebufferHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPixelBuffer() {
|
||||||
|
return pixelBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] copyPixelBuffer() {
|
||||||
|
return Arrays.copyOf(pixelBuffer, pixelBuffer.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDirty() {
|
||||||
|
return dirty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDirtyRectCount() {
|
||||||
|
return dirtyRectCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getDirtyXs() {
|
||||||
|
return dirtyXs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getDirtyYs() {
|
||||||
|
return dirtyYs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getDirtyWs() {
|
||||||
|
return dirtyWs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getDirtyHs() {
|
||||||
|
return dirtyHs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearDirty() {
|
||||||
|
dirty = false;
|
||||||
|
dirtyRectCount = 0;
|
||||||
|
dirtyMinX = 0;
|
||||||
|
dirtyMinY = 0;
|
||||||
|
dirtyMaxX = 0;
|
||||||
|
dirtyMaxY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validDraw(int x, int y, int w, int h) {
|
||||||
|
return w > 0
|
||||||
|
&& h > 0
|
||||||
|
&& x >= 0
|
||||||
|
&& y >= 0
|
||||||
|
&& x + w <= framebufferWidth
|
||||||
|
&& y + h <= framebufferHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int offset(int x, int y) {
|
||||||
|
return (y * framebufferWidth + x) * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markDirty(int x, int y, int w, int h) {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
int minX = Math.max(0, x);
|
||||||
|
int minY = Math.max(0, y);
|
||||||
|
int maxX = Math.min(framebufferWidth, x + w);
|
||||||
|
int maxY = Math.min(framebufferHeight, y + h);
|
||||||
|
|
||||||
|
if (minX >= maxX || minY >= maxY) return;
|
||||||
|
|
||||||
|
if (!dirty) {
|
||||||
|
dirty = true;
|
||||||
|
dirtyRectCount = 0;
|
||||||
|
dirtyMinX = minX;
|
||||||
|
dirtyMinY = minY;
|
||||||
|
dirtyMaxX = maxX;
|
||||||
|
dirtyMaxY = maxY;
|
||||||
|
} else {
|
||||||
|
dirtyMinX = Math.min(dirtyMinX, minX);
|
||||||
|
dirtyMinY = Math.min(dirtyMinY, minY);
|
||||||
|
dirtyMaxX = Math.max(dirtyMaxX, maxX);
|
||||||
|
dirtyMaxY = Math.max(dirtyMaxY, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirtyRectCount >= 0 && dirtyRectCount < MAX_DIRTY_RECTS) {
|
||||||
|
dirtyXs[dirtyRectCount] = minX;
|
||||||
|
dirtyYs[dirtyRectCount] = minY;
|
||||||
|
dirtyWs[dirtyRectCount] = maxX - minX;
|
||||||
|
dirtyHs[dirtyRectCount] = maxY - minY;
|
||||||
|
dirtyRectCount++;
|
||||||
|
} else {
|
||||||
|
dirtyRectCount = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fillRect(int x, int y, int w, int h, int color) {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
int startX = Math.max(0, x);
|
||||||
|
int startY = Math.max(0, y);
|
||||||
|
int endX = Math.min(framebufferWidth, x + w);
|
||||||
|
int endY = Math.min(framebufferHeight, y + h);
|
||||||
|
|
||||||
|
if (startX >= endX || startY >= endY) return;
|
||||||
|
|
||||||
|
int clippedW = endX - startX;
|
||||||
|
int clippedH = endY - startY;
|
||||||
|
int rowBytes = clippedW * 4;
|
||||||
|
|
||||||
|
if (rowTemplateBuffer == null || rowTemplateBuffer.length < rowBytes) {
|
||||||
|
rowTemplateBuffer = new byte[rowBytes];
|
||||||
|
}
|
||||||
|
|
||||||
|
byte b = (byte) (color & 0xFF);
|
||||||
|
byte g = (byte) ((color >> 8) & 0xFF);
|
||||||
|
byte r = (byte) ((color >> 16) & 0xFF);
|
||||||
|
byte a = (byte) 255;
|
||||||
|
|
||||||
|
for (int i = 0; i < rowBytes; i += 4) {
|
||||||
|
rowTemplateBuffer[i] = b;
|
||||||
|
rowTemplateBuffer[i + 1] = g;
|
||||||
|
rowTemplateBuffer[i + 2] = r;
|
||||||
|
rowTemplateBuffer[i + 3] = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int yy = startY; yy < endY; yy++) {
|
||||||
|
System.arraycopy(
|
||||||
|
rowTemplateBuffer,
|
||||||
|
0,
|
||||||
|
pixelBuffer,
|
||||||
|
offset(startX, yy),
|
||||||
|
rowBytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(startX, startY, clippedW, clippedH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyRect(int srcX, int srcY, int dstX, int dstY, int w, int h) {
|
||||||
|
if (w <= 0 || h <= 0) return;
|
||||||
|
if (!validDraw(srcX, srcY, w, h)) return;
|
||||||
|
if (!validDraw(dstX, dstY, w, h)) return;
|
||||||
|
|
||||||
|
int rowBytes = w * 4;
|
||||||
|
|
||||||
|
boolean overlap =
|
||||||
|
dstY < srcY + h &&
|
||||||
|
srcY < dstY + h &&
|
||||||
|
dstX < srcX + w &&
|
||||||
|
srcX < dstX + w;
|
||||||
|
|
||||||
|
if (!overlap) {
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
System.arraycopy(
|
||||||
|
pixelBuffer,
|
||||||
|
offset(srcX, srcY + row),
|
||||||
|
pixelBuffer,
|
||||||
|
offset(dstX, dstY + row),
|
||||||
|
rowBytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(dstX, dstY, w, h);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int required = rowBytes * h;
|
||||||
|
|
||||||
|
if (copyBuffer == null || copyBuffer.length < required) {
|
||||||
|
copyBuffer = new byte[required];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
System.arraycopy(
|
||||||
|
pixelBuffer,
|
||||||
|
offset(srcX, srcY + row),
|
||||||
|
copyBuffer,
|
||||||
|
row * rowBytes,
|
||||||
|
rowBytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
System.arraycopy(
|
||||||
|
copyBuffer,
|
||||||
|
row * rowBytes,
|
||||||
|
pixelBuffer,
|
||||||
|
offset(dstX, dstY + row),
|
||||||
|
rowBytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(dstX, dstY, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRgbPixels(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int w,
|
||||||
|
int h,
|
||||||
|
int[] src,
|
||||||
|
int srcOffset,
|
||||||
|
int srcStride
|
||||||
|
) {
|
||||||
|
if (src == null || w <= 0 || h <= 0) return;
|
||||||
|
|
||||||
|
int startX = Math.max(0, x);
|
||||||
|
int startY = Math.max(0, y);
|
||||||
|
int endX = Math.min(framebufferWidth, x + w);
|
||||||
|
int endY = Math.min(framebufferHeight, y + h);
|
||||||
|
|
||||||
|
if (startX >= endX || startY >= endY) return;
|
||||||
|
|
||||||
|
int clippedW = endX - startX;
|
||||||
|
int clippedH = endY - startY;
|
||||||
|
|
||||||
|
int srcXOffset = startX - x;
|
||||||
|
int srcYOffset = startY - y;
|
||||||
|
|
||||||
|
for (int row = 0; row < clippedH; row++) {
|
||||||
|
int srcIndex = srcOffset + (srcYOffset + row) * srcStride + srcXOffset;
|
||||||
|
int dstIndex = offset(startX, startY + row);
|
||||||
|
|
||||||
|
for (int col = 0; col < clippedW; col++) {
|
||||||
|
int color = src[srcIndex++];
|
||||||
|
|
||||||
|
pixelBuffer[dstIndex++] = (byte) (color & 0xFF);
|
||||||
|
pixelBuffer[dstIndex++] = (byte) ((color >> 8) & 0xFF);
|
||||||
|
pixelBuffer[dstIndex++] = (byte) ((color >> 16) & 0xFF);
|
||||||
|
pixelBuffer[dstIndex++] = (byte) 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markDirty(startX, startY, clippedW, clippedH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markFullDirty() {
|
||||||
|
markDirty(0, 0, framebufferWidth, framebufferHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markSingleDirtyRect(int x, int y, int w, int h) {
|
||||||
|
markDirty(x, y, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
|
||||||
|
public class GreetClient implements AutoCloseable {
|
||||||
|
private Socket clientSocket;
|
||||||
|
private PrintWriter out;
|
||||||
|
private BufferedReader in;
|
||||||
|
|
||||||
|
public void startConnection(String ip, int port) throws IOException {
|
||||||
|
clientSocket = new Socket(ip, port);
|
||||||
|
clientSocket.setSoTimeout(10000);
|
||||||
|
|
||||||
|
out = new PrintWriter(clientSocket.getOutputStream(), true);
|
||||||
|
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String sendMessage(String msg) throws IOException {
|
||||||
|
if (out == null || in == null) {
|
||||||
|
throw new IllegalStateException("Connection not started");
|
||||||
|
}
|
||||||
|
|
||||||
|
out.println(msg);
|
||||||
|
String response = in.readLine();
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
throw new SocketTimeoutException("No response from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopConnection() {
|
||||||
|
try {
|
||||||
|
if (in != null) in.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out != null) {
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (clientSocket != null && !clientSocket.isClosed()) {
|
||||||
|
clientSocket.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
stopConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
public abstract class InStreamDesktop {
|
||||||
|
|
||||||
|
protected byte[] b;
|
||||||
|
protected int ptr;
|
||||||
|
protected int end;
|
||||||
|
|
||||||
|
public byte[] getbuf() { return b; }
|
||||||
|
public int getptr() { return ptr; }
|
||||||
|
public int getend() { return end; }
|
||||||
|
public void setptr(int p) { ptr = p; }
|
||||||
|
|
||||||
|
public void check(int itemSize) throws Exception {
|
||||||
|
if (ptr + itemSize > end) {
|
||||||
|
overrun(itemSize, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readU8() throws Exception {
|
||||||
|
check(1);
|
||||||
|
return b[ptr++] & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readU16() throws Exception {
|
||||||
|
check(2);
|
||||||
|
int v = ((b[ptr] & 0xFF) << 8) | (b[ptr + 1] & 0xFF);
|
||||||
|
ptr += 2;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readS32() throws Exception {
|
||||||
|
check(4);
|
||||||
|
int v = ((b[ptr] & 0xFF) << 24)
|
||||||
|
| ((b[ptr + 1] & 0xFF) << 16)
|
||||||
|
| ((b[ptr + 2] & 0xFF) << 8)
|
||||||
|
| (b[ptr + 3] & 0xFF);
|
||||||
|
ptr += 4;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readBytes(byte[] dst, int off, int len) throws Exception {
|
||||||
|
while (len > 0) {
|
||||||
|
if (ptr >= end) {
|
||||||
|
overrun(1, 1);
|
||||||
|
}
|
||||||
|
int n = Math.min(len, end - ptr);
|
||||||
|
System.arraycopy(b, ptr, dst, off, n);
|
||||||
|
ptr += n;
|
||||||
|
off += n;
|
||||||
|
len -= n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract int overrun(int itemSize, int nItems) throws Exception;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
public class MemInStreamDesktop extends InStreamDesktop {
|
||||||
|
|
||||||
|
public MemInStreamDesktop(byte[] data, int offset, int len) {
|
||||||
|
b = data;
|
||||||
|
ptr = offset;
|
||||||
|
end = offset + len;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int pos() {
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int overrun(int itemSize, int nItems) throws Exception {
|
||||||
|
throw new Exception("MemInStream overrun: end of stream");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class RfbProtoDesktop {
|
||||||
|
|
||||||
|
public static final String VERSION_MSG_3_3 = "RFB 003.003\n";
|
||||||
|
public static final String VERSION_MSG_3_7 = "RFB 003.007\n";
|
||||||
|
public static final String VERSION_MSG_3_8 = "RFB 003.008\n";
|
||||||
|
|
||||||
|
public static final int SEC_TYPE_INVALID = 0;
|
||||||
|
public static final int SEC_TYPE_NONE = 1;
|
||||||
|
public static final int SEC_TYPE_VNC_AUTH = 2;
|
||||||
|
public static final int SEC_TYPE_TIGHT = 16;
|
||||||
|
public static final int SEC_TYPE_ULTRA_34 = 0xfffffffa;
|
||||||
|
|
||||||
|
public static final int VNC_AUTH_OK = 0;
|
||||||
|
public static final int VNC_AUTH_FAILED = 1;
|
||||||
|
public static final int VNC_AUTH_TOO_MANY = 2;
|
||||||
|
|
||||||
|
public static final int FRAMEBUFFER_UPDATE = 0;
|
||||||
|
public static final int SET_COLOUR_MAP_ENTRIES = 1;
|
||||||
|
public static final int BELL = 2;
|
||||||
|
|
||||||
|
public static final int SET_PIXEL_FORMAT = 0;
|
||||||
|
public static final int SET_ENCODINGS = 2;
|
||||||
|
public static final int FRAMEBUFFER_UPDATE_REQUEST = 3;
|
||||||
|
public static final int POINTER_EVENT = 5;
|
||||||
|
|
||||||
|
public static final int ENCODING_RAW = 0;
|
||||||
|
public static final int ENCODING_COPY_RECT = 1;
|
||||||
|
public static final int ENCODING_RRE = 2;
|
||||||
|
public static final int ENCODING_CORRE = 4;
|
||||||
|
public static final int ENCODING_HEXTILE = 5;
|
||||||
|
public static final int ENCODING_ZLIB = 6;
|
||||||
|
public static final int ENCODING_ZRLE = 16;
|
||||||
|
public static final int ENCODING_TIGHT = 7;
|
||||||
|
|
||||||
|
public static final int ENCODING_X_CURSOR = 0xFFFFFF10;
|
||||||
|
public static final int ENCODING_RICH_CURSOR = 0xFFFFFF11;
|
||||||
|
public static final int ENCODING_POINTER_POS = 0xFFFFFF18;
|
||||||
|
public static final int ENCODING_LAST_RECT = 0xFFFFFF20;
|
||||||
|
public static final int ENCODING_NEW_FB_SIZE = 0xFFFFFF21;
|
||||||
|
|
||||||
|
public static final int HEXTILE_RAW = 1;
|
||||||
|
public static final int HEXTILE_BACKGROUND_SPECIFIED = 2;
|
||||||
|
public static final int HEXTILE_FOREGROUND_SPECIFIED = 4;
|
||||||
|
public static final int HEXTILE_ANY_SUBRECTS = 8;
|
||||||
|
public static final int HEXTILE_SUBRECTS_COLOURED = 16;
|
||||||
|
|
||||||
|
private static final int CONNECT_TIMEOUT_MS = 8000;
|
||||||
|
private static final int READ_TIMEOUT_MS = 30000;
|
||||||
|
private static final int IO_BUFFER_SIZE = 65536;
|
||||||
|
|
||||||
|
private final String host;
|
||||||
|
private final int port;
|
||||||
|
private final Socket sock;
|
||||||
|
private final DataInputStream is;
|
||||||
|
private final OutputStream os;
|
||||||
|
|
||||||
|
private boolean inNormalProtocol = false;
|
||||||
|
private boolean wereZlibUpdates = false;
|
||||||
|
|
||||||
|
private int serverMajor;
|
||||||
|
private int serverMinor;
|
||||||
|
private int clientMajor;
|
||||||
|
private int clientMinor;
|
||||||
|
|
||||||
|
private String desktopName;
|
||||||
|
private int framebufferWidth;
|
||||||
|
private int framebufferHeight;
|
||||||
|
private int bitsPerPixel;
|
||||||
|
private int depth;
|
||||||
|
private boolean bigEndian;
|
||||||
|
private boolean trueColour;
|
||||||
|
private int redMax;
|
||||||
|
private int greenMax;
|
||||||
|
private int blueMax;
|
||||||
|
private int redShift;
|
||||||
|
private int greenShift;
|
||||||
|
private int blueShift;
|
||||||
|
|
||||||
|
private int updateNRects;
|
||||||
|
private int updateRectX;
|
||||||
|
private int updateRectY;
|
||||||
|
private int updateRectW;
|
||||||
|
private int updateRectH;
|
||||||
|
private int updateRectEncoding;
|
||||||
|
|
||||||
|
private int copyRectSrcX;
|
||||||
|
private int copyRectSrcY;
|
||||||
|
|
||||||
|
private final byte[] framebufferUpdateRequest = new byte[10];
|
||||||
|
private byte[] setEncodingsBuf = new byte[64];
|
||||||
|
private final byte[] eventBuf = new byte[256];
|
||||||
|
private int eventBufLen;
|
||||||
|
|
||||||
|
public RfbProtoDesktop(String host, int port) throws IOException {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
|
||||||
|
this.sock = new Socket();
|
||||||
|
// DO NOT set TCP_NODELAY — Nagle coalesces small writes (pointer
|
||||||
|
// events, framebuffer requests) into fewer packets, which massively
|
||||||
|
// reduces per-packet overhead on the udp2raw/WireGuard path.
|
||||||
|
this.sock.setKeepAlive(true);
|
||||||
|
this.sock.setReuseAddress(true);
|
||||||
|
this.sock.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS);
|
||||||
|
this.sock.setSoTimeout(READ_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Increase receive buffer — large ZRLE/Tight frames arrive in bursts.
|
||||||
|
this.sock.setReceiveBufferSize(65536);
|
||||||
|
|
||||||
|
this.is = new DataInputStream(new BufferedInputStream(sock.getInputStream(), IO_BUFFER_SIZE));
|
||||||
|
this.os = new BufferedOutputStream(sock.getOutputStream(), IO_BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataInputStream getInputStream() {
|
||||||
|
return is;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInNormalProtocol() {
|
||||||
|
return inNormalProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasZlibUpdates() {
|
||||||
|
return wereZlibUpdates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getServerMajor() { return serverMajor; }
|
||||||
|
public int getServerMinor() { return serverMinor; }
|
||||||
|
public int getClientMajor() { return clientMajor; }
|
||||||
|
public int getClientMinor() { return clientMinor; }
|
||||||
|
|
||||||
|
public String getDesktopName() { return desktopName; }
|
||||||
|
public int getFramebufferWidth() { return framebufferWidth; }
|
||||||
|
public int getFramebufferHeight() { return framebufferHeight; }
|
||||||
|
public int getBitsPerPixel() { return bitsPerPixel; }
|
||||||
|
public int getDepth() { return depth; }
|
||||||
|
public boolean isBigEndian() { return bigEndian; }
|
||||||
|
public boolean isTrueColour() { return trueColour; }
|
||||||
|
public int getRedMax() { return redMax; }
|
||||||
|
public int getGreenMax() { return greenMax; }
|
||||||
|
public int getBlueMax() { return blueMax; }
|
||||||
|
public int getRedShift() { return redShift; }
|
||||||
|
public int getGreenShift() { return greenShift; }
|
||||||
|
public int getBlueShift() { return blueShift; }
|
||||||
|
|
||||||
|
public int getUpdateNRects() { return updateNRects; }
|
||||||
|
public int getUpdateRectX() { return updateRectX; }
|
||||||
|
public int getUpdateRectY() { return updateRectY; }
|
||||||
|
public int getUpdateRectW() { return updateRectW; }
|
||||||
|
public int getUpdateRectH() { return updateRectH; }
|
||||||
|
public int getUpdateRectEncoding() { return updateRectEncoding; }
|
||||||
|
|
||||||
|
public int getCopyRectSrcX() { return copyRectSrcX; }
|
||||||
|
public int getCopyRectSrcY() { return copyRectSrcY; }
|
||||||
|
|
||||||
|
public synchronized void close() {
|
||||||
|
try {
|
||||||
|
sock.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readVersionMsg() throws Exception {
|
||||||
|
byte[] b = new byte[12];
|
||||||
|
readFully(b);
|
||||||
|
|
||||||
|
if ((b[0] != 'R') || (b[1] != 'F') || (b[2] != 'B') || (b[3] != ' ')
|
||||||
|
|| (b[4] < '0') || (b[4] > '9') || (b[5] < '0') || (b[5] > '9')
|
||||||
|
|| (b[6] < '0') || (b[6] > '9') || (b[7] != '.')
|
||||||
|
|| (b[8] < '0') || (b[8] > '9') || (b[9] < '0') || (b[9] > '9')
|
||||||
|
|| (b[10] < '0') || (b[10] > '9') || (b[11] != '\n')) {
|
||||||
|
throw new Exception("Host " + host + " port " + port + " is not an RFB server");
|
||||||
|
}
|
||||||
|
|
||||||
|
serverMajor = (b[4] - '0') * 100 + (b[5] - '0') * 10 + (b[6] - '0');
|
||||||
|
serverMinor = (b[8] - '0') * 100 + (b[9] - '0') * 10 + (b[10] - '0');
|
||||||
|
|
||||||
|
if (serverMajor < 3) {
|
||||||
|
throw new Exception("RFB server does not support protocol version 3");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writeVersionMsg() throws IOException {
|
||||||
|
clientMajor = 3;
|
||||||
|
if (serverMajor > 3 || serverMinor >= 8) {
|
||||||
|
clientMinor = 8;
|
||||||
|
os.write(VERSION_MSG_3_8.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
} else if (serverMinor >= 7) {
|
||||||
|
clientMinor = 7;
|
||||||
|
os.write(VERSION_MSG_3_7.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
} else {
|
||||||
|
clientMinor = 3;
|
||||||
|
os.write(VERSION_MSG_3_3.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int negotiateSecurity() throws Exception {
|
||||||
|
return (clientMinor >= 7) ? selectSecurityType() : readSecurityType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readSecurityType() throws Exception {
|
||||||
|
int secType = is.readInt();
|
||||||
|
|
||||||
|
switch (secType) {
|
||||||
|
case SEC_TYPE_INVALID:
|
||||||
|
readConnFailedReason();
|
||||||
|
return SEC_TYPE_INVALID;
|
||||||
|
case SEC_TYPE_NONE:
|
||||||
|
case SEC_TYPE_VNC_AUTH:
|
||||||
|
return secType;
|
||||||
|
default:
|
||||||
|
throw new Exception("Unknown security type from RFB server: " + secType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int selectSecurityType() throws Exception {
|
||||||
|
int nSecTypes = is.readUnsignedByte();
|
||||||
|
if (nSecTypes == 0) {
|
||||||
|
readConnFailedReason();
|
||||||
|
return SEC_TYPE_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] secTypes = new byte[nSecTypes];
|
||||||
|
readFully(secTypes);
|
||||||
|
|
||||||
|
int selected = SEC_TYPE_INVALID;
|
||||||
|
|
||||||
|
// Prefer VNC auth over no-auth if both are offered.
|
||||||
|
for (byte secType : secTypes) {
|
||||||
|
if ((secType & 0xFF) == SEC_TYPE_VNC_AUTH) {
|
||||||
|
selected = SEC_TYPE_VNC_AUTH;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected == SEC_TYPE_INVALID) {
|
||||||
|
for (byte secType : secTypes) {
|
||||||
|
if ((secType & 0xFF) == SEC_TYPE_NONE) {
|
||||||
|
selected = SEC_TYPE_NONE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected == SEC_TYPE_INVALID) {
|
||||||
|
throw new Exception("Server did not offer supported security type");
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write(selected);
|
||||||
|
os.flush();
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void authenticateVNC(String pw) throws Exception {
|
||||||
|
byte[] challenge = new byte[16];
|
||||||
|
readFully(challenge);
|
||||||
|
|
||||||
|
if (pw == null) {
|
||||||
|
pw = "";
|
||||||
|
}
|
||||||
|
if (pw.length() > 8) pw = pw.substring(0, 8);
|
||||||
|
int firstZero = pw.indexOf(0);
|
||||||
|
if (firstZero != -1) pw = pw.substring(0, firstZero);
|
||||||
|
|
||||||
|
byte[] key = new byte[8];
|
||||||
|
byte[] pwBytes = pw.getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
System.arraycopy(pwBytes, 0, key, 0, Math.min(pwBytes.length, key.length));
|
||||||
|
|
||||||
|
DesCipherDesktop des = new DesCipherDesktop(key);
|
||||||
|
des.encrypt(challenge, 0, challenge, 0);
|
||||||
|
des.encrypt(challenge, 8, challenge, 8);
|
||||||
|
|
||||||
|
os.write(challenge);
|
||||||
|
os.flush();
|
||||||
|
|
||||||
|
readSecurityResult("VNC authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readSecurityResult(String authType) throws Exception {
|
||||||
|
int securityResult = is.readInt();
|
||||||
|
|
||||||
|
switch (securityResult) {
|
||||||
|
case VNC_AUTH_OK:
|
||||||
|
return;
|
||||||
|
case VNC_AUTH_FAILED:
|
||||||
|
if (clientMinor >= 8) readConnFailedReason();
|
||||||
|
throw new Exception(authType + ": failed");
|
||||||
|
case VNC_AUTH_TOO_MANY:
|
||||||
|
throw new Exception(authType + ": failed, too many tries");
|
||||||
|
default:
|
||||||
|
throw new Exception(authType + ": unknown result " + securityResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void readConnFailedReason() throws Exception {
|
||||||
|
int reasonLen = is.readInt();
|
||||||
|
if (reasonLen < 0 || reasonLen > 1024 * 1024) {
|
||||||
|
throw new IOException("Invalid RFB failure reason length: " + reasonLen);
|
||||||
|
}
|
||||||
|
byte[] reason = new byte[reasonLen];
|
||||||
|
readFully(reason);
|
||||||
|
throw new Exception(new String(reason, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writeClientInit() throws IOException {
|
||||||
|
os.write(1);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readServerInit() throws IOException {
|
||||||
|
framebufferWidth = is.readUnsignedShort();
|
||||||
|
framebufferHeight = is.readUnsignedShort();
|
||||||
|
bitsPerPixel = is.readUnsignedByte();
|
||||||
|
depth = is.readUnsignedByte();
|
||||||
|
bigEndian = (is.readUnsignedByte() != 0);
|
||||||
|
trueColour = (is.readUnsignedByte() != 0);
|
||||||
|
redMax = is.readUnsignedShort();
|
||||||
|
greenMax = is.readUnsignedShort();
|
||||||
|
blueMax = is.readUnsignedShort();
|
||||||
|
redShift = is.readUnsignedByte();
|
||||||
|
greenShift = is.readUnsignedByte();
|
||||||
|
blueShift = is.readUnsignedByte();
|
||||||
|
|
||||||
|
skipFully(3);
|
||||||
|
|
||||||
|
int nameLength = is.readInt();
|
||||||
|
if (nameLength < 0 || nameLength > 16 * 1024 * 1024) {
|
||||||
|
throw new IOException("Invalid desktop name length: " + nameLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] name = new byte[nameLength];
|
||||||
|
readFully(name);
|
||||||
|
desktopName = new String(name, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
inNormalProtocol = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFramebufferSize(int width, int height) {
|
||||||
|
framebufferWidth = Math.max(0, width);
|
||||||
|
framebufferHeight = Math.max(0, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readServerMessageType() throws IOException {
|
||||||
|
return is.readUnsignedByte();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readFramebufferUpdate() throws IOException {
|
||||||
|
is.readUnsignedByte(); // padding
|
||||||
|
updateNRects = is.readUnsignedShort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readFramebufferUpdateRectHdr() throws Exception {
|
||||||
|
updateRectX = is.readUnsignedShort();
|
||||||
|
updateRectY = is.readUnsignedShort();
|
||||||
|
updateRectW = is.readUnsignedShort();
|
||||||
|
updateRectH = is.readUnsignedShort();
|
||||||
|
updateRectEncoding = is.readInt();
|
||||||
|
|
||||||
|
if (updateRectEncoding == ENCODING_ZLIB || updateRectEncoding == ENCODING_ZRLE) {
|
||||||
|
wereZlibUpdates = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateRectEncoding == ENCODING_LAST_RECT) {
|
||||||
|
updateNRects = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateRectEncoding == ENCODING_NEW_FB_SIZE) {
|
||||||
|
setFramebufferSize(updateRectW, updateRectH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateRectEncoding == ENCODING_POINTER_POS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pseudo-encodings are negative. Their payload handling is done by the caller.
|
||||||
|
if (updateRectEncoding < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRectX = Math.max(0, Math.min(updateRectX, framebufferWidth));
|
||||||
|
updateRectY = Math.max(0, Math.min(updateRectY, framebufferHeight));
|
||||||
|
updateRectW = Math.max(0, Math.min(updateRectW, framebufferWidth - updateRectX));
|
||||||
|
updateRectH = Math.max(0, Math.min(updateRectH, framebufferHeight - updateRectY));
|
||||||
|
|
||||||
|
if (updateRectX + updateRectW > framebufferWidth || updateRectY + updateRectH > framebufferHeight) {
|
||||||
|
throw new Exception("Framebuffer update rectangle too large: " + updateRectW + "x" + updateRectH
|
||||||
|
+ " at (" + updateRectX + "," + updateRectY + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readCopyRect() throws IOException {
|
||||||
|
copyRectSrcX = is.readUnsignedShort();
|
||||||
|
copyRectSrcY = is.readUnsignedShort();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writeFramebufferUpdateRequest(int x, int y, int w, int h, boolean incremental) throws IOException {
|
||||||
|
framebufferUpdateRequest[0] = (byte) FRAMEBUFFER_UPDATE_REQUEST;
|
||||||
|
framebufferUpdateRequest[1] = (byte) (incremental ? 1 : 0);
|
||||||
|
framebufferUpdateRequest[2] = (byte) ((x >> 8) & 0xff);
|
||||||
|
framebufferUpdateRequest[3] = (byte) (x & 0xff);
|
||||||
|
framebufferUpdateRequest[4] = (byte) ((y >> 8) & 0xff);
|
||||||
|
framebufferUpdateRequest[5] = (byte) (y & 0xff);
|
||||||
|
framebufferUpdateRequest[6] = (byte) ((w >> 8) & 0xff);
|
||||||
|
framebufferUpdateRequest[7] = (byte) (w & 0xff);
|
||||||
|
framebufferUpdateRequest[8] = (byte) ((h >> 8) & 0xff);
|
||||||
|
framebufferUpdateRequest[9] = (byte) (h & 0xff);
|
||||||
|
|
||||||
|
os.write(framebufferUpdateRequest);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writeSetPixelFormat(
|
||||||
|
int bitsPerPixel, int depth, boolean bigEndian, boolean trueColour,
|
||||||
|
int redMax, int greenMax, int blueMax,
|
||||||
|
int redShift, int greenShift, int blueShift, boolean greyScale
|
||||||
|
) throws IOException {
|
||||||
|
byte[] b = new byte[20];
|
||||||
|
b[0] = (byte) SET_PIXEL_FORMAT;
|
||||||
|
b[1] = 0;
|
||||||
|
b[2] = 0;
|
||||||
|
b[3] = 0;
|
||||||
|
b[4] = (byte) bitsPerPixel;
|
||||||
|
b[5] = (byte) depth;
|
||||||
|
b[6] = (byte) (bigEndian ? 1 : 0);
|
||||||
|
b[7] = (byte) (trueColour ? 1 : 0);
|
||||||
|
b[8] = (byte) ((redMax >> 8) & 0xff);
|
||||||
|
b[9] = (byte) (redMax & 0xff);
|
||||||
|
b[10] = (byte) ((greenMax >> 8) & 0xff);
|
||||||
|
b[11] = (byte) (greenMax & 0xff);
|
||||||
|
b[12] = (byte) ((blueMax >> 8) & 0xff);
|
||||||
|
b[13] = (byte) (blueMax & 0xff);
|
||||||
|
b[14] = (byte) redShift;
|
||||||
|
b[15] = (byte) greenShift;
|
||||||
|
b[16] = (byte) blueShift;
|
||||||
|
b[17] = (byte) (greyScale ? 1 : 0);
|
||||||
|
b[18] = 0;
|
||||||
|
b[19] = 0;
|
||||||
|
|
||||||
|
os.write(b);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writeSetEncodings(int[] encs, int len) throws IOException {
|
||||||
|
int required = 4 + 4 * len;
|
||||||
|
if (setEncodingsBuf.length < required) {
|
||||||
|
setEncodingsBuf = new byte[Math.max(required, setEncodingsBuf.length * 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
setEncodingsBuf[0] = (byte) SET_ENCODINGS;
|
||||||
|
setEncodingsBuf[1] = 0;
|
||||||
|
setEncodingsBuf[2] = (byte) ((len >> 8) & 0xff);
|
||||||
|
setEncodingsBuf[3] = (byte) (len & 0xff);
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int enc = encs[i];
|
||||||
|
int p = 4 + 4 * i;
|
||||||
|
setEncodingsBuf[p] = (byte) ((enc >> 24) & 0xff);
|
||||||
|
setEncodingsBuf[p + 1] = (byte) ((enc >> 16) & 0xff);
|
||||||
|
setEncodingsBuf[p + 2] = (byte) ((enc >> 8) & 0xff);
|
||||||
|
setEncodingsBuf[p + 3] = (byte) (enc & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
os.write(setEncodingsBuf, 0, required);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void writePointerEvent(int x, int y, int modifiers, int pointerMask) throws IOException {
|
||||||
|
eventBufLen = 0;
|
||||||
|
appendPointerEvent(x, y, pointerMask);
|
||||||
|
os.write(eventBuf, 0, eventBufLen);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lower-latency click path: sends up/down/up plus one incremental framebuffer request
|
||||||
|
* in a single write/flush instead of four separate writes/flushes.
|
||||||
|
*/
|
||||||
|
public synchronized void writeClickAndFramebufferRequest(
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int framebufferWidth,
|
||||||
|
int framebufferHeight
|
||||||
|
) throws IOException {
|
||||||
|
eventBufLen = 0;
|
||||||
|
|
||||||
|
appendPointerEvent(x, y, 0);
|
||||||
|
appendPointerEvent(x, y, 1);
|
||||||
|
appendPointerEvent(x, y, 0);
|
||||||
|
appendFramebufferUpdateRequest(0, 0, framebufferWidth, framebufferHeight, true);
|
||||||
|
|
||||||
|
os.write(eventBuf, 0, eventBufLen);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful for drag/move support: caller can coalesce many pointer moves and flush once.
|
||||||
|
*/
|
||||||
|
public synchronized void writePointerEventNoFlush(int x, int y, int pointerMask) throws IOException {
|
||||||
|
eventBufLen = 0;
|
||||||
|
appendPointerEvent(x, y, pointerMask);
|
||||||
|
os.write(eventBuf, 0, eventBufLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void flush() throws IOException {
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendPointerEvent(int x, int y, int pointerMask) {
|
||||||
|
ensureEventCapacity(eventBufLen + 6);
|
||||||
|
eventBuf[eventBufLen++] = (byte) POINTER_EVENT;
|
||||||
|
eventBuf[eventBufLen++] = (byte) pointerMask;
|
||||||
|
eventBuf[eventBufLen++] = (byte) ((x >> 8) & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) (x & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) ((y >> 8) & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) (y & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendFramebufferUpdateRequest(int x, int y, int w, int h, boolean incremental) {
|
||||||
|
ensureEventCapacity(eventBufLen + 10);
|
||||||
|
eventBuf[eventBufLen++] = (byte) FRAMEBUFFER_UPDATE_REQUEST;
|
||||||
|
eventBuf[eventBufLen++] = (byte) (incremental ? 1 : 0);
|
||||||
|
eventBuf[eventBufLen++] = (byte) ((x >> 8) & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) (x & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) ((y >> 8) & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) (y & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) ((w >> 8) & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) (w & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) ((h >> 8) & 0xff);
|
||||||
|
eventBuf[eventBufLen++] = (byte) (h & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureEventCapacity(int needed) {
|
||||||
|
if (needed > eventBuf.length) {
|
||||||
|
throw new IllegalStateException("Internal event buffer too small: " + needed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readFully(byte[] b) throws IOException {
|
||||||
|
readFully(b, 0, b.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readFully(byte[] b, int off, int len) throws IOException {
|
||||||
|
is.readFully(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void skipFully(int len) throws IOException {
|
||||||
|
int remaining = len;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int skipped = is.skipBytes(remaining);
|
||||||
|
if (skipped <= 0) {
|
||||||
|
if (is.read() == -1) {
|
||||||
|
throw new EOFException("Unexpected EOF while skipping " + len + " bytes");
|
||||||
|
}
|
||||||
|
skipped = 1;
|
||||||
|
}
|
||||||
|
remaining -= skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
|||||||
|
package com.litoralregas.backend.vnc.rfb;
|
||||||
|
|
||||||
|
import java.util.zip.DataFormatException;
|
||||||
|
import java.util.zip.Inflater;
|
||||||
|
|
||||||
|
public class ZlibInStreamDesktop extends InStreamDesktop {
|
||||||
|
|
||||||
|
static final int DEFAULT_BUF_SIZE = 16384;
|
||||||
|
|
||||||
|
private InStreamDesktop underlying;
|
||||||
|
private int bufSize;
|
||||||
|
private int ptrOffset;
|
||||||
|
private Inflater inflater;
|
||||||
|
private int bytesIn;
|
||||||
|
|
||||||
|
public ZlibInStreamDesktop(int bufSize) {
|
||||||
|
this.bufSize = bufSize;
|
||||||
|
this.b = new byte[bufSize];
|
||||||
|
this.ptr = this.end = this.ptrOffset = 0;
|
||||||
|
this.inflater = new Inflater();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZlibInStreamDesktop() {
|
||||||
|
this(DEFAULT_BUF_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnderlying(InStreamDesktop is, int bytesIn) {
|
||||||
|
this.underlying = is;
|
||||||
|
this.bytesIn = bytesIn;
|
||||||
|
this.ptr = this.end = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() throws Exception {
|
||||||
|
ptr = end = 0;
|
||||||
|
if (underlying == null) return;
|
||||||
|
|
||||||
|
while (bytesIn > 0) {
|
||||||
|
decompress();
|
||||||
|
end = 0;
|
||||||
|
}
|
||||||
|
underlying = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int pos() {
|
||||||
|
return ptrOffset + ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int overrun(int itemSize, int nItems) throws Exception {
|
||||||
|
if (itemSize > bufSize) {
|
||||||
|
throw new Exception("ZlibInStream overrun: max itemSize exceeded");
|
||||||
|
}
|
||||||
|
if (underlying == null) {
|
||||||
|
throw new Exception("ZlibInStream overrun: no underlying stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end - ptr != 0) {
|
||||||
|
System.arraycopy(b, ptr, b, 0, end - ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrOffset += ptr;
|
||||||
|
end -= ptr;
|
||||||
|
ptr = 0;
|
||||||
|
|
||||||
|
while (end < itemSize) {
|
||||||
|
decompress();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemSize * nItems > end) {
|
||||||
|
nItems = end / itemSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decompress() throws Exception {
|
||||||
|
try {
|
||||||
|
underlying.check(1);
|
||||||
|
int availIn = underlying.getend() - underlying.getptr();
|
||||||
|
if (availIn > bytesIn) availIn = bytesIn;
|
||||||
|
|
||||||
|
if (inflater.needsInput()) {
|
||||||
|
inflater.setInput(underlying.getbuf(), underlying.getptr(), availIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = inflater.inflate(b, end, bufSize - end);
|
||||||
|
end += n;
|
||||||
|
|
||||||
|
if (inflater.needsInput()) {
|
||||||
|
bytesIn -= availIn;
|
||||||
|
underlying.setptr(underlying.getptr() + availIn);
|
||||||
|
}
|
||||||
|
} catch (DataFormatException e) {
|
||||||
|
throw new Exception("ZlibInStream inflate failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package com.litoralregas.backend.vnc.websocket;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.litoralregas.backend.vnc.rfb.VncClientDesktop;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.BinaryMessage;
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.BinaryWebSocketHandler;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class VncStreamHandler extends BinaryWebSocketHandler {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private final Map<String, VncClientDesktop> clients = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void handleTextMessage(
|
||||||
|
WebSocketSession session,
|
||||||
|
TextMessage message
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
JsonNode json = objectMapper.readTree(message.getPayload());
|
||||||
|
String type = json.path("type").asText();
|
||||||
|
|
||||||
|
if ("connect".equals(type)) {
|
||||||
|
handleConnect(session, json);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("click".equals(type)) {
|
||||||
|
handleClick(session, json);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("disconnect".equals(type)) {
|
||||||
|
handleDisconnect(session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendErrorSafe(session, "Unknown VNC message type: " + type);
|
||||||
|
} catch (Exception error) {
|
||||||
|
sendErrorSafe(session, error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleConnect(
|
||||||
|
WebSocketSession session,
|
||||||
|
JsonNode json
|
||||||
|
) {
|
||||||
|
closeClient(session);
|
||||||
|
|
||||||
|
String host = json.path("host").asText();
|
||||||
|
int port = json.path("port").asInt(5900);
|
||||||
|
String password = json.path("password").asText();
|
||||||
|
|
||||||
|
if (host == null || host.isBlank()) {
|
||||||
|
sendErrorSafe(session, "Missing VNC host");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendStateSafe(session, "CONNECTING");
|
||||||
|
|
||||||
|
VncClientDesktop client = new VncClientDesktop();
|
||||||
|
client.setListener(createFrameListener(session));
|
||||||
|
|
||||||
|
clients.put(session.getId(), client);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.connect(host, port, password);
|
||||||
|
} catch (Exception error) {
|
||||||
|
clients.remove(session.getId());
|
||||||
|
sendErrorSafe(session, error.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClick(
|
||||||
|
WebSocketSession session,
|
||||||
|
JsonNode json
|
||||||
|
) {
|
||||||
|
VncClientDesktop client = clients.get(session.getId());
|
||||||
|
|
||||||
|
if (client == null) {
|
||||||
|
sendErrorSafe(session, "No active VNC client for this websocket session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float x = (float) json.path("x").asDouble();
|
||||||
|
float y = (float) json.path("y").asDouble();
|
||||||
|
|
||||||
|
client.sendClick(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDisconnect(WebSocketSession session) {
|
||||||
|
closeClient(session);
|
||||||
|
sendStateSafe(session, "DISCONNECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
private VncClientDesktop.VncFrameListener createFrameListener(WebSocketSession session) {
|
||||||
|
return new VncClientDesktop.VncFrameListener() {
|
||||||
|
@Override
|
||||||
|
public void onConnected() {
|
||||||
|
sendStateSafe(session, "CONNECTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFirstFrameReceived() {
|
||||||
|
sendStateSafe(session, "FIRST_FRAME");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFrameUpdated(byte[] pixelBuffer, int width, int height) {
|
||||||
|
sendFrameSafe(session, pixelBuffer, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(String message) {
|
||||||
|
sendErrorSafe(session, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected() {
|
||||||
|
sendStateSafe(session, "DISCONNECTED");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendFrameSafe(
|
||||||
|
WebSocketSession session,
|
||||||
|
byte[] pixels,
|
||||||
|
int width,
|
||||||
|
int height
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!session.isOpen()) {
|
||||||
|
closeClient(session);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] header = ByteBuffer.allocate(8)
|
||||||
|
.putInt(width)
|
||||||
|
.putInt(height)
|
||||||
|
.array();
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(header.length + pixels.length);
|
||||||
|
buffer.put(header);
|
||||||
|
buffer.put(pixels);
|
||||||
|
buffer.flip();
|
||||||
|
|
||||||
|
synchronized (session) {
|
||||||
|
session.sendMessage(new BinaryMessage(buffer));
|
||||||
|
}
|
||||||
|
} catch (Exception error) {
|
||||||
|
closeClient(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendStateSafe(
|
||||||
|
WebSocketSession session,
|
||||||
|
String state
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!session.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (session) {
|
||||||
|
session.sendMessage(new TextMessage(
|
||||||
|
objectMapper.writeValueAsString(Map.of(
|
||||||
|
"type", "state",
|
||||||
|
"state", state
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendErrorSafe(
|
||||||
|
WebSocketSession session,
|
||||||
|
String message
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (!session.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (session) {
|
||||||
|
session.sendMessage(new TextMessage(
|
||||||
|
objectMapper.writeValueAsString(Map.of(
|
||||||
|
"type", "error",
|
||||||
|
"message", message == null ? "Unknown VNC error" : message
|
||||||
|
))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeClient(WebSocketSession session) {
|
||||||
|
VncClientDesktop client = clients.remove(session.getId());
|
||||||
|
|
||||||
|
if (client != null) {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(
|
||||||
|
WebSocketSession session,
|
||||||
|
CloseStatus status
|
||||||
|
) {
|
||||||
|
closeClient(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.litoralregas.backend.vnc.websocket;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocket
|
||||||
|
public class VncWebSocketConfig implements WebSocketConfigurer {
|
||||||
|
|
||||||
|
private final VncStreamHandler vncStreamHandler;
|
||||||
|
|
||||||
|
public VncWebSocketConfig(VncStreamHandler vncStreamHandler) {
|
||||||
|
this.vncStreamHandler = vncStreamHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
|
registry.addHandler(vncStreamHandler, "/ws/vnc")
|
||||||
|
.setAllowedOrigins("*");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user