1
0
Fork 0
mirror of https://github.com/openstf/stf synced 2025-10-05 02:29:26 +02:00

Merge branch 'vnc'

This commit is contained in:
Simo Kinnunen 2015-09-28 15:01:43 +09:00
commit ae449a631a
14 changed files with 927 additions and 60 deletions

View file

@ -10,6 +10,7 @@ addons:
- libprotobuf-dev - libprotobuf-dev
- graphicsmagick - graphicsmagick
- rethinkdb - rethinkdb
- yasm
script: script:
- gulp build - gulp build
before_script: before_script:

View file

@ -1,4 +1,4 @@
FROM openstf/base:v1.0.2 FROM openstf/base:v1.0.4
# Sneak the stf executable into $PATH. # Sneak the stf executable into $PATH.
ENV PATH /app/bin:$PATH ENV PATH /app/bin:$PATH

View file

@ -43,6 +43,7 @@ It is currently being used at [CyberAgent](https://www.cyberagent.co.jp/en/) to
* Run any `adb` command locally, including shell access * Run any `adb` command locally, including shell access
* [Android Studio](http://developer.android.com/tools/studio/index.html) and other IDE support, debug your app while watching the device screen on your browser * [Android Studio](http://developer.android.com/tools/studio/index.html) and other IDE support, debug your app while watching the device screen on your browser
* Supports [Chrome remote debug tools](https://developer.chrome.com/devtools/docs/remote-debugging) * Supports [Chrome remote debug tools](https://developer.chrome.com/devtools/docs/remote-debugging)
- Experimental VNC support (work in progress)
* Manage your device inventory * Manage your device inventory
- See which devices are connected, offline/unavailable (indicating a weak USB connection), unauthorized or unplugged - See which devices are connected, offline/unavailable (indicating a weak USB connection), unauthorized or unplugged
- See who's using a device - See who's using a device
@ -65,14 +66,15 @@ As the product has evolved from an internal tool running in our internal network
* [GraphicsMagick](http://www.graphicsmagick.org/) (for resizing screenshots) * [GraphicsMagick](http://www.graphicsmagick.org/) (for resizing screenshots)
* [ZeroMQ](http://zeromq.org/) libraries installed * [ZeroMQ](http://zeromq.org/) libraries installed
* [Protocol Buffers](https://github.com/google/protobuf) libraries installed * [Protocol Buffers](https://github.com/google/protobuf) libraries installed
* [yasm](http://yasm.tortall.net/) installed (for compiling embedded [libjpeg-turbo](https://github.com/sorccu/node-jpeg-turbo))
* [pkg-config](http://www.freedesktop.org/wiki/Software/pkg-config/) so that Node.js can find the libraries * [pkg-config](http://www.freedesktop.org/wiki/Software/pkg-config/) so that Node.js can find the libraries
Note that you need these dependencies even if you've installed STF directly from [NPM](https://www.npmjs.com/), because they can't be included. Note that you need these dependencies even if you've installed STF directly from [NPM](https://www.npmjs.com/), because they can't be included in the package.
On OS X, you can use [homebrew](http://brew.sh/) to install most of the dependencies: On OS X, you can use [homebrew](http://brew.sh/) to install most of the dependencies:
```bash ```bash
brew install rethinkdb graphicsmagick zeromq protobuf pkg-config brew install rethinkdb graphicsmagick zeromq protobuf yasm pkg-config
``` ```
On Windows you're on your own. In theory you might be able to get STF installed via [Cygwin](https://www.cygwin.com/) or similar, but we've never tried. In principle we will not provide any Windows installation support, but please do send a documentation pull request if you figure out what to do. On Windows you're on your own. In theory you might be able to get STF installed via [Cygwin](https://www.cygwin.com/) or similar, but we've never tried. In principle we will not provide any Windows installation support, but please do send a documentation pull request if you figure out what to do.

View file

@ -68,6 +68,10 @@ program
, 'adb connect URL pattern' , 'adb connect URL pattern'
, String , String
, '${publicIp}:${publicPort}') , '${publicIp}:${publicPort}')
.option('--vnc-initial-size <size>'
, 'initial VNC size'
, cliutil.size
, [600, 800])
.option('--mute-master' .option('--mute-master'
, 'whether to mute master volume when devices are being used') , 'whether to mute master volume when devices are being used')
.option('--lock-rotation' .option('--lock-rotation'
@ -103,6 +107,7 @@ program
, '--connect-push', options.connectPush.join(',') , '--connect-push', options.connectPush.join(',')
, '--screen-port', ports.shift() , '--screen-port', ports.shift()
, '--connect-port', ports.shift() , '--connect-port', ports.shift()
, '--vnc-port', ports.shift()
, '--public-ip', options.publicIp , '--public-ip', options.publicIp
, '--group-timeout', options.groupTimeout , '--group-timeout', options.groupTimeout
, '--storage-url', options.storageUrl , '--storage-url', options.storageUrl
@ -111,6 +116,7 @@ program
, '--screen-ws-url-pattern', options.screenWsUrlPattern , '--screen-ws-url-pattern', options.screenWsUrlPattern
, '--connect-url-pattern', options.connectUrlPattern , '--connect-url-pattern', options.connectUrlPattern
, '--heartbeat-interval', options.heartbeatInterval , '--heartbeat-interval', options.heartbeatInterval
, '--vnc-initial-size', options.vncInitialSize.join('x')
] ]
.concat(options.muteMaster ? ['--mute-master'] : []) .concat(options.muteMaster ? ['--mute-master'] : [])
.concat(options.lockRotation ? ['--lock-rotation'] : [])) .concat(options.lockRotation ? ['--lock-rotation'] : []))
@ -142,6 +148,13 @@ program
.option('--connect-port <port>' .option('--connect-port <port>'
, 'port allocated to adb connect' , 'port allocated to adb connect'
, Number) , Number)
.option('--vnc-port <port>'
, 'port allocated to vnc'
, Number)
.option('--vnc-initial-size <size>'
, 'initial VNC size'
, cliutil.size
, [600, 800])
.option('--connect-url-pattern <pattern>' .option('--connect-url-pattern <pattern>'
, 'adb connect URL pattern' , 'adb connect URL pattern'
, String , String
@ -193,6 +206,9 @@ program
if (!options.connectPort) { if (!options.connectPort) {
this.missingArgument('--connect-port') this.missingArgument('--connect-port')
} }
if (!options.vncPort) {
this.missingArgument('--vnc-port')
}
if (!options.storageUrl) { if (!options.storageUrl) {
this.missingArgument('--storage-url') this.missingArgument('--storage-url')
} }
@ -213,6 +229,8 @@ program
, screenPort: options.screenPort , screenPort: options.screenPort
, connectUrlPattern: options.connectUrlPattern , connectUrlPattern: options.connectUrlPattern
, connectPort: options.connectPort , connectPort: options.connectPort
, vncPort: options.vncPort
, vncInitialSize: options.vncInitialSize
, heartbeatInterval: options.heartbeatInterval , heartbeatInterval: options.heartbeatInterval
, muteMaster: options.muteMaster , muteMaster: options.muteMaster
, lockRotation: options.lockRotation , lockRotation: options.lockRotation
@ -946,6 +964,10 @@ program
.option('--user-profile-url <url>' .option('--user-profile-url <url>'
, 'URL to external user profile page' , 'URL to external user profile page'
, String) , String)
.option('--vnc-initial-size <size>'
, 'initial VNC size'
, cliutil.size
, [600, 800])
.option('--mute-master' .option('--mute-master'
, 'whether to mute master volume when devices are being used') , 'whether to mute master volume when devices are being used')
.option('--lock-rotation' .option('--lock-rotation'
@ -1013,6 +1035,7 @@ program
, util.format('http://localhost:%d/', options.poorxyPort) , util.format('http://localhost:%d/', options.poorxyPort)
, '--adb-host', options.adbHost , '--adb-host', options.adbHost
, '--adb-port', options.adbPort , '--adb-port', options.adbPort
, '--vnc-initial-size', options.vncInitialSize.join('x')
] ]
.concat(options.allowRemote ? ['--allow-remote'] : []) .concat(options.allowRemote ? ['--allow-remote'] : [])
.concat(options.muteMaster ? ['--mute-master'] : []) .concat(options.muteMaster ? ['--mute-master'] : [])

View file

@ -20,6 +20,7 @@ module.exports = function(options) {
.dependency(require('./plugins/solo')) .dependency(require('./plugins/solo'))
.dependency(require('./plugins/screen/stream')) .dependency(require('./plugins/screen/stream'))
.dependency(require('./plugins/screen/capture')) .dependency(require('./plugins/screen/capture'))
.dependency(require('./plugins/vnc'))
.dependency(require('./plugins/service')) .dependency(require('./plugins/service'))
.dependency(require('./plugins/browser')) .dependency(require('./plugins/browser'))
.dependency(require('./plugins/store')) .dependency(require('./plugins/store'))

View file

@ -25,7 +25,6 @@ module.exports = syrup.serial()
.dependency(require('./options')) .dependency(require('./options'))
.define(function(options, adb, minicap, display, screenOptions) { .define(function(options, adb, minicap, display, screenOptions) {
var log = logger.createLogger('device:plugins:screen:stream') var log = logger.createLogger('device:plugins:screen:stream')
var plugin = Object.create(null)
function FrameProducer(config) { function FrameProducer(config) {
EventEmitter.call(this) EventEmitter.call(this)
@ -443,9 +442,9 @@ module.exports = syrup.serial()
return createServer() return createServer()
.then(function(wss) { .then(function(wss) {
var broadcastSet = new BroadcastSet()
var frameProducer = new FrameProducer( var frameProducer = new FrameProducer(
new FrameConfig(display.properties, display.properties)) new FrameConfig(display.properties, display.properties))
var broadcastSet = frameProducer.broadcastSet = new BroadcastSet()
broadcastSet.on('nonempty', function() { broadcastSet.on('nonempty', function() {
frameProducer.start() frameProducer.start()
@ -455,18 +454,56 @@ module.exports = syrup.serial()
frameProducer.stop() frameProducer.stop()
}) })
broadcastSet.on('insert', function(id) {
// If two clients join a session in the middle, one of them
// may not release the initial size because the projection
// doesn't necessarily change, and the producer doesn't Getting
// restarted. Therefore we have to call onStart() manually
// if the producer is already up and running.
switch (frameProducer.runningState) {
case FrameProducer.STATE_STARTED:
broadcastSet.get(id).onStart(frameProducer)
break
}
})
display.on('rotationChange', function(newRotation) { display.on('rotationChange', function(newRotation) {
frameProducer.updateRotation(newRotation) frameProducer.updateRotation(newRotation)
}) })
frameProducer.on('start', function() { frameProducer.on('start', function() {
broadcastSet.keys().map(function(id) {
return broadcastSet.get(id).onStart(frameProducer)
})
})
frameProducer.on('readable', function next() {
var frame
if ((frame = frameProducer.nextFrame())) {
Promise.settle([broadcastSet.keys().map(function(id) {
return broadcastSet.get(id).onFrame(frame)
})]).then(next)
}
else {
frameProducer.needFrame()
}
})
frameProducer.on('error', function(err) {
log.fatal('Frame producer had an error', err.stack)
lifecycle.fatal()
})
wss.on('connection', function(ws) {
var id = uuid.v4()
function wsStartNotifier() {
return new Promise(function(resolve, reject) {
var message = util.format( var message = util.format(
'start %s' 'start %s'
, JSON.stringify(frameProducer.banner) , JSON.stringify(frameProducer.banner)
) )
broadcastSet.keys().forEach(function(id) {
var ws = broadcastSet.get(id)
switch (ws.readyState) { switch (ws.readyState) {
case WebSocket.OPENING: case WebSocket.OPENING:
// This should never happen. // This should never happen.
@ -474,7 +511,9 @@ module.exports = syrup.serial()
break break
case WebSocket.OPEN: case WebSocket.OPEN:
// This is what SHOULD happen. // This is what SHOULD happen.
ws.send(message) ws.send(message, function(err) {
return err ? reject(err) : resolve()
})
break break
case WebSocket.CLOSING: case WebSocket.CLOSING:
// Ok, a 'close' event should remove the client from the set // Ok, a 'close' event should remove the client from the set
@ -487,14 +526,10 @@ module.exports = syrup.serial()
break break
} }
}) })
}) }
frameProducer.on('readable', function next() { function wsFrameNotifier(frame) {
var frame
if ((frame = frameProducer.nextFrame())) {
Promise.settle([broadcastSet.keys().map(function(id) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var ws = broadcastSet.get(id)
switch (ws.readyState) { switch (ws.readyState) {
case WebSocket.OPENING: case WebSocket.OPENING:
// This should never happen. // This should never happen.
@ -519,27 +554,17 @@ module.exports = syrup.serial()
'Unable to send frame to CLOSED client "%s"', id))) 'Unable to send frame to CLOSED client "%s"', id)))
} }
}) })
})]).then(next)
} }
else {
frameProducer.needFrame()
}
})
frameProducer.on('error', function(err) {
log.fatal('Frame producer had an error', err.stack)
lifecycle.fatal()
})
wss.on('connection', function(ws) {
var id = uuid.v4()
ws.on('message', function(data) { ws.on('message', function(data) {
var match var match
if ((match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data))) { if ((match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data))) {
switch (match[2] || match[1]) { switch (match[2] || match[1]) {
case 'on': case 'on':
broadcastSet.insert(id, ws) broadcastSet.insert(id, {
onStart: wsStartNotifier
, onFrame: wsFrameNotifier
})
break break
case 'off': case 'off':
broadcastSet.remove(id) broadcastSet.remove(id)
@ -563,6 +588,7 @@ module.exports = syrup.serial()
lifecycle.observe(function() { lifecycle.observe(function() {
frameProducer.stop() frameProducer.stop()
}) })
return frameProducer
}) })
.return(plugin)
}) })

View file

@ -0,0 +1,201 @@
var net = require('net')
var util = require('util')
var os = require('os')
var syrup = require('stf-syrup')
var Promise = require('bluebird')
var uuid = require('node-uuid')
var jpeg = require('jpeg-turbo')
var logger = require('../../../../util/logger')
var lifecycle = require('../../../../util/lifecycle')
var VncServer = require('./util/server')
var VncConnection = require('./util/connection')
var PointerTranslator = require('./util/pointertranslator')
module.exports = syrup.serial()
.dependency(require('../screen/stream'))
.dependency(require('../touch'))
.define(function(options, screenStream, touch) {
var log = logger.createLogger('device:plugins:vnc')
function createServer() {
log.info('Starting VNC server on port %d', options.vncPort)
var opts = {
name: options.serial
, width: options.vncInitialSize[0]
, height: options.vncInitialSize[1]
}
var vnc = new VncServer(net.createServer({
allowHalfOpen: true
}), opts)
var listeningListener, errorListener
return new Promise(function(resolve, reject) {
listeningListener = function() {
return resolve(vnc)
}
errorListener = function(err) {
return reject(err)
}
vnc.on('listening', listeningListener)
vnc.on('error', errorListener)
vnc.listen(options.vncPort)
})
.finally(function() {
vnc.removeListener('listening', listeningListener)
vnc.removeListener('error', errorListener)
})
}
return createServer()
.then(function(vnc) {
vnc.on('connection', function(conn) {
var id = util.format('vnc-%s', uuid.v4())
var connState = {
lastFrame: null
, lastFrameTime: null
, frameWidth: 0
, frameHeight: 0
, sentFrameTime: null
, updateRequests: 0
, frameConfig: {
format: jpeg.FORMAT_RGB
}
}
var pointerTranslator = new PointerTranslator()
pointerTranslator.on('touchdown', function(event) {
touch.touchDown(event)
})
pointerTranslator.on('touchmove', function(event) {
touch.touchMove(event)
})
pointerTranslator.on('touchup', function(event) {
touch.touchUp(event)
})
pointerTranslator.on('touchcommit', function() {
touch.touchCommit()
})
function vncStartListener(frameProducer) {
return new Promise(function(resolve/*, reject*/) {
connState.frameWidth = frameProducer.banner.virtualWidth
connState.frameHeight = frameProducer.banner.virtualHeight
resolve()
})
}
function vncFrameListener(frame) {
return new Promise(function(resolve/*, reject*/) {
connState.lastFrame = frame
connState.lastFrameTime = Date.now()
maybeSendFrame()
resolve()
})
}
function maybeSendFrame() {
if (!connState.updateRequests) {
return
}
if (!connState.lastFrame) {
return
}
if (connState.lastFrameTime === connState.sentFrameTime) {
return
}
var decoded = jpeg.decompressSync(
connState.lastFrame, connState.frameConfig)
conn.writeFramebufferUpdate([
{ xPosition: 0
, yPosition: 0
, width: decoded.width
, height: decoded.height
, encodingType: VncConnection.ENCODING_RAW
, data: decoded.data
}
, { xPosition: 0
, yPosition: 0
, width: decoded.width
, height: decoded.height
, encodingType: VncConnection.ENCODING_DESKTOPSIZE
}
])
connState.updateRequests = 0
connState.sentFrameTime = connState.lastFrameTime
}
conn.on('authenticated', function() {
screenStream.updateProjection(
options.vncInitialSize[0], options.vncInitialSize[1])
screenStream.broadcastSet.insert(id, {
onStart: vncStartListener
, onFrame: vncFrameListener
})
})
conn.on('fbupdaterequest', function() {
connState.updateRequests += 1
maybeSendFrame()
})
conn.on('formatchange', function(format) {
var same = os.endianness() == 'BE' == format.bigEndianFlag
switch (format.bitsPerPixel) {
case 8:
connState.frameConfig = {
format: jpeg.FORMAT_GRAY
}
break
case 24:
connState.frameConfig = {
format: ((format.redShift > format.blueShift) === same)
? jpeg.FORMAT_BGR
: jpeg.FORMAT_RGB
}
break
case 32:
connState.frameConfig = {
format: ((format.redShift > format.blueShift) === same)
? (format.blueShift === 0
? jpeg.FORMAT_BGRX
: jpeg.FORMAT_XBGR)
: (format.redShift === 0
? jpeg.FORMAT_RGBX
: jpeg.FORMAT_XRGB)
}
break
}
})
conn.on('pointer', function(event) {
pointerTranslator.push(event)
})
conn.on('close', function() {
screenStream.broadcastSet.remove(id)
})
})
lifecycle.observe(function() {
vnc.close()
})
})
})

View file

@ -0,0 +1,474 @@
var util = require('util')
var os = require('os')
var EventEmitter = require('eventemitter3').EventEmitter
var debug = require('debug')('vnc:connection')
var PixelFormat = require('./pixelformat')
function VncConnection(conn, options) {
this.options = options
this._bound = {
_errorListener: this._errorListener.bind(this)
, _readableListener: this._readableListener.bind(this)
, _endListener: this._endListener.bind(this)
, _closeListener: this._closeListener.bind(this)
}
this._buffer = null
this._state = 0
this._changeState(VncConnection.STATE_NEED_CLIENT_VERSION)
this._serverVersion = VncConnection.V3_008
this._serverSupportedSecurity = [VncConnection.SECURITY_NONE]
this._serverWidth = this.options.width
this._serverHeight = this.options.height
this._serverPixelFormat = new PixelFormat({
bitsPerPixel: 32
, depth: 24
, bigEndianFlag: os.endianness() == 'BE' ? 1 : 0
, trueColorFlag: 1
, redMax: 255
, greenMax: 255
, blueMax: 255
, redShift: 16
, greenShift: 8
, blueShift: 0
})
this._serverName = this.options.name
this._clientVersion = null
this._clientShare = false
this._clientPixelFormat = this._serverPixelFormat
this._clientEncodingCount = 0
this._clientEncodings = []
this._clientCutTextLength = 0
this.conn = conn
.on('error', this._bound._errorListener)
.on('readable', this._bound._readableListener)
.on('end', this._bound._endListener)
.on('close', this._bound._closeListener)
this._writeServerVersion()
this._read()
}
util.inherits(VncConnection, EventEmitter)
VncConnection.V3_003 = 3003
VncConnection.V3_007 = 3007
VncConnection.V3_008 = 3008
VncConnection.SECURITY_NONE = 1
VncConnection.SECURITY_VNC = 2
VncConnection.SECURITYRESULT_OK = 0
VncConnection.SECURITYRESULT_FAIL = 1
VncConnection.CLIENT_MESSAGE_SETPIXELFORMAT = 0
VncConnection.CLIENT_MESSAGE_SETENCODINGS = 2
VncConnection.CLIENT_MESSAGE_FBUPDATEREQUEST = 3
VncConnection.CLIENT_MESSAGE_KEYEVENT = 4
VncConnection.CLIENT_MESSAGE_POINTEREVENT = 5
VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT = 6
VncConnection.SERVER_MESSAGE_FBUPDATE = 0
var StateReverse = Object.create(null), State = {
STATE_NEED_CLIENT_VERSION: 10
, STATE_NEED_CLIENT_SECURITY: 20
, STATE_NEED_CLIENT_INIT: 30
, STATE_NEED_CLIENT_MESSAGE: 40
, STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT: 50
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS: 60
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE: 61
, STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST: 70
, STATE_NEED_CLIENT_MESSAGE_KEYEVENT: 80
, STATE_NEED_CLIENT_MESSAGE_POINTEREVENT: 90
, STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT: 100
, STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE: 101
}
VncConnection.ENCODING_RAW = 0
VncConnection.ENCODING_DESKTOPSIZE = -223
Object.keys(State).map(function(name) {
VncConnection[name] = State[name]
StateReverse[State[name]] = name
})
VncConnection.prototype.end = function() {
this.conn.end()
}
VncConnection.prototype.writeFramebufferUpdate = function(rectangles) {
var chunk = new Buffer(4)
chunk[0] = VncConnection.SERVER_MESSAGE_FBUPDATE
chunk[1] = 0
chunk.writeUInt16BE(rectangles.length, 2)
this._write(chunk)
rectangles.forEach(function(rect) {
var chunk = new Buffer(12)
chunk.writeUInt16BE(rect.xPosition, 0)
chunk.writeUInt16BE(rect.yPosition, 2)
chunk.writeUInt16BE(rect.width, 4)
chunk.writeUInt16BE(rect.height, 6)
chunk.writeInt32BE(rect.encodingType, 8)
this._write(chunk)
switch (rect.encodingType) {
case VncConnection.ENCODING_RAW:
this._write(rect.data)
break
case VncConnection.ENCODING_DESKTOPSIZE:
this._serverWidth = rect.width
this._serverHeight = rect.height
break
default:
throw new Error(util.format(
'Unsupported encoding type', rect.encodingType))
}
}, this)
}
VncConnection.prototype._error = function(err) {
this.emit('error', err)
this.end()
}
VncConnection.prototype._errorListener = function(err) {
this._error(err)
}
VncConnection.prototype._endListener = function() {
this.emit('end')
}
VncConnection.prototype._closeListener = function() {
this.emit('close')
}
VncConnection.prototype._writeServerVersion = function() {
// Yes, we could just format the string instead. Didn't feel like it.
switch (this._serverVersion) {
case VncConnection.V3_003:
this._write(new Buffer('RFB 003.003\n'))
break
case VncConnection.V3_007:
this._write(new Buffer('RFB 003.007\n'))
break
case VncConnection.V3_008:
this._write(new Buffer('RFB 003.008\n'))
break
}
}
VncConnection.prototype._writeSupportedSecurity = function() {
var chunk = new Buffer(1 + this._serverSupportedSecurity.length)
chunk[0] = this._serverSupportedSecurity.length
this._serverSupportedSecurity.forEach(function(security, i) {
chunk[1 + i] = security
})
this._write(chunk)
}
VncConnection.prototype._writeSelectedSecurity = function() {
var chunk = new Buffer(4)
chunk.writeUInt32BE(VncConnection.SECURITY_NONE, 0)
this._write(chunk)
}
VncConnection.prototype._writeSecurityResult = function(result, reason) {
var chunk
switch (result) {
case VncConnection.SECURITYRESULT_OK:
chunk = new Buffer(4)
chunk.writeUInt32BE(result, 0)
this._write(chunk)
break
case VncConnection.SECURITYRESULT_FAIL:
chunk = new Buffer(4 + 4 + reason.length)
chunk.writeUInt32BE(result, 0)
chunk.writeUInt32BE(reason.length, 4)
chunk.write(reason, 8, reason.length)
this._write(chunk)
break
}
}
VncConnection.prototype._writeServerInit = function() {
debug('server pixel format', this._serverPixelFormat)
var chunk = new Buffer(2 + 2 + 16 + 4 + this._serverName.length)
chunk.writeUInt16BE(this._serverWidth, 0)
chunk.writeUInt16BE(this._serverHeight, 2)
chunk[4] = this._serverPixelFormat.bitsPerPixel
chunk[5] = this._serverPixelFormat.depth
chunk[6] = this._serverPixelFormat.bigEndianFlag
chunk[7] = this._serverPixelFormat.trueColorFlag
chunk.writeUInt16BE(this._serverPixelFormat.redMax, 8)
chunk.writeUInt16BE(this._serverPixelFormat.greenMax, 10)
chunk.writeUInt16BE(this._serverPixelFormat.blueMax, 12)
chunk[14] = this._serverPixelFormat.redShift
chunk[15] = this._serverPixelFormat.greenShift
chunk[16] = this._serverPixelFormat.blueShift
chunk[17] = 0 // padding
chunk[18] = 0 // padding
chunk[19] = 0 // padding
chunk.writeUInt32BE(this._serverName.length, 20)
chunk.write(this._serverName, 24, this._serverName.length)
this._write(chunk)
}
VncConnection.prototype._readableListener = function() {
this._read()
}
VncConnection.prototype._read = function() {
var chunk, lo, hi
while (this._append(this.conn.read())) {
do {
debug('state', StateReverse[this._state])
chunk = null
switch (this._state) {
case VncConnection.STATE_NEED_CLIENT_VERSION:
if ((chunk = this._consume(12))) {
if ((this._clientVersion = this._parseVersion(chunk)) === null) {
this.end()
return
}
debug('client version', this._clientVersion)
this._writeSupportedSecurity()
this._changeState(VncConnection.STATE_NEED_CLIENT_SECURITY)
}
break
case VncConnection.STATE_NEED_CLIENT_SECURITY:
if ((chunk = this._consume(1))) {
if ((this._clientSecurity = this._parseSecurity(chunk)) === null) {
this._writeSecurityResult(
VncConnection.SECURITYRESULT_FAIL, 'Unsupported security type')
this.end()
return
}
debug('client security', this._clientSecurity)
this._writeSecurityResult(VncConnection.SECURITYRESULT_OK)
this.emit('authenticated')
this._changeState(VncConnection.STATE_NEED_CLIENT_INIT)
}
break
case VncConnection.STATE_NEED_CLIENT_INIT:
if ((chunk = this._consume(1))) {
this._clientShare = chunk[0]
debug('client shareFlag', this._clientShare)
this._writeServerInit()
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE:
if ((chunk = this._consume(1))) {
switch (chunk[0]) {
case VncConnection.CLIENT_MESSAGE_SETPIXELFORMAT:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT)
break
case VncConnection.CLIENT_MESSAGE_SETENCODINGS:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS)
break
case VncConnection.CLIENT_MESSAGE_FBUPDATEREQUEST:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST)
break
case VncConnection.CLIENT_MESSAGE_KEYEVENT:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT)
break
case VncConnection.CLIENT_MESSAGE_POINTEREVENT:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT)
break
case VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT)
break
default:
this._error(new Error(util.format(
'Unsupported message type %d', chunk[0])))
return
}
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT:
if ((chunk = this._consume(19))) {
// [0b, 3b) padding
this._clientPixelFormat = new PixelFormat({
bitsPerPixel: chunk[3]
, depth: chunk[4]
, bigEndianFlag: chunk[5]
, trueColorFlag: chunk[6]
, redMax: chunk.readUInt16BE(7, true)
, greenMax: chunk.readUInt16BE(9, true)
, blueMax: chunk.readUInt16BE(11, true)
, redShift: chunk[13]
, greenShift: chunk[14]
, blueShift: chunk[15]
})
// [16b, 19b) padding
debug('client pixel format', this._clientPixelFormat)
this.emit('formatchange', this._clientPixelFormat)
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS:
if ((chunk = this._consume(3))) {
// [0b, 1b) padding
this._clientEncodingCount = chunk.readUInt16BE(1, true)
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE:
lo = 0
hi = 4 * this._clientEncodingCount
if ((chunk = this._consume(hi))) {
this._clientEncodings = []
while (lo < hi) {
this._clientEncodings.push(chunk.readInt32BE(lo, true))
lo += 4
}
debug('client encodings', this._clientEncodings)
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST:
if ((chunk = this._consume(9))) {
this.emit('fbupdaterequest', {
incremental: chunk[0]
, xPosition: chunk.readUInt16BE(1, true)
, yPosition: chunk.readUInt16BE(3, true)
, width: chunk.readUInt16BE(5, true)
, height: chunk.readUInt16BE(7, true)
})
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT:
if ((chunk = this._consume(7))) {
// downFlag = chunk[0]
// [1b, 3b) padding
// key = chunk.readUInt32BE(3, true)
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT:
if ((chunk = this._consume(5))) {
this.emit('pointer', {
buttonMask: chunk[0]
, xPosition: chunk.readUInt16BE(1, true) / this._serverWidth
, yPosition: chunk.readUInt16BE(3, true) / this._serverHeight
})
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT:
if ((chunk = this._consume(7))) {
// [0b, 3b) padding
this._clientCutTextLength = chunk.readUInt32BE(3)
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE:
if ((chunk = this._consume(this._clientCutTextLength))) {
// value = chunk
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
default:
throw new Error(util.format('Impossible state %d', this._state))
}
}
while (chunk)
}
}
VncConnection.prototype._parseVersion = function(chunk) {
if (chunk.equals(new Buffer('RFB 003.008\n'))) {
return VncConnection.V3_008
}
if (chunk.equals(new Buffer('RFB 003.007\n'))) {
return VncConnection.V3_007
}
if (chunk.equals(new Buffer('RFB 003.003\n'))) {
return VncConnection.V3_003
}
return null
}
VncConnection.prototype._parseSecurity = function(chunk) {
switch (chunk[0]) {
case VncConnection.SECURITY_NONE:
case VncConnection.SECURITY_VNC:
return chunk[0]
default:
return null
}
}
VncConnection.prototype._changeState = function(state) {
this._state = state
}
VncConnection.prototype._append = function(chunk) {
if (!chunk) {
return false
}
debug('in', chunk)
if (this._buffer) {
this._buffer = Buffer.concat(
[this._buffer, chunk], this._buffer.length + chunk.length)
}
else {
this._buffer = chunk
}
return true
}
VncConnection.prototype._consume = function(n) {
var chunk
if (!this._buffer) {
return null
}
if (n < this._buffer.length) {
chunk = this._buffer.slice(0, n)
this._buffer = this._buffer.slice(n)
return chunk
}
if (n === this._buffer.length) {
chunk = this._buffer
this._buffer = null
return chunk
}
return null
}
VncConnection.prototype._write = function(chunk) {
debug('out', chunk)
this.conn.write(chunk)
}
module.exports = VncConnection

View file

@ -0,0 +1,14 @@
function PixelFormat(values) {
this.bitsPerPixel = values.bitsPerPixel
this.depth = values.depth
this.bigEndianFlag = values.bigEndianFlag
this.trueColorFlag = values.trueColorFlag
this.redMax = values.redMax
this.greenMax = values.greenMax
this.blueMax = values.blueMax
this.redShift = values.redShift
this.greenShift = values.greenShift
this.blueShift = values.blueShift
}
module.exports = PixelFormat

View file

@ -0,0 +1,66 @@
var util = require('util')
var EventEmitter = require('eventemitter3').EventEmitter
function PointerTranslator() {
this.previousEvent = null
}
util.inherits(PointerTranslator, EventEmitter)
PointerTranslator.prototype.push = function(event) {
if (event.buttonMask & 0xFE) {
// Non-primary buttons included, ignore.
return
}
if (this.previousEvent) {
var buttonChanges = event.buttonMask ^ this.previousEvent.buttonMask
// If the primary button changed, we have an up/down event.
if (buttonChanges & 1) {
// If it's pressed now, that's a down event.
if (event.buttonMask & 1) {
this.emit('touchdown', {
contact: 1
, x: event.xPosition
, y: event.yPosition
})
this.emit('touchcommit')
}
// It's not pressed, so we have an up event.
else {
this.emit('touchup', {
contact: 1
})
this.emit('touchcommit')
}
}
// Otherwise, if we're still holding the primary button down,
// that's a move event.
else if (event.buttonMask & 1) {
this.emit('touchmove', {
contact: 1
, x: event.xPosition
, y: event.yPosition
})
this.emit('touchcommit')
}
}
else {
// If it's the first event we get and the primary button's pressed,
// it's a down event.
if (event.buttonMask & 1) {
this.emit('touchdown', {
contact: 1
, x: event.xPosition
, y: event.yPosition
})
this.emit('touchcommit')
}
}
this.previousEvent = event
}
module.exports = PointerTranslator

View file

@ -0,0 +1,52 @@
var util = require('util')
var EventEmitter = require('eventemitter3').EventEmitter
var debug = require('debug')('vnc:server')
var VncConnection = require('./connection')
function VncServer(server, options) {
this.options = options
this._bound = {
_listeningListener: this._listeningListener.bind(this)
, _connectionListener: this._connectionListener.bind(this)
, _closeListener: this._closeListener.bind(this)
, _errorListener: this._errorListener.bind(this)
}
this.server = server
.on('listening', this._bound._listeningListener)
.on('connection', this._bound._connectionListener)
.on('close', this._bound._closeListener)
.on('error', this._bound._errorListener)
}
util.inherits(VncServer, EventEmitter)
VncServer.prototype.close = function() {
this.server.close()
}
VncServer.prototype.listen = function() {
this.server.listen.apply(this.server, arguments)
}
VncServer.prototype._listeningListener = function() {
this.emit('listening')
}
VncServer.prototype._connectionListener = function(conn) {
debug('connection', conn.remoteAddress, conn.remotePort)
this.emit('connection', new VncConnection(conn, this.options))
}
VncServer.prototype._closeListener = function() {
this.emit('close')
}
VncServer.prototype._errorListener = function(err) {
this.emit('error', err)
}
module.exports = VncServer

View file

@ -316,7 +316,7 @@ module.exports = function(options) {
// Spawn a device worker // Spawn a device worker
function spawn() { function spawn() {
var allocatedPorts = ports.splice(0, 2) var allocatedPorts = ports.splice(0, 4)
, proc = options.fork(device, allocatedPorts) , proc = options.fork(device, allocatedPorts)
, resolver = Promise.defer() , resolver = Promise.defer()

View file

@ -2,6 +2,11 @@ module.exports.list = function(val) {
return val.split(/\s*,\s*/g).filter(Boolean) return val.split(/\s*,\s*/g).filter(Boolean)
} }
module.exports.size = function(val) {
var match = /^(\d+)x(\d+)$/.exec(val)
return match ? [+match[1], +match[2]] : undefined
}
module.exports.allUnknownArgs = function(args) { module.exports.allUnknownArgs = function(args) {
return [].slice.call(args, 0, -1).filter(Boolean) return [].slice.call(args, 0, -1).filter(Boolean)
} }

View file

@ -40,6 +40,7 @@
"compression": "^1.5.2", "compression": "^1.5.2",
"cookie-session": "^1.2.0", "cookie-session": "^1.2.0",
"csurf": "^1.7.0", "csurf": "^1.7.0",
"debug": "^2.2.0",
"eventemitter3": "^0.1.6", "eventemitter3": "^0.1.6",
"express": "^4.13.3", "express": "^4.13.3",
"express-validator": "^2.17.1", "express-validator": "^2.17.1",
@ -49,6 +50,7 @@
"http-proxy": "^1.11.2", "http-proxy": "^1.11.2",
"in-publish": "^2.0.0", "in-publish": "^2.0.0",
"jade": "^1.9.2", "jade": "^1.9.2",
"jpeg-turbo": "^0.3.0",
"jws": "^3.1.0", "jws": "^3.1.0",
"ldapjs": "git+https://github.com/mcavage/node-ldapjs.git#acc1ca8f4314fd9d67561feabc8ce4c235076a5e", "ldapjs": "git+https://github.com/mcavage/node-ldapjs.git#acc1ca8f4314fd9d67561feabc8ce4c235076a5e",
"lodash": "^3.10.1", "lodash": "^3.10.1",