mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 01:39:21 +02:00
refactor(adb): completely separate daemon authenticator
This commit is contained in:
parent
6db5d8eb74
commit
96824ef984
5 changed files with 277 additions and 274 deletions
|
@ -1,2 +1,2 @@
|
|||
export * from "./manager.js";
|
||||
export * from "./storage/index.js";
|
||||
export * from "./store.js";
|
||||
|
|
|
@ -1,13 +1,47 @@
|
|||
import type {
|
||||
AdbCredentialStore,
|
||||
AdbCredentialManager,
|
||||
AdbDaemonDefaultAuthenticationProcessorInit,
|
||||
AdbPrivateKey,
|
||||
MaybeError,
|
||||
} from "@yume-chan/adb";
|
||||
import { rsaParsePrivateKey } from "@yume-chan/adb";
|
||||
import {
|
||||
AdbDaemonDefaultAuthenticationProcessor,
|
||||
AdbDaemonDefaultAuthenticator,
|
||||
rsaParsePrivateKey,
|
||||
} from "@yume-chan/adb";
|
||||
|
||||
import type { TangoKeyStorage } from "./storage/index.js";
|
||||
|
||||
export class AdbWebCryptoCredentialManager implements AdbCredentialStore {
|
||||
export class AdbWebCryptoCredentialManager implements AdbCredentialManager {
|
||||
static createDefaultAuthenticationProcessor(
|
||||
storage: TangoKeyStorage,
|
||||
init?: Omit<
|
||||
AdbDaemonDefaultAuthenticationProcessorInit,
|
||||
"credentialStore"
|
||||
> & {
|
||||
name?: string;
|
||||
},
|
||||
) {
|
||||
return new AdbDaemonDefaultAuthenticationProcessor({
|
||||
...init,
|
||||
credentialManager: new this(storage, init?.name),
|
||||
});
|
||||
}
|
||||
|
||||
static createDefaultAuthenticator(
|
||||
storage: TangoKeyStorage,
|
||||
init?: Omit<
|
||||
AdbDaemonDefaultAuthenticationProcessorInit,
|
||||
"credentialStore"
|
||||
> & {
|
||||
name?: string;
|
||||
},
|
||||
) {
|
||||
return new AdbDaemonDefaultAuthenticator(() =>
|
||||
this.createDefaultAuthenticationProcessor(storage, init),
|
||||
);
|
||||
}
|
||||
|
||||
readonly #storage: TangoKeyStorage;
|
||||
|
||||
readonly #name: string | undefined;
|
|
@ -5,13 +5,16 @@ import { encodeUtf8 } from "@yume-chan/struct";
|
|||
|
||||
import { decodeBase64 } from "../utils/base64.js";
|
||||
|
||||
import type { AdbCredentialStore } from "./auth.js";
|
||||
import { AdbAuthType, AdbDefaultAuthenticator } from "./auth.js";
|
||||
import type { AdbCredentialManager } from "./auth.js";
|
||||
import {
|
||||
AdbAuthType,
|
||||
AdbDaemonDefaultAuthenticationProcessor,
|
||||
} from "./auth.js";
|
||||
import type { SimpleRsaPrivateKey } from "./crypto.js";
|
||||
import { rsaParsePrivateKey } from "./crypto.js";
|
||||
import { AdbCommand } from "./packet.js";
|
||||
|
||||
class MockCredentialStore implements AdbCredentialStore {
|
||||
class MockCredentialStore implements AdbCredentialManager {
|
||||
key: SimpleRsaPrivateKey;
|
||||
name: string | undefined;
|
||||
|
||||
|
@ -75,17 +78,19 @@ const PUBLIC_KEY =
|
|||
"QAAAANVsDNqDk46/2Qg74n5POy5nK/XA8glCLkvXMks9p885+GQ2WiVUctG8LP/W5cII11Pk1KsZ+90ccZV2fdjv+tnW/8li9iEWTC+G1udFMxsIQ+HRPvJF0Xl9JXDsC6pvdo9ic4d6r5BC9BGiijd0enoG/tHkJhMhbPf/j7+MWXDrF+BeJeyj0mWArbqS599IO2qUCZiNjRakAa/iESG6Om4xCJWTT8wGhSTs81cHcEeSmQ2ixRwS+uaa/8iK/mv6BvCep5qgFrJW1G9LD2WciVgTpOSc6B1N/OA92hwJYp2lHLPWZl6bJIYHqrzdHCxc4EEVVYHkSBdFy1w2vhg2YgRTlpbP00NVrZb6Car8BTqPnwTRIkHBC6nnrg6cWMQ0xusMtxChKBoYGhCLHY4iKK6ra3P1Ou1UXu0WySau3s+Av9FFXxtAuMAJUA+5GSMQGGECRhwLX910OfnHHN+VxqJkHQye4vNhIH5C1dJ39HJoxAdwH2tF7v7GF2fwsy2lUa3Vj6bBssWivCB9cKyJR0GVPZJZ1uah24ecvspwtAqbtxvj7ZD9l7AD92geEJdLrsbfhNaDyAioQ2grI32gdp80su/7BrdAsPaSomxCYBB8opmS+oJq6qTYxNZ0doT9EEyT5D9rl9UXXxq+rQbDpKV1rOQo5zJJ2GkELhUrslFm6n4+JQEAAQA=";
|
||||
|
||||
describe("auth", () => {
|
||||
describe("AdbDefaultAuthenticator", () => {
|
||||
describe("AdbDaemonDefaultAuthenticationProcessor", () => {
|
||||
it("should generate correct public key without name", async () => {
|
||||
const store = new MockCredentialStore(
|
||||
new Uint8Array(PRIVATE_KEY),
|
||||
undefined,
|
||||
);
|
||||
|
||||
const authenticator = new AdbDefaultAuthenticator(store);
|
||||
const authenticator = new AdbDaemonDefaultAuthenticationProcessor({
|
||||
credentialManager: store,
|
||||
});
|
||||
const challenge = new Uint8Array(20);
|
||||
|
||||
const first = await authenticator.authenticate({
|
||||
const first = await authenticator.process({
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Token,
|
||||
arg1: 0,
|
||||
|
@ -96,7 +101,7 @@ describe("auth", () => {
|
|||
assert.strictEqual(first.command, AdbCommand.Auth);
|
||||
assert.strictEqual(first.arg0, AdbAuthType.Signature);
|
||||
|
||||
const result = await authenticator.authenticate({
|
||||
const result = await authenticator.process({
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Token,
|
||||
arg1: 0,
|
||||
|
@ -119,10 +124,12 @@ describe("auth", () => {
|
|||
name,
|
||||
);
|
||||
|
||||
const authenticator = new AdbDefaultAuthenticator(store);
|
||||
const authenticator = new AdbDaemonDefaultAuthenticationProcessor({
|
||||
credentialManager: store,
|
||||
});
|
||||
const challenge = new Uint8Array(20);
|
||||
|
||||
const first = await authenticator.authenticate({
|
||||
const first = await authenticator.process({
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Token,
|
||||
arg1: 0,
|
||||
|
@ -133,7 +140,7 @@ describe("auth", () => {
|
|||
assert.strictEqual(first.command, AdbCommand.Auth);
|
||||
assert.strictEqual(first.arg0, AdbAuthType.Signature);
|
||||
|
||||
const result = await authenticator.authenticate({
|
||||
const result = await authenticator.process({
|
||||
command: AdbCommand.Auth,
|
||||
arg0: AdbAuthType.Token,
|
||||
arg1: 0,
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
import { EventEmitter } from "@yume-chan/event";
|
||||
import { EmptyUint8Array } from "@yume-chan/struct";
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import type { WritableStreamDefaultWriter } from "@yume-chan/stream-extra";
|
||||
import {
|
||||
AbortController,
|
||||
Consumable,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { decodeUtf8, EmptyUint8Array } from "@yume-chan/struct";
|
||||
|
||||
import { AdbDeviceFeatures, AdbFeature } from "../features.js";
|
||||
import {
|
||||
calculateBase64EncodedLength,
|
||||
encodeBase64,
|
||||
|
@ -15,8 +22,13 @@ import {
|
|||
adbGetPublicKeySize,
|
||||
rsaSign,
|
||||
} from "./crypto.js";
|
||||
import type { AdbPacketData } from "./packet.js";
|
||||
import { AdbCommand } from "./packet.js";
|
||||
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||
import { AdbCommand, calculateChecksum } from "./packet.js";
|
||||
import type { AdbDaemonTransportInit } from "./transport.js";
|
||||
import {
|
||||
ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
||||
AdbDaemonTransport,
|
||||
} from "./transport.js";
|
||||
|
||||
export interface AdbPrivateKey extends SimpleRsaPrivateKey {
|
||||
name?: string | undefined;
|
||||
|
@ -28,7 +40,7 @@ export type AdbKeyIterable =
|
|||
| Iterable<MaybeError<AdbPrivateKey>>
|
||||
| AsyncIterable<MaybeError<AdbPrivateKey>>;
|
||||
|
||||
export interface AdbCredentialStore {
|
||||
export interface AdbCredentialManager {
|
||||
/**
|
||||
* Generates and stores a RSA private key with modulus length `2048` and public exponent `65537`.
|
||||
*/
|
||||
|
@ -64,14 +76,29 @@ export const AdbAuthType = {
|
|||
|
||||
export type AdbAuthType = (typeof AdbAuthType)[keyof typeof AdbAuthType];
|
||||
|
||||
export interface AdbAuthenticator {
|
||||
authenticate(packet: AdbPacketData): Promise<AdbPacketData>;
|
||||
export interface AdbDaemonAuthenticationProcessor {
|
||||
process(packet: AdbPacketData): Promise<AdbPacketData>;
|
||||
|
||||
close?(): MaybePromiseLike<undefined>;
|
||||
}
|
||||
|
||||
export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
||||
#credentialStore: AdbCredentialStore;
|
||||
export interface AdbDaemonDefaultAuthenticationProcessorInit {
|
||||
credentialManager: AdbCredentialManager;
|
||||
onKeyLoadError?: ((error: Error) => void) | undefined;
|
||||
onSignatureAuthentication?: ((key: AdbKeyInfo) => void) | undefined;
|
||||
onSignatureRejected?: ((key: AdbKeyInfo) => void) | undefined;
|
||||
onPublicKeyAuthentication?: ((key: AdbKeyInfo) => void) | undefined;
|
||||
}
|
||||
|
||||
export class AdbDaemonDefaultAuthenticationProcessor
|
||||
implements AdbDaemonAuthenticationProcessor
|
||||
{
|
||||
#credentialStore: AdbCredentialManager;
|
||||
#onKeyLoadError: ((error: Error) => void) | undefined;
|
||||
#onSignatureAuthentication: ((key: AdbKeyInfo) => void) | undefined;
|
||||
#onSignatureRejected: ((key: AdbKeyInfo) => void) | undefined;
|
||||
#onPublicKeyAuthentication: ((key: AdbKeyInfo) => void) | undefined;
|
||||
|
||||
#iterator:
|
||||
| Iterator<MaybeError<AdbPrivateKey>, void, void>
|
||||
| AsyncIterator<MaybeError<AdbPrivateKey>, void, void>
|
||||
|
@ -80,28 +107,12 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
#prevKeyInfo: AdbKeyInfo | undefined;
|
||||
#firstKey: AdbPrivateKey | undefined;
|
||||
|
||||
#onKeyLoadError = new EventEmitter<Error>();
|
||||
get onKeyLoadError() {
|
||||
return this.#onKeyLoadError.event;
|
||||
}
|
||||
|
||||
#onSignatureAuthentication = new EventEmitter<AdbKeyInfo>();
|
||||
get onSignatureAuthentication() {
|
||||
return this.#onSignatureAuthentication.event;
|
||||
}
|
||||
|
||||
#onSignatureRejected = new EventEmitter<AdbKeyInfo>();
|
||||
get onSignatureRejected() {
|
||||
return this.#onSignatureRejected.event;
|
||||
}
|
||||
|
||||
#onPublicKeyAuthentication = new EventEmitter<AdbKeyInfo>();
|
||||
get onPublicKeyAuthentication() {
|
||||
return this.#onPublicKeyAuthentication.event;
|
||||
}
|
||||
|
||||
constructor(credentialStore: AdbCredentialStore) {
|
||||
this.#credentialStore = credentialStore;
|
||||
constructor(init: AdbDaemonDefaultAuthenticationProcessorInit) {
|
||||
this.#credentialStore = init.credentialManager;
|
||||
this.#onKeyLoadError = init.onKeyLoadError;
|
||||
this.#onSignatureAuthentication = init.onSignatureAuthentication;
|
||||
this.#onSignatureRejected = init.onSignatureRejected;
|
||||
this.#onPublicKeyAuthentication = init.onPublicKeyAuthentication;
|
||||
}
|
||||
|
||||
async #iterate(token: Uint8Array): Promise<AdbPacketData | undefined> {
|
||||
|
@ -122,7 +133,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
}
|
||||
|
||||
if (result instanceof Error) {
|
||||
this.#onKeyLoadError.fire(result);
|
||||
this.#onKeyLoadError?.(result);
|
||||
return await this.#iterate(token);
|
||||
}
|
||||
|
||||
|
@ -132,12 +143,12 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
|
||||
// A new token implies the previous signature was rejected.
|
||||
if (this.#prevKeyInfo) {
|
||||
this.#onSignatureRejected.fire(this.#prevKeyInfo);
|
||||
this.#onSignatureRejected?.(this.#prevKeyInfo);
|
||||
}
|
||||
|
||||
const fingerprint = getFingerprint(result);
|
||||
this.#prevKeyInfo = { fingerprint, name: result.name };
|
||||
this.#onSignatureAuthentication.fire(this.#prevKeyInfo);
|
||||
this.#onSignatureAuthentication?.(this.#prevKeyInfo);
|
||||
|
||||
return {
|
||||
command: AdbCommand.Auth,
|
||||
|
@ -147,7 +158,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
};
|
||||
}
|
||||
|
||||
async authenticate(packet: AdbPacketData): Promise<AdbPacketData> {
|
||||
async process(packet: AdbPacketData): Promise<AdbPacketData> {
|
||||
if (packet.arg0 !== AdbAuthType.Token) {
|
||||
throw new Error("Unsupported authentication packet");
|
||||
}
|
||||
|
@ -186,7 +197,7 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
publicKeyBuffer.set(nameBuffer, publicKeyBase64Length + 1);
|
||||
}
|
||||
|
||||
this.#onPublicKeyAuthentication.fire({
|
||||
this.#onPublicKeyAuthentication?.({
|
||||
fingerprint: getFingerprint(key),
|
||||
name: key.name,
|
||||
});
|
||||
|
@ -206,3 +217,169 @@ export class AdbDefaultAuthenticator implements AdbAuthenticator {
|
|||
this.#firstKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export type AdbDaemonAuthenticateOptions = Pick<
|
||||
AdbDaemonTransportInit,
|
||||
| "serial"
|
||||
| "connection"
|
||||
| "features"
|
||||
| "initialDelayedAckBytes"
|
||||
| "preserveConnection"
|
||||
| "readTimeLimit"
|
||||
>;
|
||||
|
||||
export interface AdbDaemonAuthenticator {
|
||||
authenticate(
|
||||
options: AdbDaemonAuthenticateOptions,
|
||||
): Promise<AdbDaemonTransport>;
|
||||
}
|
||||
|
||||
export class AdbDaemonDefaultAuthenticator implements AdbDaemonAuthenticator {
|
||||
static authenticate(
|
||||
processor: AdbDaemonDefaultAuthenticationProcessor,
|
||||
options: AdbDaemonAuthenticateOptions,
|
||||
): Promise<AdbDaemonTransport> {
|
||||
const authenticator = new AdbDaemonDefaultAuthenticator(
|
||||
() => processor,
|
||||
);
|
||||
return authenticator.authenticate(options);
|
||||
}
|
||||
|
||||
#createProcessor: () => AdbDaemonAuthenticationProcessor;
|
||||
|
||||
constructor(createProcessor: () => AdbDaemonAuthenticationProcessor) {
|
||||
this.#createProcessor = createProcessor;
|
||||
}
|
||||
|
||||
#sendPacket(
|
||||
writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>,
|
||||
init: AdbPacketData,
|
||||
): Promise<void>;
|
||||
#sendPacket(
|
||||
writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>,
|
||||
init: AdbPacketInit,
|
||||
) {
|
||||
// Always send checksum in auth steps
|
||||
// Because we don't know if the device needs it or not.
|
||||
init.checksum = calculateChecksum(init.payload);
|
||||
init.magic = init.command ^ 0xffffffff;
|
||||
return Consumable.WritableStream.write(writer, init);
|
||||
}
|
||||
|
||||
async authenticate({
|
||||
serial,
|
||||
connection,
|
||||
features = AdbDeviceFeatures,
|
||||
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
||||
preserveConnection,
|
||||
readTimeLimit,
|
||||
}: AdbDaemonAuthenticateOptions): Promise<AdbDaemonTransport> {
|
||||
const processor = this.#createProcessor();
|
||||
|
||||
// Initially, set to highest-supported version and payload size.
|
||||
let version = 0x01000001;
|
||||
// Android 4: 4K, Android 7: 256K, Android 9: 1M
|
||||
let maxPayloadSize = 1024 * 1024;
|
||||
|
||||
const resolver = new PromiseResolver<string>();
|
||||
|
||||
// Here is similar to `AdbPacketDispatcher`,
|
||||
// But the received packet types and send packet processing are different.
|
||||
const abortController = new AbortController();
|
||||
|
||||
const writer = connection.writable.getWriter();
|
||||
|
||||
const pipe = connection.readable
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: async (packet) => {
|
||||
switch (packet.command) {
|
||||
case AdbCommand.Connect:
|
||||
version = Math.min(version, packet.arg0);
|
||||
maxPayloadSize = Math.min(
|
||||
maxPayloadSize,
|
||||
packet.arg1,
|
||||
);
|
||||
resolver.resolve(decodeUtf8(packet.payload));
|
||||
break;
|
||||
case AdbCommand.Auth: {
|
||||
await this.#sendPacket(
|
||||
writer,
|
||||
await processor.process(packet),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Maybe the previous ADB client exited without reading all packets,
|
||||
// so they are still waiting in OS internal buffer.
|
||||
// Just ignore them.
|
||||
// Because a `Connect` packet will reset the device,
|
||||
// Eventually there will be `Connect` and `Auth` response packets.
|
||||
break;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
// Don't cancel the source ReadableStream on AbortSignal abort.
|
||||
preventCancel: true,
|
||||
signal: abortController.signal,
|
||||
},
|
||||
)
|
||||
.then(
|
||||
async () => {
|
||||
await processor.close?.();
|
||||
|
||||
// If `resolver` is already settled, call `reject` won't do anything.
|
||||
resolver.reject(
|
||||
new Error("Connection closed unexpectedly"),
|
||||
);
|
||||
},
|
||||
async (e) => {
|
||||
await processor.close?.();
|
||||
|
||||
resolver.reject(e);
|
||||
},
|
||||
);
|
||||
|
||||
if (initialDelayedAckBytes <= 0) {
|
||||
const index = features.indexOf(AdbFeature.DelayedAck);
|
||||
if (index !== -1) {
|
||||
features = features.toSpliced(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let banner: string;
|
||||
try {
|
||||
await this.#sendPacket(writer, {
|
||||
command: AdbCommand.Connect,
|
||||
arg0: version,
|
||||
arg1: maxPayloadSize,
|
||||
// The terminating `;` is required in formal definition
|
||||
// But ADB daemon (all versions) can still work without it
|
||||
payload: encodeUtf8(`host::features=${features.join(",")}`),
|
||||
});
|
||||
|
||||
banner = await resolver.promise;
|
||||
} finally {
|
||||
// When failed, release locks on `connection` so the caller can try again.
|
||||
// When success, also release locks so `AdbPacketDispatcher` can use them.
|
||||
abortController.abort();
|
||||
writer.releaseLock();
|
||||
|
||||
// Wait until pipe stops (`ReadableStream` lock released)
|
||||
await pipe;
|
||||
}
|
||||
|
||||
return new AdbDaemonTransport({
|
||||
serial,
|
||||
connection,
|
||||
version,
|
||||
maxPayloadSize,
|
||||
banner,
|
||||
features,
|
||||
initialDelayedAckBytes,
|
||||
preserveConnection: preserveConnection,
|
||||
readTimeLimit: readTimeLimit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import type { MaybePromiseLike } from "@yume-chan/async";
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import type { ReadableWritablePair } from "@yume-chan/stream-extra";
|
||||
import {
|
||||
AbortController,
|
||||
Consumable,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { decodeUtf8, encodeUtf8 } from "@yume-chan/struct";
|
||||
import type { Consumable, ReadableWritablePair } from "@yume-chan/stream-extra";
|
||||
|
||||
import type {
|
||||
AdbIncomingSocketHandler,
|
||||
|
@ -17,14 +10,11 @@ import { AdbBanner } from "../banner.js";
|
|||
import { AdbDeviceFeatures, AdbFeature } from "../features.js";
|
||||
|
||||
import type {
|
||||
AdbAuthenticator,
|
||||
AdbCredentialStore,
|
||||
AdbKeyInfo,
|
||||
AdbDaemonAuthenticateOptions,
|
||||
AdbDaemonAuthenticator,
|
||||
} from "./auth.js";
|
||||
import { AdbDefaultAuthenticator } from "./auth.js";
|
||||
import { AdbPacketDispatcher } from "./dispatcher.js";
|
||||
import type { AdbPacketData, AdbPacketInit } from "./packet.js";
|
||||
import { AdbCommand, calculateChecksum } from "./packet.js";
|
||||
|
||||
export const ADB_DAEMON_VERSION_OMIT_CHECKSUM = 0x01000001;
|
||||
export const ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE = 32 * 1024 * 1024;
|
||||
|
@ -34,53 +24,7 @@ export type AdbDaemonConnection = ReadableWritablePair<
|
|||
Consumable<AdbPacketInit>
|
||||
>;
|
||||
|
||||
export interface AdbDaemonAuthenticationOptions {
|
||||
serial: string;
|
||||
connection: AdbDaemonConnection;
|
||||
features?: readonly AdbFeature[];
|
||||
|
||||
/**
|
||||
* The number of bytes the device can send before receiving an ack packet.
|
||||
* Using delayed ack can improve the throughput,
|
||||
* especially when the device is connected over Wi-Fi (so the latency is higher).
|
||||
*
|
||||
* Set to 0 or any negative value to disable delayed ack in handshake.
|
||||
* Otherwise the value must be in the range of unsigned 32-bit integer.
|
||||
*
|
||||
* Delayed ack was added in Android 14,
|
||||
* this option will be ignored when the device doesn't support it.
|
||||
*
|
||||
* @default ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE
|
||||
*/
|
||||
initialDelayedAckBytes?: number;
|
||||
|
||||
/**
|
||||
* Whether to keep the `connection` open (don't call `writable.close` and `readable.cancel`)
|
||||
* when `AdbDaemonTransport.close` is called.
|
||||
*
|
||||
* Note that when `authenticate` fails,
|
||||
* no matter which value this option has,
|
||||
* the `connection` is always kept open, so it can be used in another `authenticate` call.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
preserveConnection?: boolean | undefined;
|
||||
|
||||
/**
|
||||
* When set, the transport will throw an error when
|
||||
* one of the socket readable stalls for this amount of milliseconds.
|
||||
*
|
||||
* Because ADB is a multiplexed protocol, blocking one socket will also block all other sockets.
|
||||
* It's important to always read from all sockets to prevent stalling.
|
||||
*
|
||||
* This option is helpful to detect bugs in the client code.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
readTimeLimit?: number | undefined;
|
||||
}
|
||||
|
||||
interface AdbDaemonTransportInit {
|
||||
export interface AdbDaemonTransportInit {
|
||||
serial: string;
|
||||
connection: AdbDaemonConnection;
|
||||
version: number;
|
||||
|
@ -128,170 +72,11 @@ interface AdbDaemonTransportInit {
|
|||
* An ADB Transport that connects to ADB Daemons directly.
|
||||
*/
|
||||
export class AdbDaemonTransport implements AdbTransport {
|
||||
/**
|
||||
* Authenticate with the ADB Daemon and create a new transport.
|
||||
*/
|
||||
static async authenticate({
|
||||
serial,
|
||||
connection,
|
||||
features = AdbDeviceFeatures,
|
||||
initialDelayedAckBytes = ADB_DAEMON_DEFAULT_INITIAL_PAYLOAD_SIZE,
|
||||
...options
|
||||
}: AdbDaemonAuthenticationOptions &
|
||||
(
|
||||
| { authenticator: AdbAuthenticator }
|
||||
| {
|
||||
credentialStore: AdbCredentialStore;
|
||||
onKeyLoadError?: ((error: Error) => void) | undefined;
|
||||
onSignatureAuthentication?:
|
||||
| ((key: AdbKeyInfo) => void)
|
||||
| undefined;
|
||||
onSignatureRejected?: ((key: AdbKeyInfo) => void) | undefined;
|
||||
onPublicKeyAuthentication?:
|
||||
| ((key: AdbKeyInfo) => void)
|
||||
| undefined;
|
||||
}
|
||||
)): Promise<AdbDaemonTransport> {
|
||||
// Initially, set to highest-supported version and payload size.
|
||||
let version = 0x01000001;
|
||||
// Android 4: 4K, Android 7: 256K, Android 9: 1M
|
||||
let maxPayloadSize = 1024 * 1024;
|
||||
|
||||
const resolver = new PromiseResolver<string>();
|
||||
let authenticator: AdbAuthenticator;
|
||||
if ("authenticator" in options) {
|
||||
authenticator = options.authenticator;
|
||||
} else {
|
||||
const defaultAuthenticator = new AdbDefaultAuthenticator(
|
||||
options.credentialStore,
|
||||
);
|
||||
if (options.onKeyLoadError) {
|
||||
defaultAuthenticator.onKeyLoadError(options.onKeyLoadError);
|
||||
}
|
||||
if (options.onSignatureAuthentication) {
|
||||
defaultAuthenticator.onSignatureAuthentication(
|
||||
options.onSignatureAuthentication,
|
||||
);
|
||||
}
|
||||
if (options.onSignatureRejected) {
|
||||
defaultAuthenticator.onSignatureRejected(
|
||||
options.onSignatureRejected,
|
||||
);
|
||||
}
|
||||
if (options.onPublicKeyAuthentication) {
|
||||
defaultAuthenticator.onPublicKeyAuthentication(
|
||||
options.onPublicKeyAuthentication,
|
||||
);
|
||||
}
|
||||
authenticator = defaultAuthenticator;
|
||||
}
|
||||
|
||||
// Here is similar to `AdbPacketDispatcher`,
|
||||
// But the received packet types and send packet processing are different.
|
||||
const abortController = new AbortController();
|
||||
const pipe = connection.readable
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
async write(packet) {
|
||||
switch (packet.command) {
|
||||
case AdbCommand.Connect:
|
||||
version = Math.min(version, packet.arg0);
|
||||
maxPayloadSize = Math.min(
|
||||
maxPayloadSize,
|
||||
packet.arg1,
|
||||
);
|
||||
resolver.resolve(decodeUtf8(packet.payload));
|
||||
break;
|
||||
case AdbCommand.Auth: {
|
||||
await sendPacket(
|
||||
await authenticator.authenticate(packet),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Maybe the previous ADB client exited without reading all packets,
|
||||
// so they are still waiting in OS internal buffer.
|
||||
// Just ignore them.
|
||||
// Because a `Connect` packet will reset the device,
|
||||
// Eventually there will be `Connect` and `Auth` response packets.
|
||||
break;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
// Don't cancel the source ReadableStream on AbortSignal abort.
|
||||
preventCancel: true,
|
||||
signal: abortController.signal,
|
||||
},
|
||||
)
|
||||
.then(
|
||||
async () => {
|
||||
await authenticator.close?.();
|
||||
|
||||
// If `resolver` is already settled, call `reject` won't do anything.
|
||||
resolver.reject(
|
||||
new Error("Connection closed unexpectedly"),
|
||||
);
|
||||
},
|
||||
async (e) => {
|
||||
await authenticator.close?.();
|
||||
|
||||
resolver.reject(e);
|
||||
},
|
||||
);
|
||||
|
||||
const writer = connection.writable.getWriter();
|
||||
async function sendPacket(init: AdbPacketData) {
|
||||
// Always send checksum in auth steps
|
||||
// Because we don't know if the device needs it or not.
|
||||
(init as AdbPacketInit).checksum = calculateChecksum(init.payload);
|
||||
(init as AdbPacketInit).magic = init.command ^ 0xffffffff;
|
||||
await Consumable.WritableStream.write(
|
||||
writer,
|
||||
init as AdbPacketInit,
|
||||
);
|
||||
}
|
||||
|
||||
if (initialDelayedAckBytes <= 0) {
|
||||
const index = features.indexOf(AdbFeature.DelayedAck);
|
||||
if (index !== -1) {
|
||||
features = features.toSpliced(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let banner: string;
|
||||
try {
|
||||
await sendPacket({
|
||||
command: AdbCommand.Connect,
|
||||
arg0: version,
|
||||
arg1: maxPayloadSize,
|
||||
// The terminating `;` is required in formal definition
|
||||
// But ADB daemon (all versions) can still work without it
|
||||
payload: encodeUtf8(`host::features=${features.join(",")}`),
|
||||
});
|
||||
|
||||
banner = await resolver.promise;
|
||||
} finally {
|
||||
// When failed, release locks on `connection` so the caller can try again.
|
||||
// When success, also release locks so `AdbPacketDispatcher` can use them.
|
||||
abortController.abort();
|
||||
writer.releaseLock();
|
||||
|
||||
// Wait until pipe stops (`ReadableStream` lock released)
|
||||
await pipe;
|
||||
}
|
||||
|
||||
return new AdbDaemonTransport({
|
||||
serial,
|
||||
connection,
|
||||
version,
|
||||
maxPayloadSize,
|
||||
banner,
|
||||
features,
|
||||
initialDelayedAckBytes,
|
||||
preserveConnection: options.preserveConnection,
|
||||
readTimeLimit: options.readTimeLimit,
|
||||
});
|
||||
static authenticate(
|
||||
authenticator: AdbDaemonAuthenticator,
|
||||
options: AdbDaemonAuthenticateOptions,
|
||||
) {
|
||||
return authenticator.authenticate(options);
|
||||
}
|
||||
|
||||
#connection: AdbDaemonConnection;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue