UNPKG

@u4/adbkit

Version:

A Typescript client for the Android Debug Bridge.

269 lines 10.6 kB
"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