Skip to content

Commit 02b1a12

Browse files
add plugin commands to add listeners
1 parent 36b8a0e commit 02b1a12

File tree

6 files changed

+158
-15
lines changed

6 files changed

+158
-15
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"uninstall": "export APPIUM_HOME=/tmp/some-temp-dir && (appium plugin uninstall appium-interceptor || exit 0)",
1414
"install-plugin": "export APPIUM_HOME=/tmp/some-temp-dir && npm run build && appium plugin install --source=local $(pwd)",
1515
"reinstall-plugin": "export APPIUM_HOME=/tmp/some-temp-dir && (appium plugin uninstall appium-interceptor || exit 0) && npm run install-plugin",
16-
"run-server": "export APPIUM_HOME=/tmp/some-temp-dir && appium server -ka 800 --use-plugins=appium-interceptor -pa /wd/hub "
16+
"run-server": "export APPIUM_HOME=/tmp/some-temp-dir && appium server -ka 800 --use-plugins=appium-interceptor -pa /wd/hub"
1717
},
1818
"contributors": [
1919
{

src/api-sniffer.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { RequestInfo, SniffConfig } from './types';
2+
import { doesUrlMatch } from './utils/proxy';
3+
4+
export class ApiSniffer {
5+
private readonly requests: RequestInfo[] = [];
6+
7+
constructor(private id: string, private config: SniffConfig) {}
8+
9+
getId() {
10+
return this.id;
11+
}
12+
13+
onApiRequest(request: RequestInfo) {
14+
if (this.doesRequestMatchesConfig(request)) {
15+
this.requests.push(request);
16+
}
17+
}
18+
19+
getRequests() {
20+
return this.requests;
21+
}
22+
23+
private doesRequestMatchesConfig(request: RequestInfo) {
24+
const doesIncludeRuleMatches = !this.config.include
25+
? true
26+
: this.config.include.some((config) => doesUrlMatch(config.url, request.url));
27+
const doesExcludeRuleMatches = !this.config.exclude
28+
? true
29+
: !this.config.exclude.some((config) => doesUrlMatch(config.url, request.url));
30+
31+
return doesIncludeRuleMatches && doesExcludeRuleMatches;
32+
}
33+
}

src/interceptor.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { ErrorCallback, IContext } from 'http-mitm-proxy';
22
import stream from 'stream';
3-
import { constructURLFromRequest } from './utils/proxy';
3+
import { constructURLFromHttpRequest } from './utils/proxy';
44
import responseDecoder from './response-decoder';
55
import parseHeader from 'parse-headers';
66

7-
function interceptBody(writable: stream.Writable | undefined, callback: (value: any) => void) {
7+
function readBodyFromStream(writable: stream.Writable | undefined, callback: (value: any) => void) {
88
if (!writable) {
99
return callback(null);
1010
}
@@ -30,26 +30,27 @@ function RequestInterceptor(requestCompletionCallback: (value: any) => void) {
3030
ctx.use(responseDecoder);
3131
const requestData = {} as any;
3232
ctx.onRequestEnd((ctx, callback) => {
33-
interceptBody(ctx.proxyToServerRequest, (requestBody) => {
34-
requestData['postBody'] = requestBody;
33+
readBodyFromStream(ctx.proxyToServerRequest, (requestBody) => {
34+
requestData['requestBody'] = requestBody;
3535
requestData['requestHeaders'] = ctx.proxyToServerRequest?.getHeaders();
3636
});
3737
callback();
3838
});
3939

40-
interceptBody(ctx.proxyToClientResponse, (response) => {
40+
readBodyFromStream(ctx.proxyToClientResponse, (response) => {
4141
const { headers, url } = ctx.clientToProxyRequest;
4242
const protocol = ctx.isSSL ? 'https://' : 'http://';
43-
const _url = constructURLFromRequest({
43+
const _url = constructURLFromHttpRequest({
4444
host: headers.host!,
4545
path: url!,
4646
protocol,
4747
});
4848
const responseHeaders = parseHeader((ctx.proxyToClientResponse as any)?._header || '');
49-
requestData['url'] = _url;
49+
requestData['url'] = _url.toString();
5050
requestData['method'] = ctx.clientToProxyRequest.method;
5151
requestData['responseBody'] = response;
5252
requestData['responseHeaders'] = responseHeaders;
53+
requestData['statusCode'] = ctx.proxyToClientResponse.statusCode;
5354

5455
requestCompletionCallback(requestData);
5556
});

src/plugin.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { BasePlugin } from 'appium/plugin';
22
import http from 'http';
33
import { Application } from 'express';
4-
import { CliArg, ISessionCapability, MockConfig } from './types';
4+
import { CliArg, ISessionCapability, MockConfig, SniffConfig } from './types';
55
import _ from 'lodash';
66
import { configureWifiProxy, isRealDevice } from './utils/adb';
77
import { cleanUpProxyServer, sanitizeMockConfig, setupProxyServer } from './utils/proxy';
88
import proxyCache from './proxy-cache';
99
import logger from './logger';
10-
import { validateMockConfig } from './schema';
1110

1211
export class AppiumInterceptorPlugin extends BasePlugin {
1312
static executeMethodMap = {
@@ -20,6 +19,26 @@ export class AppiumInterceptorPlugin extends BasePlugin {
2019
command: 'removeMock',
2120
params: { required: ['id'] },
2221
},
22+
23+
'interceptor: disableMock': {
24+
command: 'disableMock',
25+
params: { required: ['id'] },
26+
},
27+
28+
'interceptor: enableMock': {
29+
command: 'enableMock',
30+
params: { required: ['id'] },
31+
},
32+
33+
'interceptor: startListening': {
34+
command: 'startListening',
35+
params: { optional: ['config'] },
36+
},
37+
38+
'interceptor: stopListening': {
39+
command: 'stopListening',
40+
params: { optional: ['id'] },
41+
},
2342
};
2443

2544
constructor(name: string, cliArgs: CliArg) {
@@ -99,6 +118,46 @@ export class AppiumInterceptorPlugin extends BasePlugin {
99118
proxy.removeMock(id);
100119
}
101120

121+
async disableMock(next: any, driver: any, id: any) {
122+
const proxy = proxyCache.get(driver.sessionId);
123+
if (!proxy) {
124+
logger.error('Proxy is not running');
125+
throw new Error('Proxy is not active for current session');
126+
}
127+
128+
proxy.disableMock(id);
129+
}
130+
131+
async enableMock(next: any, driver: any, id: any) {
132+
const proxy = proxyCache.get(driver.sessionId);
133+
if (!proxy) {
134+
logger.error('Proxy is not running');
135+
throw new Error('Proxy is not active for current session');
136+
}
137+
138+
proxy.enableMock(id);
139+
}
140+
141+
async startListening(next: any, driver: any, config: SniffConfig) {
142+
const proxy = proxyCache.get(driver.sessionId);
143+
if (!proxy) {
144+
logger.error('Proxy is not running');
145+
throw new Error('Proxy is not active for current session');
146+
}
147+
148+
return proxy?.addSniffer(config);
149+
}
150+
151+
async stopListening(next: any, driver: any, id: any) {
152+
const proxy = proxyCache.get(driver.sessionId);
153+
if (!proxy) {
154+
logger.error('Proxy is not running');
155+
throw new Error('Proxy is not active for current session');
156+
}
157+
158+
return proxy.removeSniffer(id);
159+
}
160+
102161
async execute(next: any, driver: any, script: any, args: any) {
103162
return await this.executeMethod(next, driver, script, args);
104163
}

src/proxy.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MockConfig } from './types';
1+
import { MockConfig, RequestInfo, SniffConfig } from './types';
22
import { Proxy as HttpProxy, IContext, IProxyOptions } from 'http-mitm-proxy';
33
import { v4 as uuid } from 'uuid';
44
import {
@@ -15,6 +15,8 @@ import {
1515
import ResponseDecoder from './response-decoder';
1616
import { Mock } from './mock';
1717
import { RequestInterceptor } from './interceptor';
18+
import { ApiSniffer } from './api-sniffer';
19+
import _ from 'lodash';
1820

1921
export interface ProxyOptions {
2022
deviceUDID: string;
@@ -27,6 +29,8 @@ export interface ProxyOptions {
2729
export class Proxy {
2830
private _started = false;
2931
private readonly mocks = new Map<string, Mock>();
32+
private readonly sniffers = new Map<string, ApiSniffer>();
33+
3034
private readonly httpProxy: HttpProxy;
3135

3236
public isStarted(): boolean {
@@ -66,9 +70,9 @@ export class Proxy {
6670

6771
this.httpProxy.onRequest(
6872
RequestInterceptor((requestData: any) => {
69-
// console.log('****** REQUEST **********');
70-
// console.log(JSON.stringify(requestData, null, 2));
71-
// console.log('****************************');
73+
for (const sniffer of this.sniffers.values()) {
74+
sniffer.onApiRequest(requestData);
75+
}
7276
})
7377
);
7478
this.httpProxy.onRequest(this.handleMockApiRequest.bind(this));
@@ -97,6 +101,33 @@ export class Proxy {
97101
this.mocks.delete(id);
98102
}
99103

104+
public enableMock(id: string): void {
105+
this.mocks.get(id)?.setEnableStatus(true);
106+
}
107+
108+
public disableMock(id: string): void {
109+
this.mocks.get(id)?.setEnableStatus(false);
110+
}
111+
112+
public addSniffer(sniffConfg: SniffConfig): string {
113+
const id = uuid();
114+
this.sniffers.set(id, new ApiSniffer(id, sniffConfg));
115+
return id;
116+
}
117+
118+
public removeSniffer(id?: string): RequestInfo[] {
119+
const _sniffers = [...this.sniffers.values()];
120+
if (id && !_.isNil(this.sniffers.get(id))) {
121+
_sniffers.push(this.sniffers.get(id)!);
122+
}
123+
const apiRequests = _sniffers.reduce((acc, sniffer) => {
124+
acc.push(...sniffer.getRequests());
125+
return acc;
126+
}, [] as RequestInfo[]);
127+
_sniffers.forEach((sniffer) => this.sniffers.delete(sniffer.getId()));
128+
return apiRequests;
129+
}
130+
100131
private async handleMockApiRequest(ctx: IContext, next: () => void): Promise<void> {
101132
const matchedMocks = await this.findMatchingMocks(ctx);
102133
if (matchedMocks.length) {
@@ -122,7 +153,11 @@ export class Proxy {
122153
const matchedMocks: MockConfig[] = [];
123154
for (const mock of this.mocks.values()) {
124155
const config = mock.getConfig();
125-
if (doesUrlMatch(config.url, url) && doesHttpMethodMatch(request, config.method)) {
156+
if (
157+
mock.isEnabled() &&
158+
doesUrlMatch(config.url, url) &&
159+
doesHttpMethodMatch(request, config.method)
160+
) {
126161
matchedMocks.push(config);
127162
}
128163
}

src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,18 @@ export type MockConfig = {
3737
responseBody?: string;
3838
updateResponseBody?: UpdateBodySpec[];
3939
};
40+
41+
export type SniffConfig = {
42+
include?: Array<{ url: UrlPattern }>;
43+
exclude?: Array<{ url: UrlPattern }>;
44+
};
45+
46+
export type RequestInfo = {
47+
url: string;
48+
method: string;
49+
requestBody: any;
50+
statusCode: number;
51+
requestHeaders: Record<string, string | string[]>;
52+
responseBody: any;
53+
responseHeaders: Record<string, string | string[]>;
54+
};

0 commit comments

Comments
 (0)