diff --git a/js/x11/examples/vncviewer/rfbclient.js b/js/x11/examples/vncviewer/rfbclient.js new file mode 100644 index 0000000..2b45e54 --- /dev/null +++ b/js/x11/examples/vncviewer/rfbclient.js @@ -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;