Skip to content

Commit fee1f1e

Browse files
committed
Rough but working existing-chrome support
1 parent 2061cd5 commit fee1f1e

File tree

4 files changed

+292
-33
lines changed

4 files changed

+292
-33
lines changed

src/interceptors/chromium-based-interceptors.ts

Lines changed: 197 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { generateSPKIFingerprint } from 'mockttp';
44
import { HtkConfig } from '../config';
55

66
import { getAvailableBrowsers, launchBrowser, BrowserInstance, Browser } from '../browsers';
7-
import { delay, readFile, deleteFolder } from '../util';
7+
import { delay, readFile, deleteFolder, listRunningProcesses } from '../util';
88
import { HideWarningServer } from '../hide-warning-server';
99
import { Interceptor } from '.';
1010
import { reportError } from '../error-tracking';
@@ -16,7 +16,34 @@ const getBrowserDetails = async (config: HtkConfig, variant: string): Promise<Br
1616
return _.find(browsers, b => b.name === variant);
1717
};
1818

19-
abstract class ChromiumBasedInterceptor implements Interceptor {
19+
const getChromiumLaunchOptions = async (
20+
browser: string,
21+
config: HtkConfig,
22+
proxyPort: number,
23+
hideWarningServer: HideWarningServer
24+
) => {
25+
const certificatePem = await readFile(config.https.certPath, 'utf8');
26+
const spkiFingerprint = generateSPKIFingerprint(certificatePem);
27+
28+
return {
29+
browser,
30+
proxy: `https://127.0.0.1:${proxyPort}`,
31+
noProxy: [
32+
// Force even localhost requests to go through the proxy
33+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=899126#c17
34+
'<-loopback>',
35+
// Don't intercept our warning hiding requests. Note that this must be
36+
// the 2nd rule here, or <-loopback> would override it.
37+
hideWarningServer.host
38+
],
39+
options: [
40+
// Trust our CA certificate's fingerprint:
41+
`--ignore-certificate-errors-spki-list=${spkiFingerprint}`
42+
]
43+
};
44+
}
45+
46+
abstract class FreshChromiumBasedInterceptor implements Interceptor {
2047

2148
readonly abstract id: string;
2249
readonly abstract version: string;
@@ -42,30 +69,19 @@ abstract class ChromiumBasedInterceptor implements Interceptor {
4269
async activate(proxyPort: number) {
4370
if (this.isActive(proxyPort)) return;
4471

45-
const certificatePem = await readFile(this.config.https.certPath, 'utf8');
46-
const spkiFingerprint = generateSPKIFingerprint(certificatePem);
47-
4872
const hideWarningServer = new HideWarningServer(this.config);
4973
await hideWarningServer.start('https://amiusing.httptoolkit.tech');
5074

5175
const browserDetails = await getBrowserDetails(this.config, this.variantName);
5276

53-
const browser = await launchBrowser(hideWarningServer.hideWarningUrl, {
54-
browser: browserDetails ? browserDetails.name : this.variantName,
55-
proxy: `https://127.0.0.1:${proxyPort}`,
56-
noProxy: [
57-
// Force even localhost requests to go through the proxy
58-
// See https://bugs.chromium.org/p/chromium/issues/detail?id=899126#c17
59-
'<-loopback>',
60-
// Don't intercept our warning hiding requests. Note that this must be
61-
// the 2nd rule here, or <-loopback> would override it.
62-
hideWarningServer.host
63-
],
64-
options: [
65-
// Trust our CA certificate's fingerprint:
66-
`--ignore-certificate-errors-spki-list=${spkiFingerprint}`
67-
]
68-
}, this.config.configPath);
77+
const browser = await launchBrowser(hideWarningServer.hideWarningUrl,
78+
await getChromiumLaunchOptions(
79+
browserDetails ? browserDetails.name : this.variantName,
80+
this.config,
81+
proxyPort,
82+
hideWarningServer
83+
)
84+
, this.config.configPath);
6985

7086
if (browser.process.stdout) browser.process.stdout.pipe(process.stdout);
7187
if (browser.process.stderr) browser.process.stderr.pipe(process.stderr);
@@ -122,7 +138,144 @@ abstract class ChromiumBasedInterceptor implements Interceptor {
122138
}
123139
};
124140

125-
export class FreshChrome extends ChromiumBasedInterceptor {
141+
abstract class ExistingChromiumBasedInterceptor implements Interceptor {
142+
143+
readonly abstract id: string;
144+
readonly abstract version: string;
145+
146+
private activeBrowser: { // We can only intercept one instance
147+
proxyPort: number,
148+
browser: BrowserInstance
149+
} | undefined;
150+
151+
constructor(
152+
private config: HtkConfig,
153+
private variantName: string
154+
) { }
155+
156+
async browserDetails() {
157+
return getBrowserDetails(this.config, this.variantName);
158+
}
159+
160+
isActive(proxyPort: number | string) {
161+
const activeBrowser = this.activeBrowser;
162+
return !!activeBrowser &&
163+
activeBrowser.proxyPort === proxyPort &&
164+
!!activeBrowser.browser.pid;
165+
}
166+
167+
async isActivable() {
168+
if (this.activeBrowser) return false;
169+
return !!this.browserDetails();
170+
}
171+
172+
async findExistingPid(): Promise<number | undefined> {
173+
const processes = await listRunningProcesses();
174+
175+
const browserDetails = await this.browserDetails();
176+
if (!browserDetails) {
177+
throw new Error("Can't intercept existing browser without browser details");
178+
}
179+
180+
const browserProcesses = processes.filter((proc) => {
181+
if (process.platform === 'darwin') {
182+
if (!proc.command.startsWith(browserDetails.command)) return false;
183+
184+
const appBundlePath = proc.command.substring(browserDetails.command.length);
185+
186+
// Only *.app/Contents/MacOS/* is the main app process:
187+
return appBundlePath.match(/^\/Contents\/MacOS\//);
188+
} else {
189+
return proc.bin && (
190+
// Find a binary that exactly matches the specific command:
191+
proc.bin === browserDetails.command ||
192+
// Or whose binary who's matches the path for this specific variant:
193+
proc.bin.includes(`${browserDetails.name}/${browserDetails.type}`)
194+
);
195+
}
196+
});
197+
198+
const rootProcess = browserProcesses.find(({ args }) =>
199+
// Find the main process, skipping any renderer/util processes
200+
args !== undefined && !args.includes('--type=')
201+
);
202+
203+
return rootProcess && rootProcess.pid;
204+
}
205+
206+
async activate(proxyPort: number, options: { closeConfirmed: boolean } = { closeConfirmed: false }) {
207+
if (!this.isActivable()) return;
208+
209+
const certificatePem = await readFile(this.config.https.certPath, 'utf8');
210+
const spkiFingerprint = generateSPKIFingerprint(certificatePem);
211+
212+
const hideWarningServer = new HideWarningServer(this.config);
213+
await hideWarningServer.start('https://amiusing.httptoolkit.tech');
214+
215+
const existingPid = await this.findExistingPid();
216+
if (existingPid) {
217+
if (!options.closeConfirmed) {
218+
// Fail, with metadata requesting the UI to confirm that Chrome should be killed
219+
throw Object.assign(
220+
new Error(`Not killing ${this.variantName}: not confirmed`),
221+
{ metadata: { closeConfirmRequired: true }, reportable: false }
222+
);
223+
}
224+
225+
process.kill(existingPid);
226+
await delay(1000);
227+
}
228+
229+
const browserDetails = await getBrowserDetails(this.config, this.variantName);
230+
const launchOptions = await getChromiumLaunchOptions(
231+
browserDetails ? browserDetails.name : this.variantName,
232+
this.config,
233+
proxyPort,
234+
hideWarningServer
235+
);
236+
237+
if (existingPid) {
238+
// If we killed something, use --restore-last-session to ensure it comes back:
239+
launchOptions.options.push('--restore-last-session');
240+
}
241+
242+
const browser = await launchBrowser(hideWarningServer.hideWarningUrl, {
243+
...launchOptions,
244+
profile: null // Enforce that we use the default profile
245+
}, this.config.configPath);
246+
247+
if (browser.process.stdout) browser.process.stdout.pipe(process.stdout);
248+
if (browser.process.stderr) browser.process.stderr.pipe(process.stderr);
249+
250+
await hideWarningServer.completedPromise;
251+
await hideWarningServer.stop();
252+
253+
this.activeBrowser = { browser, proxyPort };
254+
browser.process.once('close', async () => {
255+
delete this.activeBrowser;
256+
});
257+
258+
// Delay the approx amount of time it normally takes the browser to really open, just to be sure
259+
await delay(500);
260+
}
261+
262+
async deactivate(proxyPort: number | string) {
263+
if (this.isActive(proxyPort)) {
264+
const { browser } = this.activeBrowser!;
265+
const exitPromise = new Promise((resolve) => browser!.process.once('close', resolve));
266+
browser!.stop();
267+
await exitPromise;
268+
}
269+
}
270+
271+
async deactivateAll(): Promise<void> {
272+
if (this.activeBrowser) {
273+
await this.deactivate(this.activeBrowser.proxyPort);
274+
}
275+
}
276+
};
277+
278+
export class FreshChrome extends FreshChromiumBasedInterceptor {
126279

127280
id = 'fresh-chrome';
128281
version = '1.0.0';
@@ -133,7 +286,18 @@ export class FreshChrome extends ChromiumBasedInterceptor {
133286

134287
};
135288

136-
export class FreshChromeBeta extends ChromiumBasedInterceptor {
289+
export class ExistingChrome extends ExistingChromiumBasedInterceptor {
290+
291+
id = 'existing-chrome';
292+
version = '1.0.0';
293+
294+
constructor(config: HtkConfig) {
295+
super(config, 'chrome');
296+
}
297+
298+
};
299+
300+
export class FreshChromeBeta extends FreshChromiumBasedInterceptor {
137301

138302
id = 'fresh-chrome-beta';
139303
version = '1.0.0';
@@ -144,7 +308,7 @@ export class FreshChromeBeta extends ChromiumBasedInterceptor {
144308

145309
};
146310

147-
export class FreshChromeDev extends ChromiumBasedInterceptor {
311+
export class FreshChromeDev extends FreshChromiumBasedInterceptor {
148312

149313
id = 'fresh-chrome-dev';
150314
version = '1.0.0';
@@ -155,7 +319,7 @@ export class FreshChromeDev extends ChromiumBasedInterceptor {
155319

156320
};
157321

158-
export class FreshChromeCanary extends ChromiumBasedInterceptor {
322+
export class FreshChromeCanary extends FreshChromiumBasedInterceptor {
159323

160324
id = 'fresh-chrome-canary';
161325
version = '1.0.0';
@@ -166,7 +330,7 @@ export class FreshChromeCanary extends ChromiumBasedInterceptor {
166330

167331
};
168332

169-
export class FreshChromium extends ChromiumBasedInterceptor {
333+
export class FreshChromium extends FreshChromiumBasedInterceptor {
170334

171335
id = 'fresh-chromium';
172336
version = '1.0.0';
@@ -177,7 +341,7 @@ export class FreshChromium extends ChromiumBasedInterceptor {
177341

178342
};
179343

180-
export class FreshChromiumDev extends ChromiumBasedInterceptor {
344+
export class FreshChromiumDev extends FreshChromiumBasedInterceptor {
181345

182346
id = 'fresh-chromium-dev';
183347
version = '1.0.0';
@@ -188,7 +352,7 @@ export class FreshChromiumDev extends ChromiumBasedInterceptor {
188352

189353
};
190354

191-
export class FreshEdge extends ChromiumBasedInterceptor {
355+
export class FreshEdge extends FreshChromiumBasedInterceptor {
192356

193357
id = 'fresh-edge';
194358
version = '1.0.0';
@@ -199,7 +363,7 @@ export class FreshEdge extends ChromiumBasedInterceptor {
199363

200364
};
201365

202-
export class FreshEdgeBeta extends ChromiumBasedInterceptor {
366+
export class FreshEdgeBeta extends FreshChromiumBasedInterceptor {
203367

204368
id = 'fresh-edge-beta';
205369
version = '1.0.0';
@@ -210,7 +374,7 @@ export class FreshEdgeBeta extends ChromiumBasedInterceptor {
210374

211375
};
212376

213-
export class FreshEdgeDev extends ChromiumBasedInterceptor {
377+
export class FreshEdgeDev extends FreshChromiumBasedInterceptor {
214378

215379
id = 'fresh-edge-dev';
216380
version = '1.0.0';
@@ -221,7 +385,7 @@ export class FreshEdgeDev extends ChromiumBasedInterceptor {
221385

222386
};
223387

224-
export class FreshEdgeCanary extends ChromiumBasedInterceptor {
388+
export class FreshEdgeCanary extends FreshChromiumBasedInterceptor {
225389

226390
id = 'fresh-edge-canary';
227391
version = '1.0.0';
@@ -232,7 +396,7 @@ export class FreshEdgeCanary extends ChromiumBasedInterceptor {
232396

233397
};
234398

235-
export class FreshBrave extends ChromiumBasedInterceptor {
399+
export class FreshBrave extends FreshChromiumBasedInterceptor {
236400

237401
id = 'fresh-brave';
238402
version = '1.0.0';
@@ -243,7 +407,7 @@ export class FreshBrave extends ChromiumBasedInterceptor {
243407

244408
};
245409

246-
export class FreshOpera extends ChromiumBasedInterceptor {
410+
export class FreshOpera extends FreshChromiumBasedInterceptor {
247411

248412
id = 'fresh-opera';
249413
version = '1.0.3';

src/interceptors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { HtkConfig } from '../config';
55
import { FreshFirefox } from './fresh-firefox';
66
import {
77
FreshChrome,
8+
ExistingChrome,
89
FreshChromeBeta,
910
FreshChromeCanary,
1011
FreshChromeDev,
@@ -47,6 +48,7 @@ export interface ActivationError extends Error {
4748
export function buildInterceptors(config: HtkConfig): _.Dictionary<Interceptor> {
4849
const interceptors = [
4950
new FreshChrome(config),
51+
new ExistingChrome(config),
5052
new FreshChromeBeta(config),
5153
new FreshChromeDev(config),
5254
new FreshChromeCanary(config),

0 commit comments

Comments
 (0)