From e9a6374385f0c5f13e1a955d9ebfbdec3c2a5441 Mon Sep 17 00:00:00 2001 From: P-Courteille Date: Tue, 14 Oct 2025 12:14:12 +0200 Subject: [PATCH 1/3] customCertificate --- package.json | 6 +++++- src/config.ts | 5 ----- src/interfaces.ts | 9 +++++++++ src/plugin.ts | 7 ++++++- src/scripts/test-connection.ts | 9 ++++++--- src/utils/proxy.ts | 15 +++++++++------ 6 files changed, 35 insertions(+), 16 deletions(-) delete mode 100644 src/config.ts create mode 100644 src/interfaces.ts diff --git a/package.json b/package.json index b221196..7cac9c8 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,11 @@ "$schema": "http://json-schema.org/draft-07/schema", "additionalProperties": false, "description": "Appium configuration schema for the interceptor plugin.", - "properties": {}, + "properties": { + "certdirectory": { + "type": "string" + } + }, "title": "Appium interceptor plugin", "type": "object" }, diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 8a2f633..0000000 --- a/src/config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import path from 'path'; - -export default { - certDirectory: path.join(__dirname, '..', 'certificate'), -}; diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..6176ff3 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,9 @@ +import path from 'path'; + +export interface IPluginArgs { + certdirectory: string; +} + +export const DefaultPluginArgs: IPluginArgs = { + certdirectory: path.join(__dirname, '..', 'certificate'), +}; \ No newline at end of file diff --git a/src/plugin.ts b/src/plugin.ts index 9e3415d..2c3b46a 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -2,6 +2,7 @@ import { BasePlugin } from 'appium/plugin'; import http from 'http'; import { Application } from 'express'; import { CliArg, ISessionCapability, MockConfig, RecordConfig, RequestInfo, ReplayConfig, SniffConfig } from './types'; +import { DefaultPluginArgs, IPluginArgs } from './interfaces'; import _ from 'lodash'; import { configureWifiProxy, isRealDevice } from './utils/adb'; import { cleanUpProxyServer, sanitizeMockConfig, setupProxyServer } from './utils/proxy'; @@ -10,6 +11,7 @@ import logger from './logger'; import log from './logger'; export class AppiumInterceptorPlugin extends BasePlugin { + private pluginArgs: IPluginArgs = Object.assign({}, DefaultPluginArgs); static executeMethodMap = { 'interceptor: addMock': { command: 'addMock', @@ -68,7 +70,9 @@ export class AppiumInterceptorPlugin extends BasePlugin { }; constructor(name: string, cliArgs: CliArg) { + log.debug(`📱 Plugin Args: ${JSON.stringify(cliArgs)}`); super(name, cliArgs); + this.pluginArgs = Object.assign({}, DefaultPluginArgs, cliArgs as unknown as IPluginArgs); } static async updateServer(expressApp: Application, httpServer: http.Server, cliArgs: CliArg) {} @@ -89,6 +93,7 @@ export class AppiumInterceptorPlugin extends BasePlugin { const mergedCaps = { ...caps.alwaysMatch, ..._.get(caps, 'firstMatch[0]', {}) }; const interceptFlag = mergedCaps['appium:intercept']; const { deviceUDID, platformName } = response.value[1]; + const certDirectory = this.pluginArgs.certdirectory; const sessionId = response.value[0]; const adb = driver.sessions[sessionId]?.adb; @@ -98,7 +103,7 @@ export class AppiumInterceptorPlugin extends BasePlugin { return response; } const realDevice = await isRealDevice(adb, deviceUDID); - const proxy = await setupProxyServer(sessionId, deviceUDID, realDevice); + const proxy = await setupProxyServer(sessionId, deviceUDID, realDevice, certDirectory); await configureWifiProxy(adb, deviceUDID, realDevice, proxy); proxyCache.add(sessionId, proxy); } diff --git a/src/scripts/test-connection.ts b/src/scripts/test-connection.ts index 8a93c88..4574613 100644 --- a/src/scripts/test-connection.ts +++ b/src/scripts/test-connection.ts @@ -7,13 +7,16 @@ import { ADBInstance, UDID, configureWifiProxy, isRealDevice, openUrl } from '.. import { v4 as uuid } from 'uuid'; import { setupProxyServer } from '../utils/proxy'; import { Proxy } from '../proxy'; +import path from 'path'; type VerifyOptions = { udid: string; + certdirectory: string; }; const defaultOptions: VerifyOptions = { udid: '', + certdirectory: path.join(__dirname, '..', 'certificate'), }; const MOCK_BACKEND_HTML = `Appium Mock @@ -76,9 +79,9 @@ async function addMock(proxy: Proxy) { }); } -async function verifyDeviceConnection(adb: ADBInstance, udid: UDID) { +async function verifyDeviceConnection(adb: ADBInstance, udid: UDID, certDirectory: string) { const realDevice = await isRealDevice(adb, udid); - const proxy = await setupProxyServer(uuid(), udid, realDevice); + const proxy = await setupProxyServer(uuid(), udid, realDevice, certDirectory); addMock(proxy); await configureWifiProxy(adb, udid, realDevice, proxy); await openUrl(adb, udid, MOCK_BACKEND_URL); @@ -102,7 +105,7 @@ async function main() { const options = getOptions(); const udid = await pickDeviceToTest(adb, options); await registerExitHook(adb, udid); - await verifyDeviceConnection(adb, udid); + await verifyDeviceConnection(adb, udid, options.certdirectory); } main().catch(console.log); diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index d9cf5c3..fdae61e 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -13,7 +13,6 @@ import { Proxy } from '../proxy'; import ip from 'ip'; import os from 'os'; import path from 'path'; -import config from '../config'; import fs from 'fs-extra'; import { minimatch } from 'minimatch'; import http from 'http'; @@ -112,9 +111,10 @@ export function modifyResponseBody(ctx: IContext, mockConfig: MockConfig) { export async function setupProxyServer( sessionId: string, deviceUDID: string, - isRealDevice: boolean + isRealDevice: boolean, + certDirectory: string, ) { - const certificatePath = prepareCertificate(sessionId); + 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 }); @@ -131,9 +131,12 @@ export async function cleanUpProxyServer(proxy: Proxy) { fs.rmdirSync(proxy.certificatePath, { recursive: true, force: true }); } -function prepareCertificate(sessionId: string) { +function prepareCertificate(sessionId: string, certDirectory: string) { const sessionCertDirectory = path.join(os.tmpdir(), sessionId); - fs.copySync(config.certDirectory, sessionCertDirectory); + if(fs.existsSync(certDirectory)){ + log.errorWithException(`Error certDirectory doesn't exist (${certDirectory})`); + } + fs.copySync(certDirectory, sessionCertDirectory); return sessionCertDirectory; } @@ -203,7 +206,7 @@ export function doesUrlMatch(pattern: UrlPattern, url: string) { ? jsonOrStringUrl.test(url) : minimatch(url, jsonOrStringUrl); } catch (err) { - log.error(`Error validaing url ${pattern} against url ${url}`); + log.error(`Error validating url ${pattern} against url ${url}`); return false; } } From 1b291beadc9aab89d527e783e9bac598eed2ce3d Mon Sep 17 00:00:00 2001 From: P-Courteille Date: Tue, 14 Oct 2025 12:43:54 +0200 Subject: [PATCH 2/3] Readme update --- README.md | 8 ++++++++ docs/commands.md | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e549f7..accfb80 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ The plugin will not be active unless turned on when invoking the Appium server: `appium server -ka 800 --use-plugins=appium-interceptor -pa /wd/hub` +## Custom certificate + +If you need to use a custom certificate, it can be done by passing `certdirectory` as an argument of the plugin: + +`appium server -ka 800 --use-plugins=appium-interceptor --plugin-appium-interceptor-certdirectory="" -pa /wd/hub` + +Please keep the same directory structure as the existing certificate folder. + ## 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/docs/commands.md b/docs/commands.md index cca7ab4..54db1d3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -33,7 +33,7 @@ Mock configuration is a json object that defines the specification for filtering | statusCode | number | no | Updates the response status code with the given value | To simulate any unexpected error you can send some of the below statusCode
1. `500` - Internal server error
2. `400` - Bad request
3. `401` - Unauthorized | | responseHeaders | object | no | Map of key value pairs to be added or removed from the response header | Same syntax as `headers` key. But this will update the response header | | responseBody | object | no | This will replace the original response data returned by the api server and updates it with new data | Passing the config as `{"url" : "/api/login/g" , "responseBody": "{\"error\": \"User account locked\"}", `statusCode`: 400 }` will simulate a error scenario when logged in with any user credentilas | -| updateResponseBody | string | no | This is similar to responseBody but instead of fully mocking the server response, you can replace any value in the response using Regular expression or jsonpath | Consider you application returns user data as `{\"username\": \"someusername\", \"email\": \"someemail@email.com\", \"isAdmin\" : \"false\"}` as a response for get user api request and you want to update the values for email and IsAdmin fiels, then you can pass
`{"updateRequestBody": [{ "jsonPath": "$.email", "newemail@email.com" }, { "jsonPath": "$.isAdmin", "true" }]}` and it will update the email and isAdmin field before sending the response back to the application| +| updateResponseBody | string | no | This is similar to responseBody but instead of fully mocking the server response, you can replace any value in the response using Regular expression or jsonpath | Consider you application returns user data as `{\"username\": \"someusername\", \"email\": \"someemail@email.com\", \"isAdmin\" : \"false\"}` as a response for get user api request and you want to update the values for email and IsAdmin fiels, then you can pass
`{"updateRequestBody": [{ "jsonPath": "$.email", value: "newemail@email.com" }, { "jsonPath": "$.isAdmin", value: "true" }]}` and it will update the email and isAdmin field before sending the response back to the application| ## Commands: From ba102c455cf89d65ee9e5cb61dc93fadb9320354 Mon Sep 17 00:00:00 2001 From: P-Courteille Date: Tue, 14 Oct 2025 18:27:22 +0200 Subject: [PATCH 3/3] hotfix --- package-lock.json | 4 ++-- src/utils/proxy.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 308cba9..1d036bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "appium-interceptor", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "appium-interceptor", - "version": "1.0.2", + "version": "1.0.3", "license": "ISC", "dependencies": { "@appium/support": "^7.0.0", diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index fdae61e..0e9e18f 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -133,8 +133,8 @@ export async function cleanUpProxyServer(proxy: Proxy) { function prepareCertificate(sessionId: string, certDirectory: string) { const sessionCertDirectory = path.join(os.tmpdir(), sessionId); - if(fs.existsSync(certDirectory)){ - log.errorWithException(`Error certDirectory doesn't exist (${certDirectory})`); + if(!fs.existsSync(certDirectory)){ + throw new Error(`Error certDirectory doesn't exist (${certDirectory})`); } fs.copySync(certDirectory, sessionCertDirectory); return sessionCertDirectory;