mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
refactor: split core and web packages
This commit is contained in:
parent
c1b437999c
commit
14a10a65d5
33 changed files with 925 additions and 502 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -4,7 +4,9 @@
|
||||||
"CNXN",
|
"CNXN",
|
||||||
"RSASSA",
|
"RSASSA",
|
||||||
"WRTE",
|
"WRTE",
|
||||||
|
"addrs",
|
||||||
"fluentui",
|
"fluentui",
|
||||||
|
"getprop",
|
||||||
"lapo",
|
"lapo",
|
||||||
"reimplement",
|
"reimplement",
|
||||||
"tcpip",
|
"tcpip",
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "lerna bootstrap",
|
"postinstall": "lerna bootstrap",
|
||||||
"build": "lerna run --scope @yume-chan/webadb build",
|
"build": "lerna run --scope @yume-chan/adb-webusb build",
|
||||||
"build:watch": "lerna run --scope @yume-chan/webadb --stream build:watch",
|
"build:watch": "lerna run --scope @yume-chan/adb-webusb --stream build:watch",
|
||||||
"build:demo": "lerna run --scope demo --stream build",
|
"build:demo": "lerna run --scope demo --stream build",
|
||||||
"start:demo": "lerna run --scope demo --stream start"
|
"start:demo": "lerna run --scope demo --stream start"
|
||||||
},
|
},
|
||||||
|
|
11
packages/adb-webusb/README.md
Normal file
11
packages/adb-webusb/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# `adb-webusb`
|
||||||
|
|
||||||
|
> TODO: description
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
const adbWebusb = require('adb-webusb');
|
||||||
|
|
||||||
|
// TODO: DEMONSTRATE API
|
||||||
|
```
|
24
packages/adb-webusb/package-lock.json
generated
Normal file
24
packages/adb-webusb/package-lock.json
generated
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
packages/adb-webusb/package.json
Normal file
33
packages/adb-webusb/package.json
Normal file
|
@ -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 <cnsimonchan@live.com>",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,5 @@
|
||||||
export interface WebAdbTransportation {
|
import { AdbBackend, decodeBase64, encodeBase64 } from '@yume-chan/adb';
|
||||||
readonly name: string | undefined;
|
import { EventEmitter } from '@yume-chan/event';
|
||||||
|
|
||||||
write(buffer: ArrayBuffer): void | Promise<void>;
|
|
||||||
|
|
||||||
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
|
|
||||||
|
|
||||||
dispose(): void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||||
classCode: 0xFF,
|
classCode: 0xFF,
|
||||||
|
@ -14,7 +7,12 @@ export const WebUsbDeviceFilter: USBDeviceFilter = {
|
||||||
protocolCode: 1,
|
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) {
|
public static async fromDevice(device: USBDevice) {
|
||||||
await device.open();
|
await device.open();
|
||||||
|
|
||||||
|
@ -44,13 +42,13 @@ export class WebUsbTransportation implements WebAdbTransportation {
|
||||||
case 'in':
|
case 'in':
|
||||||
inEndpointNumber = endpoint.endpointNumber;
|
inEndpointNumber = endpoint.endpointNumber;
|
||||||
if (outEndpointNumber !== undefined) {
|
if (outEndpointNumber !== undefined) {
|
||||||
return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber);
|
return new WebUsbAdbBackend(device, inEndpointNumber, outEndpointNumber);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'out':
|
case 'out':
|
||||||
outEndpointNumber = endpoint.endpointNumber;
|
outEndpointNumber = endpoint.endpointNumber;
|
||||||
if (inEndpointNumber !== undefined) {
|
if (inEndpointNumber !== undefined) {
|
||||||
return new WebUsbTransportation(device, inEndpointNumber, outEndpointNumber);
|
return new WebUsbAdbBackend(device, inEndpointNumber, outEndpointNumber);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -66,7 +64,7 @@ export class WebUsbTransportation implements WebAdbTransportation {
|
||||||
public static async pickDevice() {
|
public static async pickDevice() {
|
||||||
try {
|
try {
|
||||||
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
|
const device = await navigator.usb.requestDevice({ filters: [WebUsbDeviceFilter] });
|
||||||
return WebUsbTransportation.fromDevice(device);
|
return WebUsbAdbBackend.fromDevice(device);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
switch (e.name) {
|
switch (e.name) {
|
||||||
case 'NotFoundError':
|
case 'NotFoundError':
|
||||||
|
@ -81,6 +79,9 @@ export class WebUsbTransportation implements WebAdbTransportation {
|
||||||
|
|
||||||
public get name() { return this._device.productName; }
|
public get name() { return this._device.productName; }
|
||||||
|
|
||||||
|
private readonly onDisconnectedEvent = new EventEmitter<void>();
|
||||||
|
public readonly onDisconnected = this.onDisconnectedEvent.event;
|
||||||
|
|
||||||
private _inEndpointNumber!: number;
|
private _inEndpointNumber!: number;
|
||||||
private _outEndpointNumber!: number;
|
private _outEndpointNumber!: number;
|
||||||
|
|
||||||
|
@ -90,11 +91,45 @@ export class WebUsbTransportation implements WebAdbTransportation {
|
||||||
this._outEndpointNumber = outEndPointNumber;
|
this._outEndpointNumber = outEndPointNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public *iterateKeys(): Generator<ArrayBuffer, void, void> {
|
||||||
|
const privateKey = window.localStorage.getItem(PrivateKeyStorageKey);
|
||||||
|
if (privateKey) {
|
||||||
|
yield decodeBase64(privateKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateKey(): Promise<ArrayBuffer> {
|
||||||
|
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<void> {
|
public async write(buffer: ArrayBuffer): Promise<void> {
|
||||||
await this._device.transferOut(this._outEndpointNumber, buffer);
|
await this._device.transferOut(this._outEndpointNumber, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async read(length: number): Promise<ArrayBuffer> {
|
public async read(length: number): Promise<ArrayBuffer> {
|
||||||
|
try {
|
||||||
const result = await this._device.transferIn(this._inEndpointNumber, length);
|
const result = await this._device.transferIn(this._inEndpointNumber, length);
|
||||||
|
|
||||||
if (result.status === 'stall') {
|
if (result.status === 'stall') {
|
||||||
|
@ -102,9 +137,17 @@ export class WebUsbTransportation implements WebAdbTransportation {
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data!.buffer;
|
return result.data!.buffer;
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === 'NotFoundError') {
|
||||||
|
this.onDisconnectedEvent.fire();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dispose() {
|
public async dispose() {
|
||||||
|
this.onDisconnectedEvent.dispose();
|
||||||
await this._device.close();
|
await this._device.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
19
packages/adb-webusb/tsconfig.json
Normal file
19
packages/adb-webusb/tsconfig.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
21
packages/adb/LICENSE
Normal file
21
packages/adb/LICENSE
Normal file
|
@ -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.
|
|
@ -1,14 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@yume-chan/webadb",
|
"name": "@yume-chan/adb",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"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": {
|
"@yume-chan/async-operation-manager": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-2.0.0.tgz",
|
|
@ -1,10 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@yume-chan/webadb",
|
"name": "@yume-chan/adb",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "ADB (Android Debugging Bridge) for Web browsers",
|
"description": "ADB (Android Debugging Bridge) for Web browsers",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"adb",
|
"adb"
|
||||||
"webusb"
|
|
||||||
],
|
],
|
||||||
"author": "Simon Chan <cnsimonchan@live.com>",
|
"author": "Simon Chan <cnsimonchan@live.com>",
|
||||||
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
|
||||||
|
@ -25,7 +24,6 @@
|
||||||
"typescript": "4.0.2"
|
"typescript": "4.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/w3c-web-usb": "^1.0.4",
|
|
||||||
"@yume-chan/async-operation-manager": "^2.0.0",
|
"@yume-chan/async-operation-manager": "^2.0.0",
|
||||||
"@yume-chan/event": "^0.0.1",
|
"@yume-chan/event": "^0.0.1",
|
||||||
"tslib": "^2.0.1"
|
"tslib": "^2.0.1"
|
|
@ -1,23 +1,9 @@
|
||||||
import { PromiseResolver } from '@yume-chan/async-operation-manager';
|
import { PromiseResolver } from '@yume-chan/async-operation-manager';
|
||||||
import { AdbAuthHandler, PublicKeyAuthMethod, SignatureAuthMethod } from './auth';
|
import { AutoDisposable, DisposableList } from '@yume-chan/event';
|
||||||
import { AdbPacket } from './packet';
|
import { AdbAuthenticationHandler, AdbDefaultAuthenticators } from './auth';
|
||||||
import { AdbStream, AdbStreamDispatcher } from './stream';
|
import { AdbBackend } from './backend';
|
||||||
import { WebAdbTransportation } from './transportation';
|
import { AdbCommand } from './packet';
|
||||||
|
import { AdbPacketDispatcher, AdbStream } from './stream';
|
||||||
export enum AdbCommand {
|
|
||||||
Connect = 'CNXN',
|
|
||||||
Auth = 'AUTH',
|
|
||||||
OK = 'OKAY',
|
|
||||||
Close = 'CLSE',
|
|
||||||
Write = 'WRTE',
|
|
||||||
Open = 'OPEN',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AdbAuthType {
|
|
||||||
Token = 1,
|
|
||||||
Signature = 2,
|
|
||||||
PublicKey = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum AdbPropKey {
|
export enum AdbPropKey {
|
||||||
Product = 'ro.product.name',
|
Product = 'ro.product.name',
|
||||||
|
@ -26,10 +12,15 @@ export enum AdbPropKey {
|
||||||
Features = 'features',
|
Features = 'features',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebAdb {
|
export class Adb {
|
||||||
private _transportation: WebAdbTransportation;
|
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;
|
private _product: string | undefined;
|
||||||
public get product() { return this._product; }
|
public get product() { return this._product; }
|
||||||
|
@ -43,14 +34,16 @@ export class WebAdb {
|
||||||
private _features: string[] | undefined;
|
private _features: string[] | undefined;
|
||||||
public get features() { return this._features; }
|
public get features() { return this._features; }
|
||||||
|
|
||||||
private streamDispatcher: AdbStreamDispatcher;
|
private packetDispatcher: AdbPacketDispatcher;
|
||||||
|
|
||||||
public constructor(transportation: WebAdbTransportation) {
|
public constructor(backend: AdbBackend) {
|
||||||
this._transportation = transportation;
|
this.backend = backend;
|
||||||
this.streamDispatcher = new AdbStreamDispatcher(transportation);
|
this.packetDispatcher = new AdbPacketDispatcher(backend);
|
||||||
|
|
||||||
|
backend.onDisconnected(this.dispose, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect() {
|
public async connect(authenticators = AdbDefaultAuthenticators) {
|
||||||
const version = 0x01000001;
|
const version = 0x01000001;
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
|
@ -73,8 +66,9 @@ export class WebAdb {
|
||||||
].join(',');
|
].join(',');
|
||||||
|
|
||||||
const resolver = new PromiseResolver<void>();
|
const resolver = new PromiseResolver<void>();
|
||||||
const authHandler = new AdbAuthHandler([new SignatureAuthMethod(), PublicKeyAuthMethod]);
|
const authHandler = new AdbAuthenticationHandler(authenticators, this.backend);
|
||||||
const removeListener = this.streamDispatcher.onPacket(async (e) => {
|
const disposableList = new DisposableList();
|
||||||
|
disposableList.add(this.packetDispatcher.onPacket(async (e) => {
|
||||||
e.handled = true;
|
e.handled = true;
|
||||||
|
|
||||||
const { packet } = e;
|
const { packet } = e;
|
||||||
|
@ -89,12 +83,8 @@ export class WebAdb {
|
||||||
resolver.resolve();
|
resolver.resolve();
|
||||||
break;
|
break;
|
||||||
case AdbCommand.Auth:
|
case AdbCommand.Auth:
|
||||||
if (packet.arg0 !== AdbAuthType.Token) {
|
|
||||||
throw new Error('Unknown auth type');
|
|
||||||
}
|
|
||||||
|
|
||||||
const authPacket = await authHandler.tryNextAuth(e.packet);
|
const authPacket = await authHandler.tryNextAuth(e.packet);
|
||||||
await this.streamDispatcher.sendPacket(authPacket);
|
await this.packetDispatcher.sendPacket(authPacket);
|
||||||
break;
|
break;
|
||||||
case AdbCommand.Close:
|
case AdbCommand.Close:
|
||||||
// Last connection was interrupted
|
// 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');
|
throw new Error('Device not in correct state. Reconnect your device and try again');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.dispose();
|
|
||||||
resolver.reject(e);
|
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 {
|
try {
|
||||||
await resolver.promise;
|
await resolver.promise;
|
||||||
|
this._connected = true;
|
||||||
} finally {
|
} finally {
|
||||||
removeListener();
|
disposableList.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,9 +150,9 @@ export class WebAdb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async shell(command: string, ...args: string[]): Promise<string>;
|
public shell(command: string, ...args: string[]): Promise<string>;
|
||||||
public async shell(): Promise<AdbStream>;
|
public shell(): Promise<AdbStream>;
|
||||||
public async shell(command?: string, ...args: string[]): Promise<AdbStream | string> {
|
public shell(command?: string, ...args: string[]): Promise<AdbStream | string> {
|
||||||
if (!command) {
|
if (!command) {
|
||||||
return this.createStream('shell:');
|
return this.createStream('shell:');
|
||||||
} else {
|
} else {
|
||||||
|
@ -161,16 +160,35 @@ export class WebAdb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async tcpip(port = 5555): Promise<string> {
|
public async getDaemonTcpAddresses(): Promise<string[]> {
|
||||||
|
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<string> {
|
||||||
return this.createStreamAndReadAll(`tcpip:${port}`);
|
return this.createStreamAndReadAll(`tcpip:${port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public usb(): Promise<string> {
|
public disableDaemonTcp(): Promise<string> {
|
||||||
return this.createStreamAndReadAll('usb:');
|
return this.createStreamAndReadAll('usb:');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createStream(payload: string): Promise<AdbStream> {
|
public async createStream(service: string): Promise<AdbStream> {
|
||||||
return this.streamDispatcher.createStream(payload);
|
return this.packetDispatcher.createStream(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createStreamAndReadAll(payload: string): Promise<string> {
|
public async createStreamAndReadAll(payload: string): Promise<string> {
|
||||||
|
@ -179,7 +197,7 @@ export class WebAdb {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async dispose() {
|
public async dispose() {
|
||||||
await this.streamDispatcher.dispose();
|
await this.packetDispatcher.dispose();
|
||||||
await this._transportation.dispose();
|
await this.backend.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
110
packages/adb/src/auth.ts
Normal file
110
packages/adb/src/auth.ts
Normal file
|
@ -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<AdbPacket | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<AdbPacket | undefined> {
|
||||||
|
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<AdbPacket> {
|
||||||
|
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<AdbPacket> {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
23
packages/adb/src/backend.ts
Normal file
23
packages/adb/src/backend.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Event } from '@yume-chan/event';
|
||||||
|
|
||||||
|
export type AdbKeyIterator = Iterator<ArrayBuffer> | AsyncIterator<ArrayBuffer>;
|
||||||
|
|
||||||
|
export interface AdbBackend {
|
||||||
|
readonly name: string | undefined;
|
||||||
|
|
||||||
|
readonly onDisconnected: Event<void>;
|
||||||
|
|
||||||
|
iterateKeys(): AdbKeyIterator;
|
||||||
|
|
||||||
|
generateKey(): ArrayBuffer | Promise<ArrayBuffer>;
|
||||||
|
|
||||||
|
encodeUtf8(input: string): ArrayBuffer;
|
||||||
|
|
||||||
|
decodeUtf8(buffer: ArrayBuffer): string;
|
||||||
|
|
||||||
|
write(buffer: ArrayBuffer): void | Promise<void>;
|
||||||
|
|
||||||
|
read(length: number): ArrayBuffer | Promise<ArrayBuffer>;
|
||||||
|
|
||||||
|
dispose(): void | Promise<void>;
|
||||||
|
}
|
159
packages/adb/src/base64.ts
Normal file
159
packages/adb/src/base64.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -113,21 +113,7 @@ export function modInverse(a: number, m: number) {
|
||||||
return (y % m + m) % m
|
return (y % m + m) % m
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateKey(): Promise<[privateKey: ArrayBuffer, publicKey: ArrayBuffer]> {
|
export function calculatePublicKey(privateKey: ArrayBuffer): 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);
|
|
||||||
|
|
||||||
// Android has its own public key generation algorithm
|
// 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
|
// 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
|
// exponent
|
||||||
publicKeyView.setUint32(8 + 256 + 256, 65537, true);
|
publicKeyView.setUint32(8 + 256 + 256, 65537, true);
|
||||||
|
|
||||||
return [privateKey, publicKey];
|
return publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Sha1DigestLength = 20;
|
export const Sha1DigestLength = 20;
|
7
packages/adb/src/index.ts
Normal file
7
packages/adb/src/index.ts
Normal file
|
@ -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';
|
149
packages/adb/src/packet.ts
Normal file
149
packages/adb/src/packet.ts
Normal file
|
@ -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<AdbPacket> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,65 +1,68 @@
|
||||||
import AsyncOperationManager, { PromiseResolver } from '@yume-chan/async-operation-manager';
|
import AsyncOperationManager, { PromiseResolver } from '@yume-chan/async-operation-manager';
|
||||||
import { AutoDisposable, Disposable, Event, EventEmitter } from '@yume-chan/event';
|
import { AutoDisposable, Disposable, Event, EventEmitter } from '@yume-chan/event';
|
||||||
import { decode } from './decode';
|
|
||||||
import { AdbPacket } from './packet';
|
import { AdbPacket } from './packet';
|
||||||
import { WebAdbTransportation } from './transportation';
|
import { AdbBackend } from './backend';
|
||||||
import { AdbCommand } from './webadb';
|
import { AdbCommand } from './packet';
|
||||||
|
|
||||||
class AutoResetEvent {
|
class AutoResetEvent implements Disposable {
|
||||||
private _list: PromiseResolver<void>[] = [];
|
private readonly list: PromiseResolver<void>[] = [];
|
||||||
|
|
||||||
private _blocking: boolean = false;
|
private blocking: boolean = false;
|
||||||
|
|
||||||
public wait(): Promise<void> {
|
public wait(): Promise<void> {
|
||||||
if (!this._blocking) {
|
if (!this.blocking) {
|
||||||
this._blocking = true;
|
this.blocking = true;
|
||||||
|
|
||||||
if (this._list.length === 0) {
|
if (this.list.length === 0) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolver = new PromiseResolver<void>();
|
const resolver = new PromiseResolver<void>();
|
||||||
this._list.push(resolver);
|
this.list.push(resolver);
|
||||||
return resolver.promise;
|
return resolver.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public notify() {
|
public notify() {
|
||||||
if (this._list.length !== 0) {
|
if (this.list.length !== 0) {
|
||||||
this._list.pop()!.resolve();
|
this.list.pop()!.resolve();
|
||||||
} else {
|
} 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 {
|
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 readonly localId: number;
|
||||||
public get localId() { return this._localId; }
|
|
||||||
|
|
||||||
private _remoteId: number;
|
public readonly remoteId: number;
|
||||||
public get remoteId() { return this._remoteId; }
|
|
||||||
|
|
||||||
public onDataEvent = this.addDisposable(new EventEmitter<ArrayBuffer>());
|
public readonly onDataEvent = this.addDisposable(new EventEmitter<ArrayBuffer>());
|
||||||
|
|
||||||
public onCloseEvent = this.addDisposable(new EventEmitter<void>());
|
public readonly onCloseEvent = this.addDisposable(new EventEmitter<void>());
|
||||||
|
|
||||||
public constructor(localId: number, remoteId: number, dispatcher: AdbStreamDispatcher) {
|
public constructor(localId: number, remoteId: number, dispatcher: AdbPacketDispatcher) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._localId = localId;
|
this.localId = localId;
|
||||||
this._remoteId = remoteId;
|
this.remoteId = remoteId;
|
||||||
this.dispatcher = dispatcher;
|
this.dispatcher = dispatcher;
|
||||||
dispatcher.addStreamController(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async write(data: ArrayBuffer): Promise<void> {
|
public async write(data: ArrayBuffer): Promise<void> {
|
||||||
await this.writeLock.wait();
|
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() {
|
public ack() {
|
||||||
|
@ -67,49 +70,75 @@ export class AdbStreamController extends AutoDisposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public close() {
|
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;
|
handled: boolean;
|
||||||
|
|
||||||
packet: AdbPacket;
|
packet: AdbPacket;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdbStreamDispatcher implements Disposable {
|
export class AdbPacketDispatcher extends AutoDisposable {
|
||||||
private transportation: WebAdbTransportation;
|
public readonly backend: AdbBackend;
|
||||||
|
|
||||||
// ADB requires stream id to start from 1
|
// ADB requires stream id to start from 1
|
||||||
// (0 means open failed)
|
// (0 means open failed)
|
||||||
private initializers = new AsyncOperationManager(1);
|
private readonly initializers = new AsyncOperationManager(1);
|
||||||
private streams = new Map<number, AdbStreamController>();
|
private readonly streams = new Map<number, AdbStreamController>();
|
||||||
|
|
||||||
private onPacketEvent = new EventEmitter<OnAdbPacketEventArgs>();
|
private readonly onPacketEvent = this.addDisposable(new EventEmitter<AdbPacketArrivedEventArgs>());
|
||||||
public get onPacket() { return this.onPacketEvent.event; }
|
public readonly onPacket = this.onPacketEvent.event;
|
||||||
|
|
||||||
|
private readonly onReceiveErrorEvent = this.addDisposable(new EventEmitter<Error>());
|
||||||
|
public readonly onReceiveError = this.onReceiveErrorEvent.event;
|
||||||
|
|
||||||
private _running = true;
|
private _running = true;
|
||||||
public get running() { return this._running; }
|
public get running() { return this._running; }
|
||||||
|
|
||||||
public constructor(transportation: WebAdbTransportation) {
|
public constructor(backend: AdbBackend) {
|
||||||
this.transportation = transportation;
|
super();
|
||||||
|
|
||||||
|
this.backend = backend;
|
||||||
this.receiveLoop();
|
this.receiveLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createStream(payload: string): Promise<AdbStream> {
|
public async createStream(service: string): Promise<AdbStream> {
|
||||||
const [localId, initializer] = this.initializers.add<number>();
|
const [localId, initializer] = this.initializers.add<number>();
|
||||||
await this.sendPacket(new AdbPacket(AdbCommand.Open, localId, 0, payload));
|
await this.sendPacket(AdbCommand.Open, localId, 0, service);
|
||||||
|
|
||||||
const remoteId = await initializer;
|
const remoteId = await initializer;
|
||||||
return new AdbStream(localId, remoteId, this);
|
const controller = new AdbStreamController(localId, remoteId, this);
|
||||||
}
|
|
||||||
|
|
||||||
public addStreamController(controller: AdbStreamController) {
|
|
||||||
this.streams.set(controller.localId, controller);
|
this.streams.set(controller.localId, controller);
|
||||||
|
|
||||||
|
return new AdbStream(controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendPacket(packet: AdbPacket): Promise<void> {
|
public sendPacket(packet: AdbPacket): Promise<void>;
|
||||||
return packet.writeTo(this.transportation);
|
public sendPacket(
|
||||||
|
command: AdbCommand,
|
||||||
|
arg0: number,
|
||||||
|
arg1: number,
|
||||||
|
payload?: string | ArrayBuffer
|
||||||
|
): Promise<void>;
|
||||||
|
public sendPacket(
|
||||||
|
packetOrCommand: AdbPacket | AdbCommand,
|
||||||
|
arg0?: number,
|
||||||
|
arg1?: number,
|
||||||
|
payload?: string | ArrayBuffer
|
||||||
|
): Promise<void> {
|
||||||
|
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() {
|
public async dispose() {
|
||||||
|
@ -121,23 +150,14 @@ export class AdbStreamDispatcher implements Disposable {
|
||||||
stream.dispose();
|
stream.dispose();
|
||||||
}
|
}
|
||||||
this.streams.clear();
|
this.streams.clear();
|
||||||
}
|
|
||||||
|
|
||||||
private async receivePacket() {
|
super.dispose();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async receiveLoop() {
|
private async receiveLoop() {
|
||||||
while (this._running) {
|
while (this._running) {
|
||||||
try {
|
try {
|
||||||
const packet = await this.receivePacket();
|
const packet = await AdbPacket.parse(this.backend);
|
||||||
let handled = false;
|
let handled = false;
|
||||||
switch (packet.command) {
|
switch (packet.command) {
|
||||||
case AdbCommand.OK:
|
case AdbCommand.OK:
|
||||||
|
@ -152,7 +172,7 @@ export class AdbStreamDispatcher implements Disposable {
|
||||||
// Last connection sent an OPEN to device,
|
// Last connection sent an OPEN to device,
|
||||||
// device now sent OKAY to this connection
|
// device now sent OKAY to this connection
|
||||||
// tell the device to close the stream
|
// 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;
|
break;
|
||||||
case AdbCommand.Close:
|
case AdbCommand.Close:
|
||||||
|
@ -175,14 +195,14 @@ export class AdbStreamDispatcher implements Disposable {
|
||||||
case AdbCommand.Write:
|
case AdbCommand.Write:
|
||||||
if (this.streams.has(packet.arg1)) {
|
if (this.streams.has(packet.arg1)) {
|
||||||
this.streams.get(packet.arg1)!.onDataEvent.fire(packet.payload!);
|
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;
|
handled = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
const args: OnAdbPacketEventArgs = {
|
const args: AdbPacketArrivedEventArgs = {
|
||||||
handled: false,
|
handled: false,
|
||||||
packet,
|
packet,
|
||||||
}
|
}
|
||||||
|
@ -197,7 +217,7 @@ export class AdbStreamDispatcher implements Disposable {
|
||||||
// ignore error
|
// ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
this.onReceiveErrorEvent.fire(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,15 +234,15 @@ export class AdbStream {
|
||||||
|
|
||||||
public get onClose(): Event<void> { return this.controller.onCloseEvent.event; }
|
public get onClose(): Event<void> { return this.controller.onCloseEvent.event; }
|
||||||
|
|
||||||
public constructor(localId: number, remoteId: number, dispatcher: AdbStreamDispatcher) {
|
public constructor(controller: AdbStreamController) {
|
||||||
this.controller = new AdbStreamController(localId, remoteId, dispatcher);
|
this.controller = controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readAll(): Promise<string> {
|
public async readAll(): Promise<string> {
|
||||||
const resolver = new PromiseResolver<string>();
|
const resolver = new PromiseResolver<string>();
|
||||||
let output = '';
|
let output = '';
|
||||||
this.onData((data) => {
|
this.onData((data) => {
|
||||||
output += decode(data);
|
output += this.controller.dispatcher.backend.decodeUtf8(data);
|
||||||
});
|
});
|
||||||
this.onClose(() => {
|
this.onClose(() => {
|
||||||
resolver.resolve(output);
|
resolver.resolve(output);
|
165
packages/demo/package-lock.json
generated
165
packages/demo/package-lock.json
generated
|
@ -40,33 +40,44 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@fluentui/react-focus": {
|
"@fluentui/react-focus": {
|
||||||
"version": "7.13.1",
|
"version": "7.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.13.2.tgz",
|
||||||
"integrity": "sha512-O61h/aC8ZZ0IEU0oqgBmrk2b/yNMBM0nghJ5IKwbGOD7s1bt/+FnAnJc+rmSD5Atrt7otPkMJma0yUu6qXqxHQ==",
|
"integrity": "sha512-t5BKR5uZw8A9VFH0BpkKA25Bx0iJfz9cDqrdOzFCJl3tTcTZYnTYxr3SSuw7Psp2ApdYWcnOZI6frozS+AvMnA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@fluentui/keyboard-key": "^0.2.11",
|
"@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/set-version": "^7.0.22",
|
||||||
"@uifabric/styling": "^7.15.1",
|
"@uifabric/styling": "^7.15.2",
|
||||||
"@uifabric/utilities": "^7.29.1",
|
"@uifabric/utilities": "^7.30.0",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@fluentui/react-icons": {
|
"@fluentui/react-icons": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-0.2.2.tgz",
|
||||||
"integrity": "sha512-FfHgiFrOmQEoGtnfzoL/1L6jmWXcZ+5GHc++TktSs6q/2XXDbU7qjE9pOankoKV+pJXKGHUPCLuNfI8j2eFnbA==",
|
"integrity": "sha512-pofZsHgbBVcyVr3I/LlbrX3P06NZnToxlH7cS9xy3LFcyrj629S5+iEXFdVk4O496MJ6qd4/FyxI/3Ru9UNlDg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@microsoft/load-themed-styles": "^1.10.26",
|
"@microsoft/load-themed-styles": "^1.10.26",
|
||||||
"@uifabric/set-version": "^7.0.22",
|
"@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"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@microsoft/load-themed-styles": {
|
"@microsoft/load-themed-styles": {
|
||||||
"version": "1.10.78",
|
"version": "1.10.79",
|
||||||
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.78.tgz",
|
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.79.tgz",
|
||||||
"integrity": "sha512-ehoPzYqpoVNkKheGHR4B3wOax+5L745KeGN8m+N6Qm/yypmnahFMp0Jk8ilTvrAFNcZ4UQl8I2YRbTXLCGdP/g=="
|
"integrity": "sha512-p4Ym7B2d8HT1K0VHAQkUw1SRmXQrko59lmqg/cr8ORClXIhoqc6Zt3m6I53olz4af2pXzpiI0ynHu6KmYYLOfQ=="
|
||||||
},
|
},
|
||||||
"@types/glob": {
|
"@types/glob": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
|
@ -149,31 +160,31 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uifabric/foundation": {
|
"@uifabric/foundation": {
|
||||||
"version": "7.8.1",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.8.2.tgz",
|
||||||
"integrity": "sha512-66P4Y2hJosLTl/QXSD780aSnxVR+ySmlugjgLgJkAUHPai84Ztbblv08p07GjDyN7SVs1/NRK7k9u7joMz8h8g==",
|
"integrity": "sha512-TQKJ73zcpnSaGSmwuCrL06q9zmOJhhBaOJn3bxENb9038lMwuoMikgTWWouD1C0woH8/J7lCxd/hcO4USe0MSw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@uifabric/merge-styles": "^7.16.7",
|
"@uifabric/merge-styles": "^7.17.0",
|
||||||
"@uifabric/set-version": "^7.0.22",
|
"@uifabric/set-version": "^7.0.22",
|
||||||
"@uifabric/styling": "^7.15.1",
|
"@uifabric/styling": "^7.15.2",
|
||||||
"@uifabric/utilities": "^7.29.1",
|
"@uifabric/utilities": "^7.30.0",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uifabric/icons": {
|
"@uifabric/icons": {
|
||||||
"version": "7.4.1",
|
"version": "7.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.4.2.tgz",
|
||||||
"integrity": "sha512-KgCYshGmcbLPoIIQK+OuKucM39smDSjsz7O4Hnu61xZmK1mOBCbFtBlgJRMlINZr0d6IH0vU2yrkp8SXHF2h0A==",
|
"integrity": "sha512-dd+IxpuQzso3ys2D873nOl/C2vvw3vHEDkWjJxIWKoaTqB7KoWMKf50Q7ObBei19ue/WgqKvTMOtKG1bgJP0bw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@uifabric/set-version": "^7.0.22",
|
"@uifabric/set-version": "^7.0.22",
|
||||||
"@uifabric/styling": "^7.15.1",
|
"@uifabric/styling": "^7.15.2",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uifabric/merge-styles": {
|
"@uifabric/merge-styles": {
|
||||||
"version": "7.16.7",
|
"version": "7.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.17.0.tgz",
|
||||||
"integrity": "sha512-yOjCexa1/I6c7p4lsgKGWbzoljVidg4IxigbeLjcvACvTdOFoLXguxWBIXn31Ralo28Ih4GC6ykhUTBGfsNTXg==",
|
"integrity": "sha512-UFG0pKqjz6iGcYhDFDSb2br5TEOys90mCaXeXPln8QrIc5NTJG1gWeO4znG6r65e23Ab/hBz+lmoyfYxgr3fZw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@uifabric/set-version": "^7.0.22",
|
"@uifabric/set-version": "^7.0.22",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
|
@ -198,23 +209,24 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uifabric/styling": {
|
"@uifabric/styling": {
|
||||||
"version": "7.15.1",
|
"version": "7.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.15.2.tgz",
|
||||||
"integrity": "sha512-jNEKVEo77HBGXSMwILNqfTYzXOoPY9AMRHF2TViBodKsSd8EqaEh1Q5yDj7N76Db1bgyIWW0xgjp6W+9ZaMA0w==",
|
"integrity": "sha512-J1S1i87+Re1258saDFMIbJps8w9mtL/lxFJLa7cCwt642xpbwx9L/11mTT9zBrf2yUYLK+KxCrAOYQS92uY01Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
|
"@fluentui/theme": "^0.1.1",
|
||||||
"@microsoft/load-themed-styles": "^1.10.26",
|
"@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/set-version": "^7.0.22",
|
||||||
"@uifabric/utilities": "^7.29.1",
|
"@uifabric/utilities": "^7.30.0",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@uifabric/utilities": {
|
"@uifabric/utilities": {
|
||||||
"version": "7.29.1",
|
"version": "7.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.29.1.tgz",
|
"resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.30.0.tgz",
|
||||||
"integrity": "sha512-LaXkxFCn1SQcD1HvhX8PG1jjnK2DG4xKD7PE95rt0+QMvI+10q9qZv6dVhPD2kLCf+8S06elgBE6xbQnmTQFNg==",
|
"integrity": "sha512-NO+XkCBsaxVzOA+JytgSGAK3ihPDG5XLPQQhFbAXSEc5eC6qzcSY6JswRPy3hC1DIWicTUh+iTrDbb+x2vZ8Wg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@uifabric/merge-styles": "^7.16.7",
|
"@uifabric/merge-styles": "^7.17.0",
|
||||||
"@uifabric/set-version": "^7.0.22",
|
"@uifabric/set-version": "^7.0.22",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"tslib": "^1.10.0"
|
"tslib": "^1.10.0"
|
||||||
|
@ -743,6 +755,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
||||||
"dev": true
|
"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
|
"dev": true
|
||||||
},
|
},
|
||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
|
||||||
"integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==",
|
"integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cyclist": {
|
"cyclist": {
|
||||||
|
@ -1724,9 +1745,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"eventemitter3": {
|
"eventemitter3": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.6.tgz",
|
||||||
"integrity": "sha512-QR0rh0YiPuxuDQ6+T9GAO/xWTExXpxIes1Nl9RykNGTnE1HJmkuEfxJH9cubjIOQZ/GH4qNBR4u8VSHaKiWs4g==",
|
"integrity": "sha512-s3GJL04SQoM+gn2c14oyqxvZ3Pcq7cduSDqy3sBFXx6UPSUmgVYwQM9zwkTn9je0lrfg0gHEwR42pF3Q2dCQkQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"events": {
|
"events": {
|
||||||
|
@ -2670,12 +2691,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
||||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"icss-utils": {
|
"icss-utils": {
|
||||||
|
@ -3603,23 +3624,35 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"office-ui-fabric-react": {
|
"office-ui-fabric-react": {
|
||||||
"version": "7.130.0",
|
"version": "7.131.0",
|
||||||
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.130.0.tgz",
|
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.131.0.tgz",
|
||||||
"integrity": "sha512-HV5LFSZAp8t3uJrGXhwPc/KHFPSGOka3BehRQnzN+7Ke++Abmynq7sIJSj36sNPa13uSiESfCFf30eY5ajnqPQ==",
|
"integrity": "sha512-KweqH/OFq78z/W64Z1cSHxh8CBcrVOuYW5MwDtRIGlPorgOAYeS+z/Kx6cqJhlZIwRHLCrIgTKZ032vYXXtOrA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@fluentui/date-time-utilities": "^7.6.0",
|
"@fluentui/date-time-utilities": "^7.6.0",
|
||||||
"@fluentui/react-focus": "^7.13.1",
|
"@fluentui/react-focus": "^7.13.2",
|
||||||
"@fluentui/react-icons": "^0.2.1",
|
"@fluentui/react-icons": "^0.2.2",
|
||||||
"@microsoft/load-themed-styles": "^1.10.26",
|
"@microsoft/load-themed-styles": "^1.10.26",
|
||||||
"@uifabric/foundation": "^7.8.1",
|
"@uifabric/foundation": "^7.8.2",
|
||||||
"@uifabric/icons": "^7.4.1",
|
"@uifabric/icons": "^7.4.2",
|
||||||
"@uifabric/merge-styles": "^7.16.7",
|
"@uifabric/merge-styles": "^7.17.0",
|
||||||
"@uifabric/react-hooks": "^7.9.1",
|
"@uifabric/react-hooks": "^7.10.0",
|
||||||
"@uifabric/set-version": "^7.0.22",
|
"@uifabric/set-version": "^7.0.22",
|
||||||
"@uifabric/styling": "^7.15.1",
|
"@uifabric/styling": "^7.15.2",
|
||||||
"@uifabric/utilities": "^7.29.1",
|
"@uifabric/utilities": "^7.30.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"tslib": "^1.10.0"
|
"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": {
|
"on-finished": {
|
||||||
|
@ -4146,6 +4179,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||||
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
|
||||||
"dev": true
|
"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",
|
"loader-utils": "^2.0.0",
|
||||||
"schema-utils": "^2.7.0",
|
"schema-utils": "^2.7.0",
|
||||||
"source-map": "^0.6.1"
|
"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": {
|
"source-map-resolve": {
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react": "7.130.0",
|
"@fluentui/react": "7.130.0",
|
||||||
"@uifabric/react-hooks": "7.9.1",
|
"@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": "16.13.1",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { DefaultButton, Dialog, DialogFooter, DialogType, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
|
import { DefaultButton, Dialog, DialogFooter, DialogType, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
|
||||||
import { useBoolean } from '@uifabric/react-hooks';
|
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 React, { useCallback, useEffect, useState } from 'react';
|
||||||
import withDisplayName from './withDisplayName';
|
import withDisplayName from './withDisplayName';
|
||||||
|
|
||||||
interface ConnectProps {
|
interface ConnectProps {
|
||||||
device: WebAdb | undefined;
|
device: Adb | undefined;
|
||||||
|
|
||||||
onDeviceChange: (device: WebAdb | undefined) => void;
|
onDeviceChange: (device: Adb | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withDisplayName('Connect', ({
|
export default withDisplayName('Connect', ({
|
||||||
|
@ -21,12 +22,17 @@ export default withDisplayName('Connect', ({
|
||||||
|
|
||||||
const connect = useCallback(async () => {
|
const connect = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const transportation = await WebUsbTransportation.pickDevice();
|
const backend = await WebUsbAdbBackend.pickDevice();
|
||||||
if (transportation) {
|
if (backend) {
|
||||||
const device = new WebAdb(transportation);
|
const device = new Adb(backend);
|
||||||
|
try {
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
await device.connect();
|
await device.connect();
|
||||||
onDeviceChange(device);
|
onDeviceChange(device);
|
||||||
|
} catch (e) {
|
||||||
|
device.dispose();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErrorMessage(e.message);
|
setErrorMessage(e.message);
|
||||||
|
@ -47,17 +53,9 @@ export default withDisplayName('Connect', ({
|
||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handler(e: USBConnectionEvent) {
|
return device?.onDisconnected(() => {
|
||||||
if (e.device.productName === device?.name) {
|
|
||||||
onDeviceChange(undefined);
|
onDeviceChange(undefined);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
window.navigator.usb.addEventListener('disconnect', handler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.navigator.usb.removeEventListener('disconnect', handler);
|
|
||||||
};
|
|
||||||
}, [device, onDeviceChange]);
|
}, [device, onDeviceChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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 React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
@ -13,7 +13,7 @@ const containerStyle: CSSProperties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ShellProps {
|
export interface ShellProps {
|
||||||
device: WebAdb | undefined;
|
device: Adb | undefined;
|
||||||
|
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Label, Link, MessageBar, Nav, PrimaryButton, Separator, Stack, StackItem, Text, TextField } from '@fluentui/react';
|
import { Label, Link, MessageBar, Nav, PrimaryButton, Separator, Stack, StackItem, Text, TextField } from '@fluentui/react';
|
||||||
import { useId } from '@uifabric/react-hooks';
|
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 { initializeIcons } from 'office-ui-fabric-react/lib/Icons';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
@ -14,12 +14,12 @@ initializeIcons();
|
||||||
function App(): JSX.Element | null {
|
function App(): JSX.Element | null {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const [device, setDevice] = useState<WebAdb | undefined>();
|
const [device, setDevice] = useState<Adb | undefined>();
|
||||||
|
|
||||||
const [tcpPort, setTcpPort] = useState<number | undefined>();
|
const [tcpPort, setTcpAddresses] = useState<string[] | undefined>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!device) {
|
if (!device) {
|
||||||
setTcpPort(undefined);
|
setTcpAddresses(undefined);
|
||||||
}
|
}
|
||||||
}, [device]);
|
}, [device]);
|
||||||
const queryTcpPort = useCallback(async () => {
|
const queryTcpPort = useCallback(async () => {
|
||||||
|
@ -27,8 +27,8 @@ function App(): JSX.Element | null {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await device.shell('getprop service.adb.tcp.port');
|
const result = await device.getDaemonTcpAddresses();
|
||||||
setTcpPort(Number.parseInt(result, 10));
|
setTcpAddresses(result);
|
||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
const [tcpPortValue, setTcpPortValue] = useState('5555');
|
const [tcpPortValue, setTcpPortValue] = useState('5555');
|
||||||
|
@ -38,7 +38,7 @@ function App(): JSX.Element | null {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await device.tcpip(Number.parseInt(tcpPortValue, 10));
|
const result = await device.setDaemonTcpPort(Number.parseInt(tcpPortValue, 10));
|
||||||
console.log(result);
|
console.log(result);
|
||||||
}, [device, tcpPortValue]);
|
}, [device, tcpPortValue]);
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ function App(): JSX.Element | null {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await device.usb();
|
const result = await device.disableDaemonTcp();
|
||||||
console.log(result);
|
console.log(result);
|
||||||
}, [device]);
|
}, [device]);
|
||||||
|
|
||||||
|
@ -132,8 +132,8 @@ function App(): JSX.Element | null {
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
{tcpPort !== undefined &&
|
{tcpPort !== undefined &&
|
||||||
(tcpPort !== 0
|
(tcpPort.length !== 0
|
||||||
? `Enabled at port ${tcpPort}`
|
? `Enabled at ${tcpPort.join(', ')}`
|
||||||
: 'Disabled')}
|
: 'Disabled')}
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -161,7 +161,7 @@ function App(): JSX.Element | null {
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
text="Disable"
|
text="Disable"
|
||||||
disabled={!device || tcpPort === undefined || tcpPort === 0}
|
disabled={!device || tcpPort === undefined || tcpPort.length === 0}
|
||||||
onClick={disableTcp}
|
onClick={disableTcp}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
|
21
packages/event/LICENSE
Normal file
21
packages/event/LICENSE
Normal file
|
@ -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.
|
|
@ -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<AdbPacket | undefined>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<AdbPacket | undefined> {
|
|
||||||
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<AdbPacket> {
|
|
||||||
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<AdbPacket> {
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './webadb';
|
|
||||||
export * from './transportation';
|
|
|
@ -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<void> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,6 @@
|
||||||
"target": "ES2016", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
"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'. */
|
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||||
"lib": [ // /* Specify library files to be included in the compilation. */
|
"lib": [ // /* Specify library files to be included in the compilation. */
|
||||||
"DOM",
|
|
||||||
"ESNext"
|
"ESNext"
|
||||||
],
|
],
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue