Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): adb connection forward #190

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 93 additions & 0 deletions src/app/googDevice/client/ConnectionForward.ts
Original file line number Diff line number Diff line change
@@ -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<ParamsDeviceTracker, never> {
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;
}
}
5 changes: 5 additions & 0 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ window.onload = async function (): Promise<void> {
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) => {
Expand Down
1 change: 1 addition & 0 deletions src/common/ChannelCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
47 changes: 47 additions & 0 deletions src/server/goog-device/AdbUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<boolean | void> => {
return new Bluebird<boolean | void>((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;
}
}
73 changes: 73 additions & 0 deletions src/server/goog-device/mw/ConnectionForward.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
}
}
}
12 changes: 0 additions & 12 deletions src/server/goog-device/mw/FileListing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
5 changes: 5 additions & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
13 changes: 13 additions & 0 deletions src/server/mw/Mw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion src/style/devicelist.css
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,15 @@ 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;
overflow: hidden;
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);
}

Expand Down
1 change: 1 addition & 0 deletions webpack/default.build.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}