UNPKG

adbkit

Version:

A pure Node.js client for the Android Debug Bridge.

315 lines (269 loc) 9.73 kB
var Auth, EventEmitter, Forge, Packet, PacketReader, Parser, Promise, Protocol, RollingCounter, Service, ServiceMap, Socket, crypto, debug, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; crypto = require('crypto'); EventEmitter = require('events').EventEmitter; Promise = require('bluebird'); Forge = require('node-forge'); debug = require('debug')('adb:tcpusb:socket'); Parser = require('../parser'); Protocol = require('../protocol'); Auth = require('../auth'); Packet = require('./packet'); PacketReader = require('./packetreader'); Service = require('./service'); ServiceMap = require('./servicemap'); RollingCounter = require('./rollingcounter'); Socket = (function(superClass) { var AUTH_RSAPUBLICKEY, AUTH_SIGNATURE, AUTH_TOKEN, TOKEN_LENGTH, UINT16_MAX, UINT32_MAX; extend(Socket, superClass); UINT32_MAX = 0xFFFFFFFF; UINT16_MAX = 0xFFFF; AUTH_TOKEN = 1; AUTH_SIGNATURE = 2; AUTH_RSAPUBLICKEY = 3; TOKEN_LENGTH = 20; function Socket(client, serial, socket, options) { var base; this.client = client; this.serial = serial; this.socket = socket; this.options = options != null ? options : {}; (base = this.options).auth || (base.auth = Promise.resolve(true)); this.ended = false; this.socket.setNoDelay(true); this.reader = new PacketReader(this.socket).on('packet', this._handle.bind(this)).on('error', (function(_this) { return function(err) { debug("PacketReader error: " + err.message); return _this.end(); }; })(this)).on('end', this.end.bind(this)); this.version = 1; this.maxPayload = 4096; this.authorized = false; this.syncToken = new RollingCounter(UINT32_MAX); this.remoteId = new RollingCounter(UINT32_MAX); this.services = new ServiceMap; this.remoteAddress = this.socket.remoteAddress; this.token = null; this.signature = null; } Socket.prototype.end = function() { if (this.ended) { return this; } this.services.end(); this.socket.end(); this.ended = true; this.emit('end'); return this; }; Socket.prototype._error = function(err) { this.emit('error', err); return this.end(); }; Socket.prototype._handle = function(packet) { if (this.ended) { return; } this.emit('userActivity', packet); return Promise["try"]((function(_this) { return function() { switch (packet.command) { case Packet.A_SYNC: return _this._handleSyncPacket(packet); case Packet.A_CNXN: return _this._handleConnectionPacket(packet); case Packet.A_OPEN: return _this._handleOpenPacket(packet); case Packet.A_OKAY: case Packet.A_WRTE: case Packet.A_CLSE: return _this._forwardServicePacket(packet); case Packet.A_AUTH: return _this._handleAuthPacket(packet); default: throw new Error("Unknown command " + packet.command); } }; })(this))["catch"](Socket.AuthError, (function(_this) { return function() { return _this.end(); }; })(this))["catch"](Socket.UnauthorizedError, (function(_this) { return function() { return _this.end(); }; })(this))["catch"]((function(_this) { return function(err) { return _this._error(err); }; })(this)); }; Socket.prototype._handleSyncPacket = function(packet) { debug('I:A_SYNC'); debug('O:A_SYNC'); return this.write(Packet.assemble(Packet.A_SYNC, 1, this.syncToken.next(), null)); }; Socket.prototype._handleConnectionPacket = function(packet) { var version; debug('I:A_CNXN', packet); version = Packet.swap32(packet.arg0); this.maxPayload = Math.min(UINT16_MAX, packet.arg1); return this._createToken().then((function(_this) { return function(token) { _this.token = token; debug("Created challenge '" + (_this.token.toString('base64')) + "'"); debug('O:A_AUTH'); return _this.write(Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, _this.token)); }; })(this)); }; Socket.prototype._handleAuthPacket = function(packet) { debug('I:A_AUTH', packet); switch (packet.arg0) { case AUTH_SIGNATURE: debug("Received signature '" + (packet.data.toString('base64')) + "'"); if (!this.signature) { this.signature = packet.data; } debug('O:A_AUTH'); return this.write(Packet.assemble(Packet.A_AUTH, AUTH_TOKEN, 0, this.token)); case AUTH_RSAPUBLICKEY: if (!this.signature) { throw new Socket.AuthError("Public key sent before signature"); } if (!(packet.data && packet.data.length >= 2)) { throw new Socket.AuthError("Empty RSA public key"); } debug("Received RSA public key '" + (packet.data.toString('base64')) + "'"); return Auth.parsePublicKey(this._skipNull(packet.data)).then((function(_this) { return function(key) { var digest, sig; digest = _this.token.toString('binary'); sig = _this.signature.toString('binary'); if (!key.verify(digest, sig)) { debug("Signature mismatch"); throw new Socket.AuthError("Signature mismatch"); } debug("Signature verified"); return key; }; })(this)).then((function(_this) { return function(key) { return _this.options.auth(key)["catch"](function(err) { debug("Connection rejected by user-defined auth handler"); throw new Socket.AuthError("Rejected by user-defined handler"); }); }; })(this)).then((function(_this) { return function() { return _this._deviceId(); }; })(this)).then((function(_this) { return function(id) { _this.authorized = true; debug('O:A_CNXN'); return _this.write(Packet.assemble(Packet.A_CNXN, Packet.swap32(_this.version), _this.maxPayload, id)); }; })(this)); default: throw new Error("Unknown authentication method " + packet.arg0); } }; Socket.prototype._handleOpenPacket = function(packet) { var localId, name, remoteId, service; if (!this.authorized) { throw new Socket.UnauthorizedError(); } remoteId = packet.arg0; localId = this.remoteId.next(); if (!(packet.data && packet.data.length >= 2)) { throw new Error("Empty service name"); } name = this._skipNull(packet.data); debug("Calling " + name); service = new Service(this.client, this.serial, localId, remoteId, this); return new Promise((function(_this) { return function(resolve, reject) { service.on('error', reject); service.on('end', resolve); _this.services.insert(localId, service); debug("Handling " + _this.services.count + " services simultaneously"); return service.handle(packet); }; })(this))["catch"](function(err) { return true; })["finally"]((function(_this) { return function() { _this.services.remove(localId); debug("Handling " + _this.services.count + " services simultaneously"); return service.end(); }; })(this)); }; Socket.prototype._forwardServicePacket = function(packet) { var localId, remoteId, service; if (!this.authorized) { throw new Socket.UnauthorizedError(); } remoteId = packet.arg0; localId = packet.arg1; if (service = this.services.get(localId)) { return service.handle(packet); } else { return debug("Received a packet to a service that may have been closed already"); } }; Socket.prototype.write = function(chunk) { if (this.ended) { return; } return this.socket.write(chunk); }; Socket.prototype._createToken = function() { return Promise.promisify(crypto.randomBytes)(TOKEN_LENGTH); }; Socket.prototype._skipNull = function(data) { return data.slice(0, -1); }; Socket.prototype._deviceId = function() { debug("Loading device properties to form a standard device ID"); return this.client.getProperties(this.serial).then(function(properties) { var id, prop; id = ((function() { var i, len, ref, results; ref = ['ro.product.name', 'ro.product.model', 'ro.product.device']; results = []; for (i = 0, len = ref.length; i < len; i++) { prop = ref[i]; results.push(prop + "=" + properties[prop] + ";"); } return results; })()).join(''); return new Buffer("device::" + id + "\0"); }); }; return Socket; })(EventEmitter); Socket.AuthError = (function(superClass) { extend(AuthError, superClass); function AuthError(message) { this.message = message; Error.call(this); this.name = 'AuthError'; Error.captureStackTrace(this, Socket.AuthError); } return AuthError; })(Error); Socket.UnauthorizedError = (function(superClass) { extend(UnauthorizedError, superClass); function UnauthorizedError() { Error.call(this); this.name = 'UnauthorizedError'; this.message = "Unauthorized access"; Error.captureStackTrace(this, Socket.UnauthorizedError); } return UnauthorizedError; })(Error); module.exports = Socket;