1+ import * as _ from 'lodash' ;
2+ import { generateSPKIFingerprint } from 'mockttp' ;
3+
4+ import { HtkConfig } from '../config' ;
5+
6+ import { getAvailableBrowsers , launchBrowser , BrowserInstance , Browser } from '../browsers' ;
7+ import { delay , readFile , deleteFolder } from '../util' ;
8+ import { HideWarningServer } from '../hide-warning-server' ;
9+ import { Interceptor } from '.' ;
10+ import { reportError } from '../error-tracking' ;
11+
12+ const getBrowserDetails = async ( config : HtkConfig , variant : string ) : Promise < Browser | undefined > => {
13+ const browsers = await getAvailableBrowsers ( config . configPath ) ;
14+
15+ // Get the details for the first of these browsers that is installed.
16+ return _ . find ( browsers , b => b . name === variant ) ;
17+ } ;
18+
19+ abstract class ChromiumBasedInterceptor implements Interceptor {
20+
21+ readonly abstract id : string ;
22+ readonly abstract version : string ;
23+
24+ private readonly activeBrowsers : _ . Dictionary < BrowserInstance > = { } ;
25+
26+ constructor (
27+ private config : HtkConfig ,
28+ private variantName : string
29+ ) { }
30+
31+
32+ isActive ( proxyPort : number | string ) {
33+ const browser = this . activeBrowsers [ proxyPort ] ;
34+ return ! ! browser && ! ! browser . pid ;
35+ }
36+
37+ async isActivable ( ) {
38+ return ! ! ( await getBrowserDetails ( this . config , this . variantName ) ) ;
39+ }
40+
41+ async activate ( proxyPort : number ) {
42+ if ( this . isActive ( proxyPort ) ) return ;
43+
44+ const certificatePem = await readFile ( this . config . https . certPath , 'utf8' ) ;
45+ const spkiFingerprint = generateSPKIFingerprint ( certificatePem ) ;
46+
47+ const hideWarningServer = new HideWarningServer ( this . config ) ;
48+ await hideWarningServer . start ( 'https://amiusing.httptoolkit.tech' ) ;
49+
50+ const browserDetails = await getBrowserDetails ( this . config , this . variantName ) ;
51+
52+ const browser = await launchBrowser ( hideWarningServer . hideWarningUrl , {
53+ browser : browserDetails ? browserDetails . name : this . variantName ,
54+ proxy : `https://127.0.0.1:${ proxyPort } ` ,
55+ noProxy : [
56+ // Force even localhost requests to go through the proxy
57+ // See https://bugs.chromium.org/p/chromium/issues/detail?id=899126#c17
58+ '<-loopback>' ,
59+ // Don't intercept our warning hiding requests. Note that this must be
60+ // the 2nd rule here, or <-loopback> would override it.
61+ hideWarningServer . host
62+ ] ,
63+ options : [
64+ // Trust our CA certificate's fingerprint:
65+ `--ignore-certificate-errors-spki-list=${ spkiFingerprint } `
66+ ]
67+ } , this . config . configPath ) ;
68+
69+ if ( browser . process . stdout ) browser . process . stdout . pipe ( process . stdout ) ;
70+ if ( browser . process . stderr ) browser . process . stderr . pipe ( process . stderr ) ;
71+
72+ await hideWarningServer . completedPromise ;
73+ await hideWarningServer . stop ( ) ;
74+
75+ this . activeBrowsers [ proxyPort ] = browser ;
76+ browser . process . once ( 'close' , ( ) => {
77+ delete this . activeBrowsers [ proxyPort ] ;
78+
79+ if ( Object . keys ( this . activeBrowsers ) . length === 0 && browserDetails && _ . isString ( browserDetails . profile ) ) {
80+ // If we were the last browser, and we have a profile path, and it's in our config
81+ // (just in case something's gone wrong) -> delete the profile to reset everything.
82+
83+ const profilePath = browserDetails . profile ;
84+ if ( ! profilePath . startsWith ( this . config . configPath ) ) {
85+ reportError (
86+ `Unexpected ${ this . variantName } profile location, not deleting: ${ profilePath } `
87+ ) ;
88+ } else {
89+ deleteFolder ( browserDetails . profile ) . catch ( reportError ) ;
90+ }
91+ }
92+ } ) ;
93+
94+ // Delay the approx amount of time it normally takes the browser to really open, just to be sure
95+ await delay ( 500 ) ;
96+ }
97+
98+ async deactivate ( proxyPort : number | string ) {
99+ if ( this . isActive ( proxyPort ) ) {
100+ const browser = this . activeBrowsers [ proxyPort ] ;
101+ const exitPromise = new Promise ( ( resolve ) => browser ! . process . once ( 'close' , resolve ) ) ;
102+ browser ! . stop ( ) ;
103+ await exitPromise ;
104+ }
105+ }
106+
107+ async deactivateAll ( ) : Promise < void > {
108+ await Promise . all (
109+ Object . keys ( this . activeBrowsers ) . map ( ( proxyPort ) => this . deactivate ( proxyPort ) )
110+ ) ;
111+ }
112+ } ;
113+
114+ export class FreshChrome extends ChromiumBasedInterceptor {
115+
116+ id = 'fresh-chrome' ;
117+ version = '1.0.0' ;
118+
119+ constructor ( config : HtkConfig ) {
120+ super ( config , 'chrome' ) ;
121+ }
122+
123+ } ;
124+
125+ export class FreshChromeBeta extends ChromiumBasedInterceptor {
126+
127+ id = 'fresh-chrome-beta' ;
128+ version = '1.0.0' ;
129+
130+ constructor ( config : HtkConfig ) {
131+ super ( config , 'chrome-beta' ) ;
132+ }
133+
134+ } ;
135+
136+ export class FreshChromeDev extends ChromiumBasedInterceptor {
137+
138+ id = 'fresh-chrome-dev' ;
139+ version = '1.0.0' ;
140+
141+ constructor ( config : HtkConfig ) {
142+ super ( config , 'chrome-dev' ) ;
143+ }
144+
145+ } ;
146+
147+ export class FreshChromeCanary extends ChromiumBasedInterceptor {
148+
149+ id = 'fresh-chrome-canary' ;
150+ version = '1.0.0' ;
151+
152+ constructor ( config : HtkConfig ) {
153+ super ( config , 'chrome-canary' ) ;
154+ }
155+
156+ } ;
157+
158+ export class FreshChromium extends ChromiumBasedInterceptor {
159+
160+ id = 'fresh-chromium' ;
161+ version = '1.0.0' ;
162+
163+ constructor ( config : HtkConfig ) {
164+ super ( config , 'chromium' ) ;
165+ }
166+
167+ } ;
168+
169+ export class FreshChromiumDev extends ChromiumBasedInterceptor {
170+
171+ id = 'fresh-chromium-dev' ;
172+ version = '1.0.0' ;
173+
174+ constructor ( config : HtkConfig ) {
175+ super ( config , 'chromium-dev' ) ;
176+ }
177+
178+ } ;
179+
180+ export class FreshEdge extends ChromiumBasedInterceptor {
181+
182+ id = 'fresh-edge' ;
183+ version = '1.0.0' ;
184+
185+ constructor ( config : HtkConfig ) {
186+ super ( config , 'msedge' ) ;
187+ }
188+
189+ } ;
190+
191+ export class FreshEdgeBeta extends ChromiumBasedInterceptor {
192+
193+ id = 'fresh-edge-beta' ;
194+ version = '1.0.0' ;
195+
196+ constructor ( config : HtkConfig ) {
197+ super ( config , 'msedge-beta' ) ;
198+ }
199+
200+ } ;
201+
202+ export class FreshEdgeCanary extends ChromiumBasedInterceptor {
203+
204+ id = 'fresh-edge-canary' ;
205+ version = '1.0.0' ;
206+
207+ constructor ( config : HtkConfig ) {
208+ super ( config , 'msedge-canary' ) ;
209+ }
210+
211+ } ;
0 commit comments