@@ -4,7 +4,7 @@ import { generateSPKIFingerprint } from 'mockttp';
44import { HtkConfig } from '../config' ;
55
66import { getAvailableBrowsers , launchBrowser , BrowserInstance , Browser } from '../browsers' ;
7- import { delay , readFile , deleteFolder } from '../util' ;
7+ import { delay , readFile , deleteFolder , listRunningProcesses } from '../util' ;
88import { HideWarningServer } from '../hide-warning-server' ;
99import { Interceptor } from '.' ;
1010import { 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 ( / ^ \/ C o n t e n t s \/ M a c O S \/ / ) ;
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' ;
0 commit comments