-
Notifications
You must be signed in to change notification settings - Fork 1
/
util.ts
345 lines (323 loc) · 12.5 KB
/
util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
import { ensureSdkmanager, getAndroidDevToolPath } from 'andromatic';
import { ctrlc } from 'ctrlc-windows';
import dns from 'dns';
import type { ExecaChildProcess } from 'execa';
import { execa } from 'execa';
import { access } from 'fs/promises';
import timeout from 'p-timeout';
import { promisify } from 'util';
/**
* A mitmproxy certificate object representing a TLS certificate.
*
* @see https://docs.mitmproxy.org/stable/api/mitmproxy/certs.html#Cert
*/
export type MitmproxyCertificate = {
/** The certificate's common name. */
cn: string | null;
/** The certificate's alternate names (`SubjectAlternativeName`). */
altnames: string[];
/** The certificate's serial number. */
serial: number;
/** The timestamp when the certificate becomes valid. */
notbefore: number;
/** The timestamp when the certificate expires. */
notafter: number;
/**
* The key information of the certificate, consisting of the algorithm and the bit size.
*
* @example
*
* ```ts
* ['RSA', 2048];
* ```
*/
keyinfo: [string, number];
/** The organization name of the certificate owner. */
organization: string | null;
/**
* The issuer information of the certificate, as an array of key-value pairs.
*
* @example
*
* ```ts
* [
* ['C', 'US'],
* ['O', 'DigiCert Inc'],
* ['OU', 'www.digicert.com'],
* ['CN', 'GeoTrust TLS RSA CA G1'],
* ];
* ```
*/
issuer: [string, string][];
/**
* The subject information of the certificate, as an array of key-value pairs.
*
* @example
*
* ```ts
* [
* ['C', 'US'],
* ['O', 'DigiCert Inc'],
* ['OU', 'www.digicert.com'],
* ['CN', 'GeoTrust TLS RSA CA G1'],
* ];
* ```
*/
subject: [string, string][];
};
/**
* A mitmproxy connection object.
*
* @see https://docs.mitmproxy.org/stable/api/mitmproxy/connection.html#Connection
*/
export type MitmproxyConnection = {
/** The connection's unique ID. */
id: string;
/** The connection's state. */
state: 'CLOSED' | 'CAN_READ' | 'CAN_WRITE' | 'OPEN';
/** The connection's protocol. */
transportProtocol: 'tcp' | 'udp';
/** The remote's `[ip, port]` tuple for this connection. */
peername: [string, number] | null;
/** The local's `[ip, port]` tuple for this connection. */
sockname: [string, number] | null;
/**
* A string describing a general error with connections to this address.
*
* The purpose of this property is to signal that new connections to the particular endpoint should not be
* attempted, for example because it uses an untrusted TLS certificate. Regular (unexpected) disconnects do not set
* the error property. This property is only reused per client connection.
*/
error: string | null;
/**
* `true` if TLS should be established, `false` otherwise. Note that this property only describes if a connection
* should eventually be protected using TLS. To check if TLS has already been established, use
* {@link MitmproxyConnection.tlsEstablished}.
*/
tls: boolean;
/** The TLS certificate list as sent by the peer. The first certificate is the end-entity certificate. */
certificateList: MitmproxyCertificate[];
/** The active cipher name as returned by OpenSSL's `SSL_CIPHER_get_name`. */
cipher: string;
/** Ciphers accepted by the proxy server on this connection. */
cipherList: string[];
/** The active TLS version. */
tlsVersion: string | null;
/** The Server Name Indication (SNI) sent in the ClientHello. */
sni: string | null;
/** Timestamp of when the TCP SYN was received (client) or sent (server). */
timestampStart: number | null;
/** Timestamp of when the connection has been closed. */
timestampEnd: number | null;
/** Timestamp of when the TLS handshake has been completed successfully. */
timestampTlsSetup: number | null;
/** `true` if {@link MitmproxyConnection.state} is `OPEN`, `false` otherwise. */
connected: boolean;
/** `true` if TLS has been established, `false` otherwise. */
tlsEstablished: boolean;
};
/**
* Mitmproxy's event data for the `tls_start_client`, `tls_start_server`, and `tls_handshake` event hooks.
*
* @see https://docs.mitmproxy.org/stable/api/mitmproxy/tls.html#TlsData
*/
export type MitmproxyTlsData = {
/** Convenience alias for the client address in human-readable format (`<address>:<port>`). */
clientAddress: string;
/** Convenience alias for the server address in human-readable format (SNI hostname or `<address>:<port>`). */
serverAddress: string;
/** The client connection. */
client: MitmproxyClient;
/** The server connection. */
server: MitmproxyServer;
/** If set to `true`, indicates that it is a DTLS event. */
isDtls: boolean;
};
/**
* A mitmproxy client object, represents a connection between a client and mitmproxy.
*
* @see https://docs.mitmproxy.org/stable/api/mitmproxy/connection.html#Client
*/
export type MitmproxyClient = MitmproxyConnection & {
/** The certificate used by mitmproxy to establish TLS with the client. */
mitmCertificate: MitmproxyCertificate | null;
/**
* The spec for the proxy server this client has been connecting to. This is the full proxy mode spec as entered by
* the user.
*/
proxyMode: string;
};
/**
* A mitmproxy server object, representing a connection between mitmproxy and an upstream server.
*
* @see https://docs.mitmproxy.org/stable/api/mitmproxy/connection.html#Server
*/
export type MitmproxyServer = MitmproxyConnection & {
/** The server's `[host, port]` address tuple. The host can either be a domain or a plain IP address. */
address: [string, number] | null;
/** Timestamp of when the TCP ACK was received. */
timestampTcpSetup: number | null;
/** An proxy server specification via which the connection should be established. */
via: ['http' | 'https' | 'tls' | 'dtls' | 'tcp' | 'udp' | 'dns', string | number] | null;
};
/**
* A `mitmproxy.proxy.mode_servers.ServerInstance` object.
*
* @see https://github.com/mitmproxy/mitmproxy/blob/8f1329377147538afdf06344179c2fd90795e93a/mitmproxy/proxy/mode_servers.py#L172.
*/
export type MitmproxyServerSpec<Type extends 'wireguard' | 'regular' | string> = {
type: Type;
description: string;
fullSpec: string;
isRunning: boolean;
lastException: string | null;
listenAddrs: Array<[string, number]>;
wireguardConf: Type extends 'wireguard' ? string | null : null;
};
/** The events sent by the mitmproxy IPC events addon. */
export type MitmproxyEvent =
| {
/**
* Status of the mitmproxy instance:
*
* - `running`: mitmproxy just started.
* - `done`: mitmproxy shut down.
* - `tlsFailed`: A TLS error occurred.
* - `tlsEstablished`: TLS has been successfully established.
* - `clientConnected`: A client connected to mitmproxy.
* - `clientDisconnected`: A client disconnected from mitmproxy.
* - `proxyChanged`: The proxy server configuration changed.
*/
status: 'running' | 'done';
}
| {
status: 'clientConnected' | 'clientDisconnected';
/** Contains additional information on the status such as the connected client address. */
context: {
/** Convenience alias for the client address in human-readable format (`<address>:<port>`). */
address: [string, number];
/** Contains additional information on the client connection. */
client: MitmproxyClient;
};
}
| {
status: 'tlsFailed';
context: MitmproxyTlsData & {
/** Convenience alias for the TLS error message. */
error: string | null;
};
}
| {
status: 'tlsEstablished';
context: MitmproxyTlsData;
}
| {
status: 'proxyChanged';
context: {
isRunning: boolean;
/** An array of server specs which contains all the running servers, one for each mode. */
servers: MitmproxyServerSpec<'wireguard' | 'regular' | string>[];
};
};
/** A promise wrapper around `dns.lookup`. */
export const dnsLookup = promisify(dns.lookup);
/**
* The function tries to kill a child process gracefully by sending a SIGINT and if the process didn’t exit, it sends a
* SIGKILL to force kill it.
*/
export const killProcess = async (proc?: ExecaChildProcess) => {
if (proc) {
if (process.platform === 'win32' && proc.pid) ctrlc(proc.pid);
else proc.kill();
await timeout(proc, { milliseconds: 15000 }).catch(() => proc.kill(9));
}
};
export const onMitmproxyEvent = (proc: ExecaChildProcess<string>, callback: (msg: MitmproxyEvent) => void | 'end') => {
const listener = (chunk: string | Buffer | undefined) => {
const lines = chunk?.toString().split('\n') || [];
for (const line of lines) {
if (!line.startsWith('cyanoacrylate:')) continue;
const msg = JSON.parse(line.replace(/^cyanoacrylate:/, '')) as MitmproxyEvent;
if (callback(msg) === 'end') proc.stdout?.removeListener('data', listener);
}
};
proc.stdout?.addListener('data', listener);
};
/**
* Wait for a mitmproxy event via IPC. Resolves a promise if the condition is true.
*
* @param proc A mitmproxy child process start with the IPC events plugin.
* @param condition Condition to check the message against.
*
* @returns A promise resolving to the receviced message if the condition is true.
*/
export const awaitMitmproxyEvent = (proc: ExecaChildProcess<string>, condition: (msg: MitmproxyEvent) => boolean) =>
new Promise<MitmproxyEvent>((res) => {
onMitmproxyEvent(proc, (msg) => {
if (condition(msg)) {
res(msg);
return 'end';
}
// To make TS happy.
// eslint-disable-next-line no-useless-return
return;
});
});
/**
* 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) => {
if (chunk.includes(startMessage)) {
proc.stdout?.removeAllListeners('data');
res(true);
}
});
});
/**
* 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));
});
export const fileExists = async (path: string) => {
try {
await access(path);
return true;
} catch {
return false;
}
};
// We can't use `runAndroidDevTool` for this because that can only give you the process if you `await` it, which we
// don't want.
export const startEmulator = async (name: string, args: string[]) => {
const { env } = await ensureSdkmanager();
const toolPath = await getAndroidDevToolPath('emulator');
return () => execa(toolPath, ['-avd', name, ...args], { env });
};
/*
License for the docstrings imported from mitmproxy:
Copyright (c) 2013, Aldo Cortesi. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/