@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
269 lines • 10.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnauthorizedError = exports.AuthError = void 0;
const events_1 = __importDefault(require("events"));
const crypto_1 = __importDefault(require("crypto"));
const util_1 = require("util");
const packetreader_1 = __importDefault(require("./packetreader"));
const rollingcounter_1 = __importDefault(require("./rollingcounter"));
const packet_1 = __importDefault(require("./packet"));
const auth_1 = __importDefault(require("../auth"));
const servicemap_1 = __importDefault(require("./servicemap"));
const service_1 = __importDefault(require("./service"));
const utils_1 = __importDefault(require("../utils"));
const debug = utils_1.default.debug('adb:tcpusb:socket');
const UINT32_MAX = 0xffffffff;
const UINT16_MAX = 0xffff;
const AUTH_TOKEN = 1;
const AUTH_SIGNATURE = 2;
const AUTH_RSAPUBLICKEY = 3;
const TOKEN_LENGTH = 20;
class AuthError extends Error {
constructor(message) {
super(message);
Object.setPrototypeOf(this, AuthError.prototype);
this.name = 'AuthError';
Error.captureStackTrace(this, Socket.AuthError);
}
}
exports.AuthError = AuthError;
class UnauthorizedError extends Error {
constructor() {
super('Unauthorized access');
Object.setPrototypeOf(this, UnauthorizedError.prototype);
this.name = 'UnauthorizedError';
Error.captureStackTrace(this, Socket.UnauthorizedError);
}
}
exports.UnauthorizedError = UnauthorizedError;
class Socket extends events_1.default {
constructor(client, serial, socket, options = {}) {
super();
this.client = client;
this.serial = serial;
this.socket = socket;
this.options = options;
this.ended = false;
this.authorized = false;
this.syncToken = new rollingcounter_1.default(UINT32_MAX);
this.remoteId = new rollingcounter_1.default(UINT32_MAX);
this.services = new servicemap_1.default();
this.version = 1;
this.maxPayload = 4096;
this.on = (event, listener) => super.on(event, listener);
this.off = (event, listener) => super.off(event, listener);
this.once = (event, listener) => super.once(event, listener);
this.emit = (event, ...args) => super.emit(event, ...args);
const base = this.options;
if (!base.auth)
base.auth = () => Promise.resolve(true);
this.socket.setNoDelay(true);
this.reader = new packetreader_1.default(this.socket)
.on('packet', this._handle.bind(this))
.on('error', (err) => {
debug(`PacketReader error: ${err.message}`);
return this.end();
})
.on('end', () => this.end());
this.remoteAddress = this.socket.remoteAddress;
this.token = undefined;
this.signature = undefined;
}
end() {
if (this.ended) {
return this;
}
// End services first so that they can send a final payload before FIN.
this.services.end();
this.socket.end();
this.ended = true;
this.emit('end', true);
return this;
}
_error(err) {
this.emit('error', err);
return this.end();
}
async _handle(packet) {
if (this.ended) {
return false;
}
this.emit('userActivity', packet);
try {
switch (packet.command) {
case packet_1.default.A_SYNC:
return Promise.resolve(this._handleSyncPacket());
case packet_1.default.A_CNXN:
return this._handleConnectionPacket(packet);
case packet_1.default.A_OPEN: {
const r = this._handleOpenPacket(packet);
return !!r;
}
case packet_1.default.A_OKAY:
case packet_1.default.A_WRTE:
case packet_1.default.A_CLSE: {
const r = await this._forwardServicePacket(packet);
return !!r;
}
case packet_1.default.A_AUTH:
return this._handleAuthPacket(packet);
default:
throw new Error(`Unknown command ${packet.command}`);
}
}
catch (err) {
if (err instanceof Socket.AuthError) {
this.end();
return false;
}
if (err instanceof Socket.UnauthorizedError) {
this.end();
return false;
}
if (err instanceof Error) {
this._error(err);
return false;
}
return false;
}
}
_handleSyncPacket() {
// No need to do anything?
debug('I:A_SYNC');
debug('O:A_SYNC');
return this.write(packet_1.default.assemble(packet_1.default.A_SYNC, 1, this.syncToken.next()));
}
async _handleConnectionPacket(packet) {
debug('I:A_CNXN', packet);
this.version = packet_1.default.swap32(packet.arg0);
this.maxPayload = Math.min(UINT16_MAX, packet.arg1);
const token = await this._createToken();
this.token = token;
debug(`Created challenge '${this.token.toString('base64')}'`);
debug('O:A_AUTH');
return this.write(packet_1.default.assemble(packet_1.default.A_AUTH, AUTH_TOKEN, 0, this.token));
}
async _handleAuthPacket(packet) {
debug('I:A_AUTH', packet);
switch (packet.arg0) {
case AUTH_SIGNATURE:
// Store first signature, ignore the rest
if (packet.data)
debug(`Received signature '${packet.data.toString('base64')}'`);
if (!this.signature) {
this.signature = packet.data;
}
debug('O:A_AUTH');
const b = this.write(packet_1.default.assemble(packet_1.default.A_AUTH, AUTH_TOKEN, 0, this.token));
return b;
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')}'`);
const key = await auth_1.default.parsePublicKey(this._skipNull(packet.data).toString());
if (!this.token)
throw Error('missing token in socket:_handleAuthPacket');
const digest = this.token.toString('binary');
const sig = this.signature.toString('binary');
if (!key.verify(digest, sig)) {
debug('Signature mismatch');
throw new Socket.AuthError('Signature mismatch');
}
debug('Signature verified');
if (this.options.auth) {
try {
await this.options.auth(key);
}
catch (e) {
debug('Connection rejected by user-defined auth handler', e);
throw new Socket.AuthError('Rejected by user-defined handler');
}
}
const id = await this._deviceId();
this.authorized = true;
debug('O:A_CNXN');
return this.write(packet_1.default.assemble(packet_1.default.A_CNXN, packet_1.default.swap32(this.version), this.maxPayload, id));
default:
throw new Error(`Unknown authentication method ${packet.arg0}`);
}
}
_handleOpenPacket(packet) {
if (!this.authorized) {
throw new Socket.UnauthorizedError();
}
const remoteId = packet.arg0;
const localId = this.remoteId.next();
if (!(packet.data && packet.data.length >= 2)) {
throw new Error('Empty service name');
}
const name = this._skipNull(packet.data);
debug(`Calling ${name}`);
const service = new service_1.default(this.client, this.serial, localId, remoteId, this);
return new Promise((resolve, reject) => {
service.on('error', reject);
service.on('end', () => resolve(false));
this.services.insert(localId, service);
debug(`Handling ${this.services.count} services simultaneously`);
return service.handle(packet);
})
.catch(() => true)
.finally(() => {
this.services.remove(localId);
debug(`Handling ${this.services.count} services simultaneously`);
return service.end();
});
}
_forwardServicePacket(packet) {
if (!this.authorized) {
throw new Socket.UnauthorizedError();
}
const localId = packet.arg1;
const service = this.services.get(localId);
if (service) {
return service.handle(packet);
}
else {
debug('Received a packet to a service that may have been closed already');
return Promise.resolve(false);
}
}
write(chunk) {
if (this.ended) {
return false;
}
return this.socket.write(chunk);
}
_createToken() {
return (0, util_1.promisify)(crypto_1.default.randomBytes)(TOKEN_LENGTH);
}
_skipNull(data) {
return data.slice(0, -1); // Discard null byte at end
}
async _deviceId() {
debug('Loading device properties to form a standard device ID');
const properties = await this.client
.getDevice(this.serial)
.getProperties();
const id = (() => {
const ref = ['ro.product.name', 'ro.product.model', 'ro.product.device'];
const results = [];
for (let i = 0, len = ref.length; i < len; i++) {
const prop = ref[i];
results.push(`${prop}=${properties[prop]};`);
}
return results;
})().join('');
return Buffer.from(`device::${id}\x00`);
}
}
Socket.AuthError = AuthError;
Socket.UnauthorizedError = UnauthorizedError;
exports.default = Socket;
//# sourceMappingURL=socket.js.map