From f5ac9d2315c04eff9fcb9b657e9e7f4f0c449fae Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Mon, 14 Apr 2014 20:57:24 +0900 Subject: [PATCH] Reverse port forwarding maybe works. --- lib/roles/device.js | 1 + lib/roles/device/plugins/forward.js | 244 ++++++++++++++++++++++++++++ lib/wire/wire.proto | 35 ++++ 3 files changed, 280 insertions(+) create mode 100644 lib/roles/device/plugins/forward.js diff --git a/lib/roles/device.js b/lib/roles/device.js index 3095fbc4..784fb117 100644 --- a/lib/roles/device.js +++ b/lib/roles/device.js @@ -26,6 +26,7 @@ module.exports = function(options) { .dependency(require('./device/plugins/shell')) .dependency(require('./device/plugins/touch')) .dependency(require('./device/plugins/install')) + .dependency(require('./device/plugins/forward')) .dependency(require('./device/plugins/owner')) .define(function(options, solo) { if (process.send) { diff --git a/lib/roles/device/plugins/forward.js b/lib/roles/device/plugins/forward.js new file mode 100644 index 00000000..dc38b713 --- /dev/null +++ b/lib/roles/device/plugins/forward.js @@ -0,0 +1,244 @@ +var net = require('net') +var util = require('util') + +var syrup = require('syrup') +var Promise = require('bluebird') +var split = require('split') + +var logger = require('../../../util/logger') +var devutil = require('../../../util/devutil') +var streamutil = require('../../../util/streamutil') +var lifecycle = require('../../../util/lifecycle') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') + +module.exports = syrup.serial() + .dependency(require('../support/adb')) + .dependency(require('../support/router')) + .dependency(require('../support/push')) + .dependency(require('../resources/remote')) + .define(function(options, adb, router, push, remote) { + var log = logger.createLogger('device:plugins:forward') + + var service = { + port: 2810 + , privatePorts: (function() { + var ports = [] + for (var i = 2520; i <= 2540; ++i) { + ports.push(i) + } + return ports + })() + , forwards: Object.create(null) + } + + function openService() { + log.info('Launching reverse port forwarding service') + return devutil.ensureUnusedPort(adb, options.serial, service.port) + .timeout(10000) + .then(function() { + return adb.shell(options.serial, [ + remote.bin + , '--lib', remote.lib + , '--listen-forward', service.port + ]) + .timeout(10000) + .then(function(out) { + lifecycle.share('Forward shell', out) + streamutil.talk(log, 'Forward shell says: "%s"', out) + }) + .then(function() { + return devutil.waitForPort(adb, options.serial, service.port) + }) + .timeout(10000) + .then(function(conn) { + conn.end() + }) + }) + } + + function createForward(data) { + log.info( + 'Reverse forwarding port %d to %s:%d' + , data.devicePort + , data.targetHost + , data.targetPort + ) + + var forward = service.forwards[data.devicePort] + + if (forward) { + if (forward.targetHost === data.targetHost && + forward.targetPort === data.targetPort) { + return Promise.resolve() + } + else if (forward.system) { + return Promise.reject(new Error('Cannot rebind system port')) + } + else { + removeForward(forward) + } + } + + return adb.openTcp(options.serial, service.port) + .timeout(10000) + .then(function(conn) { + var resolver = Promise.defer() + + var forward = { + devicePort: data.devicePort + , targetHost: data.targetHost + , targetPort: data.targetPort + , system: !!data.system + , privatePort: service.privatePorts.pop() + , connection: conn + } + + var parser = conn.pipe(split()) + + parser.on('data', function(chunk) { + var cmd = chunk.toString().trim() + switch (cmd) { + case 'OKAY': + resolver.resolve(forward) + break + case 'FAIL': + resolver.reject(new Error('Remote replied with FAIL')) + break + case 'CNCT': + adb.openTcp(options.serial, forward.privatePort) + .done(function(dstream) { + return tryConnect(forward) + .then(function(ustream) { + ustream.pipe(dstream) + dstream.pipe(ustream) + }) + }) + break + } + }) + + // Keep this around + function endListener() { + removeForward(forward) + } + + conn.on('end', endListener) + + conn.write(util.format( + 'FRWD %d %d\n' + , forward.devicePort + , forward.privatePort + )) + + return resolver.promise + }) + } + + function removeForward(data) { + var forward = service.forwards[data.devicePort] + if (forward) { + forward.connection.end() + delete service.forwards[data.devicePort] + } + } + + function tryConnect(data) { + var resolver = Promise.defer() + + var conn = net.connect({ + host: data.targetHost + , port: data.targetPort + }) + + function connectListener() { + resolver.resolve() + } + + function errorListener(err) { + resolver.reject(err) + } + + conn.on('connect', connectListener) + conn.on('error', errorListener) + + return resolver.promise.finally(function() { + conn.removeListener('connect', connectListener) + conn.removeListener('error', errorListener) + conn.end() + }) + } + + function resetForwards() { + Object.keys(service.forwards).forEach(function(privatePort) { + service.forwards[privatePort].connection.end() + delete service.forwards[privatePort] + }) + } + + function listForwards() { + return Object.keys(service.forwards).map(function(privatePort) { + var forward = service.forwards[privatePort] + return { + devicePort: forward.devicePort + , targetHost: forward.targetHost + , targetPort: forward.targetPort + , system: !!forward.system + } + }) + } + + return openService() + .then(function() { + router + .on(wire.ForwardTestMessage, function(channel, message) { + var reply = wireutil.reply(options.serial) + tryConnect(message) + .then(function(conn) { + conn.end() + push.send([ + channel + , reply.okay('success') + ]) + }) + .catch(function() { + push.send([ + channel + , reply.fail('fail_connect') + ]) + }) + }) + .on(wire.ForwardMessage, function(channel, message) { + var reply = wireutil.reply(options.serial) + createForward(message) + .then(function() { + push.send([ + channel + , reply.okay('success') + ]) + }) + .catch(function(err) { + log.error('Reverse port forwarding failed', err.stack) + push.send([ + channel + , reply.fail('fail_forward') + ]) + }) + }) + .on(wire.ForwardListMessage, function(channel) { + var reply = wireutil.reply(options.serial) + push.send([ + channel + , reply.okay('success', listForwards()) + ]) + }) + .on(wire.ForwardRemoveMessage, function(channel, message) { + var reply = wireutil.reply(options.serial) + removeForward(message) + push.send([ + channel + , reply.okay('success') + ]) + }) + }) + }) diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 5cfe2951..e580c9d8 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -36,6 +36,11 @@ enum MessageType { UngroupMessage = 27; UninstallMessage = 34; RotateMessage = 35; + ForwardMessage = 36; + ForwardTestMessage = 37; + ForwardListMessage = 38; + ForwardRemoveMessage = 39; + DeviceForwardsMessage = 40; } message Envelope { @@ -346,3 +351,33 @@ message LaunchActivityMessage { message RotateMessage { required int32 rotation = 1; } + +message ForwardTestMessage { + required string targetHost = 1; + required uint32 targetPort = 2; +} + +message ForwardMessage { + required uint32 devicePort = 1; + required string targetHost = 2; + required uint32 targetPort = 3; + required bool system = 4; +} + +message ForwardListMessage { +} + +message DeviceForward { + required uint32 devicePort = 1; + required string targetHost = 2; + required uint32 targetPort = 3; + required bool system = 4; +} + +message DeviceForwardsMessage { + repeated DeviceForward forwards = 1; +} + +message ForwardRemoveMessage { + required uint32 devicePort = 1; +}