@@ -4,7 +4,7 @@ import { generateSPKIFingerprint } from 'mockttp';
4
4
import { HtkConfig } from '../config' ;
5
5
6
6
import { getAvailableBrowsers , launchBrowser , BrowserInstance , Browser } from '../browsers' ;
7
- import { delay , readFile , deleteFolder } from '../util' ;
7
+ import { delay , readFile , deleteFolder , listRunningProcesses } from '../util' ;
8
8
import { HideWarningServer } from '../hide-warning-server' ;
9
9
import { Interceptor } from '.' ;
10
10
import { reportError } from '../error-tracking' ;
@@ -16,7 +16,34 @@ const getBrowserDetails = async (config: HtkConfig, variant: string): Promise<Br
16
16
return _ . find ( browsers , b => b . name === variant ) ;
17
17
} ;
18
18
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 {
20
47
21
48
readonly abstract id : string ;
22
49
readonly abstract version : string ;
@@ -42,30 +69,19 @@ abstract class ChromiumBasedInterceptor implements Interceptor {
42
69
async activate ( proxyPort : number ) {
43
70
if ( this . isActive ( proxyPort ) ) return ;
44
71
45
- const certificatePem = await readFile ( this . config . https . certPath , 'utf8' ) ;
46
- const spkiFingerprint = generateSPKIFingerprint ( certificatePem ) ;
47
-
48
72
const hideWarningServer = new HideWarningServer ( this . config ) ;
49
73
await hideWarningServer . start ( 'https://amiusing.httptoolkit.tech' ) ;
50
74
51
75
const browserDetails = await getBrowserDetails ( this . config , this . variantName ) ;
52
76
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 ) ;
69
85
70
86
if ( browser . process . stdout ) browser . process . stdout . pipe ( process . stdout ) ;
71
87
if ( browser . process . stderr ) browser . process . stderr . pipe ( process . stderr ) ;
@@ -122,7 +138,144 @@ abstract class ChromiumBasedInterceptor implements Interceptor {
122
138
}
123
139
} ;
124
140
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 {
126
279
127
280
id = 'fresh-chrome' ;
128
281
version = '1.0.0' ;
@@ -133,7 +286,18 @@ export class FreshChrome extends ChromiumBasedInterceptor {
133
286
134
287
} ;
135
288
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 {
137
301
138
302
id = 'fresh-chrome-beta' ;
139
303
version = '1.0.0' ;
@@ -144,7 +308,7 @@ export class FreshChromeBeta extends ChromiumBasedInterceptor {
144
308
145
309
} ;
146
310
147
- export class FreshChromeDev extends ChromiumBasedInterceptor {
311
+ export class FreshChromeDev extends FreshChromiumBasedInterceptor {
148
312
149
313
id = 'fresh-chrome-dev' ;
150
314
version = '1.0.0' ;
@@ -155,7 +319,7 @@ export class FreshChromeDev extends ChromiumBasedInterceptor {
155
319
156
320
} ;
157
321
158
- export class FreshChromeCanary extends ChromiumBasedInterceptor {
322
+ export class FreshChromeCanary extends FreshChromiumBasedInterceptor {
159
323
160
324
id = 'fresh-chrome-canary' ;
161
325
version = '1.0.0' ;
@@ -166,7 +330,7 @@ export class FreshChromeCanary extends ChromiumBasedInterceptor {
166
330
167
331
} ;
168
332
169
- export class FreshChromium extends ChromiumBasedInterceptor {
333
+ export class FreshChromium extends FreshChromiumBasedInterceptor {
170
334
171
335
id = 'fresh-chromium' ;
172
336
version = '1.0.0' ;
@@ -177,7 +341,7 @@ export class FreshChromium extends ChromiumBasedInterceptor {
177
341
178
342
} ;
179
343
180
- export class FreshChromiumDev extends ChromiumBasedInterceptor {
344
+ export class FreshChromiumDev extends FreshChromiumBasedInterceptor {
181
345
182
346
id = 'fresh-chromium-dev' ;
183
347
version = '1.0.0' ;
@@ -188,7 +352,7 @@ export class FreshChromiumDev extends ChromiumBasedInterceptor {
188
352
189
353
} ;
190
354
191
- export class FreshEdge extends ChromiumBasedInterceptor {
355
+ export class FreshEdge extends FreshChromiumBasedInterceptor {
192
356
193
357
id = 'fresh-edge' ;
194
358
version = '1.0.0' ;
@@ -199,7 +363,7 @@ export class FreshEdge extends ChromiumBasedInterceptor {
199
363
200
364
} ;
201
365
202
- export class FreshEdgeBeta extends ChromiumBasedInterceptor {
366
+ export class FreshEdgeBeta extends FreshChromiumBasedInterceptor {
203
367
204
368
id = 'fresh-edge-beta' ;
205
369
version = '1.0.0' ;
@@ -210,7 +374,7 @@ export class FreshEdgeBeta extends ChromiumBasedInterceptor {
210
374
211
375
} ;
212
376
213
- export class FreshEdgeDev extends ChromiumBasedInterceptor {
377
+ export class FreshEdgeDev extends FreshChromiumBasedInterceptor {
214
378
215
379
id = 'fresh-edge-dev' ;
216
380
version = '1.0.0' ;
@@ -221,7 +385,7 @@ export class FreshEdgeDev extends ChromiumBasedInterceptor {
221
385
222
386
} ;
223
387
224
- export class FreshEdgeCanary extends ChromiumBasedInterceptor {
388
+ export class FreshEdgeCanary extends FreshChromiumBasedInterceptor {
225
389
226
390
id = 'fresh-edge-canary' ;
227
391
version = '1.0.0' ;
@@ -232,7 +396,7 @@ export class FreshEdgeCanary extends ChromiumBasedInterceptor {
232
396
233
397
} ;
234
398
235
- export class FreshBrave extends ChromiumBasedInterceptor {
399
+ export class FreshBrave extends FreshChromiumBasedInterceptor {
236
400
237
401
id = 'fresh-brave' ;
238
402
version = '1.0.0' ;
@@ -243,7 +407,7 @@ export class FreshBrave extends ChromiumBasedInterceptor {
243
407
244
408
} ;
245
409
246
- export class FreshOpera extends ChromiumBasedInterceptor {
410
+ export class FreshOpera extends FreshChromiumBasedInterceptor {
247
411
248
412
id = 'fresh-opera' ;
249
413
version = '1.0.3' ;
0 commit comments