Skip to content

Commit 1bfd76d

Browse files
refactor proxy utilities and management
1 parent 873301c commit 1bfd76d

2 files changed

Lines changed: 88 additions & 84 deletions

File tree

src/proxy.ts

Lines changed: 68 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,147 @@
11
import { MockConfig } from './types';
2-
import { Proxy as HttpProxy, IContext, IProxyOptions, ErrorCallback } from 'http-mitm-proxy';
2+
import { Proxy as HttpProxy, IContext, IProxyOptions } from 'http-mitm-proxy';
33
import { v4 as uuid } from 'uuid';
4-
import http from 'http';
54
import {
65
addDefaultMocks,
76
compileMockConfig,
8-
constructURLFromRequest,
9-
matchHttpMethod,
10-
matchUrl,
11-
updateRequestBody,
12-
updateRequestHeaders,
13-
updateRequestUrl,
14-
updateResponseBody,
7+
constructURLFromHttpRequest,
8+
doesHttpMethodMatch,
9+
doesUrlMatch,
10+
modifyRequestBody,
11+
modifyRequestHeaders,
12+
modifyRequestUrl,
13+
modifyResponseBody,
1514
} from './utils/proxy';
1615
import ResponseDecoder from './response-decoder';
1716
import { Mock } from './mock';
1817

19-
export type ProxyOptions = {
18+
export interface ProxyOptions {
2019
deviceUDID: string;
2120
sessionId: string;
2221
certificatePath: string;
2322
port: number;
2423
ip: string;
25-
};
24+
}
2625

2726
export class Proxy {
28-
private started: boolean = false;
29-
private mocks: Map<string, Mock> = new Map();
30-
private httpProxy!: HttpProxy;
27+
private _started = false;
28+
private readonly mocks = new Map<string, Mock>();
29+
private readonly httpProxy: HttpProxy;
30+
31+
public isStarted(): boolean {
32+
return this._started;
33+
}
3134

32-
constructor(private options: ProxyOptions) {
35+
constructor(private readonly options: ProxyOptions) {
3336
this.httpProxy = new HttpProxy();
3437
addDefaultMocks(this);
3538
}
3639

37-
public getPort() {
40+
public get port(): number {
3841
return this.options.port;
3942
}
4043

41-
public getIp() {
44+
public get ip(): string {
4245
return this.options.ip;
4346
}
4447

45-
public getDeviceUDID() {
48+
public get deviceUDID(): string {
4649
return this.options.deviceUDID;
4750
}
4851

49-
public getCertificatePath() {
52+
public get certificatePath(): string {
5053
return this.options.certificatePath;
5154
}
5255

53-
public addMock(mockConfig: MockConfig) {
54-
const id = uuid();
55-
this.mocks.set(id, new Mock(id, mockConfig));
56-
return id;
57-
}
56+
public async start(): Promise<boolean> {
57+
if (this._started) return true;
5858

59-
public removeMock(id: string) {
60-
this.mocks.delete(id);
61-
}
62-
63-
public isStarted() {
64-
return this.started;
65-
}
66-
67-
public async start() {
68-
if (this.isStarted()) {
69-
return this.isStarted();
70-
}
7159
const proxyOptions: IProxyOptions = {
72-
port: this.options.port,
73-
sslCaDir: this.options.certificatePath,
74-
host: '::',
60+
port: this.port,
61+
sslCaDir: this.certificatePath,
62+
host: '::', // IPv6 any
63+
forceSNI: true,
7564
};
7665

77-
this.httpProxy.onRequest(this._onMockApiRequest.bind(this));
66+
this.httpProxy.onRequest(this.handleMockApiRequest.bind(this));
7867

7968
await new Promise((resolve) => {
80-
this.httpProxy.listen({ ...proxyOptions, forceSNI: true }, () => {
81-
this.started = true;
69+
this.httpProxy.listen(proxyOptions, () => {
70+
this._started = true;
8271
resolve(true);
8372
});
8473
});
74+
75+
return true;
8576
}
8677

87-
public async stop() {
78+
public async stop(): Promise<void> {
8879
this.httpProxy.close();
8980
}
9081

91-
private async _onMockApiRequest(ctx: IContext, next: ErrorCallback) {
92-
const matchedMocks = await this.getMatchingMock(ctx);
82+
public addMock(mockConfig: MockConfig): string {
83+
const id = uuid();
84+
this.mocks.set(id, new Mock(id, mockConfig));
85+
return id;
86+
}
87+
88+
public removeMock(id: string): void {
89+
this.mocks.delete(id);
90+
}
91+
92+
private async handleMockApiRequest(ctx: IContext, next: () => void): Promise<void> {
93+
const matchedMocks = await this.findMatchingMocks(ctx);
9394
if (matchedMocks.length) {
9495
const compiledMock = compileMockConfig(matchedMocks);
95-
this.performMock(ctx, compiledMock, next);
96+
this.performMockResponse(ctx, compiledMock, next);
9697
} else {
9798
next();
9899
}
99100
}
100101

101-
private async getMatchingMock(ctx: IContext) {
102-
let request: http.IncomingMessage = ctx.clientToProxyRequest;
103-
if (!request.headers?.host) {
102+
private async findMatchingMocks(ctx: IContext): Promise<MockConfig[]> {
103+
const request = ctx.clientToProxyRequest;
104+
if (!request.headers?.host || !request.url) {
104105
return [];
105106
}
106-
const url = constructURLFromRequest({
107-
host: request.headers.host!,
108-
path: request.url!,
107+
108+
const url = constructURLFromHttpRequest({
109+
host: request.headers.host,
110+
path: request.url,
109111
protocol: ctx.isSSL ? 'https://' : 'http://',
110112
}).toString();
111113

112-
const matchedMocks = [];
113-
for (let [id, mock] of this.mocks.entries()) {
114+
const matchedMocks: MockConfig[] = [];
115+
for (const mock of this.mocks.values()) {
114116
const config = mock.getConfig();
115-
if (matchUrl(config.url, url) && matchHttpMethod(request, config.method)) {
117+
if (doesUrlMatch(config.url, url) && doesHttpMethodMatch(request, config.method)) {
116118
matchedMocks.push(config);
117119
}
118120
}
119121

120122
return matchedMocks;
121123
}
122124

123-
private performMock(ctx: IContext, mockConfig: MockConfig, callback: ErrorCallback) {
125+
private performMockResponse(ctx: IContext, mockConfig: MockConfig, next: () => void): void {
124126
ctx.use(ResponseDecoder);
125127

126-
this._updateClientRequest(ctx, mockConfig);
127-
this._updateClientResponse(ctx, mockConfig, callback);
128+
this.modifyClientRequest(ctx, mockConfig);
129+
this.modifyClientResponse(ctx, mockConfig, next);
128130
}
129131

130-
private _updateClientRequest(ctx: IContext, mockConfig: MockConfig) {
131-
updateRequestUrl(ctx, mockConfig);
132-
updateRequestHeaders(ctx, mockConfig);
133-
updateRequestBody(ctx, mockConfig);
132+
private modifyClientRequest(ctx: IContext, mockConfig: MockConfig): void {
133+
modifyRequestUrl(ctx, mockConfig);
134+
modifyRequestHeaders(ctx, mockConfig);
135+
modifyRequestBody(ctx, mockConfig);
134136
}
135137

136-
private _updateClientResponse(ctx: IContext, mockConfig: MockConfig, next: ErrorCallback) {
138+
private modifyClientResponse(ctx: IContext, mockConfig: MockConfig, next: () => void): void {
137139
if (mockConfig.statusCode && mockConfig.responseBody) {
138140
ctx.proxyToClientResponse.writeHead(mockConfig.statusCode);
139141
ctx.proxyToClientResponse.end(mockConfig.responseBody);
140-
return;
142+
} else {
143+
modifyResponseBody(ctx, mockConfig);
144+
next();
141145
}
142-
143-
updateResponseBody(ctx, mockConfig);
144-
next();
145146
}
146147
}

src/utils/proxy.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@ const MOCK_BACKEND_HTML = `<html><head><title>Appium Mock</title></head>
2929
<p style="font-size:24px">Download the certificate <a href="www.google.com">here</a></p>
3030
</body></html>`;
3131

32-
export function constructURLFromRequest(request: { protocol: string; path: string; host: string }) {
32+
export function constructURLFromHttpRequest(request: {
33+
protocol: string;
34+
path: string;
35+
host: string;
36+
}) {
3337
const urlString = `${request.protocol}${request?.host}${request.path}`;
3438
return new URL(urlString);
3539
}
3640

37-
export function updateRequestUrl(ctx: IContext, mockConfig: MockConfig) {
41+
export function modifyRequestUrl(ctx: IContext, mockConfig: MockConfig) {
3842
if (!mockConfig.updateUrl || !ctx.clientToProxyRequest || !ctx.proxyToServerRequestOptions) {
3943
return;
4044
}
4145

4246
const { headers, url } = ctx.clientToProxyRequest;
4347
const protocol = ctx.isSSL ? 'https://' : 'http://';
44-
const originalUrl = constructURLFromRequest({
48+
const originalUrl = constructURLFromHttpRequest({
4549
host: headers.host!,
4650
path: url!,
4751
protocol,
@@ -58,7 +62,7 @@ export function updateRequestUrl(ctx: IContext, mockConfig: MockConfig) {
5862
ctx.proxyToServerRequestOptions.port = updatedUrl.port || ctx.proxyToServerRequestOptions.port;
5963
}
6064

61-
export function updateRequestHeaders(ctx: IContext, mockConfig: MockConfig) {
65+
export function modifyRequestHeaders(ctx: IContext, mockConfig: MockConfig) {
6266
if (!mockConfig.headers || !ctx.proxyToServerRequestOptions) {
6367
return;
6468
}
@@ -72,7 +76,7 @@ export function updateRequestHeaders(ctx: IContext, mockConfig: MockConfig) {
7276
}
7377
}
7478

75-
export function updateRequestBody(ctx: IContext, mockConfig: MockConfig) {
79+
export function modifyRequestBody(ctx: IContext, mockConfig: MockConfig) {
7680
const requestBodyChunks: Buffer[] = [];
7781
ctx.onRequestData((ctx: IContext, chunk: Buffer, callback: OnRequestDataCallback) => {
7882
requestBodyChunks.push(chunk);
@@ -94,7 +98,7 @@ export function updateRequestBody(ctx: IContext, mockConfig: MockConfig) {
9498
});
9599
}
96100

97-
export function updateResponseBody(ctx: IContext, mockConfig: MockConfig) {
101+
export function modifyResponseBody(ctx: IContext, mockConfig: MockConfig) {
98102
const responseBodyChunks: Buffer[] = [];
99103
ctx.onResponseData((ctx: IContext, chunk: Buffer, callback: OnRequestDataCallback) => {
100104
responseBodyChunks.push(chunk);
@@ -205,7 +209,7 @@ export function parseJson(obj: any) {
205209
}
206210
}
207211

208-
export function matchUrl(pattern: UrlPattern, url: string) {
212+
export function doesUrlMatch(pattern: UrlPattern, url: string) {
209213
let jsonOrStringUrl = parseRegex(pattern);
210214

211215
try {
@@ -218,7 +222,7 @@ export function matchUrl(pattern: UrlPattern, url: string) {
218222
}
219223
}
220224

221-
export function matchHttpMethod(request: http.IncomingMessage, method: string | undefined) {
225+
export function doesHttpMethodMatch(request: http.IncomingMessage, method: string | undefined) {
222226
if (!method) {
223227
return true;
224228
}
@@ -299,20 +303,19 @@ export function sanitizeMockConfig(config: MockConfig) {
299303
config.responseHeaders = parseHeaderConfig(config.headers);
300304

301305
/* Validate if the config has corrent RegExp */
302-
[
306+
const pathsToValidate = [
303307
'$.updateUrl[*].regexp',
304308
'$.updateRequestBody[*].regexp',
305309
'$.updateResponseBody[*].regexp',
306-
].forEach((regexNodePath) => {
307-
const regexElement = jsonpath.query(config, regexNodePath);
308-
return regexElement.forEach((ele) => {
309-
console.log('00000', ele)
310-
const isValidRegExp = typeof ele === 'string' && !(parseRegex(ele) instanceof RegExp);
311-
if (!isValidRegExp) {
312-
throw new Error(`Invalid Regular expression ${ele} for field ${regexNodePath}`);
310+
];
311+
for (const regexNodePath of pathsToValidate) {
312+
const regexElements: string[] = jsonpath.query(config, regexNodePath);
313+
regexElements.forEach((regexp) => {
314+
if (typeof regexp !== 'string' || !(parseRegex(regexp) instanceof RegExp)) {
315+
throw new Error(`Invalid Regular expression ${regexp} for field ${regexNodePath}`);
313316
}
314317
});
315-
});
318+
}
316319

317320
return config;
318321
}

0 commit comments

Comments
 (0)