Skip to content

Commit f2a788c

Browse files
committed
Create a firefox interceptor
1 parent 805c14c commit f2a788c

File tree

8 files changed

+261
-11
lines changed

8 files changed

+261
-11
lines changed

custom-typings/@james-proxy/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ declare module '@james-proxy/james-browser-launcher' {
1515
detached?: boolean;
1616
noProxy?: string | string[];
1717
headless?: boolean;
18+
prefs?: { [key: string]: any };
1819
}
1920

2021
function Launch(

src/cert-check-server.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { promisify } from 'util';
2+
import * as fs from 'fs';
3+
4+
import { getLocal, Mockttp } from 'mockttp';
5+
6+
import { HttpsPathOptions } from 'mockttp/dist/util/tls';
7+
8+
const readFile = promisify(fs.readFile);
9+
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+
const testUrl = window.location.href.replace('http://', 'https://').replace('check-cert', 'test-https');
19+
const downloadUrl = window.location.href.replace('check-cert', 'download-cert');
20+
21+
fetch(testUrl)
22+
.then(() => true)
23+
.catch(() => false)
24+
.then((certificateIsTrusted) => {
25+
if (certificateIsTrusted) {
26+
window.location.replace(targetUrl);
27+
} else {
28+
if (!installingCert) {
29+
installingCert = true;
30+
const iframe = document.createElement('iframe');
31+
iframe.src = downloadUrl;
32+
document.body.appendChild(iframe);
33+
setInterval(ensureCertificateIsInstalled, 500);
34+
}
35+
}
36+
});
37+
}
38+
39+
export class CertCheckServer {
40+
41+
constructor(private config: { https: HttpsPathOptions }) { }
42+
43+
private server: Mockttp | undefined;
44+
45+
async start(targetUrl: string) {
46+
this.server = getLocal({ https: this.config.https, debug: true, cors: true });
47+
await this.server.start();
48+
49+
const certificatePem = await readFile(this.config.https.certPath);
50+
51+
this.server.get('/test-https').thenReply(200);
52+
53+
this.server.get('/download-cert').thenReply(200, certificatePem, {
54+
'Content-type': 'application/x-x509-ca-cert'
55+
});
56+
57+
this.server.get('/check-cert').thenReply(200, `
58+
<html>
59+
<title>HTTP Toolkit Certificate Setup</title>
60+
<meta charset="UTF-8" />
61+
<link href="http://fonts.googleapis.com/css?family=Lato" rel="stylesheet" />
62+
<style>
63+
body {
64+
margin: 20px;
65+
background-color: #d8e2e6;
66+
font-family: Lato, Arial;
67+
}
68+
69+
h1 {
70+
font-size: 36pt;
71+
}
72+
73+
p {
74+
font-size: 16pt;
75+
}
76+
77+
iframe {
78+
display: none;
79+
}
80+
</style>
81+
<script>
82+
let installingCert = false;
83+
const targetUrl = ${JSON.stringify(targetUrl)};
84+
85+
${ensureCertificateIsInstalled.toString()}
86+
ensureCertificateIsInstalled();
87+
</script>
88+
<body>
89+
<h1>
90+
Configuring Firefox to use HTTP Toolkit
91+
</h1>
92+
<p>
93+
To intercept HTTPS traffic, you need to trust the HTTP Toolkit certificate.
94+
</p>
95+
<p>
96+
Select 'Trust this CA to identify web sites' and press 'OK' to continue.
97+
</p>
98+
99+
<svg
100+
version="1.1"
101+
xmlns="http://www.w3.org/2000/svg"
102+
xmlns:xlink="http://www.w3.org/1999/xlink"
103+
x="0px"
104+
y="0px"
105+
width="400px"
106+
height="400px"
107+
viewBox="0 0 50 50"
108+
style="enable-background:new 0 0 50 50;"
109+
>
110+
<path fill="#b6c2ca" d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z">
111+
<animateTransform
112+
attributeType="xml"
113+
attributeName="transform"
114+
type="rotate"
115+
from="0 25 25"
116+
to="360 25 25"
117+
dur="6s"
118+
repeatCount="indefinite"
119+
/>
120+
</path>
121+
</svg>
122+
</div>
123+
</body>
124+
</html>
125+
`);
126+
}
127+
128+
get host(): string {
129+
return this.server!.url
130+
.replace('https://', '');
131+
}
132+
133+
get checkCertUrl(): string {
134+
return this.server!.url
135+
.replace('https://', 'http://')
136+
.replace(/\/?$/, '/check-cert');
137+
}
138+
139+
async stop() {
140+
if (this.server) {
141+
await this.server.stop();
142+
this.server = undefined;
143+
}
144+
}
145+
}

src/config.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { HttpsPathOptions } from "mockttp/dist/util/tls";
2+
13
export interface HtkConfig {
24
configPath: string;
5+
https: HttpsPathOptions
36
}

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async function generateHTTPSConfig(configPath: string) {
2222
canAccess(certPath, fs.constants.R_OK)
2323
]).catch(() => {
2424
const newCertPair = generateCACertificate({
25-
commonName: 'HTTP Toolkit CA - DO NOT TRUST'
25+
commonName: 'HTTP Toolkit CA'
2626
});
2727

2828
return Promise.all([
@@ -58,7 +58,8 @@ export async function runHTK(options: {
5858

5959
// Start a HTK server
6060
const htkServer = new HttpToolkitServer({
61-
configPath
61+
configPath,
62+
https: httpsConfig
6263
});
6364
await htkServer.start();
6465

src/interceptors/fresh-chrome.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class FreshChrome {
3838
const certificatePem = await readFile(path.join(this.config.configPath, 'ca.pem'), 'utf8');
3939
const spkiFingerprint = generateSPKIFingerprint(certificatePem);
4040

41-
const browser = await launchBrowser('https://example.com', {
41+
const browser = await launchBrowser('https://amiusing.httptoolkit.tech', {
4242
browser: 'chrome',
4343
proxy: `https://localhost:${proxyPort}`,
4444
options: [

src/interceptors/fresh-firefox.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as _ from 'lodash';
2+
3+
import { HtkConfig } from '../config';
4+
5+
import { getAvailableBrowsers, launchBrowser, BrowserInstance } from '../browsers';
6+
import { CertCheckServer } from '../cert-check-server';
7+
8+
let browsers: _.Dictionary<BrowserInstance> = {};
9+
10+
export class FreshFirefox {
11+
id = 'fresh-firefox';
12+
version = '1.0.0';
13+
14+
constructor(private config: HtkConfig) { }
15+
16+
isActive(proxyPort: number) {
17+
return browsers[proxyPort] != null && !!browsers[proxyPort].pid;
18+
}
19+
20+
async isActivable() {
21+
const browsers = await getAvailableBrowsers(this.config.configPath);
22+
23+
return _(browsers)
24+
.map(b => b.name)
25+
.includes('firefox')
26+
27+
}
28+
29+
async activate(proxyPort: number) {
30+
if (this.isActive(proxyPort)) return;
31+
32+
const certCheckServer = new CertCheckServer(this.config);
33+
await certCheckServer.start('https://amiusing.httptoolkit.tech');
34+
35+
// TODO: Change james launcher so I can pass a profile here,
36+
// so things persist across launches. Permanently? Yes, probably.
37+
38+
const browser = await launchBrowser(certCheckServer.checkCertUrl, {
39+
browser: 'firefox',
40+
proxy: `localhost:${proxyPort}`,
41+
// Don't intercept our cert testing requests
42+
noProxy: certCheckServer.host,
43+
prefs: {
44+
// By default james-launcher only configures HTTP, so we need to add HTTPS:
45+
'network.proxy.ssl': '"localhost"',
46+
'network.proxy.ssl_port': proxyPort,
47+
48+
// Disable the noisy captive portal check requests
49+
'network.captive-portal-service.enabled': false,
50+
51+
// Disable some annoying tip messages
52+
'browser.chrome.toolbar_tips': false,
53+
54+
// Ignore available updates:
55+
"app.update.auto": false,
56+
"browser.startup.homepage_override.mstone": "ignore",
57+
58+
// Disable exit warnings:
59+
"browser.showQuitWarning": false,
60+
"browser.tabs.warnOnClose": false,
61+
"browser.tabs.warnOnCloseOtherTabs": false,
62+
63+
// Disable 'new FF available' messages
64+
65+
// Disable various first-run things:
66+
"browser.uitour.enabled": false,
67+
'browser.usedOnWindows10': true,
68+
"browser.usedOnWindows10.introURL": "",
69+
'datareporting.healthreport.service.firstRun': false,
70+
'toolkit.telemetry.reportingpolicy.firstRun': false,
71+
'browser.reader.detectedFirstArticle': false,
72+
"datareporting.policy.dataSubmissionEnabled": false,
73+
"datareporting.policy.dataSubmissionPolicyAccepted": false,
74+
"datareporting.policy.dataSubmissionPolicyBypassNotification": true
75+
}
76+
}, this.config.configPath);
77+
78+
browsers[proxyPort] = browser;
79+
browser.process.once('exit', () => {
80+
certCheckServer.stop();
81+
delete browsers[proxyPort];
82+
});
83+
}
84+
85+
async deactivate(proxyPort: number) {
86+
if (this.isActive(proxyPort)) {
87+
const browser = browsers[proxyPort];
88+
browser!.process.kill();
89+
await new Promise((resolve) => browser!.process.once('exit', resolve));
90+
}
91+
}
92+
};

src/interceptors/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as _ from 'lodash';
33
import { HtkConfig } from '../config';
44

55
import { FreshChrome } from './fresh-chrome';
6+
import { FreshFirefox } from './fresh-firefox';
67

78
export interface Interceptor {
89
id: string;
@@ -17,6 +18,7 @@ export interface Interceptor {
1718

1819
export function buildInterceptors(config: HtkConfig): _.Dictionary<Interceptor> {
1920
return _.keyBy([
20-
new FreshChrome(config)
21+
new FreshChrome(config),
22+
new FreshFirefox(config)
2123
], (interceptor) => interceptor.id);
2224
}

test/interceptors.spec.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import * as fs from 'fs';
33
import * as path from 'path';
44
import * as tmp from 'tmp';
55
import { expect } from 'chai';
6-
import { getLocal, CompletedRequest, generateCACertificate, Mockttp } from 'mockttp';
6+
import { getLocal, CompletedRequest, generateCACertificate } from 'mockttp';
77

8-
import { buildInterceptors, Interceptor } from '../src/interceptors';
8+
import { buildInterceptors } from '../src/interceptors';
99

1010
const configPath = tmp.dirSync({ unsafeCleanup: true }).name;
1111

@@ -15,8 +15,8 @@ const newCertPair = generateCACertificate({ commonName: 'HTTP Toolkit CA - DO NO
1515
fs.writeFileSync(keyPath, newCertPair.key);
1616
fs.writeFileSync(certPath, newCertPair.cert);
1717

18-
const server = getLocal({ https: { certPath, keyPath } });
19-
const interceptors = buildInterceptors({ configPath });
18+
const server = getLocal({ https: { certPath, keyPath }, debug: true });
19+
const interceptors = buildInterceptors({ configPath, https: { certPath, keyPath } });
2020

2121
_.forEach(interceptors, (interceptor, name) =>
2222
describe(`${name} interceptor`, function () {
@@ -42,20 +42,26 @@ _.forEach(interceptors, (interceptor, name) =>
4242
expect(interceptor.isActive(server.port)).to.equal(false);
4343
});
4444

45-
it('successfully makes requests', async () => {
45+
it('successfully makes requests', async function () {
46+
// Firefox needs a manual acceptance of the cert to successfully make requests
47+
if (name === 'fresh-firefox') {
48+
await server.stop();
49+
this.skip();
50+
}
51+
4652
await server.anyRequest().thenPassThrough();
4753

4854
const exampleRequestReceived = new Promise<CompletedRequest>((resolve) =>
4955
server.on('request', (req) => {
50-
if (req.url.startsWith('https://example.com')) {
56+
if (req.url.startsWith('https://amiusing.httptoolkit.tech')) {
5157
resolve(req);
5258
}
5359
})
5460
);
5561

5662
await interceptor.activate(server.port);
5763

58-
// Only resolves if example.com request is sent successfully
64+
// Only resolves if amiusing request is sent successfully
5965
await exampleRequestReceived;
6066
});
6167
})

0 commit comments

Comments
 (0)