From 14a10a65d50b48181de675c99d8c2be20bf30975 Mon Sep 17 00:00:00 2001 From: Simon Chan Date: Thu, 27 Aug 2020 00:01:25 +0800 Subject: [PATCH] refactor: split core and web packages --- .vscode/settings.json | 2 + package.json | 4 +- packages/{webadb => adb-webusb}/LICENSE | 0 packages/adb-webusb/README.md | 11 ++ packages/adb-webusb/package-lock.json | 24 +++ packages/adb-webusb/package.json | 33 ++++ .../src/index.ts} | 79 +++++++-- packages/adb-webusb/tsconfig.json | 19 ++ packages/adb/LICENSE | 21 +++ packages/{webadb => adb}/README.md | 0 packages/{webadb => adb}/package-lock.json | 7 +- packages/{webadb => adb}/package.json | 6 +- .../{webadb/src/webadb.ts => adb/src/adb.ts} | 112 +++++++----- packages/adb/src/auth.ts | 110 ++++++++++++ packages/adb/src/backend.ts | 23 +++ packages/adb/src/base64.ts | 159 +++++++++++++++++ packages/{webadb => adb}/src/crypto.ts | 18 +- packages/adb/src/index.ts | 7 + packages/adb/src/packet.ts | 149 ++++++++++++++++ packages/{webadb => adb}/src/stream.ts | 144 ++++++++------- packages/{webadb => adb}/tsconfig.json | 0 packages/demo/package-lock.json | 165 +++++++++++------- packages/demo/package.json | 3 +- packages/demo/src/Connect.tsx | 38 ++-- packages/demo/src/Shell.tsx | 4 +- packages/demo/src/index.tsx | 22 +-- packages/event/LICENSE | 21 +++ packages/webadb/src/auth.ts | 78 --------- packages/webadb/src/base64.ts | 59 ------- packages/webadb/src/decode.ts | 27 --- packages/webadb/src/index.ts | 2 - packages/webadb/src/packet.ts | 79 --------- tsconfig.base.json | 1 - 33 files changed, 925 insertions(+), 502 deletions(-) rename packages/{webadb => adb-webusb}/LICENSE (100%) create mode 100644 packages/adb-webusb/README.md create mode 100644 packages/adb-webusb/package-lock.json create mode 100644 packages/adb-webusb/package.json rename packages/{webadb/src/transportation.ts => adb-webusb/src/index.ts} (59%) create mode 100644 packages/adb-webusb/tsconfig.json create mode 100644 packages/adb/LICENSE rename packages/{webadb => adb}/README.md (100%) rename packages/{webadb => adb}/package-lock.json (75%) rename packages/{webadb => adb}/package.json (88%) rename packages/{webadb/src/webadb.ts => adb/src/adb.ts} (60%) create mode 100644 packages/adb/src/auth.ts create mode 100644 packages/adb/src/backend.ts create mode 100644 packages/adb/src/base64.ts rename packages/{webadb => adb}/src/crypto.ts (92%) create mode 100644 packages/adb/src/index.ts create mode 100644 packages/adb/src/packet.ts rename packages/{webadb => adb}/src/stream.ts (59%) rename packages/{webadb => adb}/tsconfig.json (100%) create mode 100644 packages/event/LICENSE delete mode 100644 packages/webadb/src/auth.ts delete mode 100644 packages/webadb/src/base64.ts delete mode 100644 packages/webadb/src/decode.ts delete mode 100644 packages/webadb/src/index.ts delete mode 100644 packages/webadb/src/packet.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index e8ffbe51..5e4bbe0c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,9 @@ "CNXN", "RSASSA", "WRTE", + "addrs", "fluentui", + "getprop", "lapo", "reimplement", "tcpip", diff --git a/package.json b/package.json index 252b246e..b6349a31 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "private": true, "scripts": { "postinstall": "lerna bootstrap", - "build": "lerna run --scope @yume-chan/webadb build", - "build:watch": "lerna run --scope @yume-chan/webadb --stream build:watch", + "build": "lerna run --scope @yume-chan/adb-webusb build", + "build:watch": "lerna run --scope @yume-chan/adb-webusb --stream build:watch", "build:demo": "lerna run --scope demo --stream build", "start:demo": "lerna run --scope demo --stream start" }, diff --git a/packages/webadb/LICENSE b/packages/adb-webusb/LICENSE similarity index 100% rename from packages/webadb/LICENSE rename to packages/adb-webusb/LICENSE diff --git a/packages/adb-webusb/README.md b/packages/adb-webusb/README.md new file mode 100644 index 00000000..70148586 --- /dev/null +++ b/packages/adb-webusb/README.md @@ -0,0 +1,11 @@ +# `adb-webusb` + +> TODO: description + +## Usage + +``` +const adbWebusb = require('adb-webusb'); + +// TODO: DEMONSTRATE API +``` diff --git a/packages/adb-webusb/package-lock.json b/packages/adb-webusb/package-lock.json new file mode 100644 index 00000000..8211a3bc --- /dev/null +++ b/packages/adb-webusb/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "@yume-chan/adb-webusb", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/w3c-web-usb": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz", + "integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A==" + }, + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + }, + "typescript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.2.tgz", + "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", + "dev": true + } + } +} diff --git a/packages/adb-webusb/package.json b/packages/adb-webusb/package.json new file mode 100644 index 00000000..dbe3be38 --- /dev/null +++ b/packages/adb-webusb/package.json @@ -0,0 +1,33 @@ +{ + "name": "@yume-chan/adb-webusb", + "version": "0.0.1", + "description": "WebUSB backend for @yume-chan/adb", + "keywords": [ + "webusb", + "adb" + ], + "author": "Simon Chan ", + "homepage": "https://github.com/yume-chan/ya-webadb#readme", + "license": "MIT", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/yume-chan/ya-webadb.git" + }, + "scripts": { + "build": "tsc -b", + "build:watch": "tsc -b -w" + }, + "bugs": { + "url": "https://github.com/yume-chan/ya-webadb/issues" + }, + "devDependencies": { + "typescript": "4.0.2" + }, + "dependencies": { + "@types/w3c-web-usb": "1.0.4", + "@yume-chan/adb": "^0.0.1", + "@yume-chan/event": "^0.0.1", + "tslib": "2.0.1" + } +} diff --git a/packages/webadb/src/transportation.ts b/packages/adb-webusb/src/index.ts similarity index 59% rename from packages/webadb/src/transportation.ts rename to packages/adb-webusb/src/index.ts index 8d5ac170..4d6fc99d 100644 --- a/packages/webadb/src/transportation.ts +++ b/packages/adb-webusb/src/index.ts @@ -1,12 +1,5 @@ -export interface WebAdbTransportation { - readonly name: string | undefined; - - write(buffer: ArrayBuffer): void | Promise; - - read(length: number): ArrayBuffer | Promise; - - dispose(): void | Promise; -} +import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb'; +import { EventEmitter } from '@yume-chan/event'; export const WebUsbDeviceFilter: USBDeviceFilter = { classCode: 0xFF, @@ -14,7 +7,12 @@ export const WebUsbDeviceFilter: USBDeviceFilter = { protocolCode: 1, }; -export class WebUsbTransportation implements WebAdbTransportation { +const PrivateKeyStorageKey = 'private-key'; + +const Utf8Encoder = new TextEncoder(); +const Utf8Decoder = new TextDecoder(); + +export class WebUsbAdbBackend implements AdbBackend { public static async fromDevice(device: USBDevice) { await device.open(); @@ -44,13 +42,13 @@ export class WebUsbTransportation implements WebAdbTransportation { case 'in': inEndpointNumber = endpoint.endpointNumber; if (outEndpointNumber !== undefined) { - return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber); + return new WebUsbAdbBackend(device, inEndpointNumber, outEndpointNumber); } break; case 'out': outEndpointNumber = endpoint.endpointNumber; if (inEndpointNumber !== undefined) { - return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber); + return new WebUsbAdbBackend(device, inEndpointNumber, outEndpointNumber); } break; } @@ -66,7 +64,7 @@ export class WebUsbTransportation implements WebAdbTransportation { public static async pickDevice() { try { const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] }); - return WebUsbTransportation.fromDevice(device); + return WebUsbAdbBackend.fromDevice(device); } catch (e) { switch (e.name) { case 'NotFoundError': @@ -81,6 +79,9 @@ export class WebUsbTransportation implements WebAdbTransportation { public get name() { return this._device.productName; } + private readonly onDisconnectedEvent = new EventEmitter(); + public readonly onDisconnected = this.onDisconnectedEvent.event; + private _inEndpointNumber!: number; private _outEndpointNumber!: number; @@ -90,21 +91,63 @@ export class WebUsbTransportation implements WebAdbTransportation { this._outEndpointNumber = outEndPointNumber; } + public *iterateKeys(): Generator { + const privateKey = window.localStorage.getItem(PrivateKeyStorageKey); + if (privateKey) { + yield decodeBase64(privateKey); + } + } + + public async generateKey(): Promise { + const { privateKey: cryptoKey } = await crypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + // 65537 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: 'SHA-1', + }, + true, + ['sign', 'verify'] + ); + + const privateKey = await crypto.subtle.exportKey('pkcs8', cryptoKey); + window.localStorage.setItem(PrivateKeyStorageKey, encodeBase64(privateKey)); + return privateKey; + } + + public encodeUtf8(input: string): ArrayBuffer { + return Utf8Encoder.encode(input); + } + + public decodeUtf8(buffer: ArrayBuffer): string { + return Utf8Decoder.decode(buffer); + } + public async write(buffer: ArrayBuffer): Promise { await this._device.transferOut(this._outEndpointNumber, buffer); } public async read(length: number): Promise { - const result = await this._device.transferIn(this._inEndpointNumber, length); + try { + const result = await this._device.transferIn(this._inEndpointNumber, length); - if (result.status === 'stall') { - await this._device.clearHalt('in', this._inEndpointNumber); + if (result.status === 'stall') { + await this._device.clearHalt('in', this._inEndpointNumber); + } + + return result.data!.buffer; + } catch (e) { + if (e instanceof Error && e.name === 'NotFoundError') { + this.onDisconnectedEvent.fire(); + } + + throw e; } - - return result.data!.buffer; } public async dispose() { + this.onDisconnectedEvent.dispose(); await this._device.close(); } } diff --git a/packages/adb-webusb/tsconfig.json b/packages/adb-webusb/tsconfig.json new file mode 100644 index 00000000..a104adf0 --- /dev/null +++ b/packages/adb-webusb/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "lib", // /* Redirect output structure to the directory. */ + "rootDir": "./src", // /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "lib": [ + "ESNext", + "DOM" + ] + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../adb/tsconfig.json" + } + ] +} diff --git a/packages/adb/LICENSE b/packages/adb/LICENSE new file mode 100644 index 00000000..acc76b98 --- /dev/null +++ b/packages/adb/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Simon Chan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/webadb/README.md b/packages/adb/README.md similarity index 100% rename from packages/webadb/README.md rename to packages/adb/README.md diff --git a/packages/webadb/package-lock.json b/packages/adb/package-lock.json similarity index 75% rename from packages/webadb/package-lock.json rename to packages/adb/package-lock.json index c0817aca..1b88936f 100644 --- a/packages/webadb/package-lock.json +++ b/packages/adb/package-lock.json @@ -1,14 +1,9 @@ { - "name": "@yume-chan/webadb", + "name": "@yume-chan/adb", "version": "0.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { - "@types/w3c-web-usb": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz", - "integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A==" - }, "@yume-chan/async-operation-manager": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-2.0.0.tgz", diff --git a/packages/webadb/package.json b/packages/adb/package.json similarity index 88% rename from packages/webadb/package.json rename to packages/adb/package.json index 367478c4..5c56b474 100644 --- a/packages/webadb/package.json +++ b/packages/adb/package.json @@ -1,10 +1,9 @@ { - "name": "@yume-chan/webadb", + "name": "@yume-chan/adb", "version": "0.0.1", "description": "ADB (Android Debugging Bridge) for Web browsers", "keywords": [ - "adb", - "webusb" + "adb" ], "author": "Simon Chan ", "homepage": "https://github.com/yume-chan/ya-webadb#readme", @@ -25,7 +24,6 @@ "typescript": "4.0.2" }, "dependencies": { - "@types/w3c-web-usb": "^1.0.4", "@yume-chan/async-operation-manager": "^2.0.0", "@yume-chan/event": "^0.0.1", "tslib": "^2.0.1" diff --git a/packages/webadb/src/webadb.ts b/packages/adb/src/adb.ts similarity index 60% rename from packages/webadb/src/webadb.ts rename to packages/adb/src/adb.ts index 185c1647..9865f0cf 100644 --- a/packages/webadb/src/webadb.ts +++ b/packages/adb/src/adb.ts @@ -1,23 +1,9 @@ import { PromiseResolver } from '@yume-chan/async-operation-manager'; -import { AdbAuthHandler, PublicKeyAuthMethod, SignatureAuthMethod } from './auth'; -import { AdbPacket } from './packet'; -import { AdbStream, AdbStreamDispatcher } from './stream'; -import { WebAdbTransportation } from './transportation'; - -export enum AdbCommand { - Connect = 'CNXN', - Auth = 'AUTH', - OK = 'OKAY', - Close = 'CLSE', - Write = 'WRTE', - Open = 'OPEN', -} - -export enum AdbAuthType { - Token = 1, - Signature = 2, - PublicKey = 3, -} +import { AutoDisposable, DisposableList } from '@yume-chan/event'; +import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth'; +import { AdbBackend } from './backend'; +import { AdbCommand } from './packet'; +import { AdbPacketDispatcher, AdbStream } from './stream'; export enum AdbPropKey { Product = 'ro.product.name', @@ -26,10 +12,15 @@ export enum AdbPropKey { Features = 'features', } -export class WebAdb { - private _transportation: WebAdbTransportation; +export class Adb { + private backend: AdbBackend; - public get name() { return this._transportation.name; } + public get onDisconnected() { return this.backend.onDisconnected; } + + private _connected = false; + public get connected() { return this._connected; } + + public get name() { return this.backend.name; } private _product: string | undefined; public get product() { return this._product; } @@ -43,14 +34,16 @@ export class WebAdb { private _features: string[] | undefined; public get features() { return this._features; } - private streamDispatcher: AdbStreamDispatcher; + private packetDispatcher: AdbPacketDispatcher; - public constructor(transportation: WebAdbTransportation) { - this._transportation = transportation; - this.streamDispatcher = new AdbStreamDispatcher(transportation); + public constructor(backend: AdbBackend) { + this.backend = backend; + this.packetDispatcher = new AdbPacketDispatcher(backend); + + backend.onDisconnected(this.dispose, this); } - public async connect() { + public async connect(authenticators = AdbDefaultAuthenticators) { const version = 0x01000001; const features = [ @@ -73,8 +66,9 @@ export class WebAdb { ].join(','); const resolver = new PromiseResolver(); - const authHandler = new AdbAuthHandler([new SignatureAuthMethod(), PublicKeyAuthMethod]); - const removeListener = this.streamDispatcher.onPacket(async (e) => { + const authHandler = new AdbAuthenticationHandler(authenticators, this.backend); + const disposableList = new DisposableList(); + disposableList.add(this.packetDispatcher.onPacket(async (e) => { e.handled = true; const { packet } = e; @@ -89,12 +83,8 @@ export class WebAdb { resolver.resolve(); break; case AdbCommand.Auth: - if (packet.arg0 !== AdbAuthType.Token) { - throw new Error('Unknown auth type'); - } - const authPacket = await authHandler.tryNextAuth(e.packet); - await this.streamDispatcher.sendPacket(authPacket); + await this.packetDispatcher.sendPacket(authPacket); break; case AdbCommand.Close: // Last connection was interrupted @@ -104,17 +94,26 @@ export class WebAdb { throw new Error('Device not in correct state. Reconnect your device and try again'); } } catch (e) { - await this.dispose(); resolver.reject(e); } - }); + })); - await this.streamDispatcher.sendPacket(new AdbPacket(AdbCommand.Connect, version, 0x100000, `host::features=${features}`)); + disposableList.add(this.packetDispatcher.onReceiveError(e => { + resolver.reject(e); + })); + + await this.packetDispatcher.sendPacket( + AdbCommand.Connect, + version, + 0x100000, + `host::features=${features}` + ); try { await resolver.promise; + this._connected = true; } finally { - removeListener(); + disposableList.dispose(); } } @@ -151,9 +150,9 @@ export class WebAdb { } } - public async shell(command: string, ...args: string[]): Promise; - public async shell(): Promise; - public async shell(command?: string, ...args: string[]): Promise { + public shell(command: string, ...args: string[]): Promise; + public shell(): Promise; + public shell(command?: string, ...args: string[]): Promise { if (!command) { return this.createStream('shell:'); } else { @@ -161,16 +160,35 @@ export class WebAdb { } } - public async tcpip(port = 5555): Promise { + public async getDaemonTcpAddresses(): Promise { + const propAddr = (await this.shell('getprop', 'service.adb.listen_addrs')).trim(); + if (propAddr) { + return propAddr.split(','); + } + + let port = (await this.shell('getprop', 'service.adb.tcp.port')).trim(); + if (port) { + return [`0.0.0.0:${port}`]; + } + + port = (await this.shell('getprop', 'persist.adb.tcp.port')).trim(); + if (port) { + return [`0.0.0.0:${port}`]; + } + + return []; + } + + public setDaemonTcpPort(port = 5555): Promise { return this.createStreamAndReadAll(`tcpip:${port}`); } - public usb(): Promise { + public disableDaemonTcp(): Promise { return this.createStreamAndReadAll('usb:'); } - public async createStream(payload: string): Promise { - return this.streamDispatcher.createStream(payload); + public async createStream(service: string): Promise { + return this.packetDispatcher.createStream(service); } public async createStreamAndReadAll(payload: string): Promise { @@ -179,7 +197,7 @@ export class WebAdb { } public async dispose() { - await this.streamDispatcher.dispose(); - await this._transportation.dispose(); + await this.packetDispatcher.dispose(); + await this.backend.dispose(); } } diff --git a/packages/adb/src/auth.ts b/packages/adb/src/auth.ts new file mode 100644 index 00000000..882f00d7 --- /dev/null +++ b/packages/adb/src/auth.ts @@ -0,0 +1,110 @@ +import { AdbBackend, AdbKeyIterator } from './backend'; +import { encodeBase64 } from './base64'; +import { calculatePublicKey, sign } from './crypto'; +import { AdbCommand, AdbPacket } from './packet'; + +export enum AdbAuthType { + Token = 1, + Signature = 2, + PublicKey = 3, +} + +export interface AdbAuthenticator { + tryAuth(packet: AdbPacket): Promise; +} + +export interface AdbAuthenticatorConstructor { + new(backend: AdbBackend): AdbAuthenticator; +} + +export class AdbSignatureAuthenticator implements AdbAuthenticator { + private readonly backend: AdbBackend; + + private readonly iterator: AdbKeyIterator; + + public constructor(backend: AdbBackend) { + this.backend = backend; + this.iterator = backend.iterateKeys(); + } + + public async tryAuth(packet: AdbPacket): Promise { + if (packet.arg0 !== AdbAuthType.Token) { + return undefined; + } + + const next = await this.iterator.next(); + if (next.done) { + return undefined; + } + + const signature = sign(next.value, packet.payload!); + return new AdbPacket( + this.backend, + AdbCommand.Auth, + AdbAuthType.Signature, + 0, + signature + ); + } +} + +export class AdbPublicKeyAuthenticator implements AdbAuthenticator { + private backend: AdbBackend; + + public constructor(backend: AdbBackend) { + this.backend = backend; + } + + public async tryAuth(): Promise { + let privateKey: ArrayBuffer; + + const iterator = this.backend.iterateKeys(); + const next = await iterator.next(); + if (!next.done) { + privateKey = next.value; + } else { + privateKey = await this.backend.generateKey(); + } + + const publicKey = calculatePublicKey(privateKey); + return new AdbPacket( + this.backend, + AdbCommand.Auth, + AdbAuthType.PublicKey, + 0, + // adbd needs the extra null character + encodeBase64(publicKey) + '\0' + ); + } +} + +export const AdbDefaultAuthenticators: AdbAuthenticatorConstructor[] = [ + AdbSignatureAuthenticator, + AdbPublicKeyAuthenticator +]; + +export class AdbAuthenticationHandler { + public readonly authenticators: readonly AdbAuthenticator[]; + + private index = 0; + + public constructor( + authenticators: readonly AdbAuthenticatorConstructor[], + backend: AdbBackend + ) { + this.authenticators = authenticators.map(Constructor => new Constructor(backend)); + } + + public async tryNextAuth(packet: AdbPacket): Promise { + while (this.index < this.authenticators.length) { + const result = await this.authenticators[this.index].tryAuth(packet); + if (result) { + return result; + } + + this.index += 1; + } + + throw new Error('Cannot authenticate with device'); + } +} diff --git a/packages/adb/src/backend.ts b/packages/adb/src/backend.ts new file mode 100644 index 00000000..83dc16db --- /dev/null +++ b/packages/adb/src/backend.ts @@ -0,0 +1,23 @@ +import { Event } from '@yume-chan/event'; + +export type AdbKeyIterator = Iterator | AsyncIterator; + +export interface AdbBackend { + readonly name: string | undefined; + + readonly onDisconnected: Event; + + iterateKeys(): AdbKeyIterator; + + generateKey(): ArrayBuffer | Promise; + + encodeUtf8(input: string): ArrayBuffer; + + decodeUtf8(buffer: ArrayBuffer): string; + + write(buffer: ArrayBuffer): void | Promise; + + read(length: number): ArrayBuffer | Promise; + + dispose(): void | Promise; +} diff --git a/packages/adb/src/base64.ts b/packages/adb/src/base64.ts new file mode 100644 index 00000000..892f68f3 --- /dev/null +++ b/packages/adb/src/base64.ts @@ -0,0 +1,159 @@ +interface Base64CharRange { + start: number; + + length: number; + + end: number; + + offset: number; +} + +let ranges: Base64CharRange[] = []; +let chars: string[] = []; + +let offset = 0; +function addRange(start: string, end: string) { + const startCharCode = start.charCodeAt(0); + const endCharCode = end.charCodeAt(0); + const length = endCharCode - startCharCode + 1; + + for (let i = startCharCode; i <= endCharCode; i++) { + chars.push(String.fromCharCode(i)); + } + + ranges.push({ + start: startCharCode, + length: length, + end: endCharCode, + offset: startCharCode - offset, + }); + + offset += length; +} + +addRange('A', 'Z'); +addRange('a', 'z'); +addRange('0', '9'); +addRange('+', '+'); +addRange('/', '/'); + +ranges = ranges.sort((a, b) => a.end - b.end); + +function toValue(char: string): number { + const charCode = char.charCodeAt(0); + for (const range of ranges) { + if (charCode <= range.end) { + return charCode - range.offset; + } + } + throw new Error('Unknown Character'); +} + +export function encodeBase64(buffer: ArrayBuffer): string { + const array = new Uint8Array(buffer); + const length = buffer.byteLength; + const remainder = length % 3; + let result = ''; + + let i = 0; + for (; i < length - remainder;) { + // aaaaaabb + const x = array[i]; + i += 1; + // bbbbcccc + const y = array[i]; + i += 1; + // ccdddddd + const z = array[i]; + i += 1; + + result += chars[x >> 2]; + result += chars[((x & 0b11) << 4) | (y >> 4)]; + result += chars[((y & 0b1111) << 2) | (z >> 6)]; + result += chars[z & 0b111111]; + } + + if (remainder === 1) { + // aaaaaabb + const x = array[i]; + + result += chars[x >> 2]; + result += chars[((x & 0b11) << 4)]; + result += '=='; + } else if (remainder === 2) { + // aaaaaabb + const x = array[i]; + i += 1; + // bbbbcccc + const y = array[i]; + + result += chars[x >> 2]; + result += chars[((x & 0b11) << 4) | (y >> 4)]; + result += chars[((y & 0b1111) << 2)]; + result += '='; + } + + return result; +} + +export function decodeBase64(input: string): ArrayBuffer { + let padding: number; + if (input[input.length - 2] === '=') { + padding = 2; + } else if (input[input.length - 1] === '=') { + padding = 1; + } else { + padding = 0; + } + + const result = new Uint8Array(input.length / 4 * 3 - padding); + let sIndex = 0; + let dIndex = 0; + + while (sIndex < input.length - (padding !== 0 ? 4 : 0)) { + const a = toValue(input[sIndex]); + sIndex += 1; + + const b = toValue(input[sIndex]); + sIndex += 1; + + const c = toValue(input[sIndex]); + sIndex += 1; + + const d = toValue(input[sIndex]); + sIndex += 1; + + result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4); + dIndex += 1; + + result[dIndex] = ((b & 0b1111) << 4) | ((c & 0b11_1100) >> 2); + dIndex += 1; + + result[dIndex] = ((c & 0b11) << 6) | d; + dIndex += 1; + } + + if (padding === 1) { + const a = toValue(input[sIndex]); + sIndex += 1; + + const b = toValue(input[sIndex]); + sIndex += 1; + + const c = toValue(input[sIndex]); + + result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4); + dIndex += 1; + + result[dIndex] = ((b & 0b1111) << 4) | ((c & 0b11_1100) >> 2); + } else if (padding === 2) { + const a = toValue(input[sIndex]); + sIndex += 1; + + const b = toValue(input[sIndex]); + + result[dIndex] = (a << 2) | ((b & 0b11_0000) >> 4); + } + + return result.buffer; +} diff --git a/packages/webadb/src/crypto.ts b/packages/adb/src/crypto.ts similarity index 92% rename from packages/webadb/src/crypto.ts rename to packages/adb/src/crypto.ts index 9a713800..306a9c2c 100644 --- a/packages/webadb/src/crypto.ts +++ b/packages/adb/src/crypto.ts @@ -113,21 +113,7 @@ export function modInverse(a: number, m: number) { return (y % m + m) % m } -export async function generateKey(): Promise<[privateKey: ArrayBuffer, publicKey: ArrayBuffer]> { - const keyPair = await crypto.subtle.generateKey( - { - name: 'RSASSA-PKCS1-v1_5', - modulusLength: 2048, - // 65537 - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: 'SHA-1', - }, - true, - ['sign', 'verify'] - ); - - const privateKey = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey); - +export function calculatePublicKey(privateKey: ArrayBuffer): ArrayBuffer { // Android has its own public key generation algorithm // See https://github.com/aosp-mirror/platform_system_core/blob/e5c9bbd45381d7bd72fef232d1c6668946253ac8/libcrypto_utils/android_pubkey.cpp#L111 @@ -170,7 +156,7 @@ export async function generateKey(): Promise<[privateKey: ArrayBuffer, publicKey // exponent publicKeyView.setUint32(8 + 256 + 256, 65537, true); - return [privateKey, publicKey]; + return publicKey; } export const Sha1DigestLength = 20; diff --git a/packages/adb/src/index.ts b/packages/adb/src/index.ts new file mode 100644 index 00000000..dea93607 --- /dev/null +++ b/packages/adb/src/index.ts @@ -0,0 +1,7 @@ +export * from './adb'; +export * from './auth'; +export * from './backend'; +export * from './base64'; +export * from './crypto'; +export * from './packet'; +export * from './stream'; diff --git a/packages/adb/src/packet.ts b/packages/adb/src/packet.ts new file mode 100644 index 00000000..b823cf11 --- /dev/null +++ b/packages/adb/src/packet.ts @@ -0,0 +1,149 @@ +import { AdbBackend } from './backend'; + +namespace Assert { + export function command(command: string): void { + const length = command.length; + if (length !== 4) { + throw new Error(`AdbPacket: command.length mismatch. Expected 4, but got ${length}`); + } + } + + export function magic(view: DataView): void { + const expected = view.getUint32(0) ^ 0xFFFFFFFF; + const actual = view.getInt32(20); + if (expected !== actual) { + throw new Error(`AdbPacket: magic number mismatch. Expected ${expected}, but got ${actual}`); + } + } +} + +export enum AdbCommand { + Connect = 'CNXN', + Auth = 'AUTH', + OK = 'OKAY', + Close = 'CLSE', + Write = 'WRTE', + Open = 'OPEN', +} + +export class AdbPacket { + public static async parse(backend: AdbBackend): Promise { + let buffer = await backend.read(24); + if (buffer.byteLength !== 24) { + // Maybe it's a payload from last connection. + // Ignore and try again + buffer = await backend.read(24); + } + const view = new DataView(buffer); + Assert.magic(view); + + const command = backend.decodeUtf8(buffer.slice(0, 4)); + const arg0 = view.getUint32(4, true); + const arg1 = view.getUint32(8, true); + const payloadLength = view.getUint32(12, true); + + let payload: ArrayBuffer | undefined; + if (payloadLength !== 0) { + payload = await backend.read(payloadLength); + } + + return new AdbPacket(backend, command, arg0, arg1, payload); + } + + public static send( + backend: AdbBackend, + command: AdbCommand, + arg0: number, + arg1: number, + payload?: string | ArrayBuffer + ): Promise { + const packet = new AdbPacket(backend, command, arg0, arg1, payload); + return packet.send(); + } + + private backend: AdbBackend; + + public command: string; + + public arg0: number; + + public arg1: number; + + private _payloadLength!: number; + public get payloadLength(): number { return this._payloadLength; } + + private _payload: ArrayBuffer | undefined; + public get payload(): ArrayBuffer | undefined { return this._payload; } + public set payload(value: ArrayBuffer | undefined) { + if (value !== undefined) { + this._payloadLength = value.byteLength; + this._payload = value; + } else { + this._payloadLength = 0; + this._payload = undefined; + } + + this._payloadString = undefined; + } + + private _payloadString: string | undefined; + public get payloadString(): string | undefined { + if (!this._payload) { + return undefined; + } + + if (!this._payloadString) { + this._payloadString = this.backend.decodeUtf8(this._payload); + } + return this._payloadString; + } + public set payloadString(value: string | undefined) { + if (value !== undefined) { + this.payload = this.backend.encodeUtf8(value); + this._payloadString = value; + } else { + this.payload = undefined; + } + } + + public constructor( + backend: AdbBackend, + command: string, + arg0: number, + arg1: number, + payload?: string | ArrayBuffer + ) { + this.backend = backend; + + Assert.command(command); + this.command = command; + + this.arg0 = arg0; + this.arg1 = arg1; + + if (typeof payload === "string") { + this.payloadString = payload; + } else { + this.payload = payload; + } + } + + public async send(): Promise { + const buffer = new ArrayBuffer(24); + const array = new Uint8Array(buffer); + array.set(new Uint8Array(this.backend.encodeUtf8(this.command))); + + const view = new DataView(buffer); + view.setUint32(4, this.arg0, true); + view.setUint32(8, this.arg1, true); + view.setUint32(12, this.payloadLength, true); + view.setUint32(16, /* checksum */ 0, true); + view.setUint32(20, /* magic */ view.getUint32(0, true) ^ 0xFFFFFFFF, true); + + await this.backend.write(buffer); + + if (this.payload) { + await this.backend.write(this.payload); + } + } +} diff --git a/packages/webadb/src/stream.ts b/packages/adb/src/stream.ts similarity index 59% rename from packages/webadb/src/stream.ts rename to packages/adb/src/stream.ts index f3bda7ce..78d40c92 100644 --- a/packages/webadb/src/stream.ts +++ b/packages/adb/src/stream.ts @@ -1,65 +1,68 @@ import AsyncOperationManager, { PromiseResolver } from '@yume-chan/async-operation-manager'; import { AutoDisposable, Disposable, Event, EventEmitter } from '@yume-chan/event'; -import { decode } from './decode'; import { AdbPacket } from './packet'; -import { WebAdbTransportation } from './transportation'; -import { AdbCommand } from './webadb'; +import { AdbBackend } from './backend'; +import { AdbCommand } from './packet'; -class AutoResetEvent { - private _list: PromiseResolver[] = []; +class AutoResetEvent implements Disposable { + private readonly list: PromiseResolver[] = []; - private _blocking: boolean = false; + private blocking: boolean = false; public wait(): Promise { - if (!this._blocking) { - this._blocking = true; + if (!this.blocking) { + this.blocking = true; - if (this._list.length === 0) { + if (this.list.length === 0) { return Promise.resolve(); } } const resolver = new PromiseResolver(); - this._list.push(resolver); + this.list.push(resolver); return resolver.promise; } public notify() { - if (this._list.length !== 0) { - this._list.pop()!.resolve(); + if (this.list.length !== 0) { + this.list.pop()!.resolve(); } else { - this._blocking = false; + this.blocking = false; } } + + public dispose() { + for (const item of this.list) { + item.reject(new Error('The AutoResetEvent has been disposed')); + } + this.list.length = 0; + } } export class AdbStreamController extends AutoDisposable { - private dispatcher: AdbStreamDispatcher; + private readonly writeLock = this.addDisposable(new AutoResetEvent()); - private writeLock = new AutoResetEvent(); + public readonly dispatcher: AdbPacketDispatcher; - private _localId: number; - public get localId() { return this._localId; } + public readonly localId: number; - private _remoteId: number; - public get remoteId() { return this._remoteId; } + public readonly remoteId: number; - public onDataEvent = this.addDisposable(new EventEmitter()); + public readonly onDataEvent = this.addDisposable(new EventEmitter()); - public onCloseEvent = this.addDisposable(new EventEmitter()); + public readonly onCloseEvent = this.addDisposable(new EventEmitter()); - public constructor(localId: number, remoteId: number, dispatcher: AdbStreamDispatcher) { + public constructor(localId: number, remoteId: number, dispatcher: AdbPacketDispatcher) { super(); - this._localId = localId; - this._remoteId = remoteId; + this.localId = localId; + this.remoteId = remoteId; this.dispatcher = dispatcher; - dispatcher.addStreamController(this); } public async write(data: ArrayBuffer): Promise { await this.writeLock.wait(); - await this.dispatcher.sendPacket(new AdbPacket(AdbCommand.Write, this.localId, this.remoteId, data)); + await this.dispatcher.sendPacket(AdbCommand.Write, this.localId, this.remoteId, data); } public ack() { @@ -67,49 +70,75 @@ export class AdbStreamController extends AutoDisposable { } public close() { - return this.dispatcher.sendPacket(new AdbPacket(AdbCommand.Close, this.localId, this.remoteId)); + return this.dispatcher.sendPacket(AdbCommand.Close, this.localId, this.remoteId); } } -export interface OnAdbPacketEventArgs { +export interface AdbPacketArrivedEventArgs { handled: boolean; packet: AdbPacket; } -export class AdbStreamDispatcher implements Disposable { - private transportation: WebAdbTransportation; +export class AdbPacketDispatcher extends AutoDisposable { + public readonly backend: AdbBackend; // ADB requires stream id to start from 1 // (0 means open failed) - private initializers = new AsyncOperationManager(1); - private streams = new Map(); + private readonly initializers = new AsyncOperationManager(1); + private readonly streams = new Map(); - private onPacketEvent = new EventEmitter(); - public get onPacket() { return this.onPacketEvent.event; } + private readonly onPacketEvent = this.addDisposable(new EventEmitter()); + public readonly onPacket = this.onPacketEvent.event; + + private readonly onReceiveErrorEvent = this.addDisposable(new EventEmitter()); + public readonly onReceiveError = this.onReceiveErrorEvent.event; private _running = true; public get running() { return this._running; } - public constructor(transportation: WebAdbTransportation) { - this.transportation = transportation; + public constructor(backend: AdbBackend) { + super(); + + this.backend = backend; this.receiveLoop(); } - public async createStream(payload: string): Promise { + public async createStream(service: string): Promise { const [localId, initializer] = this.initializers.add(); - await this.sendPacket(new AdbPacket(AdbCommand.Open, localId, 0, payload)); + await this.sendPacket(AdbCommand.Open, localId, 0, service); const remoteId = await initializer; - return new AdbStream(localId, remoteId, this); - } - - public addStreamController(controller: AdbStreamController) { + const controller = new AdbStreamController(localId, remoteId, this); this.streams.set(controller.localId, controller); + + return new AdbStream(controller); } - public sendPacket(packet: AdbPacket): Promise { - return packet.writeTo(this.transportation); + public sendPacket(packet: AdbPacket): Promise; + public sendPacket( + command: AdbCommand, + arg0: number, + arg1: number, + payload?: string | ArrayBuffer + ): Promise; + public sendPacket( + packetOrCommand: AdbPacket | AdbCommand, + arg0?: number, + arg1?: number, + payload?: string | ArrayBuffer + ): Promise { + if (arguments.length === 1) { + return (packetOrCommand as AdbPacket).send(); + } else { + return AdbPacket.send( + this.backend, + packetOrCommand as AdbCommand, + arg0 as number, + arg1 as number, + payload + ); + } } public async dispose() { @@ -121,23 +150,14 @@ export class AdbStreamDispatcher implements Disposable { stream.dispose(); } this.streams.clear(); - } - private async receivePacket() { - const header = await this.transportation.read(24); - const packet = AdbPacket.parse(header); - - if (packet.payloadLength !== 0) { - packet.payload = await this.transportation.read(packet.payloadLength); - } - - return packet; + super.dispose(); } private async receiveLoop() { while (this._running) { try { - const packet = await this.receivePacket(); + const packet = await AdbPacket.parse(this.backend); let handled = false; switch (packet.command) { case AdbCommand.OK: @@ -152,7 +172,7 @@ export class AdbStreamDispatcher implements Disposable { // Last connection sent an OPEN to device, // device now sent OKAY to this connection // tell the device to close the stream - this.sendPacket(new AdbPacket(AdbCommand.Close, packet.arg1, packet.arg0)); + this.sendPacket(AdbCommand.Close, packet.arg1, packet.arg0); } break; case AdbCommand.Close: @@ -175,14 +195,14 @@ export class AdbStreamDispatcher implements Disposable { case AdbCommand.Write: if (this.streams.has(packet.arg1)) { this.streams.get(packet.arg1)!.onDataEvent.fire(packet.payload!); - await this.sendPacket(new AdbPacket(AdbCommand.OK, packet.arg1, packet.arg0)); + await this.sendPacket(AdbCommand.OK, packet.arg1, packet.arg0); handled = true; } break; } if (!handled) { - const args: OnAdbPacketEventArgs = { + const args: AdbPacketArrivedEventArgs = { handled: false, packet, } @@ -197,7 +217,7 @@ export class AdbStreamDispatcher implements Disposable { // ignore error } - throw e; + this.onReceiveErrorEvent.fire(e); } } } @@ -214,15 +234,15 @@ export class AdbStream { public get onClose(): Event { return this.controller.onCloseEvent.event; } - public constructor(localId: number, remoteId: number, dispatcher: AdbStreamDispatcher) { - this.controller = new AdbStreamController(localId, remoteId, dispatcher); + public constructor(controller: AdbStreamController) { + this.controller = controller; } public async readAll(): Promise { const resolver = new PromiseResolver(); let output = ''; this.onData((data) => { - output += decode(data); + output += this.controller.dispatcher.backend.decodeUtf8(data); }); this.onClose(() => { resolver.resolve(output); diff --git a/packages/webadb/tsconfig.json b/packages/adb/tsconfig.json similarity index 100% rename from packages/webadb/tsconfig.json rename to packages/adb/tsconfig.json diff --git a/packages/demo/package-lock.json b/packages/demo/package-lock.json index d610fd5f..94d11f79 100644 --- a/packages/demo/package-lock.json +++ b/packages/demo/package-lock.json @@ -40,33 +40,44 @@ } }, "@fluentui/react-focus": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.13.1.tgz", - "integrity": "sha512-O61h/aC8ZZ0IEU0oqgBmrk2b/yNMBM0nghJ5IKwbGOD7s1bt/+FnAnJc+rmSD5Atrt7otPkMJma0yUu6qXqxHQ==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.13.2.tgz", + "integrity": "sha512-t5BKR5uZw8A9VFH0BpkKA25Bx0iJfz9cDqrdOzFCJl3tTcTZYnTYxr3SSuw7Psp2ApdYWcnOZI6frozS+AvMnA==", "requires": { "@fluentui/keyboard-key": "^0.2.11", - "@uifabric/merge-styles": "^7.16.7", + "@uifabric/merge-styles": "^7.17.0", "@uifabric/set-version": "^7.0.22", - "@uifabric/styling": "^7.15.1", - "@uifabric/utilities": "^7.29.1", + "@uifabric/styling": "^7.15.2", + "@uifabric/utilities": "^7.30.0", "tslib": "^1.10.0" } }, "@fluentui/react-icons": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.2.1.tgz", - "integrity": "sha512-FfHgiFrOmQEoGtnfzoL/1L6jmWXcZ+5GHc++TktSs6q/2XXDbU7qjE9pOankoKV+pJXKGHUPCLuNfI8j2eFnbA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.2.2.tgz", + "integrity": "sha512-pofZsHgbBVcyVr3I/LlbrX3P06NZnToxlH7cS9xy3LFcyrj629S5+iEXFdVk4O496MJ6qd4/FyxI/3Ru9UNlDg==", "requires": { "@microsoft/load-themed-styles": "^1.10.26", "@uifabric/set-version": "^7.0.22", - "@uifabric/utilities": "^7.29.1", + "@uifabric/utilities": "^7.30.0", + "tslib": "^1.10.0" + } + }, + "@fluentui/theme": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-0.1.1.tgz", + "integrity": "sha512-RwFv1YUKVeOuUNjLIO2SWBhqrwjUm1Ja8p0NLKInFd1UM/tAvwwZJtXpdzgz4SAQHxy1ceBLznzKkOiJv1+mBg==", + "requires": { + "@uifabric/merge-styles": "^7.17.0", + "@uifabric/set-version": "^7.0.22", + "@uifabric/utilities": "^7.30.0", "tslib": "^1.10.0" } }, "@microsoft/load-themed-styles": { - "version": "1.10.78", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.78.tgz", - "integrity": "sha512-ehoPzYqpoVNkKheGHR4B3wOax+5L745KeGN8m+N6Qm/yypmnahFMp0Jk8ilTvrAFNcZ4UQl8I2YRbTXLCGdP/g==" + "version": "1.10.79", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.79.tgz", + "integrity": "sha512-p4Ym7B2d8HT1K0VHAQkUw1SRmXQrko59lmqg/cr8ORClXIhoqc6Zt3m6I53olz4af2pXzpiI0ynHu6KmYYLOfQ==" }, "@types/glob": { "version": "7.1.3", @@ -149,31 +160,31 @@ } }, "@uifabric/foundation": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.8.1.tgz", - "integrity": "sha512-66P4Y2hJosLTl/QXSD780aSnxVR+ySmlugjgLgJkAUHPai84Ztbblv08p07GjDyN7SVs1/NRK7k9u7joMz8h8g==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.8.2.tgz", + "integrity": "sha512-TQKJ73zcpnSaGSmwuCrL06q9zmOJhhBaOJn3bxENb9038lMwuoMikgTWWouD1C0woH8/J7lCxd/hcO4USe0MSw==", "requires": { - "@uifabric/merge-styles": "^7.16.7", + "@uifabric/merge-styles": "^7.17.0", "@uifabric/set-version": "^7.0.22", - "@uifabric/styling": "^7.15.1", - "@uifabric/utilities": "^7.29.1", + "@uifabric/styling": "^7.15.2", + "@uifabric/utilities": "^7.30.0", "tslib": "^1.10.0" } }, "@uifabric/icons": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.4.1.tgz", - "integrity": "sha512-KgCYshGmcbLPoIIQK+OuKucM39smDSjsz7O4Hnu61xZmK1mOBCbFtBlgJRMlINZr0d6IH0vU2yrkp8SXHF2h0A==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.4.2.tgz", + "integrity": "sha512-dd+IxpuQzso3ys2D873nOl/C2vvw3vHEDkWjJxIWKoaTqB7KoWMKf50Q7ObBei19ue/WgqKvTMOtKG1bgJP0bw==", "requires": { "@uifabric/set-version": "^7.0.22", - "@uifabric/styling": "^7.15.1", + "@uifabric/styling": "^7.15.2", "tslib": "^1.10.0" } }, "@uifabric/merge-styles": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.7.tgz", - "integrity": "sha512-yOjCexa1/I6c7p4lsgKGWbzoljVidg4IxigbeLjcvACvTdOFoLXguxWBIXn31Ralo28Ih4GC6ykhUTBGfsNTXg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.17.0.tgz", + "integrity": "sha512-UFG0pKqjz6iGcYhDFDSb2br5TEOys90mCaXeXPln8QrIc5NTJG1gWeO4znG6r65e23Ab/hBz+lmoyfYxgr3fZw==", "requires": { "@uifabric/set-version": "^7.0.22", "tslib": "^1.10.0" @@ -198,23 +209,24 @@ } }, "@uifabric/styling": { - "version": "7.15.1", - "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.15.1.tgz", - "integrity": "sha512-jNEKVEo77HBGXSMwILNqfTYzXOoPY9AMRHF2TViBodKsSd8EqaEh1Q5yDj7N76Db1bgyIWW0xgjp6W+9ZaMA0w==", + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.15.2.tgz", + "integrity": "sha512-J1S1i87+Re1258saDFMIbJps8w9mtL/lxFJLa7cCwt642xpbwx9L/11mTT9zBrf2yUYLK+KxCrAOYQS92uY01Q==", "requires": { + "@fluentui/theme": "^0.1.1", "@microsoft/load-themed-styles": "^1.10.26", - "@uifabric/merge-styles": "^7.16.7", + "@uifabric/merge-styles": "^7.17.0", "@uifabric/set-version": "^7.0.22", - "@uifabric/utilities": "^7.29.1", + "@uifabric/utilities": "^7.30.0", "tslib": "^1.10.0" } }, "@uifabric/utilities": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.29.1.tgz", - "integrity": "sha512-LaXkxFCn1SQcD1HvhX8PG1jjnK2DG4xKD7PE95rt0+QMvI+10q9qZv6dVhPD2kLCf+8S06elgBE6xbQnmTQFNg==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.30.0.tgz", + "integrity": "sha512-NO+XkCBsaxVzOA+JytgSGAK3ihPDG5XLPQQhFbAXSEc5eC6qzcSY6JswRPy3hC1DIWicTUh+iTrDbb+x2vZ8Wg==", "requires": { - "@uifabric/merge-styles": "^7.16.7", + "@uifabric/merge-styles": "^7.17.0", "@uifabric/set-version": "^7.0.22", "prop-types": "^15.7.2", "tslib": "^1.10.0" @@ -743,6 +755,15 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } } } }, @@ -1346,9 +1367,9 @@ "dev": true }, "csstype": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz", - "integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz", + "integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==", "dev": true }, "cyclist": { @@ -1724,9 +1745,9 @@ "dev": true }, "eventemitter3": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.5.tgz", - "integrity": "sha512-QR0rh0YiPuxuDQ6+T9GAO/xWTExXpxIes1Nl9RykNGTnE1HJmkuEfxJH9cubjIOQZ/GH4qNBR4u8VSHaKiWs4g==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.6.tgz", + "integrity": "sha512-s3GJL04SQoM+gn2c14oyqxvZ3Pcq7cduSDqy3sBFXx6UPSUmgVYwQM9zwkTn9je0lrfg0gHEwR42pF3Q2dCQkQ==", "dev": true }, "events": { @@ -2670,12 +2691,12 @@ "dev": true }, "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", "dev": true, "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "icss-utils": { @@ -3603,23 +3624,35 @@ "dev": true }, "office-ui-fabric-react": { - "version": "7.130.0", - "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.130.0.tgz", - "integrity": "sha512-HV5LFSZAp8t3uJrGXhwPc/KHFPSGOka3BehRQnzN+7Ke++Abmynq7sIJSj36sNPa13uSiESfCFf30eY5ajnqPQ==", + "version": "7.131.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.131.0.tgz", + "integrity": "sha512-KweqH/OFq78z/W64Z1cSHxh8CBcrVOuYW5MwDtRIGlPorgOAYeS+z/Kx6cqJhlZIwRHLCrIgTKZ032vYXXtOrA==", "requires": { "@fluentui/date-time-utilities": "^7.6.0", - "@fluentui/react-focus": "^7.13.1", - "@fluentui/react-icons": "^0.2.1", + "@fluentui/react-focus": "^7.13.2", + "@fluentui/react-icons": "^0.2.2", "@microsoft/load-themed-styles": "^1.10.26", - "@uifabric/foundation": "^7.8.1", - "@uifabric/icons": "^7.4.1", - "@uifabric/merge-styles": "^7.16.7", - "@uifabric/react-hooks": "^7.9.1", + "@uifabric/foundation": "^7.8.2", + "@uifabric/icons": "^7.4.2", + "@uifabric/merge-styles": "^7.17.0", + "@uifabric/react-hooks": "^7.10.0", "@uifabric/set-version": "^7.0.22", - "@uifabric/styling": "^7.15.1", - "@uifabric/utilities": "^7.29.1", + "@uifabric/styling": "^7.15.2", + "@uifabric/utilities": "^7.30.0", "prop-types": "^15.7.2", "tslib": "^1.10.0" + }, + "dependencies": { + "@uifabric/react-hooks": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@uifabric/react-hooks/-/react-hooks-7.10.0.tgz", + "integrity": "sha512-zbxsFrXexfj5bPDBDh4BNFZj8vs8Jf6jmvZYRk+r/ZtvfceDyjuWcF+fYc2fjCon6pDfL5N8/S5Bc4NRw6sfvw==", + "requires": { + "@uifabric/set-version": "^7.0.22", + "@uifabric/utilities": "^7.30.0", + "tslib": "^1.10.0" + } + } } }, "on-finished": { @@ -4146,6 +4179,15 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } } } }, @@ -4810,17 +4852,6 @@ "loader-utils": "^2.0.0", "schema-utils": "^2.7.0", "source-map": "^0.6.1" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } } }, "source-map-resolve": { diff --git a/packages/demo/package.json b/packages/demo/package.json index 647597ac..e82cbeab 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -34,7 +34,8 @@ "dependencies": { "@fluentui/react": "7.130.0", "@uifabric/react-hooks": "7.9.1", - "@yume-chan/webadb": "^0.0.1", + "@yume-chan/adb": "^0.0.1", + "@yume-chan/adb-webusb": "^0.0.1", "react": "16.13.1", "react-dom": "16.13.1", "react-router-dom": "5.2.0", diff --git a/packages/demo/src/Connect.tsx b/packages/demo/src/Connect.tsx index 2b5f101c..3838f13f 100644 --- a/packages/demo/src/Connect.tsx +++ b/packages/demo/src/Connect.tsx @@ -1,13 +1,14 @@ import { DefaultButton, Dialog, DialogFooter, DialogType, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react'; import { useBoolean } from '@uifabric/react-hooks'; -import { WebAdb, WebUsbTransportation } from '@yume-chan/webadb'; +import { Adb } from '@yume-chan/adb'; +import { WebUsbAdbBackend } from '@yume-chan/adb-webusb'; import React, { useCallback, useEffect, useState } from 'react'; import withDisplayName from './withDisplayName'; interface ConnectProps { - device: WebAdb | undefined; + device: Adb | undefined; - onDeviceChange: (device: WebAdb | undefined) => void; + onDeviceChange: (device: Adb | undefined) => void; } export default withDisplayName('Connect', ({ @@ -21,12 +22,17 @@ export default withDisplayName('Connect', ({ const connect = useCallback(async () => { try { - const transportation = await WebUsbTransportation.pickDevice(); - if (transportation) { - const device = new WebAdb(transportation); - setConnecting(true); - await device.connect(); - onDeviceChange(device); + const backend = await WebUsbAdbBackend.pickDevice(); + if (backend) { + const device = new Adb(backend); + try { + setConnecting(true); + await device.connect(); + onDeviceChange(device); + } catch (e) { + device.dispose(); + throw e; + } } } catch (e) { setErrorMessage(e.message); @@ -47,17 +53,9 @@ export default withDisplayName('Connect', ({ }, [device]); useEffect(() => { - function handler(e: USBConnectionEvent) { - if (e.device.productName === device?.name) { - onDeviceChange(undefined); - } - } - - window.navigator.usb.addEventListener('disconnect', handler); - - return () => { - window.navigator.usb.removeEventListener('disconnect', handler); - }; + return device?.onDisconnected(() => { + onDeviceChange(undefined); + }); }, [device, onDeviceChange]); return ( diff --git a/packages/demo/src/Shell.tsx b/packages/demo/src/Shell.tsx index 752bd96f..176ad44b 100644 --- a/packages/demo/src/Shell.tsx +++ b/packages/demo/src/Shell.tsx @@ -1,4 +1,4 @@ -import { WebAdb } from '@yume-chan/webadb'; +import { Adb } from '@yume-chan/adb'; import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; @@ -13,7 +13,7 @@ const containerStyle: CSSProperties = { }; export interface ShellProps { - device: WebAdb | undefined; + device: Adb | undefined; visible: boolean; } diff --git a/packages/demo/src/index.tsx b/packages/demo/src/index.tsx index 765d701a..1cabe577 100644 --- a/packages/demo/src/index.tsx +++ b/packages/demo/src/index.tsx @@ -1,6 +1,6 @@ import { Label, Link, MessageBar, Nav, PrimaryButton, Separator, Stack, StackItem, Text, TextField } from '@fluentui/react'; import { useId } from '@uifabric/react-hooks'; -import { WebAdb } from '@yume-chan/webadb'; +import { Adb } from '@yume-chan/adb'; import { initializeIcons } from 'office-ui-fabric-react/lib/Icons'; import React, { useCallback, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; @@ -14,12 +14,12 @@ initializeIcons(); function App(): JSX.Element | null { const location = useLocation(); - const [device, setDevice] = useState(); + const [device, setDevice] = useState(); - const [tcpPort, setTcpPort] = useState(); + const [tcpPort, setTcpAddresses] = useState(); useEffect(() => { if (!device) { - setTcpPort(undefined); + setTcpAddresses(undefined); } }, [device]); const queryTcpPort = useCallback(async () => { @@ -27,8 +27,8 @@ function App(): JSX.Element | null { return; } - const result = await device.shell('getprop service.adb.tcp.port'); - setTcpPort(Number.parseInt(result, 10)); + const result = await device.getDaemonTcpAddresses(); + setTcpAddresses(result); }, [device]); const [tcpPortValue, setTcpPortValue] = useState('5555'); @@ -38,7 +38,7 @@ function App(): JSX.Element | null { return; } - const result = await device.tcpip(Number.parseInt(tcpPortValue, 10)); + const result = await device.setDaemonTcpPort(Number.parseInt(tcpPortValue, 10)); console.log(result); }, [device, tcpPortValue]); @@ -47,7 +47,7 @@ function App(): JSX.Element | null { return; } - const result = await device.usb(); + const result = await device.disableDaemonTcp(); console.log(result); }, [device]); @@ -132,8 +132,8 @@ function App(): JSX.Element | null { {tcpPort !== undefined && - (tcpPort !== 0 - ? `Enabled at port ${tcpPort}` + (tcpPort.length !== 0 + ? `Enabled at ${tcpPort.join(', ')}` : 'Disabled')} @@ -161,7 +161,7 @@ function App(): JSX.Element | null { diff --git a/packages/event/LICENSE b/packages/event/LICENSE new file mode 100644 index 00000000..acc76b98 --- /dev/null +++ b/packages/event/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Simon Chan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/webadb/src/auth.ts b/packages/webadb/src/auth.ts deleted file mode 100644 index 61baf5a3..00000000 --- a/packages/webadb/src/auth.ts +++ /dev/null @@ -1,78 +0,0 @@ -import base64Encode from './base64'; -import { generateKey, sign } from './crypto'; -import { stringToArrayBuffer } from './decode'; -import { AdbPacket } from './packet'; -import { AdbAuthType, AdbCommand } from './webadb'; - -export interface AdbAuthMethod { - tryAuth(packet: AdbPacket): Promise; -} - -const PublicKeyStorageKey = 'public-key'; -const PrivateKeyStorageKey = 'private-key'; - -export class SignatureAuthMethod implements AdbAuthMethod { - private keys: string[] = []; - - private index = 0; - - public constructor() { - const privateKeyBase64 = window.localStorage.getItem(PrivateKeyStorageKey); - if (privateKeyBase64) { - this.keys.push(privateKeyBase64); - } - } - - public async tryAuth(packet: AdbPacket): Promise { - if (this.index === this.keys.length) { - return undefined - } - - const privateKey = stringToArrayBuffer(atob(this.keys[this.index])); - this.index += 1; - - const signature = sign(privateKey, packet.payload!); - return new AdbPacket(AdbCommand.Auth, AdbAuthType.Signature, 0, signature); - } -} - -export const PublicKeyAuthMethod: AdbAuthMethod = { - async tryAuth(): Promise { - let publicKeyBase64 = window.localStorage.getItem(PublicKeyStorageKey); - if (!publicKeyBase64) { - const [privateKey, publicKey] = await generateKey(); - - publicKeyBase64 = base64Encode(publicKey); - window.localStorage.setItem(PublicKeyStorageKey, publicKeyBase64); - - const privateKeyBase64 = base64Encode(privateKey); - window.localStorage.setItem(PrivateKeyStorageKey, privateKeyBase64); - } - - // adbd needs the extra null character - return new AdbPacket(AdbCommand.Auth, AdbAuthType.PublicKey, 0, publicKeyBase64 + '\0'); - } -} - -export class AdbAuthHandler { - public readonly methods: readonly AdbAuthMethod[]; - - private index = 0; - - public constructor(methods: readonly AdbAuthMethod[]) { - this.methods = methods; - } - - public async tryNextAuth(packet: AdbPacket): Promise { - while (this.index < this.methods.length) { - const result = await this.methods[this.index].tryAuth(packet); - if (result) { - return result; - } - - this.index += 1; - } - - throw new Error('Cannot authenticate with device'); - } -} diff --git a/packages/webadb/src/base64.ts b/packages/webadb/src/base64.ts deleted file mode 100644 index f1db5fcc..00000000 --- a/packages/webadb/src/base64.ts +++ /dev/null @@ -1,59 +0,0 @@ -let characterSet: string[] = []; -const pairs = [ - ['A', 'Z'], - ['a', 'z'], - ['0', '9'], -].map(pair => pair.map(character => character.charCodeAt(0))); - -for (const [begin, end] of pairs) { - for (let i = begin; i <= end; i += 1) { - characterSet.push(String.fromCharCode(i)); - } -} -characterSet.push('+', '/'); - -export default function base64Encode(buffer: ArrayBuffer) { - const array = new Uint8Array(buffer); - const length = buffer.byteLength; - const remainder = length % 3; - let result = ''; - - for (let i = 0; i < length - remainder; i += 3) { - // aaaaaabb - const x = array[i]; - // bbbbcccc - const y = array[i + 1]; - // ccdddddd - const z = array[i + 2]; - - const a = x >> 2; - const b = ((x & 0b11) << 4) | (y >> 4); - const c = ((y & 0b1111) << 2) | (z >> 6); - const d = z & 0b111111; - - result += characterSet[a] + characterSet[b] + characterSet[c] + characterSet[d]; - } - - if (remainder === 1) { - // aaaaaabb - const x = array[length - 1]; - - const a = x >> 2; - const b = ((x & 0b11) << 4); - - result += characterSet[a] + characterSet[b] + '=='; - } else if (remainder === 2) { - // aaaaaabb - const x = array[length - 2]; - // bbbbcccc - const y = array[length - 1]; - - const a = x >> 2; - const b = ((x & 0b11) << 4) | (y >> 4); - const c = ((y & 0b1111) << 2); - - result += characterSet[a] + characterSet[b] + characterSet[c] + '='; - } - - return result; -} diff --git a/packages/webadb/src/decode.ts b/packages/webadb/src/decode.ts deleted file mode 100644 index a457f2d1..00000000 --- a/packages/webadb/src/decode.ts +++ /dev/null @@ -1,27 +0,0 @@ -const textEncoder = new TextEncoder(); -const textDecoder = new TextDecoder(); - -// Polyfill for Chrome < 74 -if (!TextEncoder.prototype.encodeInto) { - TextEncoder.prototype.encodeInto = function (source: string, destination: Uint8Array) { - const array = this.encode(source); - destination.set(array); - return { read: source.length, written: array.length }; - } -} - -export function decode(buffer: ArrayBuffer): string { - return textDecoder.decode(buffer); -} - -export function encode(string: string): ArrayBuffer { - return textEncoder.encode(string); -} - -export function encodeInto(string: string, buffer: Uint8Array): void { - textEncoder.encodeInto(string, buffer); -} - -export function stringToArrayBuffer(input: string): ArrayBuffer { - return new Uint8Array(Array.from(input, c => c.charCodeAt(0))).buffer; -} diff --git a/packages/webadb/src/index.ts b/packages/webadb/src/index.ts deleted file mode 100644 index 78c12c45..00000000 --- a/packages/webadb/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './webadb'; -export * from './transportation'; diff --git a/packages/webadb/src/packet.ts b/packages/webadb/src/packet.ts deleted file mode 100644 index 9c6adc7b..00000000 --- a/packages/webadb/src/packet.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { decode, encode, encodeInto } from './decode'; -import { WebAdbTransportation } from './transportation'; - -export class AdbPacket { - public static parse(buffer: ArrayBuffer): AdbPacket { - const command = decode(buffer.slice(0, 4)); - - const view = new DataView(buffer); - const arg0 = view.getUint32(4, true); - const arg1 = view.getUint32(8, true); - const payloadLength = view.getUint32(12, true); - - const packet = new AdbPacket(command, arg0, arg1, undefined); - packet._payloadLength = payloadLength; - return packet; - } - - public command: string; - - public arg0: number; - - public arg1: number; - - private _payloadLength!: number; - public get payloadLength(): number { return this._payloadLength; } - - private _payload: ArrayBuffer | undefined; - public get payload(): ArrayBuffer | undefined { return this._payload; } - public set payload(value: ArrayBuffer | undefined) { - if (value !== undefined) { - this._payloadLength = value.byteLength; - this._payloadString = decode(value); - this._payload = value; - } else { - this._payloadLength = 0; - this._payloadString = undefined; - this._payload = undefined; - } - } - - private _payloadString: string | undefined; - public get payloadString(): string | undefined { return this._payloadString; } - - public constructor(command: string, arg0: number, arg1: number, payload?: string | ArrayBuffer) { - if (command.length !== 4) { - throw new TypeError('length of command must be 4'); - } - - this.command = command; - this.arg0 = arg0; - this.arg1 = arg1; - - if (typeof payload === "string") { - this.payload = encode(payload); - } else { - this.payload = payload; - } - } - - public async writeTo(transportation: WebAdbTransportation): Promise { - const buffer = new ArrayBuffer(24); - const array = new Uint8Array(buffer); - const view = new DataView(buffer); - - encodeInto(this.command, array); - - view.setUint32(4, this.arg0, true); - view.setUint32(8, this.arg1, true); - view.setUint32(12, this.payloadLength, true); - view.setUint32(16, /* checksum */ 0, true); - view.setUint32(20, /* magic */ view.getUint32(0, true) ^ 0xFFFFFFFF, true); - - await transportation.write(buffer); - - if (this.payload) { - await transportation.write(this.payload); - } - } -} diff --git a/tsconfig.base.json b/tsconfig.base.json index 3d1e9ce7..011107f2 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -5,7 +5,6 @@ "target": "ES2016", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "lib": [ // /* Specify library files to be included in the compilation. */ - "DOM", "ESNext" ], // "allowJs": true, /* Allow javascript files to be compiled. */