diff --git a/src/app/googDevice/client/ConnectionForward.ts b/src/app/googDevice/client/ConnectionForward.ts new file mode 100644 index 00000000..cb4bf17d --- /dev/null +++ b/src/app/googDevice/client/ConnectionForward.ts @@ -0,0 +1,93 @@ +import { ManagerClient } from '../../client/ManagerClient'; +import GoogDeviceDescriptor from '../../../types/GoogDeviceDescriptor'; +import Util from '../../Util'; +import { ParamsDeviceTracker } from '../../../types/ParamsDeviceTracker'; +import { ChannelCode } from '../../../common/ChannelCode'; +import Protocol from '@devicefarmer/adbkit/lib/adb/protocol'; + +const TAG = '[ConnectionForward]'; + +export class ConnectionForward extends ManagerClient { + public static start(params: ParamsDeviceTracker, descriptor: GoogDeviceDescriptor): ConnectionForward { + return new ConnectionForward(params, descriptor); + } + + public static createEntryForDeviceList( + descriptor: GoogDeviceDescriptor, + blockClass: string, + params: ParamsDeviceTracker, + ): HTMLElement | DocumentFragment | undefined { + if (descriptor.state !== 'device') { + return; + } + const entry = document.createElement('div'); + entry.classList.add('connection-forward', blockClass); + const button = document.createElement('button'); + button.innerText = `Forward`; + button.classList.add('active', 'action-button'); + entry.appendChild(button); + button.addEventListener('click', (e) => { + e.preventDefault(); + ConnectionForward.start(params, descriptor); + }); + return entry; + } + + private readonly serial: string; + + constructor(params: ParamsDeviceTracker, descriptor: GoogDeviceDescriptor) { + super(params); + this.serial = descriptor.udid; + this.openNewConnection(); + if (!this.ws) { + throw Error('No WebSocket'); + } + } + + protected onPortReceived(port: number): void { + console.log(TAG, port); + } + + protected onErrorReceived(message: string): void { + console.error(TAG, message); + } + + protected supportMultiplexing(): boolean { + return true; + } + + protected onSocketOpen = (): void => { + console.log(TAG, `Connection open`); + }; + + protected onSocketClose(e: CloseEvent): void { + console.log(TAG, `Connection closed: ${e.reason}`); + } + + protected onSocketMessage(e: MessageEvent): void { + const data = Buffer.from(e.data); + const reply = data.slice(0, 4).toString('ascii'); + switch (reply) { + case Protocol.DATA: + const port = data.readUInt16LE(4); + this.onPortReceived(port); + break; + case Protocol.FAIL: + const length = data.readUInt32LE(4); + const message = Util.utf8ByteArrayToString(data.slice(8, 8 + length)); + this.onErrorReceived(message); + break; + default: + console.error(`Unexpected "${reply}"`); + } + } + + protected getChannelInitData(): Buffer { + const serial = Util.stringToUtf8ByteArray(this.serial); + const buffer = Buffer.alloc(4 + 4 + serial.byteLength); + buffer.write(ChannelCode.USBF, 'ascii'); + buffer.writeUInt32LE(serial.length, 4); + buffer.set(serial, 8); + return buffer; + } +} diff --git a/src/app/index.ts b/src/app/index.ts index a9eed5f0..941b32af 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -103,6 +103,11 @@ window.onload = async function (): Promise { tools.push(FileListingClient); /// #endif + /// #if INCLUDE_CONNECTION_FORWARD + const { ConnectionForward } = await import('./googDevice/client/ConnectionForward'); + tools.push(ConnectionForward); + /// #endif + if (tools.length) { const { DeviceTracker } = await import('./googDevice/client/DeviceTracker'); tools.forEach((tool) => { diff --git a/src/common/ChannelCode.ts b/src/common/ChannelCode.ts index 3d60f012..5ec42e94 100644 --- a/src/common/ChannelCode.ts +++ b/src/common/ChannelCode.ts @@ -6,4 +6,5 @@ export enum ChannelCode { ATRC = 'ATRC', // Appl device TRaCer WDAP = 'WDAP', // WebDriverAgent Proxy QVHS = 'QVHS', // Quicktime_Video_Hack Stream + USBF = 'USBF', // Connection Forward } diff --git a/src/server/goog-device/AdbUtils.ts b/src/server/goog-device/AdbUtils.ts index c3bb23ea..e30bcc87 100644 --- a/src/server/goog-device/AdbUtils.ts +++ b/src/server/goog-device/AdbUtils.ts @@ -14,6 +14,8 @@ import Protocol from '@devicefarmer/adbkit/lib/adb/protocol'; import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; import { ReadStream } from 'fs'; import PushTransfer from '@devicefarmer/adbkit/lib/adb/sync/pushtransfer'; +import TcpUsbServer from '@devicefarmer/adbkit/lib/adb/tcpusb/server'; +import Bluebird from 'bluebird'; type IncomingMessage = { statusCode?: number; @@ -366,4 +368,49 @@ export class AdbUtils { const props = await client.getProperties(serial); return props['ro.product.model'] || 'Unknown device'; } + + public static async createTcpUsbBridge( + serial: string, + maxWaitTime: number, + ): Promise<{ server: TcpUsbServer; port: number }> { + const port = await portfinder.getPortPromise(); + const client = AdbExtended.createClient(); + const server = client.createTcpUsbBridge(serial, { + auth: (key): Bluebird => { + return new Bluebird((resolve) => { + console.log(key); + resolve(); + }); + }, + }); + const promise = new Promise<{ server: TcpUsbServer; port: number }>((resolve, reject) => { + let fulfilled = false; + const timeout = setTimeout(() => { + if (fulfilled) { + return; + } + server.end(); + fulfilled = true; + reject(new Error('Timeout')); + }, maxWaitTime); + server.on('listening', () => { + if (fulfilled) { + return; + } + fulfilled = true; + clearTimeout(timeout); + resolve({ server, port }); + }); + server.on('error', (e) => { + if (fulfilled) { + return; + } + fulfilled = true; + clearTimeout(timeout); + reject(e); + }); + }); + server.listen(port); + return promise; + } } diff --git a/src/server/goog-device/mw/ConnectionForward.ts b/src/server/goog-device/mw/ConnectionForward.ts new file mode 100644 index 00000000..7fba4084 --- /dev/null +++ b/src/server/goog-device/mw/ConnectionForward.ts @@ -0,0 +1,73 @@ +import WS from 'ws'; +import { Mw } from '../../mw/Mw'; +import { AdbUtils } from '../AdbUtils'; +import Util from '../../../app/Util'; +import Protocol from '@devicefarmer/adbkit/lib/adb/protocol'; +import { Multiplexer } from '../../../packages/multiplexer/Multiplexer'; +import { ChannelCode } from '../../../common/ChannelCode'; +import TcpUsbServer from '@devicefarmer/adbkit/lib/adb/tcpusb/server'; + +export class ConnectionForward extends Mw { + public static readonly TAG = 'ConnectionForward'; + + public static processChannel(ws: Multiplexer, code: string, data: ArrayBuffer): Mw | undefined { + if (code !== ChannelCode.USBF) { + return; + } + if (!data || data.byteLength < 4) { + return; + } + const buffer = Buffer.from(data); + const length = buffer.readInt32LE(0); + const serial = Util.utf8ByteArrayToString(buffer.slice(4, 4 + length)); + return new ConnectionForward(ws, serial); + } + + private server?: TcpUsbServer; + + constructor(private readonly channel: Multiplexer, private readonly serial: string) { + super(channel); + this.name = `[${ConnectionForward.TAG}|${serial}]`; + this.initServer(); + } + + protected onSocketClose(): void { + super.onSocketClose(); + if (this.server) { + this.server.end(); + this.server = undefined; + } + } + + protected sendMessage = (): void => { + throw Error('Do not use this method. You must send data over channels'); + }; + + protected onSocketMessage(event: WS.MessageEvent): void { + console.log(this.name, 'onSocketMessage', event.data); + } + + protected async initServer(): Promise { + const maxWaitTime = 20000; + try { + const { server, port } = await AdbUtils.createTcpUsbBridge(this.serial, maxWaitTime); + this.server = server; + server.on('connection', () => { + console.log(this.name, 'Has connection'); + }); + ConnectionForward.sendPort(port, this.channel); + } catch (e) { + ConnectionForward.sendError(e.message, this.channel); + } + } + + private static sendPort(port: number, channel: Multiplexer): void { + if (channel.readyState === channel.OPEN) { + const buf = Buffer.alloc(4 + 2); + const offset = buf.write(Protocol.DATA, 'ascii'); + buf.writeUInt16LE(port, offset); + channel.send(buf); + channel.close(); + } + } +} diff --git a/src/server/goog-device/mw/FileListing.ts b/src/server/goog-device/mw/FileListing.ts index f898df52..9facf202 100644 --- a/src/server/goog-device/mw/FileListing.ts +++ b/src/server/goog-device/mw/FileListing.ts @@ -84,16 +84,4 @@ export class FileListing extends Mw { FileListing.sendError(e.message, channel); } } - - private static sendError(message: string, channel: Multiplexer): void { - if (channel.readyState === channel.OPEN) { - const length = Buffer.byteLength(message, 'utf-8'); - const buf = Buffer.alloc(4 + 4 + length); - let offset = buf.write(Protocol.FAIL, 'ascii'); - offset = buf.writeUInt32LE(length, offset); - buf.write(message, offset, 'utf-8'); - channel.send(buf); - channel.close(); - } - } } diff --git a/src/server/index.ts b/src/server/index.ts index 69c22275..1d9fd55b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -53,6 +53,11 @@ async function loadGoogModules() { mw2List.push(FileListing); /// #endif + /// #if INCLUDE_CONNECTION_FORWARD + const { ConnectionForward } = await import('./goog-device/mw/ConnectionForward'); + mw2List.push(ConnectionForward); + /// #endif + mwList.push(WebsocketProxyOverAdb); } loadPlatformModulesPromises.push(loadGoogModules()); diff --git a/src/server/mw/Mw.ts b/src/server/mw/Mw.ts index fbd22dfa..6573c313 100644 --- a/src/server/mw/Mw.ts +++ b/src/server/mw/Mw.ts @@ -4,6 +4,7 @@ import * as querystring from 'querystring'; import url from 'url'; import { Multiplexer } from '../../packages/multiplexer/Multiplexer'; import WS from 'ws'; +import Protocol from '@devicefarmer/adbkit/lib/adb/protocol'; export type RequestParameters = { request: http.IncomingMessage; @@ -47,6 +48,18 @@ export abstract class Mw { this.release(); } + protected static sendError(message: string, channel: Multiplexer): void { + if (channel.readyState === channel.OPEN) { + const length = Buffer.byteLength(message, 'utf-8'); + const buf = Buffer.alloc(4 + 4 + length); + let offset = buf.write(Protocol.FAIL, 'ascii'); + offset = buf.writeUInt32LE(length, offset); + buf.write(message, offset, 'utf-8'); + channel.send(buf); + channel.close(); + } + } + public release(): void { const { readyState, CLOSED, CLOSING } = this.ws; if (readyState !== CLOSED && readyState !== CLOSING) { diff --git a/src/style/devicelist.css b/src/style/devicelist.css index 64e4c591..4aea515d 100644 --- a/src/style/devicelist.css +++ b/src/style/devicelist.css @@ -153,6 +153,7 @@ body.list #device_list_menu { #devices .device-list div.desc-block.stream, #devices .device-list div.desc-block.server_pid, +#devices .device-list div.desc-block.connection-forward, #devices .device-list div.desc-block.net_interface { border: 1px solid var(--device-border-color); border-radius: .3em; @@ -160,7 +161,7 @@ body.list #device_list_menu { white-space: nowrap; } -#devices .device-list div.device div.desc-block.stream button.action-button { +#devices .device-list div.device div.desc-block button.action-button { color: var(--button-text-color); } diff --git a/webpack/default.build.config.json b/webpack/default.build.config.json index e64960d8..777c5d24 100644 --- a/webpack/default.build.config.json +++ b/webpack/default.build.config.json @@ -9,6 +9,7 @@ "INCLUDE_DEV_TOOLS": true, "INCLUDE_ADB_SHELL": true, "INCLUDE_FILE_LISTING": true, + "INCLUDE_CONNECTION_FORWARD": true, "INCLUDE_APPL": false, "INCLUDE_GOOG": true }