Skip to content

Commit fff89af

Browse files
committed
Tunnel ADB connections to the proxy, as a fallback route
1 parent 4c2cf08 commit fff89af

File tree

2 files changed

+38
-1
lines changed

2 files changed

+38
-1
lines changed

custom-typings/adbkit.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ declare module '@devicefarmer/adbkit' {
3838
shell(id: string, cmd: string | string[]): Promise<stream.Readable>;
3939
root(id: string): Promise<true>;
4040
on(type: 'error', handler: (error: Error) => void): void;
41+
reverse(id: string, remote: string, local: string): Promise<true>;
4142
}
4243

4344
export function createClient(options?: {

src/interceptors/android/android-adb-interceptor.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,13 @@ export class AndroidAdbInterceptor implements Interceptor {
100100
)
101101
),
102102
port: proxyPort,
103+
localTunnelPort: proxyPort,
103104
certFingerprint: generateSPKIFingerprint(this.config.https.certContent)
104105
};
105106
const intentData = urlSafeBase64(JSON.stringify(setupParams));
106107

108+
await this.adbClient.reverse(options.deviceId, 'tcp:' + proxyPort, 'tcp:' + proxyPort).catch(() => {});
109+
107110
// Use ADB to launch the app with the proxy details
108111
await this.adbClient.startActivity(options.deviceId, {
109112
wait: true,
@@ -112,7 +115,40 @@ export class AndroidAdbInterceptor implements Interceptor {
112115
});
113116

114117
this.deviceProxyMapping[proxyPort] = this.deviceProxyMapping[proxyPort] || [];
115-
this.deviceProxyMapping[proxyPort].push(options.deviceId);
118+
119+
if (!this.deviceProxyMapping[proxyPort].includes(options.deviceId)) {
120+
this.deviceProxyMapping[proxyPort].push(options.deviceId);
121+
122+
let tunnelConnectFailures = 0;
123+
124+
// The reverse tunnel can break when connecting/disconnecting from the VPN. This is a problem! It can
125+
// also break in other cases, e.g. when ADB is restarted for some reason. To handle this, we constantly
126+
// reinforce the tunnel while HTTP Toolkit is running & the device is connected.
127+
const tunnelCheckInterval = setInterval(async () => {
128+
if (this.deviceProxyMapping[proxyPort].includes(options.deviceId)) {
129+
try {
130+
await this.adbClient.reverse(options.deviceId, 'tcp:' + proxyPort, 'tcp:' + proxyPort)
131+
tunnelConnectFailures = 0;
132+
} catch (e) {
133+
tunnelConnectFailures += 1;
134+
console.log(`${options.deviceId} ADB tunnel failed`, e);
135+
136+
if (tunnelConnectFailures >= 5) {
137+
// After 10 seconds disconnected, give up
138+
console.log(`${options.deviceId} disconnected, dropping the ADB tunnel`);
139+
this.deviceProxyMapping[proxyPort] = this.deviceProxyMapping[proxyPort]
140+
.filter(id => id !== options.deviceId);
141+
clearInterval(tunnelCheckInterval);
142+
}
143+
}
144+
} else {
145+
// Deactivation at shutdown will clear the proxy data, and so clear this interval
146+
// will automatically shut down.
147+
clearInterval(tunnelCheckInterval);
148+
}
149+
}, 2000);
150+
tunnelCheckInterval.unref(); // Don't let this block shutdown
151+
}
116152
}
117153

118154
async deactivate(port: number | string): Promise<void | {}> {

0 commit comments

Comments
 (0)