Skip to content

Commit

Permalink
Fixes #1 and #5: Add addons to mitproxy to export HAR and broadcast I…
Browse files Browse the repository at this point in the history
…PC events
  • Loading branch information
zner0L committed Mar 9, 2023
1 parent 54196c6 commit 5ae684c
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 35 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,5 @@ $RECYCLE.BIN/

# End of
# https://www.toptal.com/developers/gitignore/api/linux,macos,windows,visualstudiocode,jetbrains+all,sublimetext,node

mitmproxy-addons/
16 changes: 8 additions & 8 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Functions that can be used to instrument the device and analyze apps.

#### Defined in

[cyanoacrylate/src/index.ts:38](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L38)
[cyanoacrylate/src/index.ts:31](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L31)

___

Expand All @@ -82,7 +82,7 @@ The options for the `startAnalysis()` function.

#### Defined in

[cyanoacrylate/src/index.ts:207](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L207)
[cyanoacrylate/src/index.ts:200](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L200)

___

Expand Down Expand Up @@ -113,7 +113,7 @@ Metadata about an app.

#### Defined in

[cyanoacrylate/src/index.ts:30](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L30)
[cyanoacrylate/src/index.ts:23](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L23)

___

Expand Down Expand Up @@ -145,7 +145,7 @@ Functions that can be used to control an app analysis.

#### Defined in

[cyanoacrylate/src/index.ts:81](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L81)
[cyanoacrylate/src/index.ts:74](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L74)

___

Expand All @@ -164,7 +164,7 @@ The result of an app analysis.

#### Defined in

[cyanoacrylate/src/index.ts:150](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L150)
[cyanoacrylate/src/index.ts:143](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L143)

___

Expand Down Expand Up @@ -301,7 +301,7 @@ The options for a specific platform/run target combination.

#### Defined in

[cyanoacrylate/src/index.ts:159](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L159)
[cyanoacrylate/src/index.ts:152](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L152)

___

Expand All @@ -319,7 +319,7 @@ A capability supported by this library.

#### Defined in

[cyanoacrylate/src/index.ts:23](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L23)
[cyanoacrylate/src/index.ts:16](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L16)

___

Expand Down Expand Up @@ -472,4 +472,4 @@ An object that can be used to instrument the device and analyze apps.

#### Defined in

[cyanoacrylate/src/index.ts:246](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L246)
[cyanoacrylate/src/index.ts:239](https://github.com/tweaselORG/cyanoacrylate/blob/main/src/index.ts#L239)
104 changes: 77 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type { PlatformApi, SupportedPlatform, SupportedRunTarget } from 'appstraction';
import { platformApi } from 'appstraction';
import { parseAppMeta, platformApi } from 'appstraction';
import type { ExecaChildProcess } from 'execa';
import { execa } from 'execa';
import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import process from 'node:process';
import timeout, { TimeoutError } from 'p-timeout';
import { join } from 'path';
import { temporaryFile } from 'tempy';
import type { AppPath } from './path';
import { getAppPathAll, getAppPathMain } from './path';
import { awaitAndroidEmulator, awaitProcessStart, dnsLookup, killProcess } from './util';
import { awaitAndroidEmulator, awaitMitmproxyStatus, awaitProcessClose, dnsLookup, killProcess } from './util';

/** A capability supported by this library. */
export type SupportedCapability<Platform extends SupportedPlatform> = Platform extends 'android'
Expand All @@ -21,7 +24,7 @@ export type App = {
/** The app's ID. */
id: string;
/** The app's version. */
version: string | undefined;
version?: string;
};

/** Functions that can be used to instrument the device and analyze apps. */
Expand Down Expand Up @@ -209,6 +212,12 @@ export type AnalysisOptions<
* run.
*/
capabilities: Capabilities;
/**
* An object with the name of the addon and a path to the script to use with `mitmproxy -s`. It expects `ipcEvents`
* and `harDump` to be present and defaults to `./mitmproxy-addons/ipc_events_addon.py` and
* `./mitmproxy-addons/har_dump.py` if not set.
*/
mitmproxyAddons?: { ipcEvents: string; harDump: string; [key: string | symbol]: string };
} & (RunTargetOptions<Capabilities>[Platform][RunTarget] extends object
? {
/** The options for the selected platform/run target combination. */
Expand Down Expand Up @@ -240,6 +249,12 @@ export function startAnalysis<
targetOptions: options.targetOptions as any,
});

const mitmproxyAddons = {
ipcEvents: join(process.cwd(), 'mitmproxy-addons/ipc_events_addon.py'),
harDump: join(process.cwd(), 'mitmproxy-addons/har_dump.py'),
...options.mitmproxyAddons,
};

let emulatorProcess: ExecaChildProcess | undefined;
return {
platform,
Expand Down Expand Up @@ -304,17 +319,16 @@ export function startAnalysis<
},
startAppAnalysis: async (appPath, options) => {
const appPathMain = getAppPathMain(appPath);
const id = await platform.getAppId(appPathMain);
const version = await platform.getAppVersion(appPathMain);
if (!id) throw new Error(`Could not start analysis with invalid app: "${appPathMain}"`);
const appMeta = await parseAppMeta(appPathMain);
if (!appMeta) throw new Error(`Could not start analysis with invalid app: "${appPathMain}"`);

const res: AppAnalysisResult = {
app: { id, version },
app: appMeta,
traffic: {},
};

const installApp = () => platform.installApp(getAppPathAll(appPath).join(' '));
const uninstallApp = () => platform.uninstallApp(id);
const uninstallApp = () => platform.uninstallApp(appMeta.id);

let inProgressTrafficCollectionName: string | undefined;
let mitmproxyState: { proc: ExecaChildProcess; flowsOutputPath: string } | undefined;
Expand All @@ -337,12 +351,12 @@ export function startAnalysis<
}

return {
app: { id, version },
app: appMeta,

installApp,
setAppPermissions: (permissions) => platform.setAppPermissions(id, permissions),
setAppPermissions: (permissions) => platform.setAppPermissions(appMeta.id, permissions),
uninstallApp,
startApp: () => platform.startApp(id),
startApp: () => platform.startApp(appMeta.id),

startTrafficCollection: async (name) => {
if (inProgressTrafficCollectionName)
Expand All @@ -352,25 +366,63 @@ export function startAnalysis<

inProgressTrafficCollectionName = name ?? new Date().toISOString();

const flowsOutputPath = temporaryFile();
const flowsOutputPath = temporaryFile({ extension: 'har' });

mitmproxyState = {
proc: execa('mitmdump', ['-w', flowsOutputPath]),
proc: execa(
'mitmdump',
[
...Object.entries(mitmproxyAddons).map(([addonName, addonPath]) => {
if (!existsSync(addonPath))
throw new Error(
`No "${addonName}" addon for mitmproxy found at "${addonPath}".${
addonName === 'ipcEvents' || addonName === 'harDump'
? ' The mitmproxy capability requires that this addons is present.'
: ''
}`
);
return `-s ${addonPath}`;
}),
`--set hardump=${flowsOutputPath}`,
],
{
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
env: { ...process.env, IPC_PIPE_FD: '3' },
shell: true, // We have to set this, because mitmdump doesn’t accept options starting with '--' as options in non-shell mode.
}
),
flowsOutputPath,
};
await timeout(awaitProcessStart(mitmproxyState.proc, 'Proxy server listening'), {

await timeout(awaitMitmproxyStatus(mitmproxyState.proc, 'running'), {
milliseconds: 30000,
}).catch((e) => {
if (e.name === 'TimeoutError')
throw new TimeoutError('Starting mitmproxy failed after a timeout.');
throw e;
});
},
stopTrafficCollection: async () => {
killProcess(mitmproxyState?.proc);
if (mitmproxyState?.flowsOutputPath && inProgressTrafficCollectionName) {
const trafficDump = await readFile(mitmproxyState?.flowsOutputPath, 'utf-8');
res.traffic[inProgressTrafficCollectionName] = trafficDump;
}

inProgressTrafficCollectionName = undefined;
// eslint-disable-next-line require-atomic-updates
mitmproxyState = undefined;
if (!mitmproxyState?.proc) throw new Error('No traffic collection is running.');
await Promise.all([
awaitProcessClose(mitmproxyState.proc).then(async () => {
if (mitmproxyState?.flowsOutputPath && inProgressTrafficCollectionName) {
try {
const trafficDump = await readFile(mitmproxyState?.flowsOutputPath, 'utf-8');
res.traffic[inProgressTrafficCollectionName] = JSON.parse(trafficDump);
} catch {
throw new Error(
`Reading the flows from the temporary file @ "${mitmproxyState.flowsOutputPath}" failed.`
);
}
}

inProgressTrafficCollectionName = undefined;
// eslint-disable-next-line require-atomic-updates
mitmproxyState = undefined;
}),
mitmproxyState.proc.kill(),
]);
},

stop: async (stopOptions) => {
Expand All @@ -390,14 +442,12 @@ export function startAnalysis<
};
}

export {
export { androidPermissions, iosPermissions, pause } from 'appstraction';
export type {
AndroidPermission,
androidPermissions,
DeviceAttribute,
GetDeviceAttributeOptions,
IosPermission,
iosPermissions,
pause,
PlatformApi,
SupportedPlatform,
SupportedRunTarget,
Expand Down
78 changes: 78 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,55 @@ import { execa } from 'execa';
import timeout from 'p-timeout';
import { promisify } from 'util';

/** The events send by the mitmproxy IPC Events Addon. */
type MitmproxyEvent =
| {
/**
* Status of the mitmproxy instance.\
* `runnning` – mitmproxy just started.\
* `done` – mitmproxy shut down.\
* `tls_failed` – A TLS error occured.\
* `tls_established` – TLS has been successfully established.\
* `client_connected` - A client connected to mitmproxy.\
* `client_disconnected` - A client disconnected from mitmproxy.
*/
status: 'running' | 'done';
}
| {
status: 'client_connected' | 'client_disconnected';
/** Contains additional information on the status such as the connected client address. */
context: {
/** IP address and port of the client as an array (from mitmproxy’s `connection.Client.peername`). */
address: [string, number];
};
}
| {
status: 'tls_failed';
context: {
/** IP address and port of the client as an array (from mitmproxy’s `connection.Client.peername`). */
client_address: [string, number];
/**
* IP address or hostname and port of the client as a string (from mitmproxy’s
* `connection.Server.address`).
*/
server_address: string;
/** If an error occured, contains an error message (from mitmproxy’s `connection.Connection.error`). */
error?: string;
};
}
| {
status: 'tls_established';
context: {
/** IP address and port of the client as an array (from mitmproxy’s `connection.Client.peername`). */
client_address: [string, number];
/**
* IP address or hostname and port of the client as a string (from mitmproxy’s
* `connection.Server.address`).
*/
server_address: string;
};
};

export const dnsLookup = promisify(dns.lookup);

export const killProcess = async (proc?: ExecaChildProcess) => {
Expand All @@ -14,6 +63,25 @@ export const killProcess = async (proc?: ExecaChildProcess) => {
}
};

/**
* Wait for a mitmproxy event status via IPC. Resolves a promise if the status is received.
*
* @param proc A mitmproxy child process start with the IPC events plugin.
* @param status The status to wait for.
*/
export const awaitMitmproxyStatus = (proc: ExecaChildProcess<string>, status: MitmproxyEvent['status']) =>
new Promise<true>((res) => {
proc.on('message', (msg: MitmproxyEvent) => {
if (msg.status === status) res(true);
});
});

/**
* Wait for a message to appear in stdout and resolve the promise if the message is detected.
*
* @param proc A child process.
* @param startMessage The message to look for in stdout.
*/
export const awaitProcessStart = (proc: ExecaChildProcess<string>, startMessage: string) =>
new Promise<true>((res) => {
proc.stdout?.addListener('data', (chunk: string) => {
Expand All @@ -24,6 +92,16 @@ export const awaitProcessStart = (proc: ExecaChildProcess<string>, startMessage:
});
});

/**
* Wait for a process to close, meaning it stopped completely and closed the stdout, and resolve the promise if it did.
*
* @param proc A child processs.
*/
export const awaitProcessClose = (proc: ExecaChildProcess<string>) =>
new Promise<true>((res) => {
proc.on('close', () => res(true));
});

// Adapted after: https://proandroiddev.com/automated-android-emulator-setup-and-configuration-23accc11a325 and
// https://gist.github.com/mrk-han/db70c7ce2dfdc8ac3e8ae4bec823ba51
export const awaitAndroidEmulator = async () => {
Expand Down

0 comments on commit 5ae684c

Please sign in to comment.