Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 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`

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.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@
"properties": {
"certdirectory": {
"type": "string"
},
"whitelisteddomains": {
"type": "string"
},
"blacklisteddomains": {
"type": "string"
}
},
"title": "Appium interceptor plugin",
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
};
25 changes: 23 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand All @@ -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);
}
Expand Down
53 changes: 53 additions & 0 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -30,6 +31,8 @@ export interface ProxyOptions {
port: number;
ip: string;
previousGlobalProxy?: ProxyOptions;
whitelistedDomains?: string[];
blacklistedDomains?: string[];
}

export class Proxy {
Expand Down Expand Up @@ -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()) {
Expand Down
15 changes: 13 additions & 2 deletions src/utils/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down