mirror of
https://github.com/danbulant/node-x11
synced 2026-06-17 13:41:11 +00:00
595 lines
18 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
}
|