Skip to content

Commit 4ecd28d

Browse files
authored
fix: Remove reverse tunner (#85)
- Indentation - Add new function removeReverseTunnel in adb.ts to correctly remove reverse tunnel when deleteSession and onUnexpectedShutdown hooks are triggered
1 parent bb311ad commit 4ecd28d

File tree

2 files changed

+106
-48
lines changed

2 files changed

+106
-48
lines changed

src/plugin.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import { BasePlugin } from 'appium/plugin';
22
import http from 'http';
33
import { Application } from 'express';
4-
import { CliArg, ISessionCapability, MockConfig, RecordConfig, RequestInfo, ReplayConfig, SniffConfig } from './types';
4+
import {
5+
CliArg,
6+
ISessionCapability,
7+
MockConfig,
8+
RecordConfig,
9+
RequestInfo,
10+
ReplayConfig,
11+
SniffConfig,
12+
} from './types';
513
import { DefaultPluginArgs, IPluginArgs } from './interfaces';
614
import _ from 'lodash';
7-
import { configureWifiProxy, isRealDevice, getCurrentWifiProxyConfig, getAdbReverseTunnels } from './utils/adb';
15+
import {
16+
configureWifiProxy,
17+
isRealDevice,
18+
getCurrentWifiProxyConfig,
19+
getAdbReverseTunnels,
20+
removeReverseTunnel,
21+
} from './utils/adb';
822
import { cleanUpProxyServer, sanitizeMockConfig, setupProxyServer } from './utils/proxy';
923
import proxyCache from './proxy-cache';
1024
import logger from './logger';
@@ -86,7 +100,7 @@ export class AppiumInterceptorPlugin extends BasePlugin {
86100
driver: any,
87101
jwpDesCaps: any,
88102
jwpReqCaps: any,
89-
caps: ISessionCapability
103+
caps: ISessionCapability,
90104
) {
91105
const response = await next();
92106
//If session creation failed
@@ -102,25 +116,35 @@ export class AppiumInterceptorPlugin extends BasePlugin {
102116
const adb = driver.sessions[sessionId]?.adb;
103117

104118
if (interceptFlag && platformName.toLowerCase().trim() === 'android') {
105-
if(!adb) {
106-
log.info(`Unable to find adb instance from session ${sessionId}. So skipping api interception.`);
119+
if (!adb) {
120+
log.info(
121+
`Unable to find adb instance from session ${sessionId}. So skipping api interception.`,
122+
);
107123
return response;
108124
}
109125
const realDevice = await isRealDevice(adb, deviceUDID);
110-
const currentWifiProxyConfig = await getCurrentWifiProxyConfig(adb, deviceUDID)
111-
const proxy = await setupProxyServer(sessionId, deviceUDID, realDevice, certDirectory, currentWifiProxyConfig);
126+
const currentWifiProxyConfig = await getCurrentWifiProxyConfig(adb, deviceUDID);
127+
const proxy = await setupProxyServer(
128+
sessionId,
129+
deviceUDID,
130+
realDevice,
131+
certDirectory,
132+
currentWifiProxyConfig,
133+
);
112134
await configureWifiProxy(adb, deviceUDID, realDevice, proxy.options);
113135
proxyCache.add(sessionId, proxy);
114136
}
115-
log.info("Creating session for appium interceptor");
137+
log.info('Creating session for appium interceptor');
116138
return response;
117139
}
118140

119141
async deleteSession(next: () => any, driver: any, sessionId: any) {
120142
const proxy = proxyCache.get(sessionId);
121143
if (proxy) {
122144
const adb = driver.sessions[sessionId]?.adb;
123-
await configureWifiProxy(adb, proxy.deviceUDID, false, proxy.previousGlobalProxy);
145+
const deviceUDID = proxy.deviceUDID;
146+
await configureWifiProxy(adb, deviceUDID, false, proxy.previousGlobalProxy);
147+
await removeReverseTunnel(adb, deviceUDID, proxy.options.port);
124148
await cleanUpProxyServer(proxy);
125149
}
126150
return next();
@@ -132,7 +156,9 @@ export class AppiumInterceptorPlugin extends BasePlugin {
132156
const proxy = proxyCache.get(sessionId);
133157
if (proxy) {
134158
const adb = driver.sessions[sessionId]?.adb;
135-
await configureWifiProxy(adb, proxy.deviceUDID, false, proxy.previousGlobalProxy);
159+
const deviceUDID = proxy.deviceUDID;
160+
await configureWifiProxy(adb, deviceUDID, false, proxy.previousGlobalProxy);
161+
await removeReverseTunnel(adb, deviceUDID, proxy.options.port);
136162
await cleanUpProxyServer(proxy);
137163
}
138164
}
@@ -193,8 +219,8 @@ export class AppiumInterceptorPlugin extends BasePlugin {
193219
async getInterceptedData(next: any, driver: any, id: any): Promise<RequestInfo[]> {
194220
const proxy = proxyCache.get(driver.sessionId);
195221
if (!proxy) {
196-
logger.error('Proxy is not running');
197-
throw new Error('Proxy is not active for current session');
222+
logger.error('Proxy is not running');
223+
throw new Error('Proxy is not active for current session');
198224
}
199225

200226
log.info(`Getting intercepted requests for listener with id: ${id}`);
@@ -234,7 +260,7 @@ export class AppiumInterceptorPlugin extends BasePlugin {
234260
return proxy.removeSniffer(true, id);
235261
}
236262

237-
async startReplaying(next:any, driver:any, replayConfig: ReplayConfig) {
263+
async startReplaying(next: any, driver: any, replayConfig: ReplayConfig) {
238264
const proxy = proxyCache.get(driver.sessionId);
239265
if (!proxy) {
240266
logger.error('Proxy is not running');
@@ -245,13 +271,13 @@ export class AppiumInterceptorPlugin extends BasePlugin {
245271
return proxy.getRecordingManager().replayTraffic(replayConfig);
246272
}
247273

248-
async stopReplaying(next: any, driver:any, id:any) {
274+
async stopReplaying(next: any, driver: any, id: any) {
249275
const proxy = proxyCache.get(driver.sessionId);
250276
if (!proxy) {
251277
logger.error('Proxy is not running');
252278
throw new Error('Proxy is not active for current session');
253279
}
254-
log.info("Initiating stop replaying traffic");
280+
log.info('Initiating stop replaying traffic');
255281
proxy.getRecordingManager().stopReplay(id);
256282
}
257283

@@ -291,4 +317,4 @@ export class AppiumInterceptorPlugin extends BasePlugin {
291317
async execute(next: any, driver: any, script: any, args: any) {
292318
return await this.executeMethod(next, driver, script, args);
293319
}
294-
}
320+
}

src/utils/adb.ts

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async function adbExecWithDevice(adb: ADBInstance, udid: UDID, args: string[]):
1212
export async function getDeviceProperty(
1313
adb: ADBInstance,
1414
udid: UDID,
15-
prop: string
15+
prop: string,
1616
): Promise<string | undefined> {
1717
try {
1818
return await adbExecWithDevice(adb, udid, ['shell', 'getprop', prop]);
@@ -44,24 +44,33 @@ export async function configureWifiProxy(
4444
adb: ADBInstance,
4545
udid: UDID,
4646
isRealDevice: boolean,
47-
proxyConfig?: ProxyOptions
47+
proxyConfig?: ProxyOptions,
4848
): Promise<string> {
49-
logger.info(`configureWifiProxy(udid=${udid}, isRealDevice=${isRealDevice}, proxyConfig=${JSON.stringify(proxyConfig)})`)
49+
logger.info(
50+
`configureWifiProxy(udid=${udid}, isRealDevice=${isRealDevice}, proxyConfig=${JSON.stringify(proxyConfig)})`,
51+
);
5052
try {
51-
const isConfigValid = proxyConfig
52-
&& proxyConfig.ip
53-
&& proxyConfig.ip.trim().length > 0
54-
&& !isNaN(proxyConfig.port)
55-
&& proxyConfig.port > 0;
53+
const isConfigValid =
54+
proxyConfig &&
55+
proxyConfig.ip &&
56+
proxyConfig.ip.trim().length > 0 &&
57+
!isNaN(proxyConfig.port) &&
58+
proxyConfig.port > 0;
5659

5760
if (!isConfigValid) {
58-
logger.warn(`Invalid proxy config: ${JSON.stringify(proxyConfig)}. Proxy will be disabled for udid ${udid}.`);
61+
logger.warn(
62+
`Invalid proxy config: ${JSON.stringify(proxyConfig)}. Proxy will be disabled for udid ${udid}.`,
63+
);
5964
}
6065

6166
const host = isConfigValid ? `${proxyConfig.ip}:${proxyConfig.port}` : ':0';
6267

6368
if (isRealDevice && isConfigValid) {
64-
await adbExecWithDevice(adb, udid, ['reverse', `tcp:${proxyConfig.port}`, `tcp:${proxyConfig.port}`]);
69+
await adbExecWithDevice(adb, udid, [
70+
'reverse',
71+
`tcp:${proxyConfig.port}`,
72+
`tcp:${proxyConfig.port}`,
73+
]);
6574
}
6675

6776
return await adbExecWithDevice(adb, udid, [
@@ -83,16 +92,16 @@ export async function configureWifiProxy(
8392
*
8493
* @param adb - The ADB instance established by Appium.
8594
* @param udid - The Unique Device Identifier (UDID) of the Android device or emulator.
86-
* @returns A Promise resolving to an object containing the IP and port of the proxy
87-
* ({ ip: string, port: number }), or undefined if no proxy is configured,
95+
* @returns A Promise resolving to an object containing the IP and port of the proxy
96+
* ({ ip: string, port: number }), or undefined if no proxy is configured,
8897
* or if the configuration is invalid (e.g., malformed port).
8998
* @throws {Error} Throws an error if the ADB command execution fails.
9099
*/
91100
export async function getCurrentWifiProxyConfig(
92101
adb: ADBInstance,
93-
udid: UDID
102+
udid: UDID,
94103
): Promise<ProxyOptions | undefined> {
95-
logger.info(`getCurrentWifiProxyConfig(udid=${udid})`);
104+
logger.info(`getCurrentWifiProxyConfig(udid=${udid})`);
96105
try {
97106
// Execute ADB command to get the current global HTTP proxy setting
98107
const proxySettingsCommandResult = await adbExecWithDevice(adb, udid, [
@@ -104,14 +113,20 @@ export async function getCurrentWifiProxyConfig(
104113
]);
105114

106115
// ADB returns ":0" or "null" when the proxy is disabled.
107-
if (!proxySettingsCommandResult || proxySettingsCommandResult === ':0' || proxySettingsCommandResult === 'null') {
116+
if (
117+
!proxySettingsCommandResult ||
118+
proxySettingsCommandResult === ':0' ||
119+
proxySettingsCommandResult === 'null'
120+
) {
108121
logger.info(`No active proxy for udid ${udid}.`);
109122
return undefined;
110-
}
123+
}
111124

112125
// Ensure the format is IP:PORT (must contain at least one ':').
113126
if (!proxySettingsCommandResult.includes(':')) {
114-
logger.warn(`Invalid proxy settings format detected for udid ${udid}: '${proxySettingsCommandResult}'.`);
127+
logger.warn(
128+
`Invalid proxy settings format detected for udid ${udid}: '${proxySettingsCommandResult}'.`,
129+
);
115130
return undefined;
116131
}
117132

@@ -122,45 +137,62 @@ export async function getCurrentWifiProxyConfig(
122137
// Validate IP and port values.
123138
// IP should not be empty after trimming, and port must be a valid number greater than 0.
124139
if (!ip.trim() || isNaN(port) || port <= 0) {
125-
logger.warn(`Invalid proxy settings detected for udid ${udid}: (ip=${ip}, port=${port})`);
126-
return undefined;
140+
logger.warn(`Invalid proxy settings detected for udid ${udid}: (ip=${ip}, port=${port})`);
141+
return undefined;
127142
}
128143

129144
const proxyOptions: ProxyOptions = {
130-
ip: ip.trim(),
131-
port: port,
145+
ip: ip.trim(),
146+
port: port,
132147
} as ProxyOptions;
133148

134149
logger.info(`Found active proxy for udid ${udid}: ${JSON.stringify(proxyOptions)}`);
135150
return proxyOptions;
136-
137151
} catch (error: any) {
138152
throw new Error(`Error getting wifi proxy settings for ${udid}: ${error.message}`);
139153
}
140154
}
141155

142156
/**
143157
* Retrieves the list of all active ADB reverse port forwardings for a specific device.
144-
* * This method executes 'adb reverse --list' to identify which device ports are
145-
* currently bridged to the host machine. It is essential for diagnosing
158+
* * This method executes 'adb reverse --list' to identify which device ports are
159+
* currently bridged to the host machine. It is essential for diagnosing
146160
* connectivity between the mobile device and local proxy servers.
147161
*
148162
* @param adb - The ADB instance provided by the Appium driver.
149163
* @param udid - The Unique Device Identifier (UDID) of the target Android device.
150164
* @returns A Promise resolving to the raw string output of the 'adb reverse --list' command.
151165
* @throws {Error} If the command fails to execute or the device is unreachable.
152166
*/
153-
export async function getAdbReverseTunnels(
167+
export async function getAdbReverseTunnels(adb: ADBInstance, udid: UDID): Promise<string> {
168+
try {
169+
return await adbExecWithDevice(adb, udid, ['reverse', '--list']);
170+
} catch (error: any) {
171+
throw new Error(`Failed to list active reverse tunnels for device ${udid}: ${error.message}`);
172+
}
173+
}
174+
175+
/**
176+
* Removes a specific reverse tunnel established on the device for the given port.
177+
* * Note: While the reverse tunnel is automatically created within the
178+
* `configureWifiProxy` method (for real devices), ADB reverse tunnels are
179+
* not automatically closed when a test session ends.
180+
* * Since `configureWifiProxy` establishes a bridge between the device and the
181+
* proxy host on a specific port, failing to clear it can lead to "Port already in use"
182+
* errors in subsequent sessions.
183+
* * @param adb - The ADB instance
184+
* @param udid - The device unique identifier
185+
* @param port - The specific port to remove from reverse tunnels
186+
*/
187+
export async function removeReverseTunnel(
154188
adb: ADBInstance,
155-
udid: UDID
189+
udid: UDID,
190+
port: number | string,
156191
): Promise<string> {
157192
try {
158-
return await adbExecWithDevice(adb, udid, [
159-
'reverse',
160-
'--list',
161-
]);
162-
} catch(error: any) {
163-
throw new Error(`Failed to list active reverse tunnels for device ${udid}: ${error.message}`);
193+
return await adbExecWithDevice(adb, udid, ['reverse', '--remove', `tcp:${port}`]);
194+
} catch (error: any) {
195+
throw new Error(`Error removing reverse tunnel for port ${port} on ${udid}: ${error.message}`);
164196
}
165197
}
166198

0 commit comments

Comments
 (0)