Mon 21 Jul 22:43:21 CEST 2025

This commit is contained in:
sbosse 2025-07-21 23:06:30 +02:00
parent f9e46c2706
commit c0593d1b21

View File

@ -0,0 +1,688 @@
"use strict";
//var clog = clog;
//var clog = function() {};
var clog = console.log;
var util = require('util'); // util.inherits
var net = require('net');
var EventEmitter = require('events').EventEmitter;
var PackStream = require('./unpackstream');
var hexy = require('./hexy').hexy;
// constants
var rfb = require('./constants');
for (var key in rfb)
{
module.exports[key] = rfb[key];
}
function RfbClient(stream, params)
{
EventEmitter.call(this);
this.params = params;
var cli = this;
cli.stream = stream;
cli.pack_stream = new PackStream();
cli.pack_stream.on('data', function( data ) {
//clog(hexy(data, {prefix: 'from client '}));
cli.stream.write(data);
});
stream.on('data', function( data ) {
//var dump = data.length > 20 ? data.slice(0,20) : data;
//clog(hexy(dump, {prefix: 'from server '}));
cli.pack_stream.write(data);
});
// TODO: check if I need that at all
cli.pack_stream.serverBigEndian = !true;
cli.pack_stream.clientBigEndian = !true;
cli.readServerVersion();
}
util.inherits(RfbClient, EventEmitter);
PackStream.prototype.readString = function(strcb)
{
var stream = this;
stream.unpack('L', function(res) {
//clog(res[0]);
stream.get(res[0], function(strBuff) {
strcb(strBuff.toString());
});
});
}
RfbClient.prototype.terminate = function()
{
debugger;
this.stream.end();
}
RfbClient.prototype.readError = function()
{
var cli = this;
this.pack_stream.readString(function(str) {
console.error(str);
cli.emit('error', str);
});
}
RfbClient.prototype.readServerVersion = function()
{
var stream = this.pack_stream;
var cli = this;
stream.get(12, function(rfbver) {
cli.serverVersion = rfbver.toString('ascii');
console.log(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', rfbver]);
stream.pack('a', [ rfb.versionstring.V3_008 ]).flush();
if (cli.serverVersion == rfb.versionstring.V3_003)
{
stream.unpack('L', function(secType) {
var type = secType[0];
console.error('3.003 security type: ' + type);
if (type == 0)
{
cli.readError();
} else {
cli.securityType = type;
// 3.003 version does not send result for None security
if (type == rfb.security.None)
cli.clientInit();
else
cli.processSecurity();
}
});
return;
}
// read security types
stream.unpack('C', function(res) {
var numSecTypes = res[0];
if (numSecTypes == 0) {
console.error(['zero num sec types', res]);
cli.readError();
} else {
stream.get(numSecTypes, function(secTypes) {
// TODO: check what is in options
//
// send sec type we are going to use
//cli.securityType = rfb.security.None;
cli.securityType = rfb.security.VNC;
stream.pack('C', [cli.securityType]).flush();
cli.processSecurity();
});
}
});
});
}
RfbClient.prototype.readSecurityResult = function()
{
var stream = this.pack_stream;
var cli = this;
stream.unpack('L', function(securityResult) {
if (securityResult[0] == 0)
{
cli.clientInit();
} else {
stream.readString(function(message) {
console.error(message);
process.exit(0);
});
}
});
}
RfbClient.prototype.processSecurity = function()
{
var stream = this.pack_stream;
var cli = this;
switch(cli.securityType) {
case rfb.security.None:
// do nothing
cli.readSecurityResult();
break;
case rfb.security.VNC:
stream.get(16, function(challenge) {
var response = require('./d3des').response(challenge, cli.params.password);
stream.pack('a', [response]).flush();
cli.readSecurityResult();
});
break;
default:
console.error('unknown security type: ' + cli.securityType);
process.exit(1);
}
}
RfbClient.prototype.clientInit = function()
{
var stream = this.pack_stream;
var cli = this;
var initMessage = cli.disconnectOthers ? rfb.connectionFlag.Exclusive : rfb.connectionFlag.Shared;
stream.pack('C', [ initMessage ]).flush();
stream.unpackTo(
cli,
[
"S width",
"S height",
"C bpp", // 16-bytes pixel format
"C depth",
"C isBigEndian",
"C isTrueColor",
"S redMax",
"S greenMax",
"S blueMax",
"C redShift",
"C greenShift",
"C blueShift",
"xxx",
"L titleLength"
],
function() {
// TODO: remove next 3 lines
stream.serverBigEndian = false; //cli.isBigEndian;
stream.clientBigEndian = false; //cli.isBigEndian;
//stream.bigEndian = false; //cli.isBigEndian;
stream.get(cli.titleLength, function(titleBuf) {
cli.title = titleBuf.toString();
delete cli.titleLength;
cli.setPixelFormat();
});
}
);
}
RfbClient.prototype.setPixelFormat = function()
{
var stream = this.pack_stream;
var cli = this;
stream.pack('CxxxCCCCSSSCCCxxx',
[0, cli.bpp, cli.depth, cli.isBigEndian, cli.isTrueColor, cli.redMax, cli.greenMax, cli.blueMax,
cli.redShift, cli.greenShift, cli.blueShift]
);
stream.flush();
cli.setEncodings();
}
function repeat(str, num)
{
var res = '';
for (var i=0; i < num; ++i)
res += str;
return res;
}
RfbClient.prototype.setEncodings = function()
{
var stream = this.pack_stream;
var cli = this;
// build encodings list
// TODO: API
var encodings = [rfb.encodings.raw, rfb.encodings.copyRect, rfb.encodings.pseudoDesktopSize, rfb.encodings.hextile];
stream.pack('CxS', [rfb.clientMsgTypes.setEncodings, encodings.length]);
stream.pack(repeat('l', encodings.length), encodings);
stream.flush();
cli.requestUpdate(false, 0, 0, cli.width, cli.height);
cli.expectNewMessage();
console.log('handshake performed');
this.emit('connect');
console.log('emitted CONNECT');
}
RfbClient.prototype.expectNewMessage = function()
{
var stream = this.pack_stream;
var cli = this;
stream.get(1, function(buff) {
console.log('server message:' + buff[0]);
switch(buff[0]) {
case rfb.serverMsgTypes.fbUpdate: cli.readFbUpdate(); break;
case rfb.serverMsgTypes.setColorMap: cli.readColorMap(); break;
case rfb.serverMsgTypes.bell: cli.readBell(); break;
case rfb.serverMsgTypes.cutText: cli.readClipboardUpdate(); break;
default:
clog('unsopported server message: ' + buff[0]);
}
});
}
var decodeHandlers = {
};
RfbClient.prototype.readFbUpdate = function()
{
clog('fb update');
var stream = this.pack_stream;
var cli = this;
stream.unpack('xS', function(res) {
var numRects = res[0];
// decode each rectangle
var numRectsLeft = numRects;
clog('number of rectngles in fb updte message: ' + numRects);
function unpackRect() {
if (numRectsLeft == 0)
{
cli.expectNewMessage();
cli.requestUpdate(true, 0, 0, cli.width, cli.height);
return;
}
numRectsLeft--;
var rect = {};
stream.unpackTo(rect,
['S x', 'S y', 'S width', 'S height', 'l encoding'],
function() {
// TODO: rewrite using decodeHandlers
switch(rect.encoding) {
case rfb.encodings.raw:
cli.readRawRect(rect, unpackRect);
break;
case rfb.encodings.copyRect:
cli.readCopyRect(rect, unpackRect);
break;
case rfb.encodings.pseudoDesktopSize:
clog(['Resize', rect]);
cli.width = rect.width;
cli.height = rect.height;
cli.emit('resize', rect);
unpackRect();
break;
case rfb.encodings.hextile:
cli.readHextile(rect, unpackRect);
break;
default:
clog('unknown encoding!!! ' + rect.encoding);
}
}
);
}
unpackRect();
});
}
RfbClient.prototype.readHextile = function(rect, cb)
{
rect.emitter = new EventEmitter();
rect.on = function(eventname, cb) {
rect.emitter.on(eventname, cb);
}
rect.emit = function(eventname, param) {
rect.emitter.emit(eventname, param);
}
rect.widthTiles = (rect.width >>> 4);
rect.heightTiles = (rect.height >>> 4);
clog(['tiles: ', rect.widthTiles, rect.heightTiles]);
rect.rightRectWidth = rect.width & 0x0f;
rect.bottomRectHeight = rect.height & 0x0f;
rect.tilex = 0;
rect.tiley = 0;
rect.tiles = [];
console.log('===== emitting rect');
this.emit('rect', rect);
this.readHextileTile(rect, cb);
}
RfbClient.prototype.readHextileTile = function(rect, cb)
{
var tile = {};
var stream = this.pack_stream;
var cli = this;
tile.x = rect.tilex;
tile.y = rect.tiley;
tile.width = 16;
if (tile.x == rect.widthTiles && rect.rightRectWidth > 0)
tile.width = rect.rightRectWidt;
tile.height = 16;
if (tile.y == rect.heightTiles && rect.bottomRectHeight > 0)
tile.height = rect.bottomRectHeight;
// calculate next tilex & tiley and move up 'stack' if we at the last tile
function nextTile()
{
clog('nextTile');
rect.emit('tile', tile);
tile = {};
if (rect.tilex < rect.widthTiles)
{
rect.tilex++;
//clog([rect.tilex, rect.tiley]);
return cli.readHextileTile(rect, cb);
} else {
clog('===================== new row! ' + rect.tiley);
rect.tilex = 0;
if (rect.tiley < rect.heightTiles)
{
rect.tiley++;
//clog([rect.tilex, rect.tiley]);
return cli.readHextileTile(rect, cb);
} else {
clog('====================')
clog(rect);
return cb();
}
}
}
var bytesPerPixel = cli.bpp >> 3;
console.log('bytesPerPixel: ' + bytesPerPixel);
var tilebuflen = bytesPerPixel*tile.width*tile.height;
stream.unpack('C', function(subEnc) {
clog('tile flags: ' + subEnc[0]);
tile.subEncoding = subEnc[0];
var hextile = rfb.subEncodings.hextile;
if (tile.subEncoding & hextile.raw) {
stream.get(tilebuflen, function(rawbuff)
{
clog('raw tile');
tile.buffer = rawbuff;
nextTile();
});
return;
}
tile.buffer = new Buffer(tilebuflen);
function solidBackground() {
clog('solidBackground');
// the whole tile is just single colored width x height
for (var i=0; i < tilebuflen; i+= bytesPerPixel)
tile.backgroundColor.copy(tile.buffer, i);
}
function readBackground() {
clog('readBackground');
if (tile.subEncoding & hextile.backgroundSpecified) {
clog('hextile.backgroundSpecified');
stream.get(bytesPerPixel, function(pixelValue)
{
clog(['tile.backgroundColor', pixelValue, tile.subEncoding]);
tile.backgroundColor = pixelValue;
rect.backgroundColor = pixelValue;
readForeground();
});
} else {
tile.backgroundColor = rect.backgroundColor;
readForeground();
}
}
function readForeground() {
clog('readForeground');
// we should have background color set here
solidBackground();
if (rect.subEncoding & hextile.foregroundSpecified) {
clog('foreground specified');
stream.get(bytesPerPixel, function(pixelValue)
{
tile.foreroundColor = pixelValue;
rect.foreroundColor = pixelValue;
console.log(rect);
readSubrects();
});
} else {
clog('foreground NOT specified');
clog(rect);
tile.foregroundColor = rect.foregroundColor;
readSubrects();
}
}
function readSubrects() {
clog('readSubrects');
if (tile.subEncoding & hextile.anySubrects) {
clog('have subrects');
// read number of subrectangles
stream.get('C', function(subrectsNum) {
tile.subrectsNum = subrectsNum[0];
clog('number of subrects = ' + tile.subrectsNum);
readSubrect();
});
} else {
nextTile();
}
}
function drawRect(x, y, w, h)
{
console.log(tile);
console.log(['drawRect', x, y, w, h, tile.foregroundColor]);
// TODO: optimise
for(var px = x; px < x+w; ++px)
{
for(var py = x; py < y+h; ++py)
{
var offset = bytesPerPixel*(tile.width*py + px);
tile.foregroundColor.copy(tile.buffer, offset);
}
}
}
function readSubrect() {
clog('readSubrect');
if (tile.subEncoding & hextile.subrectsColored) {
// we have color + rect data
stream.get(bytesPerPixel, function(pixelValue)
{
clog(['coloredSubrect: ', pixelValue]);
tile.foreroundColor = pixelValue;
rect.foreroundColor = pixelValue;
readSubrectRect();
});
} else // we have just rect data
readSubrectRect();
}
function readSubrectRect() {
clog('readSubrectRect');
// read subrect x y w h encoded in two bytes
stream.get(2, function(subrectRaw) {
var x = (subrectRaw[0] & 0xf0) >> 4;
var y = (subrectRaw[0] & 0x0f);
var width = (subrectRaw[1] & 0xf0) >> 4 + 1;
var height = (subrectRaw[1] & 0x0f) + 1;
clog(['readSubrectRect', x, y, width, height, tile.subrectsNum]);
drawRect(x, y, width, height);
tile.subrectsNum--;
if (tile.subrectsNum === 0)
{
nextTile();
} else
readSubrect();
});
}
readBackground();
});
}
RfbClient.prototype.readCopyRect = function(rect, cb)
{
var stream = this.pack_stream;
var cli = this;
stream.unpack('SS', function(src) {
rect.src = { x: src[0], y: src[1] };
clog(['copy rect', src, rect]);
cli.emit('rect', rect);
cb(rect);
});
}
RfbClient.prototype.readRawRect = function(rect, cb)
{
var stream = this.pack_stream;
var cli = this;
var bytesPerPixel = cli.bpp >> 3;
stream.get(bytesPerPixel*rect.width*rect.height, function(rawbuff)
{
//clog('arrived ' + rawbuff.length + ' bytes of fb update');
rect.buffer = rawbuff;
cli.emit('rect', rect);
cb(rect);
});
}
RfbClient.prototype.readColorMap = function()
{
clog('color map');
}
RfbClient.prototype.readBell = function()
{
clog('bell');
this.expectNewMessage();
}
RfbClient.prototype.readClipboardUpdate = function()
{
clog('clipboard update');
var stream = this.pack_stream;
var cli = this;
stream.unpack('xxxL', function(res) {
clog(res[0] + ' bytes string in the buffer');
stream.get(res[0], function(buf) {
clog(buf.toString());
cli.expectNewMessage();
})
});
}
RfbClient.prototype.pointerEvent = function(x, y, buttons)
{
var stream = this.pack_stream;
stream.pack('CCSS', [rfb.clientMsgTypes.pointerEvent, buttons, x, y]);
stream.flush();
}
RfbClient.prototype.keyEvent = function(keysym, isDown)
{
var stream = this.pack_stream;
stream.pack('CCxxL', [rfb.clientMsgTypes.keyEvent, isDown, keysym]);
stream.flush();
}
RfbClient.prototype.requestUpdate = function(incremental, x, y, width, height)
{
var stream = this.pack_stream;
stream.pack('CCSSSS', [rfb.clientMsgTypes.fbUpdate, incremental, x, y, width, height]);
stream.flush();
}
// TODO: add client cutText event!
var fs = require('fs');
function createRfbStream(name)
{
var stream = new EventEmitter();
var fileStream = fs.createReadStream(name);
var pack = new PackStream();
fileStream.on('data', function( data ) {
//fileStream.pause();
//setTimeout(function() {
pack.write(data);
//clog('received from filestream:' + data.length);
//fileStream.resume();
//}, 10);
});
var start = +new Date();
function readData()
{
pack.unpack('L', function(size) {
pack.get(size[0], function(databuf) {
pack.unpack('L', function(timestamp) {
var padding = 3 - ((size - 1) & 0x03);
pack.get(padding, function() {
if (!stream.ending) {
stream.emit('data', databuf);
var now = +new Date - start;
var timediff = timestamp[0] - now;
stream.timeout = setTimeout(readData, timediff);
}
});
});
});
});
}
pack.get(12, function(fileVersion) {
readData();
});
stream.end = function() {
stream.ending = true;
if (stream.timeout)
clearTimeout(stream.timeout);
};
stream.write = function(buf) {
// ignore
}
return stream;
}
function createConnection(params)
{
var stream;
if (params.rfbfile)
{
console.log('reading from ' + params.rfbfile);
stream = createRfbStream(params.rfbfile);
}
else {
console.log('connecting to ' + params.host + ':' + params.port);
stream = net.createConnection(params.port, params.host);
}
if (params.rfbFileOut)
{
var start = +new Date();
var wstream = fs.createWriteStream(params.rfbFileOut);
wstream.write('FBS 001.001\n');
stream.on('data', function(data) {
var sizeBuf = new Buffer(4);
var timeBuf = new Buffer(4);
var size = data.length;
sizeBuf.writeInt32BE(size, 0);
wstream.write(sizeBuf);
wstream.write(data);
timeBuf.writeInt32BE(+new Date() - start, 0);
wstream.write(timeBuf);
var padding = 3 - ((size - 1) & 0x03);
var pbuf = new Buffer(padding);
wstream.write(pbuf);
}).on('end', function() {
wstream.end();
});
}
return new RfbClient(stream, params);
}
exports.createConnection = createConnection;