├── .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 |
--------------------------------------------------------------------------------