From 43b5611a39a7daeb4bcea62f3054122f5d29ccfe Mon Sep 17 00:00:00 2001 From: P-Courteille Date: Wed, 19 Nov 2025 10:18:39 +0100 Subject: [PATCH 1/3] WhiteBlackList --- README.md | 14 ++++++++++++ package.json | 6 ++++++ src/interfaces.ts | 4 ++++ src/plugin.ts | 25 ++++++++++++++++++++-- src/proxy.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils/proxy.ts | 15 +++++++++++-- 6 files changed, 113 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index accfb80..30d7dde 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,20 @@ If you need to use a custom certificate, it can be done by passing `certdirector Please keep the same directory structure as the existing certificate folder. +## Whitelist/Blacklist + +If you need to limit the calls going through the proxy, it can be done by passing `whitelisteddomains` and `blacklisteddomains` as an argument of the plugin: + +Example of `whitelisteddomains`: only the call in the domain `*.mydomain.com` got through the proxy. + +`appium server -ka 800 --use-plugins=appium-interceptor --plugin-appium-interceptor-whitelisteddomains='["*.mydomain.com"]' -pa /wd/hub` + +Example of `blacklisteddomains`: all the call go through the proxy, except the one from `*.otherdomain.com`. + +`appium server -ka 800 --use-plugins=appium-interceptor --plugin-appium-interceptor-blacklisteddomains='["*.otherdomain.com"]' -pa /wd/hub` + +Note: `whitelisteddomains` and `blacklisteddomains` are two different approach and are not supposed to be used together. If both are present, `blacklisteddomains` will be ignored. + ## what does this plugin do? For every appium session, interceptor plugin will start a proxy server and updates the device proxy settings to pass all network traffic to proxy server. Mocking is disabled by default and can be enabled from the test by passing `appium:intercept : true` in the desired capability while creating a new appium session. diff --git a/package.json b/package.json index 79cc122..d68f465 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,12 @@ "properties": { "certdirectory": { "type": "string" + }, + "whitelisteddomains": { + "type": "string" + }, + "blacklisteddomains": { + "type": "string" } }, "title": "Appium interceptor plugin", diff --git a/src/interfaces.ts b/src/interfaces.ts index 6176ff3..55e7020 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -2,8 +2,12 @@ import path from 'path'; export interface IPluginArgs { certdirectory: string; + whitelisteddomains: string[] | string; + blacklisteddomains: string[] | string; } export const DefaultPluginArgs: IPluginArgs = { certdirectory: path.join(__dirname, '..', 'certificate'), + whitelisteddomains: [], + blacklisteddomains: [], }; \ No newline at end of file diff --git a/src/plugin.ts b/src/plugin.ts index 76c7c90..406e62c 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -5,7 +5,7 @@ import { CliArg, ISessionCapability, MockConfig, RecordConfig, RequestInfo, Repl import { DefaultPluginArgs, IPluginArgs } from './interfaces'; import _ from 'lodash'; import { configureWifiProxy, isRealDevice, getGlobalProxyValue } from './utils/adb'; -import { cleanUpProxyServer, sanitizeMockConfig, setupProxyServer } from './utils/proxy'; +import { cleanUpProxyServer, parseJson, sanitizeMockConfig, setupProxyServer } from './utils/proxy'; import proxyCache from './proxy-cache'; import logger from './logger'; import log from './logger'; @@ -94,6 +94,19 @@ export class AppiumInterceptorPlugin extends BasePlugin { const interceptFlag = mergedCaps['appium:intercept']; const { deviceUDID, platformName } = response.value[1]; const certDirectory = this.pluginArgs.certdirectory; + const whitelistedDomains = + ((domains) => + Array.isArray(domains) ? domains : typeof domains === 'string' ? [domains] : [])( + typeof this.pluginArgs.whitelisteddomains === 'string' + ? parseJson(this.pluginArgs.whitelisteddomains) + : this.pluginArgs.whitelisteddomains + ); + const blacklistedDomains = + ((domains) => (Array.isArray(domains) ? domains : typeof domains === 'string' ? [domains] : []))( + typeof this.pluginArgs.blacklisteddomains === 'string' + ? parseJson(this.pluginArgs.blacklisteddomains) + : this.pluginArgs.blacklisteddomains + ); const sessionId = response.value[0]; const adb = driver.sessions[sessionId]?.adb; @@ -104,7 +117,15 @@ export class AppiumInterceptorPlugin extends BasePlugin { } const realDevice = await isRealDevice(adb, deviceUDID); const currentGlobalProxy = await getGlobalProxyValue(adb, deviceUDID) - const proxy = await setupProxyServer(sessionId, deviceUDID, realDevice, certDirectory, currentGlobalProxy); + const proxy = await setupProxyServer( + sessionId, + deviceUDID, + realDevice, + certDirectory, + currentGlobalProxy, + whitelistedDomains, + blacklistedDomains + ); await configureWifiProxy(adb, deviceUDID, realDevice, proxy.options); proxyCache.add(sessionId, proxy); } diff --git a/src/proxy.ts b/src/proxy.ts index b09eef5..a559b1e 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,5 +1,6 @@ import { MockConfig, RecordConfig, RequestInfo, SniffConfig } from './types'; import { Proxy as HttpProxy, IContext, IProxyOptions } from 'http-mitm-proxy'; +import * as net from 'net'; import { ProxyAgent } from 'proxy-agent'; import { v4 as uuid } from 'uuid'; import { @@ -30,6 +31,8 @@ export interface ProxyOptions { port: number; ip: string; previousGlobalProxy?: ProxyOptions; + whitelistedDomains?: string[]; + blacklistedDomains?: string[]; } export class Proxy { @@ -103,6 +106,56 @@ export class Proxy { log.info('Routing traffic via proxy-chain upstream agent'); } + this.httpProxy.onConnect((req, clientToProxySocket, head, callback) => { + const [hostname, port] = req.url!.split(':'); + const whitelistedDomains = this.options.whitelistedDomains ?? []; + const blacklistedDomains = this.options.blacklistedDomains ?? []; + + let shouldIntercept = true; + if (whitelistedDomains.length > 0) { + shouldIntercept = whitelistedDomains.some((domain) => doesUrlMatch(domain, hostname)); + } else if (blacklistedDomains.length > 0) { + shouldIntercept = !blacklistedDomains.some((domain) => doesUrlMatch(domain, hostname)); + } + if (shouldIntercept) { + return callback(); + } else { + clientToProxySocket.on('error', (err) => { + log.error(`Client socket error for ${hostname}: ${err.message}`); + }); + + const proxyToServerSocket = net.connect( + { + host: hostname, + port: Number(port || 80), + }, + () => { + clientToProxySocket.write('HTTP/1.1 200 Connection Established\r\n\r\n'); + if (head && head.length > 0) { + proxyToServerSocket.write(head); + } + proxyToServerSocket.pipe(clientToProxySocket); + clientToProxySocket.pipe(proxyToServerSocket); + } + ); + + proxyToServerSocket.on('close', () => { + clientToProxySocket.end(); + }); + + clientToProxySocket.on('close', () => { + proxyToServerSocket.end(); + }); + + proxyToServerSocket.on('error', (err) => { + log.error(`[Tunnel] Server socket error for ${hostname}: ${err.message}`); + clientToProxySocket.end(); + }); + + return; + } + }); + this.httpProxy.onRequest( RequestInterceptor((requestData: any) => { for (const sniffer of this.sniffers.values()) { diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 5383fb8..a1fedd6 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -113,12 +113,23 @@ export async function setupProxyServer( deviceUDID: string, isRealDevice: boolean, certDirectory: string, - currentGlobalProxy?: ProxyOptions + currentGlobalProxy?: ProxyOptions, + whitelistedDomains?: string[], + blacklistedDomains?: string[] ) { const certificatePath = prepareCertificate(sessionId, certDirectory); const port = await getPort(); const _ip = isRealDevice ? 'localhost' : ip.address('public', 'ipv4'); - const proxy = new Proxy({ deviceUDID, sessionId, certificatePath, port, ip: _ip, previousGlobalProxy: currentGlobalProxy }); + const proxy = new Proxy({ + deviceUDID, + sessionId, + certificatePath, + port, + ip: _ip, + previousGlobalProxy: currentGlobalProxy, + whitelistedDomains, + blacklistedDomains, + }); await proxy.start(); if (!proxy.isStarted()) { throw new Error('Unable to start the proxy server'); From 99b38ae01092dc8ffd9ee9d6363054c753052503 Mon Sep 17 00:00:00 2001 From: P-Courteille Date: Wed, 19 Nov 2025 10:24:54 +0100 Subject: [PATCH 2/3] Readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30d7dde..8831868 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,11 @@ Please keep the same directory structure as the existing certificate folder. If you need to limit the calls going through the proxy, it can be done by passing `whitelisteddomains` and `blacklisteddomains` as an argument of the plugin: -Example of `whitelisteddomains`: only the call in the domain `*.mydomain.com` got through the proxy. +Example of `whitelisteddomains`: only the calls for the domain `*.mydomain.com` got through the proxy. `appium server -ka 800 --use-plugins=appium-interceptor --plugin-appium-interceptor-whitelisteddomains='["*.mydomain.com"]' -pa /wd/hub` -Example of `blacklisteddomains`: all the call go through the proxy, except the one from `*.otherdomain.com`. +Example of `blacklisteddomains`: all the calls go through the proxy, except the calls for `*.otherdomain.com`. `appium server -ka 800 --use-plugins=appium-interceptor --plugin-appium-interceptor-blacklisteddomains='["*.otherdomain.com"]' -pa /wd/hub` From a79f649fe298dfff74aeada482dc980f224dd7be Mon Sep 17 00:00:00 2001 From: P-Courteille Date: Wed, 19 Nov 2025 10:29:32 +0100 Subject: [PATCH 3/3] bump up version --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1d036bb..00f1bac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "appium-interceptor", - "version": "1.0.3", + "version": "1.0.4", "license": "ISC", "dependencies": { "@appium/support": "^7.0.0",