node-x11/examples/vncviewer/rfbclient.js
Andrey Sidorov 0973f1376c VNC client
2012-01-18 16:52:15 +11:00

595 lines
18 KiB
JavaScript

"use strict";
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');
// array to flip bits in byte
var flip = [ 0, 128, 64, 192, 32, 160, 96, 224, 16, 144, 80, 208, 48, 176, 112, 240,
8, 136, 72, 200, 40, 168, 104, 232, 24, 152, 88, 216, 56, 184, 120, 248,
4, 132, 68, 196, 36, 164, 100, 228, 20, 148, 84, 212, 52, 180, 116, 244,
12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252,
2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242,
10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250,
6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246,
14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254,
1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241,
9, 137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249,
5, 133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245,
13, 141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253,
3, 131, 67, 195, 35, 163, 99, 227, 19, 147, 83, 211, 51, 179, 115, 243,
11, 139, 75, 203, 43, 171, 107, 235, 27, 155, 91, 219, 59, 187, 123, 251,
7, 135, 71, 199, 39, 167, 103, 231, 23, 151, 87, 215, 55, 183, 119, 247,
15, 143, 79, 207, 47, 175, 111, 239, 31, 159, 95, 223, 63, 191, 127, 255 ];
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 ) {
//console.log(hexy(data, {prefix: 'from client '}));
cli.stream.write(data);
});
stream.on('data', function( data ) {
//var dump = data.length > 20 ? data.slice(0,20) : data;
//console.log(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) {
//console.log(res[0]);
stream.get(res[0], function(strBuff) {
strcb(strBuff.toString());
});
});
}
RfbClient.prototype.readError = function()
{
this.pack_stream.readString(function(str) {
//console.log(str);
cli.emit('error', str);
});
}
RfbClient.prototype.readServerVersion = function()
{
var stream = this.pack_stream;
var cli = this;
stream.get(12, function(rfbver) {
//console.log(rfbver);
stream.pack('a', [ 'RFB 003.008\n' ]).flush();
// read security types
stream.unpack('C', function(res) {
var numSecTypes = res[0];
if (numSecTypes == 0) {
cli.readError();
} else {
stream.get(numSecTypes, function(secTypes) {
// 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) {
//console.log(['security result: ', 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;
//console.log('Using security type =' + cli.securityType);
switch(cli.securityType) {
case rfb.security.None:
// do nothing
cli.readSecurityResult();
break;
case rfb.security.VNC:
/* =========
from rfb protocol spec, responce = DES(challenge, password).
In reality (from http://bytecrafter.blogspot.com/2010/09/des-encryption-as-used-in-vnc.html)
1) DES is used in ECB mode.
2) The ECB Key is based upon an ASCII password.
The key must be 8 bytes long. The password is either
truncated to 8 bytes, or else zeros are added to the end
to bring it up to 8 bytes. As an additional twist, each byte
in flipped. So, if the ASCII password was "pword" [0x 70 77 6F 72 64],
the resulting key would be [0x 0E EE F6 4E 26 00 00 00].
3) The VNC Authentication scheme sends a 16 byte challenge. This challenge
should be encrypted with the key that was just described, but DES in ECB
mode can only encrypt an 8 byte message. So, the challenge is split
into two messages, encrypted separately, and then jammed back together.
=========
*/
stream.get(16, function(challenge) {
console.log(['challenge = ', challenge]);
var crypto = require('crypto');
// prepare password
var passwd = '';
for (var i=0; i < 8; ++i)
{
if (i < cli.params.password.length)
passwd += String.fromCharCode(flip[cli.params.password.charCodeAt(i)]);
else
passwd += String.fromCharCode(0);
}
var des1 = crypto.createCipher('DES-ECB', passwd);
var response1 = des1.update(challenge.slice(0, 8), 'binary');
var des2 = crypto.createCipher('DES-ECB', passwd);
var response2 = des1.update(challenge.slice(8,16), 'binary');
var response = response1 + response2;
console.log(['response = ', response]);
stream.pack('a', [response]).flush();
cli.readSecurityResult();
});
break;
default:
console.error('unknown security type: ' + cli.securityType);
process.exit(1);
}
}
function swapBytes2U(num)
{
return ( (num & 0xff) << 8 ) + ( ( num & 0xff00 ) >> 8);
}
function swapBytes4U(num)
{
return ( (num & 0xff) << 24 ) +
( (num & 0xff00 ) << 8) +
( (num & 0xff0000 ) >> 8) +
( (num & 0xff000000 ) >> 24);
}
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();
//console.log('initMessage sent');
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() {
// all ints are bigEndian except width and height in pixel format
//if (!cli.isBigEndian)
//{
// cli.width = swapBytes2U(cli.width);
// cli.height = swapBytes2U(cli.height);
// cli.redMax = swapBytes2U(cli.redMax);
// cli.greenMax = swapBytes2U(cli.greenMax);
// cli.blueMax = swapBytes2U(cli.blueMax);
//}
//console.log([cli.width, cli.height]);
stream.serverBigEndian = false; //cli.isBigEndian;
stream.clientBigEndian = false; //cli.isBigEndian;
//stream.bigEndian = false; //cli.isBigEndian;
//console.log(cli);
stream.get(cli.titleLength, function(titleBuf) {
cli.title = titleBuf.toString();
delete cli.titleLength;
cli.width = cli.width;
cli.height = cli.height;
cli.title = cli.title;
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]
);
//TODO: add actual sendPixelFormat
// ignore for now (it is optional );
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();
this.emit('connect');
}
RfbClient.prototype.expectNewMessage = function()
{
var stream = this.pack_stream;
var cli = this;
stream.get(1, function(buff) {
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:
console.log('unsopported server message: ' + buff[0]);
}
});
}
var decodeHandlers = {
};
RfbClient.prototype.readFbUpdate = function()
{
//console.log('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;
//console.log(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:
console.log(['Resize', rect]);
cli.width = rect.width;
cli.height = rect.height;
cli.emit('resize', rect);
unpackRect();
break;
default:
console.log('unknown encoding!!!');
}
}
);
}
unpackRect();
});
}
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] };
console.log(['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)
{
//console.log('arrived ' + rawbuff.length + ' bytes of fb update');
rect.buffer = rawbuff;
cli.emit('rect', rect);
cb(rect);
});
}
RfbClient.prototype.readColorMap = function()
{
console.log('color map');
}
RfbClient.prototype.readBell = function()
{
console.log('bell');
this.expectNewMessage();
}
RfbClient.prototype.readClipboardUpdate = function()
{
console.log('clipboard update');
var stream = this.pack_stream;
var cli = this;
stream.unpack('xxxL', function(res) {
console.log(res[0] + ' bytes string in the buffer');
stream.get(res[0], function(buf) {
console.log(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();
}
function createConnection(params)
{
var stream = net.createConnection(params.port, params.host);
return new RfbClient(stream, params);
}
//testing
// home RealVNC
//var r = createConnection({host: '10.0.0.6', port: 5900});
// macbook local
//var r = createConnection({port: 5900, password: 'tetris'});
// virtulbox x11vnc
var host = process.argv[2];
var port = process.argv[3];
if (!host)
{
host = '127.0.0.1';
}
if (!port)
{
port = 5900;
}
var opts = {};
opts.host = host;
opts.port = port;
console.log(opts);
var r = createConnection(opts);
r.on('connect', function() {
startUI();
});
function startUI()
{
var x11 = require('../../lib/x11');
var xclient = x11.createClient();
var Exposure = x11.eventMask.Exposure;
var PointerMotion = x11.eventMask.PointerMotion;
var ButtonPress = x11.eventMask.ButtonPress;
var ButtonRelease = x11.eventMask.ButtonRelease;
var KeyPress = x11.eventMask.KeyPress;
var KeyRelease = x11.eventMask.KeyRelease;
xclient.on('connect', function(display) {
var X = display.client;
X.require('big-requests', function(BigReq) {
BigReq.Enable(function(maxLen) {});
X.require('render', function(Render) {
var keycode2keysym = [];
var min = display.min_keycode;
var max = display.max_keycode;
X.GetKeyboardMapping(min, max-min, function(list) {
for (var i=0; i < list.length; ++i)
{
var keycode = i + min;
var keysyms = list[i];
keycode2keysym[keycode] = keysyms;
}
var root = display.screen[0].root;
var white = display.screen[0].white_pixel;
var black = display.screen[0].black_pixel;
var wid = X.AllocID();
X.CreateWindow(
wid, root,
10, 10, r.width, r.height,
1, 1, 0,
{
backgroundPixel: white, eventMask: Exposure|PointerMotion|ButtonPress|ButtonRelease|KeyPress|KeyRelease
}
);
X.MapWindow(wid);
var gc = X.AllocID();
X.CreateGC(gc, wid, { foreground: black, background: white } );
X.ChangeProperty(0, wid, X.atoms.WM_NAME, X.atoms.STRING, 8, r.title);
//var pixmap1 = X.AllocID();
//X.CreatePixmap(pixmap1, wid, 32, 128, 128);
//var pic = X.AllocID();
//Render.CreatePicture(pic, pixmap1, Render.rgba32);
var pic1 = X.AllocID();
Render.CreatePicture(pic1, wid, Render.rgb24);
var buttonsState = 0;
X.on('error', function(err) {
console.log(err);
});
X.on('event', function(ev) {
//console.log(ev);
if (ev.type == 12) // expose
{
//X.PutImage(2, wid, gc, 128, 128, 0, 0, 0, 24, bitmap);
} else if (ev.type == 6) { // mousemove
r.pointerEvent(ev.x, ev.y, buttonsState);
} else if (ev.type == 4 || ev.type == 5) { // mousedown
var buttonBit = 1 << (ev.keycode - 1);
// set button bit
if (ev.type == 4)
buttonsState |= buttonBit;
else
buttonsState &= ~buttonBit;
r.pointerEvent(ev.x, ev.y, buttonsState);
} else if (ev.type == 2 || ev.type == 3) {
//console.log(keycode2keysym[ev.keycode]);
var shift = ev.buttons & 1;
//console.log(shift);
var keysym = keycode2keysym[ev.keycode][shift];
var isDown = (ev.type == 2) ? 1 : 0;
r.keyEvent(keysym, isDown);
}
});
r.on('resize', function(rect)
{
console.log('============');
X.ResiseWindow(wid, rect.width, rect.height);
});
r.on('rect', function(rect) {
//console.log('========= in handler');
//console.log(rect);
if (rect.encoding == rfb.encodings.raw) {
// format, drawable, gc, width, height, dstX, dstY, leftPad, depth, data
X.PutImage(2, wid, gc, rect.width, rect.height, rect.x, rect.y, 0, 24, rect.buffer);
} else if (rect.encoding == rfb.encodings.copyRect) {
X.CopyArea(wid, wid, gc, rect.src.x, rect.src.y, rect.x, rect.y, rect.width, rect.height);
}
});
});
});
});
});
}