adbkit
Version:
A pure Node.js client for the Android Debug Bridge.
315 lines (269 loc) • 9.73 kB
JavaScript
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;