Skip to content

Commit 9cce354

Browse files
committed
On first run, start a non-proxy firefox instance for setup, then restart it
This is useful to avoid extra noisy proxy traffic, and also to improve some of the setup UX.
1 parent e49ed5a commit 9cce354

File tree

3 files changed

+163
-87
lines changed

3 files changed

+163
-87
lines changed

src/cert-check-server.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,20 @@ import { HttpsPathOptions } from 'mockttp/dist/util/tls';
77

88
const readFile = promisify(fs.readFile);
99

10-
// Make the types for some of the browser code below happy.
11-
let targetUrl: string;
12-
let installingCert: boolean;
13-
14-
// Check if an HTTPS cert to a server using the certificate succeeds
15-
// If it doesn't, redirect to the certificate itself (the browser will prompt to install)
16-
// Note that this function is stringified, and run in the browser, not here in node.
17-
function ensureCertificateIsInstalled() {
18-
19-
}
20-
2110
export class CertCheckServer {
2211

2312
constructor(private config: { https: HttpsPathOptions }) { }
2413

2514
private server: Mockttp | undefined;
2615

27-
async start(targetUrl: string) {
16+
async start(targetUrl?: string) {
2817
this.server = getLocal({ https: this.config.https, cors: true });
2918
await this.server.start();
3019

3120
const certificatePem = await readFile(this.config.https.certPath);
3221

22+
if (!targetUrl) targetUrl = this.server.urlFor('/spinner');
23+
3324
await Promise.all([
3425
this.server.get('/test-https').thenReply(200),
3526
this.server.get('/download-cert').thenReply(200, certificatePem, {
@@ -106,7 +97,21 @@ export class CertCheckServer {
10697
<p>
10798
Select 'Trust this CA to identify web sites' and press 'OK' to continue.
10899
</p>
109-
100+
</div>
101+
</body>
102+
</html>
103+
`),
104+
this.server.get('/spinner').thenReply(200, `
105+
<html>
106+
<title>HTTP Toolkit Certificate Setup</title>
107+
<meta charset="UTF-8" />
108+
<style>
109+
body {
110+
margin: 20px;
111+
background-color: #d8e2e6;
112+
}
113+
</style>
114+
<body>
110115
<svg
111116
version="1.1"
112117
xmlns="http://www.w3.org/2000/svg"
@@ -133,7 +138,7 @@ export class CertCheckServer {
133138
</div>
134139
</body>
135140
</html>
136-
`)
141+
`),
137142
]);
138143
}
139144

src/interceptors/fresh-firefox.ts

Lines changed: 106 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export class FreshFirefox implements Interceptor {
2929

3030
constructor(private config: HtkConfig) { }
3131

32+
private readonly firefoxProfilePath = path.join(this.config.configPath, 'firefox-profile');
33+
3234
isActive(proxyPort: number | string) {
3335
return browsers[proxyPort] != null && !!browsers[proxyPort].pid;
3436
}
@@ -42,91 +44,125 @@ export class FreshFirefox implements Interceptor {
4244

4345
}
4446

45-
async activate(proxyPort: number) {
46-
if (this.isActive(proxyPort)) return;
47+
async startFirefox(
48+
certCheckServer: CertCheckServer,
49+
proxyPort?: number,
50+
existingPrefs = {}
51+
) {
52+
const browser = await launchBrowser(certCheckServer.checkCertUrl, {
53+
browser: 'firefox',
54+
profile: this.firefoxProfilePath,
55+
proxy: proxyPort ? `127.0.0.1:${proxyPort}` : undefined,
56+
// Don't intercept our cert testing requests
57+
noProxy: proxyPort ? certCheckServer.host : undefined,
58+
prefs: _.assign(
59+
existingPrefs,
60+
proxyPort ? {
61+
// By default browser-launcher only configures HTTP, so we need to add HTTPS:
62+
'network.proxy.ssl': '"127.0.0.1"',
63+
'network.proxy.ssl_port': proxyPort,
64+
65+
// The above browser-launcher proxy/noProxy settings should do this, but don't seem to
66+
// reliably overwrite existing values, so we set them explicitly.
67+
'network.proxy.http': '"127.0.0.1"',
68+
'network.proxy.http_port': proxyPort,
69+
'network.proxy.http.network.proxy.http.no_proxies_on': "\"" + certCheckServer.host + "\"",
70+
71+
// Send localhost reqs via the proxy too
72+
'network.proxy.allow_hijacking_localhost': true,
73+
} : {},
74+
{
75+
// Disable the noisy captive portal check requests
76+
'network.captive-portal-service.enabled': false,
77+
78+
// Disable some annoying tip messages
79+
'browser.chrome.toolbar_tips': false,
80+
81+
// Ignore available updates:
82+
"app.update.auto": false,
83+
"browser.startup.homepage_override.mstone": "\"ignore\"",
84+
85+
// Disable exit warnings:
86+
"browser.showQuitWarning": false,
87+
"browser.tabs.warnOnClose": false,
88+
"browser.tabs.warnOnCloseOtherTabs": false,
89+
90+
// Disable various first-run things:
91+
"browser.uitour.enabled": false,
92+
'browser.usedOnWindows10': true,
93+
"browser.usedOnWindows10.introURL": "\"\"",
94+
'datareporting.healthreport.service.firstRun': false,
95+
'toolkit.telemetry.reportingpolicy.firstRun': false,
96+
'browser.reader.detectedFirstArticle': false,
97+
"datareporting.policy.dataSubmissionEnabled": false,
98+
"datareporting.policy.dataSubmissionPolicyAccepted": false,
99+
"datareporting.policy.dataSubmissionPolicyBypassNotification": true,
100+
"trailhead.firstrun.didSeeAboutWelcome": true
101+
}
102+
)
103+
}, this.config.configPath);
47104

105+
if (browser.process.stdout) browser.process.stdout.pipe(process.stdout);
106+
if (browser.process.stderr) browser.process.stderr.pipe(process.stderr);
107+
108+
return browser;
109+
}
110+
111+
async setupFirefoxProfile() {
48112
const certCheckServer = new CertCheckServer(this.config);
49-
await certCheckServer.start('https://amiusing.httptoolkit.tech');
113+
await certCheckServer.start();
114+
const certInstalled = certCheckServer.waitForSuccess().catch(reportError);
115+
116+
const browser = await this.startFirefox(certCheckServer);
117+
browser.process.once('exit', () => certCheckServer.stop());
118+
await certInstalled;
119+
await delay(100); // Tiny delay, so firefox can do initial setup tasks
120+
browser.stop();
121+
}
122+
123+
async activate(proxyPort: number) {
124+
if (this.isActive(proxyPort)) return;
50125

51126
const firefoxProfile = path.join(this.config.configPath, 'firefox-profile');
52127
const firefoxPrefsFile = path.join(firefoxProfile, 'prefs.js');
53128

54129
let existingPrefs: _.Dictionary<any> = {};
55130

56-
if (await canAccess(firefoxPrefsFile)) {
57-
// If the profile exists, then we've run this before and not deleted the profile,
58-
// so it probably worked. If so, reuse the existing preferences to avoid issues
59-
// where on pref setup firefox behaves badly (opening a 2nd window) on OSX.
60-
const prefContents = await readFile(firefoxPrefsFile, {
61-
encoding: 'utf8'
62-
});
63-
64-
existingPrefs = _(prefContents)
65-
.split('\n')
66-
.reduce((prefs: _.Dictionary<any>, line) => {
67-
const match = FIREFOX_PREF_REGEX.exec(line);
68-
if (match) {
69-
prefs[match[1]] = match[2];
70-
}
71-
return prefs
72-
}, {});
131+
if (await canAccess(firefoxPrefsFile) === false) {
132+
/*
133+
First time, we do a separate pre-usage startup & stop, without the proxy, for certificate setup.
134+
This helps avoid initial Firefox profile setup request noise, and tidies up some awkward UX where
135+
firefox likes to open extra welcome windows/tabs on first run, especially on OSX.
136+
*/
137+
await this.setupFirefoxProfile();
73138
}
74139

75-
const browser = await launchBrowser(certCheckServer.checkCertUrl, {
76-
browser: 'firefox',
77-
profile: firefoxProfile,
78-
proxy: `127.0.0.1:${proxyPort}`,
79-
// Don't intercept our cert testing requests
80-
noProxy: certCheckServer.host,
81-
prefs: _.assign(existingPrefs, {
82-
// By default browser-launcher only configures HTTP, so we need to add HTTPS:
83-
'network.proxy.ssl': '"127.0.0.1"',
84-
'network.proxy.ssl_port': proxyPort,
85-
86-
// The above browser-launcher proxy/noProxy settings should do this, but don't seem to
87-
// reliably overwrite existing values, so we set them explicitly.
88-
'network.proxy.http': '"127.0.0.1"',
89-
'network.proxy.http_port': proxyPort,
90-
'network.proxy.http.network.proxy.http.no_proxies_on': certCheckServer.host,
91-
92-
// Send localhost reqs via the proxy too
93-
'network.proxy.allow_hijacking_localhost': true,
94-
95-
// Disable the noisy captive portal check requests
96-
'network.captive-portal-service.enabled': false,
97-
98-
// Disable some annoying tip messages
99-
'browser.chrome.toolbar_tips': false,
100-
101-
// Ignore available updates:
102-
"app.update.auto": false,
103-
"browser.startup.homepage_override.mstone": "ignore",
104-
105-
// Disable exit warnings:
106-
"browser.showQuitWarning": false,
107-
"browser.tabs.warnOnClose": false,
108-
"browser.tabs.warnOnCloseOtherTabs": false,
109-
110-
// Disable various first-run things:
111-
"browser.uitour.enabled": false,
112-
'browser.usedOnWindows10': true,
113-
"browser.usedOnWindows10.introURL": "",
114-
'datareporting.healthreport.service.firstRun': false,
115-
'toolkit.telemetry.reportingpolicy.firstRun': false,
116-
'browser.reader.detectedFirstArticle': false,
117-
"datareporting.policy.dataSubmissionEnabled": false,
118-
"datareporting.policy.dataSubmissionPolicyAccepted": false,
119-
"datareporting.policy.dataSubmissionPolicyBypassNotification": true
120-
})
121-
}, this.config.configPath);
140+
const certCheckServer = new CertCheckServer(this.config);
141+
await certCheckServer.start('https://amiusing.httptoolkit.tech');
122142

123-
if (browser.process.stdout) browser.process.stdout.pipe(process.stdout);
124-
if (browser.process.stderr) browser.process.stderr.pipe(process.stderr);
143+
// At this stage, we've run firefox at least once, at it's working nicely.
144+
// We need to preserve & reuse any existing preferences, to avoid issues
145+
// where on pref setup firefox behaves badly (opening a 2nd window) on OSX.
146+
const prefContents = await readFile(firefoxPrefsFile, {
147+
encoding: 'utf8'
148+
});
149+
150+
existingPrefs = _(prefContents)
151+
.split('\n')
152+
.reduce((prefs: _.Dictionary<any>, line) => {
153+
const match = FIREFOX_PREF_REGEX.exec(line);
154+
if (match) {
155+
prefs[match[1]] = match[2];
156+
}
157+
return prefs
158+
}, {});
159+
160+
const browser = await this.startFirefox(certCheckServer, proxyPort, existingPrefs);
125161

126162
let success = false;
127163
certCheckServer.waitForSuccess().then(() => {
128164
success = true;
129-
}).catch(console.warn);
165+
}).catch(reportError);
130166

131167
browsers[proxyPort] = browser;
132168
browser.process.once('exit', () => {

test/interceptors/fresh-firefox.spec.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as _ from 'lodash';
2+
import fetch from 'node-fetch';
23
import { CompletedRequest } from 'mockttp';
3-
import { setupInterceptor, itIsAvailable, itCanBeActivated } from './interceptor-test-utils';
4+
import { setupInterceptor, itIsAvailable } from './interceptor-test-utils';
5+
import { expect } from 'chai';
6+
import { delay } from '../../src/util';
47

58
const interceptorSetup = setupInterceptor('fresh-firefox');
69

@@ -20,10 +23,42 @@ describe('Firefox interceptor', function () {
2023
});
2124

2225
itIsAvailable(interceptorSetup);
23-
itCanBeActivated(interceptorSetup);
26+
27+
it('can be activated', async () => {
28+
const { interceptor, server } = await interceptorSetup;
29+
30+
expect(interceptor.isActive(server.port)).to.equal(false);
31+
32+
const activation = interceptor.activate(server.port);
33+
await delay(500);
34+
await fetch('http://localhost:8001/report-success');
35+
await activation;
36+
expect(interceptor.isActive(server.port)).to.equal(true);
37+
expect(interceptor.isActive(server.port + 1)).to.equal(false);
38+
39+
await interceptor.deactivate(server.port);
40+
await delay(500); // Wait to ensure the profile is cleaned up
41+
});
42+
43+
it('can deactivate all', async () => {
44+
const { interceptor, server } = await interceptorSetup;
45+
46+
expect(interceptor.isActive(server.port)).to.equal(false);
47+
48+
const activation = interceptor.activate(server.port);
49+
await delay(500);
50+
await fetch('http://localhost:8001/report-success');
51+
await activation;
52+
expect(interceptor.isActive(server.port)).to.equal(true);
53+
expect(interceptor.isActive(server.port + 1)).to.equal(false);
54+
55+
await interceptor.deactivateAll();
56+
expect(interceptor.isActive(server.port)).to.equal(false);
57+
await delay(500); // Wait to ensure the profile is cleaned up
58+
});
2459

2560
// TODO: This doesn't work, as we need to manually accept the cert before
26-
// Firefox makes its HTTPS request to amiusing
61+
// Firefox will make any requests to any real servers. Might be doable with webdriver?
2762
it.skip('successfully makes requests', async function () {
2863
const { server, interceptor: firefoxInterceptor } = await interceptorSetup;
2964

0 commit comments

Comments
 (0)