-
Notifications
You must be signed in to change notification settings - Fork 132
Expand file tree
/
Copy pathindex.ts
More file actions
291 lines (242 loc) · 10.9 KB
/
index.ts
File metadata and controls
291 lines (242 loc) · 10.9 KB
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
import * as path from 'path';
import * as fs from 'fs';
import envPaths from 'env-paths';
import { Mutex } from 'async-mutex';
import { delay, isErrorLike } from '@httptoolkit/util';
import {
PluggableAdmin,
generateCACertificate
} from 'mockttp';
import {
MockttpAdminPlugin
} from 'mockttp/dist/pluggable-admin-api/mockttp-pluggable-admin';
import {
MockRTCAdminPlugin
} from 'mockrtc';
import updateCommand from '@oclif/plugin-update/lib/commands/update';
import { HttpToolkitServerApi } from './api/api-server';
import { checkBrowserConfig } from './browsers';
import { logError } from './error-tracking';
import { IS_PROD_BUILD, MOCKTTP_ALLOWED_ORIGINS } from './constants';
import { readFile, checkAccess, writeFile, ensureDirectoryExists } from './util/fs';
import { addShutdownHandler, registerShutdownHandler, shutdown } from './shutdown';
import { getTimeToCertExpiry, parseCert } from './certificates';
import {
startDockerInterceptionServices,
stopDockerInterceptionServices
} from './interceptors/docker/docker-interception-services';
import { clearWebExtensionConfig, updateWebExtensionConfig } from './webextension';
import { HttpClient } from './client/http-client';
async function generateHTTPSConfig(configPath: string) {
const keyPath = path.join(configPath, 'ca.key');
const certPath = path.join(configPath, 'ca.pem');
const [ certContent ] = await Promise.all([
readFile(certPath, 'utf8').then((certContent) => {
checkCertExpiry(certContent);
return certContent;
}),
checkAccess(keyPath, fs.constants.R_OK),
]).catch(async () => {
// Cert doesn't exist, or is too close/past expiry. Generate a new one:
const newCertPair = await generateCACertificate({
subject: {
commonName: 'HTTP Toolkit CA',
organizationName: 'HTTP Toolkit CA'
}
});
return Promise.all([
writeFile(certPath, newCertPair.cert).then(() => newCertPair.cert),
writeFile(keyPath, newCertPair.key, {
mode: 0o600 // Only readable for ourselves, nobody else
})
]);
});
return {
keyPath,
certPath,
certContent,
keyLength: 2048 // Reasonably secure keys please
};
}
function checkCertExpiry(certContents: string): void {
const remainingLifetime = getTimeToCertExpiry(parseCert(certContents));
if (remainingLifetime < 1000 * 60 * 60 * 48) { // Next two days
console.warn('Certificate expires soon - it must be regenerated');
throw new Error('Certificate regeneration required');
}
}
let shutdownTimer: NodeJS.Timeout | undefined;
function manageBackgroundServices(
standalone: PluggableAdmin.AdminServer<{
http: MockttpAdminPlugin,
webrtc: MockRTCAdminPlugin
}>,
httpsConfig: { certPath: string, certContent: string }
) {
let activeSessions = 0;
standalone.on('mock-session-started', async ({ http, webrtc }, sessionId) => {
activeSessions += 1;
if (shutdownTimer) {
clearTimeout(shutdownTimer);
shutdownTimer = undefined;
}
if (!http) {
console.log(`Mock session started without http plugin, skipping background services`);
return;
}
const httpProxyPort = http.getMockServer().port;
console.log(`Mock session started, http on port ${
httpProxyPort
}, webrtc ${
!!webrtc ? 'enabled' : 'disabled'
}`);
startDockerInterceptionServices(httpProxyPort, httpsConfig, ruleParameters)
.catch((error) => {
console.log("Could not start Docker components:", error);
});
updateWebExtensionConfig(sessionId, httpProxyPort, !!webrtc)
.catch((error) => {
console.log("Could not update WebRTC config:", error);
});
});
standalone.on('mock-session-stopping', ({ http }) => {
activeSessions -= 1;
if (!http) return;
const httpProxyPort = http.getMockServer().port;
stopDockerInterceptionServices(httpProxyPort, ruleParameters)
.catch((error) => {
console.log("Could not stop Docker components:", error);
});
clearWebExtensionConfig(httpProxyPort);
// In some odd cases, the server can end up running even though all UIs & desktop have exited
// completely. This can be problematic, as it leaves the server holding ports that HTTP Toolkit
// needs, and blocks future startups. To avoid this, if no Mock sessions are running at all
// for 10 minutes, the server shuts down automatically. Skipped for dev, where that might be OK.
// This should catch even hard desktop shell crashes, as sessions shut down automatically if the
// client websocket becomes non-responsive.
// We skip this on Mac, where apps don't generally close when the last window closes.
if (activeSessions <= 0 && IS_PROD_BUILD && process.platform !== 'darwin') {
if (shutdownTimer) {
clearTimeout(shutdownTimer);
shutdownTimer = undefined;
}
// We do a two-step timer here: 1 minute then a logged warning, then 9 more minutes
// until an automatic server shutdown:
shutdownTimer = setTimeout(() => {
if (activeSessions !== 0) return;
console.log('Server is inactive, preparing for auto-shutdown...');
shutdownTimer = setTimeout(() => {
if (activeSessions !== 0) return;
shutdown(99, '10 minutes inactive');
}, 1000 * 60 * 9).unref();
}, 1000 * 60 * 1).unref();
}
});
}
// A map of rule parameters, which may be referenced by the UI, to pass configuration
// set here directly within the Node process to Mockttp (e.g. to set callbacks etc that
// can't be transferred through the API or which need access to server internals).
// This is a constant but mutable dictionary, which will be modified as the appropriate
// parameters change. Mockttp reads from the dictionary each time rules are configured.
const ruleParameters: { [key: string]: any } = {};
export async function runHTK(options: {
configPath?: string
authToken?: string
} = {}) {
const startTime = Date.now();
registerShutdownHandler();
const configPath = options.configPath || envPaths('httptoolkit', { suffix: '' }).config;
await ensureDirectoryExists(configPath);
await checkBrowserConfig(configPath);
const configCheckTime = Date.now();
console.log('Config checked in', configCheckTime - startTime, 'ms');
const httpsConfig = await generateHTTPSConfig(configPath);
const certSetupTime = Date.now();
console.log('Certificates setup in', certSetupTime - configCheckTime, 'ms');
// Start a Mockttp standalone server
const standalone = new PluggableAdmin.AdminServer<{
http: MockttpAdminPlugin,
webrtc: MockRTCAdminPlugin
}>({
adminPlugins: {
http: MockttpAdminPlugin,
webrtc: MockRTCAdminPlugin
},
pluginDefaults: {
http: {
options: {
cors: false, // Don't add mocked CORS responses to intercepted traffic
recordTraffic: false, // Don't persist traffic here (keep it in the UI)
https: httpsConfig // Use our HTTPS config for HTTPS MITMs.
}
},
webrtc: {
recordMessages: false // Don't persist WebRTC traffic server-side either.
}
},
corsOptions: {
strict: true, // For the standalone admin API, require valid CORS headers
origin: MOCKTTP_ALLOWED_ORIGINS, // Only allow mock admin control from our origins
maxAge: 86400, // Cache CORS responses for as long as possible
allowPrivateNetworkAccess: true // Allow access from non-local domains in Chrome 102+
},
webSocketKeepAlive: 20000, // Send a keep-alive ping to Mockttp clients every minute
ruleParameters // Rule parameter dictionary
});
manageBackgroundServices(standalone, httpsConfig);
await standalone.start({
port: 45456,
host: '127.0.0.1'
});
addShutdownHandler(() => standalone.stop());
const standaloneSetupTime = Date.now();
console.log('Standalone server started in', standaloneSetupTime - certSetupTime, 'ms');
// Start the HTK server API
const apiServer = new HttpToolkitServerApi(
{ configPath, authToken: options.authToken, https: httpsConfig },
new HttpClient(ruleParameters),
() => standalone.ruleParameterKeys
);
const updateMutex = new Mutex();
apiServer.on('update-requested', () => {
updateMutex.runExclusive(() =>
(<Promise<void>> updateCommand.run(['stable']))
.catch((error) => {
if (isErrorLike(error)) {
// Did we receive a successful update, that wants to restart the server:
if (error.code === 'EEXIT') {
// Block future update checks for 6 hours.
// If we don't, we'll redownload the same update again every check.
// We don't want to block it completely though, in case this server
// stays open for a very long time.
return delay(1000 * 60 * 60 * 6, { unref: true });
}
if (error.code === 'EACCES') {
// We're running the server without write access to the update directory.
// Weird, but it happens and there's nothing we can do - ignore it.
console.log(`Update check failed: ${error.message}`);
return;
}
// Report any HTTP response errors cleanly & explicitly:
if (error.statusCode) {
let url: string | undefined;
if ('http' in error) {
const request = (error as any).http?.request;
url = `${request?.protocol}//${request?.host}${request?.path}`
}
logError(`Failed to check for updates due to ${error.statusCode} response ${
url
? `from ${url}`
: 'from unknown URL'
}`);
return;
}
}
console.error(`Failed to check for updates: ${error.message}`, { cause: error });
})
);
});
await apiServer.start();
console.log('Server started in', Date.now() - standaloneSetupTime, 'ms');
console.log('Total startup took', Date.now() - startTime, 'ms');
}