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