├── .gitignore ├── .idea ├── .gitignore ├── jsLibraryMappings.xml ├── modules.xml ├── vcs.xml └── vncclient.iml ├── README.md ├── constants.js ├── decoders ├── copyrect.js ├── hextile.js ├── raw.js ├── tight.js └── zrle.js ├── package.json ├── socketbuffer.js └── vncclient.js /.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | ffmpeg.exe 3 | video.mp4 4 | node_modules 5 | .idea 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vncclient.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 22 | [![Contributors][contributors-shield]][contributors-url] 23 | [![Forks][forks-shield]][forks-url] 24 | [![Stargazers][stars-shield]][stars-url] 25 | [![Issues][issues-shield]][issues-url] 26 | [![LinkedIn][linkedin-shield]][linkedin-url] 27 | 28 | 29 | 30 | 31 | 32 |

VNC-RFB-CLIENT

33 | 34 |

35 | Pure node.js implementation of RFC 6143 (RFB Protocol / VNC) client with no external dependencies. Supports Raw, CopyRect, Hextile and ZRLE encodings. 36 |
37 | Report Bug or Request Feature 38 |

39 |

40 | 41 | 42 | ## User Contributions 43 | ### ayunami2000 44 | - qemu audio (PCM) 45 | - qemu relative pointer 46 | - documentation for these WIP 47 | 48 | 49 | 50 | 51 | ## Getting Started 52 | 53 | ### Requirements 54 | 55 | Node.js >= 14 56 | 57 | ### Installation 58 | 59 | 1. Install NPM packages 60 | ```sh 61 | npm install vnc-rfb-client 62 | ``` 63 | 64 | 65 | 66 | ## Usage 67 | 68 | ```javascript 69 | const VncClient = require('vnc-rfb-client'); 70 | 71 | const initOptions = { 72 | debug: false, // Set debug logging 73 | encodings: [ // Encodings sent to server, in order of preference 74 | VncClient.consts.encodings.copyRect, 75 | VncClient.consts.encodings.zrle, 76 | VncClient.consts.encodings.hextile, 77 | VncClient.consts.encodings.raw, 78 | VncClient.consts.encodings.pseudoDesktopSize, 79 | VncClient.consts.encodings.pseudoCursor 80 | ], 81 | debugLevel: 1 // Verbosity level (1 - 5) when debug is set to true 82 | }; 83 | const client = new VncClient(initOptions); 84 | 85 | const connectionOptions = { 86 | host: '', // VNC Server 87 | password: '', // Password 88 | set8BitColor: false, // If set to true, client will request 8 bit color, only supported with Raw encoding 89 | port: 5900 // Remote server port 90 | } 91 | client.connect(connectionOptions); 92 | 93 | // Client successfully connected 94 | client.on('connected', () => { 95 | console.log('Client connected.'); 96 | }); 97 | 98 | // Connection timed out 99 | client.on('connectTimeout', () => { 100 | console.log('Connection timeout.'); 101 | }); 102 | 103 | // Client successfully authenticated 104 | client.on('authenticated', () => { 105 | console.log('Client authenticated.'); 106 | }); 107 | 108 | // Authentication error 109 | client.on('authError', () => { 110 | console.log('Client authentication error.'); 111 | }); 112 | 113 | // Bell received from server 114 | client.on('bell', () => { 115 | console.log('Bell received'); 116 | }); 117 | 118 | // Client disconnected 119 | client.on('disconnect', () => { 120 | console.log('Client disconnected.'); 121 | process.exit(); 122 | }); 123 | 124 | // Clipboard event on server 125 | client.on('cutText', (text) => { 126 | console.log('clipboard text received: ' + text); 127 | }); 128 | 129 | // Frame buffer updated 130 | client.on('firstFrameUpdate', (fb) => { 131 | console.log('First Framebuffer update received.'); 132 | }); 133 | 134 | // Frame buffer updated 135 | client.on('frameUpdated', (fb) => { 136 | console.log('Framebuffer updated.'); 137 | }); 138 | 139 | // Color map updated (8 bit color only) 140 | client.on('colorMapUpdated', (colorMap) => { 141 | console.log('Color map updated. Colors: ' + colorMap.length); 142 | }); 143 | 144 | // Rect processed 145 | client.on('rectProcessed', (rect) => { 146 | console.log('rect processed'); 147 | }); 148 | 149 | ``` 150 | 151 | ## Examples 152 | 153 | ### Save frame to jpg 154 | 155 | ```javascript 156 | const VncClient = require('vnc-rfb-client'); 157 | const Jimp = require('jimp'); 158 | 159 | const client = new VncClient(); 160 | 161 | // Just 1 update per second 162 | client.changeFps(1); 163 | client.connect({host: '127.0.0.1', port: 5900, password: 'abc123'}); 164 | 165 | client.on('frameUpdated', (data) => { 166 | new Jimp({width: client.clientWidth, height: client.clientHeight, data: client.getFb()}, (err, image) => { 167 | if (err) { 168 | console.log(err); 169 | } 170 | const fileName = `${Date.now()}.jpg`; 171 | console.log(`Saving frame to file. ${fileName}`); 172 | image.write(`${fileName}`); 173 | }); 174 | }); 175 | 176 | client.on('connectError', (err) => { 177 | console.log(err); 178 | }); 179 | 180 | client.on('authError', () => { 181 | console.log('Authentication failed.'); 182 | }); 183 | ``` 184 | 185 | ### Record session with FFMPEG 186 | 187 | ```javascript 188 | const VncClient = require('vnc-rfb-client'); 189 | const spawn = require('child_process').spawn; 190 | const fps = 10; 191 | 192 | let timerRef; 193 | const client = new VncClient({fps}); 194 | let out; 195 | 196 | client.connect({host: '127.0.0.1', port: 5900, password: 'abc123'}); 197 | 198 | client.on('firstFrameUpdate', () => { 199 | console.log('Start recording...'); 200 | out = spawn('./ffmpeg.exe', 201 | `-loglevel error -hide_banner -y -f rawvideo -vcodec rawvideo -an -pix_fmt rgba -s ${client.clientWidth}x${client.clientHeight} -r ${fps} -i - -an -r ${fps} -vcodec libx264rgb session.h264`.split(' ')); 202 | timer(); 203 | }); 204 | 205 | process.on('SIGINT', function () { 206 | console.log("Exiting."); 207 | close(); 208 | }); 209 | 210 | function timer() { 211 | timerRef = setTimeout(() => { 212 | timer(); 213 | out?.stdin?.write(client.getFb()); 214 | }, 1000 / fps); 215 | } 216 | 217 | function close() { 218 | if (timerRef) { 219 | clearTimeout(timerRef); 220 | } 221 | if (out) { 222 | out.kill('SIGINT'); 223 | out.on('exit', () => { 224 | process.exit(0); 225 | }); 226 | } 227 | } 228 | 229 | client.on('disconnect', () => { 230 | console.log('Client disconnected.'); 231 | close(); 232 | }); 233 | 234 | ``` 235 | 236 | ## Methods 237 | 238 | ```javascript 239 | /** 240 | * Request a frame update to the server 241 | */ 242 | client.requestFrameUpdate(full, increment, x, y, width, height); 243 | 244 | /** 245 | * Change the rate limit of frame buffer requests 246 | * If set to 0, a new update request will be sent as soon as the last update finish processing 247 | */ 248 | client.changeFps(10); 249 | 250 | /** 251 | * Start the connection with the server 252 | */ 253 | const connectionOptions = { 254 | host: '', // VNC Server 255 | password: '', // Password 256 | set8BitColor: false, // If set to true, client will request 8 bit color, only supported with Raw encoding 257 | port: 5900 // Remote server port 258 | } 259 | client.connect(connectionOptions); 260 | 261 | /** 262 | * Send a key board event 263 | * Check https://wiki.linuxquestions.org/wiki/List_of_keysyms for keycodes 264 | * down = true for keydown and down = false for keyup 265 | */ 266 | client.sendKeyEvent(keysym, down); 267 | 268 | /** 269 | * Send pointer event (mouse or touch) 270 | * xPosition - X Position of the pointer 271 | * yPosition - Y Position of the pointer 272 | * button1 to button 8 - True for down, false for up 273 | */ 274 | client.sendPointerEvent(xPosition, yPosition, button1, button2, button3, button4, button5, button6, button7, button8); 275 | 276 | /** 277 | * Send clipboard event to server 278 | * text - Text copied to clipboard 279 | */ 280 | client.clientCutText(text); 281 | 282 | client.resetState(); // Reset the state of the client, clear the frame buffer and purge all data 283 | 284 | client.getFb(); // Returns the framebuffer with cursor printed to it if using Cursor Pseudo Encoding 285 | 286 | ``` 287 | 288 | 289 | 290 | ## Roadmap 291 | 292 | ### Done 293 | 294 | #### Encodings Supported 295 | 296 | Raw
297 | CopyRect
298 | Hextile
299 | ZRLE
300 | PseudoDesktopSize
301 | Pseudo Cursor Encoding
302 | QEMU Audio
303 | QEMU Relative Pointer
304 | 3.7 and 3.8 protocol implementations 305 | 306 | ### TODO: 307 | 308 | Tight Encoding
309 | Save session data to file
310 | Replay session from rect data saved to file 311 | 312 | ## License 313 | 314 | Distributed under the MIT License. See `LICENSE` for more information. 315 | 316 | 317 | 318 | 319 | 320 | ## Contact 321 | 322 | Filipe Calaça - filipe@habilis.eng.br 323 | 324 | Project Link: [https://github.com/filipecbmoc/vnc-rfb-client](https://github.com/filipecbmoc/vnc-rfb-client) 325 | 326 | 327 | 328 | 329 | 330 | 331 | [contributors-shield]: https://img.shields.io/github/contributors/filipecbmoc/vnc-rfb-client?style=for-the-badge 332 | 333 | [contributors-url]: https://github.com/filipecbmoc/vnc-rfb-client/graphs/contributors 334 | 335 | [forks-shield]: https://img.shields.io/github/forks/filipecbmoc/vnc-rfb-client?style=for-the-badge 336 | 337 | [forks-url]: https://github.com/filipecbmoc/vnc-rfb-client/network/members 338 | 339 | [stars-shield]: https://img.shields.io/github/stars/filipecbmoc/vnc-rfb-client?style=for-the-badge 340 | 341 | [stars-url]: https://github.com/filipecbmoc/vnc-rfb-client/stargazers 342 | 343 | [issues-shield]: https://img.shields.io/github/issues/filipecbmoc/vnc-rfb-client?style=for-the-badge 344 | 345 | [issues-url]: https://github.com/filipecbmoc/vnc-rfb-client/issues 346 | 347 | [license-shield]: https://img.shields.io/github/license/github_username/repo.svg?style=for-the-badge 348 | 349 | [license-url]: https://github.com/filipe/vnc-rfb-client/blob/master/LICENSE.txt 350 | 351 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 352 | 353 | [linkedin-url]: https://linkedin.com/in/filipecalaca 354 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | // based on https://github.com/sidorares/node-rfb2 2 | 3 | const consts = { 4 | clientMsgTypes: { 5 | setPixelFormat: 0, 6 | setEncodings: 2, 7 | fbUpdate: 3, 8 | keyEvent: 4, 9 | pointerEvent: 5, 10 | cutText: 6, 11 | qemuAudio: 255, 12 | }, 13 | serverMsgTypes: { 14 | fbUpdate: 0, 15 | setColorMap: 1, 16 | bell: 2, 17 | cutText: 3, 18 | qemuAudio: 255, 19 | }, 20 | versionString: { 21 | V3_003: 'RFB 003.003\n', 22 | V3_007: 'RFB 003.007\n', 23 | V3_008: 'RFB 003.008\n' 24 | }, 25 | encodings: { 26 | raw: 0, 27 | copyRect: 1, 28 | rre: 2, 29 | corre: 4, 30 | hextile: 5, 31 | zlib: 6, 32 | tight: 7, 33 | zlibhex: 8, 34 | trle: 15, 35 | zrle: 16, 36 | h264: 50, 37 | pseudoCursor: -239, 38 | pseudoDesktopSize: -223, 39 | pseudoQemuPointerMotionChange: -257, 40 | pseudoQemuAudio: -259, 41 | }, 42 | security: { 43 | None: 1, 44 | VNC: 2 45 | } 46 | } 47 | 48 | module.exports = consts; 49 | -------------------------------------------------------------------------------- /decoders/copyrect.js: -------------------------------------------------------------------------------- 1 | class CopyRect { 2 | 3 | constructor(debug = false, debugLevel = 1) { 4 | this.debug = debug; 5 | this.debugLevel = debugLevel; 6 | } 7 | 8 | getPixelBytePos(x, y, width, height) { 9 | return ((y * width) + x) * 4; 10 | } 11 | 12 | decode(rect, fb, bitsPerPixel, colorMap, screenW, screenH, socket, depth, red, green, blue) { 13 | return new Promise(async (resolve, reject) => { 14 | 15 | await socket.waitBytes(4); 16 | rect.data = socket.readNBytesOffset(4); 17 | 18 | const x = rect.data.readUInt16BE(); 19 | const y = rect.data.readUInt16BE(2); 20 | 21 | for (let h = 0; h < rect.height; h++) { 22 | for (let w = 0; w < rect.width; w++) { 23 | 24 | const fbOrigBytePosOffset = this.getPixelBytePos(x + w, y + h, screenW, screenH); 25 | const fbBytePosOffset = this.getPixelBytePos(rect.x + w, rect.y + h, screenW, screenH); 26 | 27 | fb.writeUInt8(fb.readUInt8(fbOrigBytePosOffset), fbBytePosOffset); 28 | fb.writeUInt8(fb.readUInt8(fbOrigBytePosOffset + 1), fbBytePosOffset + 1); 29 | fb.writeUInt8(fb.readUInt8(fbOrigBytePosOffset + 2), fbBytePosOffset + 2); 30 | fb.writeUInt8(fb.readUInt8(fbOrigBytePosOffset + 3), fbBytePosOffset + 3); 31 | 32 | } 33 | } 34 | 35 | resolve(); 36 | 37 | }); 38 | } 39 | 40 | } 41 | 42 | module.exports = CopyRect; 43 | -------------------------------------------------------------------------------- /decoders/hextile.js: -------------------------------------------------------------------------------- 1 | class Hextile { 2 | 3 | constructor(debug = false, debugLevel = 1) { 4 | this.debug = debug; 5 | this.debugLevel = debugLevel; 6 | } 7 | 8 | getPixelBytePos(x, y, width, height) { 9 | return ((y * width) + x) * 4; 10 | } 11 | 12 | decode(rect, fb, bitsPerPixel, colorMap, screenW, screenH, socket, depth, redShift, greenShift, blueShift) { 13 | return new Promise(async (resolve, reject) => { 14 | 15 | const initialOffset = socket.offset; 16 | let dataSize = 0; 17 | 18 | let tiles; 19 | let totalTiles; 20 | let tilesX; 21 | let tilesY; 22 | 23 | let lastSubEncoding; 24 | 25 | let backgroundColor = 0; 26 | let foregroundColor = 0; 27 | 28 | tilesX = Math.ceil(rect.width / 16); 29 | tilesY = Math.ceil(rect.height / 16); 30 | tiles = tilesX * tilesY; 31 | totalTiles = tiles; 32 | 33 | while (tiles) { 34 | 35 | await socket.waitBytes(1, 'Hextile subencoding'); 36 | const subEncoding = socket.readUInt8(); 37 | dataSize++; 38 | const currTile = totalTiles - tiles; 39 | 40 | // Calculate tile position and size 41 | const tileX = currTile % tilesX; 42 | const tileY = Math.floor(currTile / tilesX); 43 | const tx = rect.x + (tileX * 16); 44 | const ty = rect.y + (tileY * 16); 45 | const tw = Math.min(16, (rect.x + rect.width) - tx); 46 | const th = Math.min(16, (rect.y + rect.height) - ty); 47 | 48 | if (subEncoding === 0) { 49 | if (lastSubEncoding & 0x01) { 50 | // We need to ignore zeroed tile after a raw tile 51 | } else { 52 | // If zeroed tile and last tile was not raw, use the last backgroundColor 53 | this.applyColor(tw, th, tx, ty, screenW, screenH, backgroundColor, fb); 54 | } 55 | } else if (subEncoding & 0x01) { 56 | // If Raw, ignore all other bits 57 | await socket.waitBytes(th * tw * (bitsPerPixel / 8)); 58 | dataSize += th * tw * (bitsPerPixel / 8); 59 | for (let h = 0; h < th; h++) { 60 | for (let w = 0; w < tw; w++) { 61 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 62 | if (bitsPerPixel === 8) { 63 | const index = socket.readUInt8(); 64 | const color = colorMap[index]; 65 | fb.writeIntBE(color, fbBytePosOffset, 4); 66 | } else if (bitsPerPixel === 24) { 67 | fb.writeIntBE(socket.readRgbPlusAlpha(redShift, greenShift, blueShift), fbBytePosOffset, 4); 68 | } else if (bitsPerPixel === 32) { 69 | fb.writeIntBE(socket.readRgba(redShift, greenShift, blueShift), fbBytePosOffset, 4); 70 | } 71 | } 72 | } 73 | lastSubEncoding = subEncoding; 74 | } else { 75 | // Background bit 76 | if (subEncoding & 0x02) { 77 | switch (bitsPerPixel) { 78 | case 8: 79 | await socket.waitBytes(1); 80 | const index = socket.readUInt8(); 81 | dataSize++; 82 | backgroundColor = colorMap[index]; 83 | break; 84 | 85 | case 24: 86 | await socket.waitBytes(3); 87 | dataSize += 3; 88 | backgroundColor = socket.readRgbPlusAlpha(redShift, greenShift, blueShift); 89 | break; 90 | 91 | case 32: 92 | await socket.waitBytes(4); 93 | dataSize += 4; 94 | backgroundColor = socket.readRgba(redShift, greenShift, blueShift); 95 | break; 96 | 97 | } 98 | } 99 | 100 | // Foreground bit 101 | if (subEncoding & 0x04) { 102 | switch (bitsPerPixel) { 103 | case 8: 104 | await socket.waitBytes(1); 105 | const index = socket.readUInt8(); 106 | dataSize++; 107 | foregroundColor = colorMap[index]; 108 | break; 109 | 110 | case 24: 111 | await socket.waitBytes(3); 112 | dataSize += 3; 113 | foregroundColor = socket.readRgbPlusAlpha(redShift, greenShift, blueShift); 114 | break; 115 | 116 | case 32: 117 | await socket.waitBytes(4); 118 | dataSize += 4; 119 | foregroundColor = socket.readRgba(redShift, greenShift, blueShift); 120 | break; 121 | 122 | } 123 | } 124 | 125 | // Initialize tile with the background color 126 | this.applyColor(tw, th, tx, ty, screenW, screenH, backgroundColor, fb); 127 | 128 | // AnySubrects bit 129 | if (subEncoding & 0x08) { 130 | 131 | await socket.waitBytes(1); 132 | let subRects = socket.readUInt8(); 133 | 134 | if (subRects) { 135 | 136 | while (subRects) { 137 | 138 | subRects--; 139 | let color = 0; 140 | 141 | // SubrectsColoured 142 | if (subEncoding & 0x10) { 143 | 144 | switch (bitsPerPixel) { 145 | 146 | case 8: 147 | await socket.waitBytes(1); 148 | const index = socket.readUInt8(); 149 | dataSize++; 150 | color = colorMap[index]; 151 | break; 152 | 153 | case 24: 154 | await socket.waitBytes(3); 155 | dataSize += 3; 156 | color = socket.readRgbPlusAlpha(redShift, greenShift, blueShift); 157 | break; 158 | 159 | case 32: 160 | await socket.waitBytes(4); 161 | dataSize += 4; 162 | color = socket.readRgba(redShift, greenShift, blueShift); 163 | break; 164 | } 165 | 166 | } else { 167 | color = foregroundColor; 168 | } 169 | 170 | await socket.waitBytes(2); 171 | const xy = socket.readUInt8(); 172 | const wh = socket.readUInt8(); 173 | dataSize += 2; 174 | 175 | const sx = (xy >> 4); 176 | const sy = (xy & 0x0f); 177 | const sw = (wh >> 4) + 1; 178 | const sh = (wh & 0x0f) + 1; 179 | 180 | this.applyColor(sw, sh, tx + sx, ty + sy, screenW, screenH, color, fb); 181 | 182 | } 183 | 184 | } else { 185 | this.applyColor(tw, th, tx, ty, screenW, screenH, backgroundColor, fb); 186 | } 187 | 188 | } else { 189 | this.applyColor(tw, th, tx, ty, screenW, screenH, backgroundColor, fb); 190 | } 191 | 192 | lastSubEncoding = subEncoding; 193 | 194 | } 195 | 196 | tiles--; 197 | 198 | } 199 | 200 | rect.data = socket.readNBytes(dataSize, initialOffset); 201 | resolve(); 202 | }); 203 | } 204 | 205 | // Apply color to a rect on buffer 206 | applyColor(tw, th, tx, ty, screenW, screenH, color, fb) { 207 | for (let h = 0; h < th; h++) { 208 | for (let w = 0; w < tw; w++) { 209 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 210 | fb.writeIntBE(color, fbBytePosOffset, 4); 211 | } 212 | } 213 | } 214 | 215 | } 216 | 217 | module.exports = Hextile; 218 | -------------------------------------------------------------------------------- /decoders/raw.js: -------------------------------------------------------------------------------- 1 | class Raw { 2 | 3 | constructor(debug = false, debugLevel = 1) { 4 | this.debug = debug; 5 | this.debugLevel = debugLevel; 6 | } 7 | 8 | getPixelBytePos(x, y, width, height) { 9 | return ((y * width) + x) * 4; 10 | } 11 | 12 | decode(rect, fb, bitsPerPixel, colorMap, screenW, screenH, socket, depth, red, green, blue) { 13 | return new Promise(async (resolve, reject) => { 14 | 15 | await socket.waitBytes(rect.width * rect.height * (bitsPerPixel / 8), 'Raw pixel data'); 16 | rect.data = socket.readNBytesOffset(rect.width * rect.height * (bitsPerPixel / 8)); 17 | 18 | for (let h = 0; h < rect.height; h++) { 19 | for (let w = 0; w < rect.width; w++) { 20 | const fbBytePosOffset = this.getPixelBytePos(rect.x + w, rect.y + h, screenW, screenH); 21 | if (bitsPerPixel === 8) { 22 | const bytePosOffset = (h * rect.width) + w; 23 | const index = rect.data.readUInt8(bytePosOffset); 24 | const color = colorMap[index]; 25 | fb.writeIntBE(color, fbBytePosOffset, 4); 26 | } else if (bitsPerPixel === 24) { 27 | const bytePosOffset = ((h * rect.width) + w) * 3; 28 | fb.writeUInt8(rect.data.readUInt8(bytePosOffset + red), fbBytePosOffset); 29 | fb.writeUInt8(rect.data.readUInt8(bytePosOffset + green), fbBytePosOffset + 1); 30 | fb.writeUInt8(rect.data.readUInt8(bytePosOffset + blue), fbBytePosOffset + 2); 31 | fb.writeUInt8(255, fbBytePosOffset + 3); 32 | } else if (bitsPerPixel === 32) { 33 | const bytePosOffset = ((h * rect.width) + w) * 4; 34 | fb.writeUInt8(rect.data.readUInt8(bytePosOffset + red), fbBytePosOffset + 2); 35 | fb.writeUInt8(rect.data.readUInt8(bytePosOffset + green), fbBytePosOffset + 1); 36 | fb.writeUInt8(rect.data.readUInt8(bytePosOffset + blue), fbBytePosOffset); 37 | fb.writeUInt8(255, fbBytePosOffset + 3); 38 | } 39 | } 40 | } 41 | resolve(); 42 | }); 43 | } 44 | 45 | } 46 | 47 | module.exports = Raw; 48 | -------------------------------------------------------------------------------- /decoders/tight.js: -------------------------------------------------------------------------------- 1 | class Tight { 2 | 3 | constructor() { 4 | 5 | } 6 | 7 | getPixelBytePos(x, y, width, height) { 8 | return ((y * width) + x) * 4; 9 | } 10 | 11 | getDataSize(rect, socket, bitsPerPixel) { 12 | 13 | } 14 | 15 | // TODO: Implement tight encoding 16 | decode(rect, fb, bitsPerPixel, colorMap, screenW, screenH, socket) { 17 | return new Promise((resolve, reject) => { 18 | resolve(); 19 | }); 20 | } 21 | 22 | } 23 | 24 | module.exports = Tight; 25 | -------------------------------------------------------------------------------- /decoders/zrle.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib'); 2 | const SocketBuffer = require('../socketbuffer'); 3 | 4 | class Zrle { 5 | 6 | constructor(debug = false, debugLevel = 1) { 7 | 8 | this.debug = debug; 9 | this.debugLevel = debugLevel; 10 | this.zlib = zlib.createInflate(); 11 | this.unBuffer = new SocketBuffer(); 12 | 13 | this.zlib.on('data', async (chunk) => { 14 | this.unBuffer.pushData(chunk); 15 | }); 16 | 17 | } 18 | 19 | getPixelBytePos(x, y, width, height) { 20 | return ((y * width) + x) * 4; 21 | } 22 | 23 | decode(rect, fb, bitsPerPixel, colorMap, screenW, screenH, socket, depth, red, green, blue) { 24 | 25 | return new Promise(async (resolve, reject) => { 26 | 27 | await socket.waitBytes(4, 'ZLIB Size'); 28 | 29 | const initialOffset = socket.offset; 30 | const dataSize = socket.readUInt32BE(); 31 | 32 | await socket.waitBytes(dataSize, 'ZLIB Data'); 33 | 34 | const compressedData = socket.readNBytesOffset(dataSize); 35 | 36 | rect.data = socket.readNBytes(dataSize + 4, initialOffset); 37 | 38 | this.unBuffer.flush(false); 39 | // this._log(`Cleaning buffer. Bytes on buffer: ${this.unBuffer.buffer.length} - Offset: ${this.unBuffer.offset}`); 40 | 41 | this.zlib.write(compressedData, async () => { 42 | // this.zlib.flush(); 43 | 44 | let tiles; 45 | let totalTiles; 46 | let tilesX; 47 | let tilesY; 48 | 49 | tilesX = Math.ceil(rect.width / 64); 50 | tilesY = Math.ceil(rect.height / 64); 51 | tiles = tilesX * tilesY; 52 | totalTiles = tiles; 53 | 54 | let firstRle = false; 55 | 56 | this._log(`Starting rect processing. ${rect.width}x${rect.height}. Compressed size: ${dataSize}. Decompressed size: ${this.unBuffer.bytesLeft()}`, true, 3); 57 | 58 | while (tiles) { 59 | 60 | let initialOffset = this.unBuffer.offset; 61 | await this.unBuffer.waitBytes(1, 'tile begin.'); 62 | const subEncoding = this.unBuffer.readUInt8(); 63 | const currTile = totalTiles - tiles; 64 | 65 | const tileX = currTile % tilesX; 66 | const tileY = Math.floor(currTile / tilesX); 67 | const tx = rect.x + (tileX * 64); 68 | const ty = rect.y + (tileY * 64); 69 | const tw = Math.min(64, (rect.x + rect.width) - tx); 70 | const th = Math.min(64, (rect.y + rect.height) - ty); 71 | 72 | let totalRun = 0; 73 | let runs = 0; 74 | 75 | let palette = []; 76 | 77 | if (subEncoding === 129) { 78 | console.log('Invalid subencoding. ' + subEncoding); 79 | } else if (subEncoding >= 17 && subEncoding <= 127) { 80 | console.log('Invalid subencoding. ' + subEncoding); 81 | } else if (subEncoding === 0) { 82 | // Raw 83 | for (let h = 0; h < th; h++) { 84 | for (let w = 0; w < tw; w++) { 85 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 86 | if (bitsPerPixel === 8) { 87 | await this.unBuffer.waitBytes(1, 'raw 8bits'); 88 | const index = this.unBuffer.readUInt8(); 89 | const color = colorMap[index]; 90 | fb.writeIntBE(color, fbBytePosOffset, 4); 91 | } else if (bitsPerPixel === 24 || (bitsPerPixel === 32 && depth === 24)) { 92 | await this.unBuffer.waitBytes(3, 'raw 24bits'); 93 | fb.writeIntBE(this.unBuffer.readRgbPlusAlpha(red, green, blue), fbBytePosOffset, 4); 94 | } else if (bitsPerPixel === 32) { 95 | await this.unBuffer.waitBytes(4, 'raw 32bits'); 96 | fb.writeIntBE(this.unBuffer.readRgba(red, green, blue), fbBytePosOffset, 4); 97 | } 98 | } 99 | } 100 | } else if (subEncoding === 1) { 101 | // Single Color 102 | let color = 0; 103 | if (bitsPerPixel === 8) { 104 | await this.unBuffer.waitBytes(1, 'single color 8bits'); 105 | const index = this.unBuffer.readUInt8(); 106 | color = colorMap[index]; 107 | } else if (bitsPerPixel === 24 || (bitsPerPixel === 32 && depth === 24)) { 108 | await this.unBuffer.waitBytes(3, 'single color 24bits'); 109 | color = this.unBuffer.readRgbPlusAlpha(red, green, blue); 110 | } else if (bitsPerPixel === 32) { 111 | await this.unBuffer.waitBytes(4, 'single color 32bits'); 112 | color = this.unBuffer.readRgba(red, green, blue); 113 | } 114 | this.applyColor(tw, th, tx, ty, screenW, screenH, color, fb); 115 | 116 | } else if (subEncoding >= 2 && subEncoding <= 16) { 117 | // Palette 118 | const palette = []; 119 | for (let x = 0; x < subEncoding; x++) { 120 | let color; 121 | if (bitsPerPixel === 24 || (bitsPerPixel === 32 && depth === 24)) { 122 | await this.unBuffer.waitBytes(3, 'palette 24 bits'); 123 | color = this.unBuffer.readRgbPlusAlpha(red, green, blue); 124 | } else if (bitsPerPixel === 32) { 125 | await this.unBuffer.waitBytes(3, 'palette 32 bits'); 126 | color = this.unBuffer.readRgba(red, green, blue); 127 | } 128 | palette.push(color); 129 | } 130 | 131 | const bitsPerIndex = subEncoding === 2 ? 1 : subEncoding < 5 ? 2 : 4; 132 | // const i = (tw * th) / (8 / bitsPerIndex); 133 | // const pixels = []; 134 | 135 | let byte; 136 | let bitPos = 0; 137 | 138 | for (let h = 0; h < th; h++) { 139 | for (let w = 0; w < tw; w++) { 140 | if (bitPos === 0 || w === 0) { 141 | await this.unBuffer.waitBytes(1, 'palette index data'); 142 | byte = this.unBuffer.readUInt8(); 143 | bitPos = 0; 144 | } 145 | let color; 146 | switch (bitsPerIndex) { 147 | case 1: 148 | if (bitPos === 0) { 149 | color = palette[(byte & 128) >> 7] || 0; 150 | } else if (bitPos === 1) { 151 | color = palette[(byte & 64) >> 6] || 0; 152 | } else if (bitPos === 2) { 153 | color = palette[(byte & 32) >> 5] || 0; 154 | } else if (bitPos === 3) { 155 | color = palette[(byte & 16) >> 4] || 0; 156 | } else if (bitPos === 4) { 157 | color = palette[(byte & 8) >> 3] || 0; 158 | } else if (bitPos === 5) { 159 | color = palette[(byte & 4) >> 2] || 0; 160 | } else if (bitPos === 6) { 161 | color = palette[(byte & 2) >> 1] || 0; 162 | } else if (bitPos === 7) { 163 | color = palette[(byte & 1)] || 0; 164 | } 165 | bitPos++; 166 | if (bitPos === 8) { 167 | bitPos = 0; 168 | } 169 | break; 170 | 171 | case 2: 172 | if (bitPos === 0) { 173 | color = palette[(byte & 196) >> 6] || 0; 174 | } else if (bitPos === 1) { 175 | color = palette[(byte & 48) >> 4] || 0; 176 | } else if (bitPos === 2) { 177 | color = palette[(byte & 12) >> 2] || 0; 178 | } else if (bitPos === 3) { 179 | color = palette[(byte & 3)] || 0; 180 | } 181 | bitPos++; 182 | if (bitPos === 4) { 183 | bitPos = 0; 184 | } 185 | break; 186 | 187 | case 4: 188 | if (bitPos === 0) { 189 | color = palette[(byte & 240) >> 4] || 0; 190 | } else if (bitPos === 1) { 191 | color = palette[(byte & 15)] || 0; 192 | } 193 | bitPos++; 194 | if (bitPos === 2) { 195 | bitPos = 0; 196 | } 197 | break; 198 | } 199 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 200 | fb.writeIntBE(color, fbBytePosOffset, 4); 201 | } 202 | } 203 | 204 | } else if (subEncoding === 128) { 205 | // Plain RLE 206 | let runLength = 0; 207 | let color = 0; 208 | 209 | for (let h = 0; h < th; h++) { 210 | for (let w = 0; w < tw; w++) { 211 | if (!runLength) { 212 | if (bitsPerPixel === 24 || (bitsPerPixel === 32 && depth === 24)) { 213 | await this.unBuffer.waitBytes(3, 'rle 24bits'); 214 | color = this.unBuffer.readRgbPlusAlpha(red, green, blue); 215 | } else if (bitsPerPixel === 32) { 216 | await this.unBuffer.waitBytes(4, 'rle 32bits'); 217 | color = this.unBuffer.readRgba(red, green, blue); 218 | } 219 | await this.unBuffer.waitBytes(1, 'rle runsize'); 220 | let runSize = this.unBuffer.readUInt8(); 221 | while (runSize === 255) { 222 | runLength += runSize; 223 | await this.unBuffer.waitBytes(1, 'rle runsize'); 224 | runSize = this.unBuffer.readUInt8(); 225 | } 226 | runLength += runSize + 1; 227 | totalRun += runLength; 228 | runs++; 229 | } 230 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 231 | fb.writeIntBE(color, fbBytePosOffset, 4); 232 | runLength--; 233 | } 234 | } 235 | 236 | } else if (subEncoding >= 130) { 237 | // Palette RLE 238 | const paletteSize = subEncoding - 128; 239 | // const palette = []; 240 | 241 | for (let x = 0; x < paletteSize; x++) { 242 | let color; 243 | if (bitsPerPixel === 24 || (bitsPerPixel === 32 && depth === 24)) { 244 | await this.unBuffer.waitBytes(3, 'paletterle 24bits'); 245 | color = this.unBuffer.readRgbPlusAlpha(red, green, blue); 246 | } else if (bitsPerPixel === 32) { 247 | await this.unBuffer.waitBytes(4, 'paletterle 32bits'); 248 | color = this.unBuffer.readRgba(red, green, blue); 249 | } 250 | 251 | if (firstRle) console.log('Cor da paleta: ' + JSON.stringify(color)); 252 | 253 | palette.push(color); 254 | } 255 | 256 | let runLength = 0; 257 | let color = 0; 258 | 259 | for (let h = 0; h < th; h++) { 260 | for (let w = 0; w < tw; w++) { 261 | if (!runLength) { 262 | await this.unBuffer.waitBytes(1, 'paletterle indexdata'); 263 | const colorIndex = this.unBuffer.readUInt8(); 264 | 265 | if (!(colorIndex & 128)) { 266 | // Run size of 1 267 | color = palette[colorIndex] ?? 0; 268 | runLength = 1; 269 | } else { 270 | color = palette[colorIndex - 128] ?? 0; 271 | await this.unBuffer.waitBytes(1, 'paletterle runlength'); 272 | let runSize = this.unBuffer.readUInt8(); 273 | while (runSize === 255) { 274 | runLength += runSize; 275 | await this.unBuffer.waitBytes(1, 'paletterle runlength'); 276 | runSize = this.unBuffer.readUInt8(); 277 | } 278 | runLength += runSize + 1; 279 | } 280 | totalRun += runLength; 281 | runs++; 282 | 283 | } 284 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 285 | fb.writeIntBE(color, fbBytePosOffset, 4); 286 | runLength--; 287 | } 288 | } 289 | 290 | firstRle = false; 291 | 292 | } 293 | // 127 and 129 are not valid 294 | // 17 to 126 are not used 295 | 296 | this._log(`Processing tile ${totalTiles - tiles}/${totalTiles} - SubEnc: ${subEncoding} - Size: ${tw}x${th} - BytesUsed: ${this.unBuffer.offset - initialOffset} - TotalRun: ${totalRun} - Runs: ${runs} - PaletteSize: ${palette.length}`, true, 3); 297 | 298 | tiles--; 299 | 300 | } 301 | 302 | this.unBuffer.flush(); 303 | resolve(); 304 | 305 | }); 306 | 307 | }); 308 | 309 | } 310 | 311 | // Apply color to a rect on buffer 312 | applyColor(tw, th, tx, ty, screenW, screenH, color, fb) { 313 | for (let h = 0; h < th; h++) { 314 | for (let w = 0; w < tw; w++) { 315 | const fbBytePosOffset = this.getPixelBytePos(tx + w, ty + h, screenW, screenH); 316 | fb.writeIntBE(color, fbBytePosOffset, 4); 317 | } 318 | } 319 | } 320 | 321 | /** 322 | * Print log info 323 | * @param text 324 | * @param debug 325 | * @param level 326 | * @private 327 | */ 328 | _log(text, debug = false, level = 1) { 329 | if (!debug || (debug && this.debug && level <= this.debugLevel)) { 330 | console.log(text); 331 | } 332 | } 333 | 334 | } 335 | 336 | module.exports = Zrle; 337 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vnc-rfb-client", 3 | "version": "0.2.0", 4 | "dependencies": {}, 5 | "main": "vncclient.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "vnc", 11 | "rfb", 12 | "zrle", 13 | "hextile", 14 | "copyrect", 15 | "rfc 6143" 16 | ], 17 | "author": "Filipe Calaça Barbosa", 18 | "license": "MIT", 19 | "devDependencies": {}, 20 | "description": "Pure nodejs VNC (RFC 6143 / RFB Protocol) client", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/filipecbmoc/vnc-rfb-client.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/filipecbmoc/vnc-rfb-client/issues" 27 | }, 28 | "homepage": "https://github.com/filipecbmoc/vnc-rfb-client#readme" 29 | } 30 | -------------------------------------------------------------------------------- /socketbuffer.js: -------------------------------------------------------------------------------- 1 | class SocketBuffer { 2 | 3 | constructor() { 4 | 5 | this.flush(); 6 | 7 | } 8 | 9 | flush(keep = true) { 10 | if (keep && this.buffer?.length) { 11 | this.buffer = this.buffer.slice(this.offset); 12 | this.offset = 0; 13 | } else { 14 | this.buffer = new Buffer.from([]); 15 | this.offset = 0; 16 | } 17 | } 18 | 19 | toString() { 20 | return this.buffer.toString(); 21 | } 22 | 23 | includes(check) { 24 | return this.buffer.includes(check); 25 | } 26 | 27 | pushData(data) { 28 | this.buffer = Buffer.concat([this.buffer, data]); 29 | } 30 | 31 | readInt32BE() { 32 | const data = this.buffer.readInt32BE(this.offset); 33 | this.offset += 4; 34 | return data; 35 | } 36 | 37 | readInt32LE() { 38 | const data = this.buffer.readInt32LE(this.offset); 39 | this.offset += 4; 40 | return data; 41 | } 42 | 43 | readUInt32BE() { 44 | const data = this.buffer.readUInt32BE(this.offset); 45 | this.offset += 4; 46 | return data; 47 | } 48 | 49 | readUInt32LE() { 50 | const data = this.buffer.readUInt32LE(this.offset); 51 | this.offset += 4; 52 | return data; 53 | } 54 | 55 | readUInt16BE() { 56 | const data = this.buffer.readUInt16BE(this.offset); 57 | this.offset += 2; 58 | return data; 59 | } 60 | 61 | readUInt16LE() { 62 | const data = this.buffer.readUInt16LE(this.offset); 63 | this.offset += 2; 64 | return data; 65 | } 66 | 67 | readUInt8() { 68 | const data = this.buffer.readUInt8(this.offset); 69 | this.offset += 1; 70 | return data; 71 | } 72 | 73 | readInt8() { 74 | const data = this.buffer.readInt8(this.offset); 75 | this.offset += 1; 76 | return data; 77 | } 78 | 79 | readNBytes(bytes, offset = this.offset) { 80 | return this.buffer.slice(offset, offset + bytes); 81 | } 82 | 83 | readRgbPlusAlpha(red, green, blue) { 84 | const colorBuf = this.buffer.slice(this.offset, this.offset + 3); 85 | this.offset += 3; 86 | return red === 0 && green === 1 && blue === 2 ? Buffer.concat([colorBuf, new Buffer.from([255])]).readIntBE(0, 4) : 87 | Buffer.concat([colorBuf.slice(red, red + 1), colorBuf.slice(green, green + 1), colorBuf.slice(blue, blue + 1), new Buffer.from([255])]).readIntBE(0, 4); 88 | } 89 | 90 | readRgbColorMap(red, green, blue, redMax, greenMax, blueMax) { 91 | const colorBuf = this.buffer.slice(this.offset, this.offset + 6); 92 | this.offset += 6; 93 | const redBytes = colorBuf.slice(red * 2, (red * 2) + 2); 94 | const greenBytes = colorBuf.slice(green * 2, (green * 2) + 2); 95 | const blueBytes = colorBuf.slice(blue * 2, (blue * 2) + 2); 96 | const redColor = Math.floor((redBytes.readUInt16BE() / redMax) * 255); 97 | const greenColor = Math.floor((greenBytes.readUInt16BE() / greenMax) * 255); 98 | const blueColor = Math.floor((blueBytes.readUInt16BE() / blueMax) * 255); 99 | return Buffer.concat([new Buffer.from(redColor), new Buffer.from(greenColor), new Buffer.from(blueColor), new Buffer.from([255])]).readIntBE(0, 4); 100 | } 101 | 102 | readRgba(red, green, blue) { 103 | if (red === 0 && green === 1 && blue === 2) { 104 | const data = this.buffer.readIntBE(this.offset, 4); 105 | this.offset += 4; 106 | return data; 107 | } else { 108 | const colorBuf = this.buffer.slice(this.offset, this.offset + 4); 109 | this.offset += 4; 110 | return Buffer.concat([colorBuf.slice(red, red + 1), colorBuf.slice(green, green + 1), colorBuf.slice(blue, blue + 1), colorBuf.slice(3, 4)]).readIntBE(0, 4); 111 | } 112 | } 113 | 114 | readNBytesOffset(bytes) { 115 | const data = this.buffer.slice(this.offset, this.offset + bytes); 116 | this.offset += bytes; 117 | return data; 118 | } 119 | 120 | setOffset(n) { 121 | this.offset = n; 122 | } 123 | 124 | bytesLeft() { 125 | return this.buffer.length - this.offset; 126 | } 127 | 128 | waitBytes(bytes, name) { 129 | if (this.bytesLeft() >= bytes) { 130 | return; 131 | } 132 | let counter = 0; 133 | return new Promise(async (resolve, reject) => { 134 | while (this.bytesLeft() < bytes) { 135 | counter++; 136 | // console.log('Esperando. BytesLeft: ' + this.bytesLeft() + ' Desejados: ' + bytes); 137 | await this.sleep(4); 138 | if (counter === 50) { 139 | console.log('Stucked on ' + name + ' - Buffer Size: ' + this.buffer.length + ' BytesLeft: ' + this.bytesLeft() + ' BytesNeeded: ' + bytes); 140 | } 141 | } 142 | resolve(); 143 | }); 144 | } 145 | 146 | fill(data) { 147 | this.buffer.fill(data, this.offset, this.offset + data.length); 148 | this.offset += data.length; 149 | } 150 | 151 | fillMultiple(data, repeats) { 152 | this.buffer.fill(data, this.offset, this.offset + (data.length * repeats)); 153 | this.offset += (data.length * repeats); 154 | } 155 | 156 | sleep(n) { 157 | return new Promise((resolve, reject) => { 158 | setTimeout(resolve, n); 159 | }) 160 | } 161 | 162 | } 163 | 164 | module.exports = SocketBuffer; 165 | -------------------------------------------------------------------------------- /vncclient.js: -------------------------------------------------------------------------------- 1 | const {versionString, encodings, serverMsgTypes, clientMsgTypes} = require('./constants'); 2 | const net = require('net'); 3 | const Events = require('events').EventEmitter; 4 | 5 | const HextileDecoder = require('./decoders/hextile'); 6 | const RawDecoder = require('./decoders/raw'); 7 | const ZrleDecoder = require('./decoders/zrle'); 8 | // const tightDecoder = require('./decoders/tight'); 9 | const CopyrectDecoder = require('./decoders/copyrect'); 10 | const SocketBuffer = require('./socketbuffer'); 11 | const crypto = require('crypto'); 12 | 13 | class VncClient extends Events { 14 | 15 | static get consts() { 16 | return {encodings}; 17 | } 18 | 19 | /** 20 | * Return if client is connected 21 | * @returns {boolean} 22 | */ 23 | get connected() { 24 | return this._connected; 25 | } 26 | 27 | /** 28 | * Return if client is authenticated 29 | * @returns {boolean} 30 | */ 31 | get authenticated() { 32 | return this._authenticated; 33 | } 34 | 35 | /** 36 | * Return negotiated protocol version 37 | * @returns {string} 38 | */ 39 | get protocolVersion() { 40 | return this._version; 41 | } 42 | 43 | /** 44 | * Return the local port used by the client 45 | * @returns {number} 46 | */ 47 | get localPort() { 48 | return this._connection ? this._connection.localPort : 0; 49 | } 50 | 51 | constructor(options = {debug: false, fps: 0, encodings: [], debugLevel: 1}) { 52 | super(); 53 | 54 | this._socketBuffer = new SocketBuffer(); 55 | 56 | this.resetState(); 57 | this.debug = options.debug || false; 58 | this.debugLevel = options.debugLevel || 1; 59 | this._fps = Number(options.fps) || 0; 60 | // Calculate interval to meet configured FPS 61 | this._timerInterval = this._fps > 0 ? 1000 / this._fps : 0; 62 | 63 | // Default encodings 64 | this.encodings = options.encodings && options.encodings.length ? options.encodings : [ 65 | encodings.copyRect, 66 | encodings.zrle, 67 | encodings.hextile, 68 | encodings.raw, 69 | encodings.pseudoDesktopSize 70 | ]; 71 | 72 | this._audioChannels = options.audioChannels || 2; 73 | this._audioFrequency = options.audioFrequency || 22050; 74 | 75 | this._rects = 0; 76 | this._decoders = {}; 77 | this._decoders[encodings.raw] = new RawDecoder(this.debug, this.debugLevel); 78 | // TODO: Implement tight encoding 79 | // this._decoders[encodings.tight] = new tightDecoder(); 80 | this._decoders[encodings.zrle] = new ZrleDecoder(this.debug, this.debugLevel); 81 | this._decoders[encodings.copyRect] = new CopyrectDecoder(this.debug, this.debugLevel); 82 | this._decoders[encodings.hextile] = new HextileDecoder(this.debug, this.debugLevel); 83 | 84 | if (this._timerInterval) { 85 | this._fbTimer(); 86 | } 87 | 88 | } 89 | 90 | /** 91 | * Timer used to limit the rate of frame update requests according to configured FPS 92 | * @private 93 | */ 94 | _fbTimer() { 95 | this._timerPointer = setTimeout(() => { 96 | this._fbTimer(); 97 | if (this._firstFrameReceived && !this._processingFrame && this._fps > 0) { 98 | this.requestFrameUpdate(); 99 | } 100 | }, this._timerInterval) 101 | } 102 | 103 | /** 104 | * Adjust the configured FPS 105 | * @param fps {number} - Number of update requests send by second 106 | */ 107 | changeFps(fps) { 108 | if (!Number.isNaN(fps)) { 109 | this._fps = Number(fps); 110 | this._timerInterval = this._fps > 0 ? 1000 / this._fps : 0; 111 | 112 | if (this._timerPointer && !this._fps) { 113 | // If FPS was zeroed stop the timer 114 | clearTimeout(this._timerPointer); 115 | this._timerPointer = null; 116 | } else if (this._fps && !this._timerPointer) { 117 | // If FPS was zero and is now set, start the timer 118 | this._fbTimer(); 119 | } 120 | 121 | } else { 122 | throw new Error('Invalid FPS. Must be a number.'); 123 | } 124 | } 125 | 126 | /** 127 | * Starts the connection with the VNC server 128 | * @param options 129 | */ 130 | connect(options = { 131 | host: '', 132 | password: '', 133 | set8BitColor: false, 134 | port: 5900 135 | }) { 136 | 137 | if (!options.host) { 138 | throw new Error('Host missing.'); 139 | } 140 | 141 | if (options.password) { 142 | this._password = options.password; 143 | } 144 | 145 | this._set8BitColor = options.set8BitColor || false; 146 | 147 | this._connection = net.connect(options.port || 5900, options.host); 148 | 149 | this._connection.on('connect', () => { 150 | this._connected = true; 151 | this.emit('connected'); 152 | }); 153 | 154 | this._connection.on('close', () => { 155 | this.resetState(); 156 | this.emit('closed'); 157 | }); 158 | 159 | this._connection.on('timeout', () => { 160 | this.emit('connectTimeout'); 161 | }); 162 | 163 | this._connection.on('error', (err) => { 164 | this.emit('connectError', err); 165 | }) 166 | 167 | this._connection.on('data', async (data) => { 168 | 169 | this._log(data.toString(), true, 5); 170 | this._socketBuffer.pushData(data); 171 | 172 | if (this._processingFrame) { 173 | return; 174 | } 175 | 176 | if (!this._handshaked) { 177 | this._handleHandshake(); 178 | } else if (this._expectingChallenge) { 179 | await this._handleAuthChallenge(); 180 | } else if (this._waitingServerInit) { 181 | await this._handleServerInit(); 182 | } else { 183 | await this._handleData(); 184 | } 185 | 186 | }); 187 | 188 | } 189 | 190 | /** 191 | * Disconnect the client 192 | */ 193 | disconnect() { 194 | if (this._connection) { 195 | this._connection.end(); 196 | this.resetState(); 197 | this.emit('disconnected'); 198 | } 199 | } 200 | 201 | /** 202 | * Request the server a frame update 203 | * @param full - If the server should send all the frame buffer or just the last changes 204 | * @param incremental - Incremental number for not full requests 205 | * @param x - X position of the update area desired, usually 0 206 | * @param y - Y position of the update area desired, usually 0 207 | * @param width - Width of the update area desired, usually client width 208 | * @param height - Height of the update area desired, usually client height 209 | */ 210 | requestFrameUpdate(full = false, incremental = 1, x = 0, y = 0, width = this.clientWidth, height = this.clientHeight) { 211 | if ((this._frameBufferReady || full) && this._connection && !this._rects && this._encodingsSent && !this._requestSent) { 212 | 213 | this._requestSent = true; 214 | this._log('Requesting frame update.', true, 3); 215 | 216 | // Request data 217 | const message = new Buffer(10); 218 | message.writeUInt8(3); // Message type 219 | message.writeUInt8(full ? 0 : incremental, 1); // Incremental 220 | message.writeUInt16BE(x, 2); // X-Position 221 | message.writeUInt16BE(y, 4); // Y-Position 222 | message.writeUInt16BE(width, 6); // Width 223 | message.writeUInt16BE(height, 8); // Height 224 | 225 | this.sendData(message); 226 | 227 | this._frameBufferReady = true; 228 | 229 | } 230 | } 231 | 232 | /** 233 | * Handle handshake msg 234 | * @private 235 | */ 236 | _handleHandshake() { 237 | // Handshake, negotiating protocol version 238 | this._log('Received: ' + this._socketBuffer.toString(), true, 2); 239 | this._log(this._socketBuffer.buffer, true, 3); 240 | if (this._socketBuffer.toString() === versionString.V3_003) { 241 | this._log('Sending 3.3', true); 242 | this.sendData(versionString.V3_003); 243 | this._version = '3.3'; 244 | } else if (this._socketBuffer.toString() === versionString.V3_007) { 245 | this._log('Sending 3.7', true); 246 | this.sendData(versionString.V3_007); 247 | this._version = '3.7'; 248 | } else if (this._socketBuffer.toString() === versionString.V3_008) { 249 | this._log('Sending 3.8', true); 250 | this.sendData(versionString.V3_008); 251 | this._version = '3.8'; 252 | } else { 253 | // Negotiating auth mechanism 254 | this._handshaked = true; 255 | if (this._socketBuffer.includes(0x02) && this._password) { 256 | this._log('Password provided and server support VNC auth. Choosing VNC auth.', true); 257 | this._expectingChallenge = true; 258 | this.sendData(new Buffer.from([0x02])); 259 | } else if (this._socketBuffer.includes(1)) { 260 | this._log('Password not provided or server does not support VNC auth. Trying none.', true); 261 | this.sendData(new Buffer.from([0x01])); 262 | if (this._version === '3.7') { 263 | this._sendClientInit(); 264 | } else { 265 | this._expectingChallenge = true; 266 | this._challengeResponseSent = true; 267 | } 268 | } else { 269 | this._log('Connection error. Msg: ' + this._socketBuffer.toString()); 270 | this.disconnect(); 271 | } 272 | } 273 | 274 | } 275 | 276 | /** 277 | * Send data to the server 278 | * @param data 279 | * @param flush - If true, flush the local buffer before sending 280 | */ 281 | sendData(data, flush = true) { 282 | if (flush) { 283 | this._socketBuffer.flush(false); 284 | } 285 | this._connection.write(data); 286 | } 287 | 288 | /** 289 | * Handle VNC auth challenge 290 | * @private 291 | */ 292 | async _handleAuthChallenge() { 293 | 294 | if (this._challengeResponseSent) { 295 | // Challenge response already sent. Checking result. 296 | 297 | if (this._socketBuffer.readUInt32BE() === 0) { 298 | // Auth success 299 | this._log('Authenticated successfully', true); 300 | this._authenticated = true; 301 | this.emit('authenticated'); 302 | this._expectingChallenge = false; 303 | this._sendClientInit(); 304 | } else { 305 | // Auth fail 306 | this._log('Authentication failed', true); 307 | this.emit('authError'); 308 | this.resetState(); 309 | } 310 | 311 | } else { 312 | 313 | this._log('Challenge received.', true); 314 | await this._socketBuffer.waitBytes(16, 'Auth challenge'); 315 | 316 | const key = new Buffer(8); 317 | key.fill(0); 318 | key.write(this._password.slice(0, 8)); 319 | 320 | this.reverseBits(key); 321 | 322 | const des1 = crypto.createCipheriv('des', key, new Buffer(8)); 323 | const des2 = crypto.createCipheriv('des', key, new Buffer(8)); 324 | 325 | const response = new Buffer(16); 326 | 327 | response.fill(des1.update(this._socketBuffer.buffer.slice(0, 8)), 0, 8); 328 | response.fill(des2.update(this._socketBuffer.buffer.slice(8, 16)), 8, 16); 329 | 330 | this._log('Sending response: ' + response.toString(), true, 2); 331 | 332 | this.sendData(response); 333 | this._challengeResponseSent = true; 334 | 335 | } 336 | 337 | } 338 | 339 | /** 340 | * Reverse bits order of a byte 341 | * @param buf - Buffer to be flipped 342 | */ 343 | reverseBits(buf) { 344 | for (let x = 0; x < buf.length; x++) { 345 | let newByte = 0; 346 | newByte += buf[x] & 128 ? 1 : 0; 347 | newByte += buf[x] & 64 ? 2 : 0; 348 | newByte += buf[x] & 32 ? 4 : 0; 349 | newByte += buf[x] & 16 ? 8 : 0; 350 | newByte += buf[x] & 8 ? 16 : 0; 351 | newByte += buf[x] & 4 ? 32 : 0; 352 | newByte += buf[x] & 2 ? 64 : 0; 353 | newByte += buf[x] & 1 ? 128 : 0; 354 | buf[x] = newByte; 355 | } 356 | } 357 | 358 | /** 359 | * Handle server init msg 360 | * @returns {Promise} 361 | * @private 362 | */ 363 | async _handleServerInit() { 364 | 365 | this._waitingServerInit = false; 366 | 367 | await this._socketBuffer.waitBytes(24, 'Server init'); 368 | 369 | this.clientWidth = this._socketBuffer.readUInt16BE(); 370 | this.clientHeight = this._socketBuffer.readUInt16BE(); 371 | this.pixelFormat.bitsPerPixel = this._socketBuffer.readUInt8(); 372 | this.pixelFormat.depth = this._socketBuffer.readUInt8(); 373 | this.pixelFormat.bigEndianFlag = this._socketBuffer.readUInt8(); 374 | this.pixelFormat.trueColorFlag = this._socketBuffer.readUInt8(); 375 | this.pixelFormat.redMax = this.bigEndianFlag ? this._socketBuffer.readUInt16BE() : this._socketBuffer.readUInt16LE(); 376 | this.pixelFormat.greenMax = this.bigEndianFlag ? this._socketBuffer.readUInt16BE() : this._socketBuffer.readUInt16LE(); 377 | this.pixelFormat.blueMax = this.bigEndianFlag ? this._socketBuffer.readUInt16BE() : this._socketBuffer.readUInt16LE(); 378 | this.pixelFormat.redShift = this._socketBuffer.readInt8() / 8; 379 | this.pixelFormat.greenShift = this._socketBuffer.readInt8() / 8; 380 | this.pixelFormat.blueShift = this._socketBuffer.readInt8() / 8; 381 | // Padding 382 | this._socketBuffer.offset += 3; 383 | this.updateFbSize(); 384 | const nameSize = this._socketBuffer.readUInt32BE(); 385 | await this._socketBuffer.waitBytes(nameSize, 'Name Size'); 386 | this.clientName = this._socketBuffer.readNBytesOffset(nameSize).toString(); 387 | 388 | this._log(`Screen size: ${this.clientWidth}x${this.clientHeight}`); 389 | this._log(`Client name: ${this.clientName}`); 390 | this._log(`pixelFormat: ${JSON.stringify(this.pixelFormat)}`); 391 | 392 | if (this._set8BitColor) { 393 | this._log(`8 bit color format requested, only raw encoding is supported.`); 394 | this._setPixelFormatToColorMap(); 395 | } 396 | 397 | this._sendEncodings(); 398 | 399 | setTimeout(() => { 400 | this.requestFrameUpdate(true); 401 | }, 1000); 402 | 403 | } 404 | 405 | /** 406 | * Update the frame buffer size according to client width and height (RGBA) 407 | */ 408 | updateFbSize() { 409 | this._log(`Updating the frame buffer size.`, true); 410 | this.fb = new Buffer(this.clientWidth * this.clientHeight * 4); 411 | } 412 | 413 | /** 414 | * Request the server to change to 8bit color format (Color palette). Only works with Raw encoding. 415 | * @private 416 | */ 417 | _setPixelFormatToColorMap() { 418 | 419 | this._log(`Requesting PixelFormat change to ColorMap (8 bits).`); 420 | 421 | const message = new Buffer(20); 422 | message.writeUInt8(0); // Tipo da mensagem 423 | message.writeUInt8(0, 1); // Padding 424 | message.writeUInt8(0, 2); // Padding 425 | message.writeUInt8(0, 3); // Padding 426 | 427 | message.writeUInt8(8, 4); // PixelFormat - BitsPerPixel 428 | message.writeUInt8(8, 5); // PixelFormat - Depth 429 | message.writeUInt8(0, 6); // PixelFormat - BigEndianFlag 430 | message.writeUInt8(0, 7); // PixelFormat - TrueColorFlag 431 | message.writeUInt16BE(255, 8); // PixelFormat - RedMax 432 | message.writeUInt16BE(255, 10); // PixelFormat - GreenMax 433 | message.writeUInt16BE(255, 12); // PixelFormat - BlueMax 434 | message.writeUInt8(0, 14); // PixelFormat - RedShift 435 | message.writeUInt8(8, 15); // PixelFormat - GreenShift 436 | message.writeUInt8(16, 16); // PixelFormat - BlueShift 437 | message.writeUInt8(0, 17); // PixelFormat - Padding 438 | message.writeUInt8(0, 18); // PixelFormat - Padding 439 | message.writeUInt8(0, 19); // PixelFormat - Padding 440 | 441 | // Send a request to change pixelFormat to colorMap 442 | this.sendData(message); 443 | 444 | this.pixelFormat.bitsPerPixel = 8; 445 | this.pixelFormat.depth = 8; 446 | 447 | } 448 | 449 | /** 450 | * Send supported encodings 451 | * @private 452 | */ 453 | _sendEncodings() { 454 | 455 | this._log('Sending encodings.'); 456 | this._encodingsSent = true; 457 | // If this._set8BitColor is set, only copyrect and raw encodings are supported 458 | const message = new Buffer(4 + ((!this._set8BitColor ? this.encodings.length : 2) * 4)); 459 | message.writeUInt8(2); // Message type 460 | message.writeUInt8(0, 1); // Padding 461 | message.writeUInt16BE(!this._set8BitColor ? this.encodings.length : 2, 2); // Padding 462 | 463 | let offset = 4; 464 | // If 8bits is not set, send all encodings configured 465 | if (!this._set8BitColor) { 466 | for (const e of this.encodings) { 467 | message.writeInt32BE(e, offset); 468 | offset += 4; 469 | } 470 | } else { 471 | message.writeInt32BE(encodings.copyRect, offset); 472 | message.writeInt32BE(encodings.raw, offset + 4); 473 | } 474 | 475 | this.sendData(message); 476 | 477 | } 478 | 479 | /** 480 | * Send client init msg 481 | * @private 482 | */ 483 | _sendClientInit() { 484 | this._log(`Sending clientInit`); 485 | this._waitingServerInit = true; 486 | // Shared bit set 487 | this.sendData(new Buffer.from([1])); 488 | } 489 | 490 | /** 491 | * Handle data msg 492 | * @returns {Promise} 493 | * @private 494 | */ 495 | async _handleData() { 496 | 497 | if (!this._rects) { 498 | switch (this._socketBuffer.buffer[0]) { 499 | case serverMsgTypes.fbUpdate: 500 | await this._handleFbUpdate(); 501 | break; 502 | 503 | case serverMsgTypes.setColorMap: 504 | await this._handleSetColorMap(); 505 | break; 506 | 507 | case serverMsgTypes.bell: 508 | this.emit('bell'); 509 | this._socketBuffer.flush(); 510 | break; 511 | 512 | case serverMsgTypes.cutText: 513 | await this._handleCutText(); 514 | break; 515 | 516 | case serverMsgTypes.qemuAudio: 517 | await this._handleQemuAudio(); 518 | break; 519 | } 520 | } 521 | 522 | } 523 | 524 | /** 525 | * Cut message (text was copied to clipboard on server) 526 | * @returns {Promise} 527 | * @private 528 | */ 529 | async _handleCutText() { 530 | this._socketBuffer.setOffset(4); 531 | await this._socketBuffer.waitBytes(1, 'Cut text size'); 532 | const length = this._socketBuffer.readUInt32BE(); 533 | await this._socketBuffer.waitBytes(length, 'Cut text data'); 534 | this.emit('cutText', this._socketBuffer.readNBytesOffset(length).toString()); 535 | this._socketBuffer.flush(); 536 | } 537 | 538 | /** 539 | * Handle a rects of update message 540 | */ 541 | async _handleRect() { 542 | 543 | const sendFbUpdate = this._rects; 544 | this._processingFrame = true; 545 | 546 | while (this._rects) { 547 | 548 | await this._socketBuffer.waitBytes(12, 'Rect begin'); 549 | const rect = {}; 550 | rect.x = this._socketBuffer.readUInt16BE(); 551 | rect.y = this._socketBuffer.readUInt16BE(); 552 | rect.width = this._socketBuffer.readUInt16BE(); 553 | rect.height = this._socketBuffer.readUInt16BE(); 554 | rect.encoding = this._socketBuffer.readInt32BE(); 555 | 556 | if (rect.encoding === encodings.pseudoQemuAudio) { 557 | this.sendAudio(true); 558 | this.sendAudioConfig(this._audioChannels, this._audioFrequency);//todo: future: setFrequency(...) to update mid thing 559 | } else if (rect.encoding === encodings.pseudoQemuPointerMotionChange) { 560 | this._relativePointer = rect.x == 0; 561 | } else if (rect.encoding === encodings.pseudoCursor) { 562 | const dataSize = rect.width * rect.height * (this.pixelFormat.bitsPerPixel / 8); 563 | const bitmaskSize = Math.floor((rect.width + 7) / 8) * rect.height; 564 | this._cursor.width = rect.width; 565 | this._cursor.height = rect.height; 566 | this._cursor.x = rect.x; 567 | this._cursor.y = rect.y; 568 | this._cursor.cursorPixels = this._socketBuffer.readNBytesOffset(dataSize); 569 | this._cursor.bitmask = this._socketBuffer.readNBytesOffset(bitmaskSize); 570 | rect.data = Buffer.concat([this._cursor.cursorPixels, this._cursor.bitmask]); 571 | } else if (rect.encoding === encodings.pseudoDesktopSize) { 572 | this._log('Frame Buffer size change requested by the server', true); 573 | this.clientHeight = rect.height; 574 | this.clientWidth = rect.width; 575 | this.updateFbSize(); 576 | this.emit('desktopSizeChanged', {width: this.clientWidth, height: this.clientHeight}); 577 | } else if (this._decoders[rect.encoding]) { 578 | await this._decoders[rect.encoding].decode(rect, this.fb, this.pixelFormat.bitsPerPixel, this._colorMap, this.clientWidth, this.clientHeight, this._socketBuffer, this.pixelFormat.depth, this.pixelFormat.redShift, this.pixelFormat.greenShift, this.pixelFormat.blueShift); 579 | } else { 580 | this._log('Non supported update received. Encoding: ' + rect.encoding); 581 | } 582 | this._rects--; 583 | this.emit('rectProcessed', rect); 584 | // 585 | // if (!this._rects) { 586 | // this._socketBuffer.flush(true); 587 | // } 588 | 589 | } 590 | 591 | if (sendFbUpdate) { 592 | if (!this._firstFrameReceived) { 593 | this._firstFrameReceived = true; 594 | this.emit('firstFrameUpdate', this.fb); 595 | } 596 | this._log('Frame buffer updated.', true, 2); 597 | this.emit('frameUpdated', this.fb); 598 | } 599 | 600 | this._processingFrame = false; 601 | 602 | if (this._fps === 0) { 603 | // If FPS is not set, request a new update as soon as the last received has been processed 604 | this.requestFrameUpdate(); 605 | } 606 | 607 | } 608 | 609 | async _handleFbUpdate() { 610 | 611 | this._socketBuffer.setOffset(2); 612 | this._rects = this._socketBuffer.readUInt16BE(); 613 | this._log('Frame update received. Rects: ' + this._rects, true); 614 | await this._handleRect(); 615 | this._requestSent = false; 616 | 617 | } 618 | 619 | /** 620 | * Handle setColorMap msg 621 | * @returns {Promise} 622 | * @private 623 | */ 624 | async _handleSetColorMap() { 625 | 626 | this._socketBuffer.setOffset(2); 627 | let firstColor = this._socketBuffer.readUInt16BE(); 628 | const numColors = this._socketBuffer.readUInt16BE(); 629 | 630 | this._log(`ColorMap received. Colors: ${numColors}.`); 631 | 632 | await this._socketBuffer.waitBytes(numColors * 6, 'Colormap data'); 633 | 634 | for (let x = 0; x < numColors; x++) { 635 | this._colorMap[firstColor] = this._socketBuffer.readRgbColorMap(this.pixelFormat.redShift, this.pixelFormat.greenShift, this.pixelFormat.blueShift, this.pixelFormat.redMax, this.pixelFormat.greenMax, this.pixelFormat.blueMax); 636 | firstColor++; 637 | } 638 | 639 | this.emit('colorMapUpdated', this._colorMap); 640 | this._socketBuffer.flush(); 641 | 642 | } 643 | 644 | async _handleQemuAudio() { 645 | this._socketBuffer.setOffset(2); 646 | let operation = this._socketBuffer.readUInt16BE(); 647 | if (operation == 2) { 648 | const length = this._socketBuffer.readUInt32BE(); 649 | 650 | //this._log(`Audio received. Length: ${length}.`); 651 | 652 | await this._socketBuffer.waitBytes(length); 653 | 654 | let audioBuffer = []; 655 | for (let i = 0; i < length / 2; i++) audioBuffer.push(this._socketBuffer.readUInt16BE()); 656 | 657 | this._audioData = audioBuffer; 658 | } 659 | 660 | this.emit('audioStream', this._audioData); 661 | this._socketBuffer.flush(); 662 | } 663 | 664 | /** 665 | * Reset the class state 666 | */ 667 | resetState() { 668 | 669 | if (this._connection) { 670 | this._connection.end(); 671 | } 672 | 673 | if (this._timerPointer) { 674 | clearInterval(this._timerPointer); 675 | } 676 | 677 | this._timerPointer = null; 678 | 679 | this._connection = null; 680 | 681 | this._connected = false; 682 | this._authenticated = false; 683 | this._version = ''; 684 | 685 | this._password = ''; 686 | 687 | this._audioChannels = 2; 688 | this._audioFrequency = 22050; 689 | 690 | this._handshaked = false; 691 | 692 | this._expectingChallenge = false; 693 | this._challengeResponseSent = false; 694 | 695 | this._frameBufferReady = false; 696 | this._firstFrameReceived = false; 697 | this._processingFrame = false; 698 | 699 | this._requestSent = false; 700 | 701 | this._encodingsSent = false; 702 | 703 | this.clientWidth = 0; 704 | this.clientHeight = 0; 705 | this.clientName = ''; 706 | 707 | this.pixelFormat = { 708 | bitsPerPixel: 0, 709 | depth: 0, 710 | bigEndianFlag: 0, 711 | trueColorFlag: 0, 712 | redMax: 0, 713 | greenMax: 0, 714 | blueMax: 0, 715 | redShift: 0, 716 | blueShift: 0, 717 | greenShift: 0 718 | } 719 | 720 | this._rects = 0 721 | 722 | this._colorMap = []; 723 | this._audioData = []; 724 | this.fb = null; 725 | 726 | this._socketBuffer?.flush(false); 727 | 728 | this._cursor = { 729 | width: 0, 730 | height: 0, 731 | x: 0, 732 | y: 0, 733 | cursorPixels: null, 734 | bitmask: null, 735 | posX: 0, 736 | posY: 0 737 | } 738 | 739 | } 740 | 741 | /** 742 | * Get the frame buffer with de cursor printed to it if using Cursor Pseudo Encoding 743 | * @returns {null|Buffer|*} 744 | */ 745 | getFb() { 746 | if (!this._cursor.width) { 747 | // If there is no cursor, just return de framebuffer 748 | return this.fb; 749 | } else { 750 | // If there is a cursor, draw a cursor on the framebuffer and return the final result 751 | const tempFb = new Buffer.from(this.fb); 752 | for (let h = 0; h < this._cursor.height; h++) { 753 | for (let w = 0; w < this._cursor.width; w++) { 754 | const fbBytePosOffset = this._getPixelBytePos(this._cursor.posX + w, this._cursor.posY + h, this.clientWidth, this.clientHeight); 755 | const cursorBytePosOffset = this._getPixelBytePos(w, h, this._cursor.width, this._cursor.height); 756 | const bitmapByte = this._cursor.bitmask.slice(Math.floor(cursorBytePosOffset / 4 / 8), Math.floor(cursorBytePosOffset / 4 / 8) + 1); 757 | const bitmapBit = (cursorBytePosOffset / 4) % 8; 758 | const activePixel = bitmapBit === 0 ? bitmapByte[0] & 128 : bitmapBit === 1 ? bitmapByte[0] & 64 : 759 | bitmapBit === 2 ? bitmapByte[0] & 32 : bitmapBit === 3 ? bitmapByte[0] & 16 : 760 | bitmapBit === 4 ? bitmapByte[0] & 8 : bitmapBit === 5 ? bitmapByte[0] & 4 : 761 | bitmapBit === 6 ? bitmapByte[0] & 2 : bitmapByte[0] & 1; 762 | 763 | if (activePixel) { 764 | if (this.pixelFormat.bitsPerPixel === 8) { 765 | const index = this._cursor.cursorPixels.readUInt8(cursorBytePosOffset); 766 | const color = this._colorMap[index]; 767 | tempFb.writeIntBE(color, fbBytePosOffset, 4); 768 | } else { 769 | const bytesLength = this.pixelFormat.bitsPerPixel / 8; 770 | const color = this._cursor.cursorPixels.readIntBE(cursorBytePosOffset, bytesLength); 771 | tempFb.writeIntBE(color, fbBytePosOffset, bytesLength); 772 | } 773 | } 774 | } 775 | } 776 | return tempFb; 777 | } 778 | } 779 | 780 | _getPixelBytePos(x, y, width, height) { 781 | return ((y * width) + x) * 4; 782 | } 783 | 784 | /** 785 | * Send a key event 786 | * @param key - Key code (keysym) defined by X Window System, check https://wiki.linuxquestions.org/wiki/List_of_keysyms 787 | * @param down - True if the key is pressed, false if it is not 788 | */ 789 | sendKeyEvent(key, down = false) { 790 | 791 | const message = new Buffer(8); 792 | message.writeUInt8(clientMsgTypes.keyEvent); // Message type 793 | message.writeUInt8(down ? 1 : 0, 1); // Down flag 794 | message.writeUInt8(0, 2); // Padding 795 | message.writeUInt8(0, 3); // Padding 796 | 797 | message.writeUInt32BE(key, 4); // Key code 798 | 799 | this.sendData(message, false); 800 | 801 | } 802 | 803 | 804 | /** 805 | * Send pointer event (mouse or touch) 806 | * @param xPosition - X Position 807 | * @param yPosition - Y Position 808 | * @param button1 - True for pressed, false for unpressed - Usually left mouse button 809 | * @param button2 - True for pressed, false for unpressed - Usually middle mouse button 810 | * @param button3 - True for pressed, false for unpressed - Usually right mouse button 811 | * @param button4 - True for pressed, false for unpressed - Usually scroll up mouse event 812 | * @param button5 - True for pressed, false for unpressed - Usually scroll down mouse event 813 | * @param button6 - True for pressed, false for unpressed 814 | * @param button7 - True for pressed, false for unpressed 815 | * @param button8 - True for pressed, false for unpressed 816 | */ 817 | sendPointerEvent(xPosition, yPosition, button1 = false, button2 = false, button3 = false, button4 = false, button5 = false, 818 | button6 = false, button7 = false, button8 = false) { 819 | 820 | let buttonMask = 0; 821 | 822 | buttonMask += button8 ? 128 : 0; 823 | buttonMask += button7 ? 64 : 0; 824 | buttonMask += button6 ? 32 : 0; 825 | buttonMask += button5 ? 16 : 0; 826 | buttonMask += button4 ? 8 : 0; 827 | buttonMask += button3 ? 4 : 0; 828 | buttonMask += button2 ? 2 : 0; 829 | buttonMask += button1 ? 1 : 0; 830 | 831 | const message = new Buffer(6); 832 | message.writeUInt8(clientMsgTypes.pointerEvent); // Message type 833 | message.writeUInt8(buttonMask, 1); // Button Mask 834 | const reladd = this._relativePointer ? 0x7FFF : 0; 835 | message.writeUInt16BE(xPosition + reladd, 2); // X Position 836 | message.writeUInt16BE(yPosition + reladd, 4); // Y Position 837 | 838 | this._cursor.posX = xPosition; 839 | this._cursor.posY = yPosition; 840 | 841 | this.sendData(message, false); 842 | 843 | } 844 | 845 | /** 846 | * Send client cut message to server 847 | * @param text - latin1 encoded 848 | */ 849 | clientCutText(text) { 850 | 851 | const textBuffer = new Buffer.from(text, 'latin1'); 852 | const message = new Buffer(8 + textBuffer.length); 853 | message.writeUInt8(clientMsgTypes.cutText); // Message type 854 | message.writeUInt8(0, 1); // Padding 855 | message.writeUInt8(0, 2); // Padding 856 | message.writeUInt8(0, 3); // Padding 857 | message.writeUInt32BE(textBuffer.length, 4); // Padding 858 | textBuffer.copy(message, 8); 859 | 860 | this.sendData(message, false); 861 | 862 | } 863 | 864 | sendAudio(enable) { 865 | const message = new Buffer(4); 866 | message.writeUInt8(clientMsgTypes.qemuAudio); // Message type 867 | message.writeUInt8(1, 1); // Submessage Type 868 | message.writeUInt16BE(enable ? 0 : 1, 2); // Operation 869 | this.sendData(message); 870 | } 871 | 872 | sendAudioConfig(channels, frequency) { 873 | const message = new Buffer(10); 874 | message.writeUInt8(clientMsgTypes.qemuAudio); // Message type 875 | message.writeUInt8(1, 1); // Submessage Type 876 | message.writeUInt16BE(2, 2); // Operation 877 | message.writeUInt8(0/*U8*/, 4); // Sample Format 878 | message.writeUInt8(channels, 5); // Number of Channels 879 | message.writeUInt32BE(frequency, 6); // Frequency 880 | this.sendData(message); 881 | } 882 | 883 | /** 884 | * Print log info 885 | * @param text 886 | * @param debug 887 | * @param level 888 | * @private 889 | */ 890 | _log(text, debug = false, level = 1) { 891 | if (!debug || (debug && this.debug && level <= this.debugLevel)) { 892 | console.log(text); 893 | } 894 | } 895 | 896 | } 897 | 898 | exports = module.exports = VncClient; 899 | --------------------------------------------------------------------------------