Skip to content

Commit 38b87d2

Browse files
committed
Add cert to Chrome, and allow multi-port interceptor activation
1 parent 86e61bc commit 38b87d2

File tree

9 files changed

+169
-117
lines changed

9 files changed

+169
-117
lines changed

bin/run

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ if (dev) {
99
require('ts-node').register({project})
1010
}
1111

12-
require(`../${dev ? 'src' : 'lib'}`).run()
12+
require(`../${dev ? 'src' : 'lib'}/cli`).run()
1313
.catch(require('@oclif/errors/handle'))

src/cli.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Command, flags } from '@oclif/command'
2+
import { runHTK } from './index';
3+
4+
class HttpToolkitServer extends Command {
5+
static description = 'run the HTTP Toolkit server'
6+
7+
static flags = {
8+
version: flags.version({char: 'v'}),
9+
help: flags.help({char: 'h'}),
10+
11+
config: flags.string({char: 'c', description: 'path in which to store config files'}),
12+
}
13+
14+
async run() {
15+
const { flags } = this.parse(HttpToolkitServer);
16+
17+
await runHTK({ configPath: flags.config });
18+
}
19+
}
20+
21+
export = HttpToolkitServer;

src/config.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface HtkConfig {
2+
configPath: string;
3+
}

src/htk-server.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import * as _ from 'lodash';
22
import { GraphQLServer } from 'graphql-yoga'
33
import { GraphQLScalarType } from 'graphql';
44

5-
import { buildInterceptors } from './interceptors';
5+
import { HtkConfig } from './config';
6+
import { buildInterceptors, Interceptor } from './interceptors';
67

78
const typeDefs = `
89
type Query {
@@ -13,32 +14,53 @@ const typeDefs = `
1314
type Mutation {
1415
activateInterceptor(
1516
id: ID!,
17+
proxyPort: Int!,
1618
options: Json
1719
): Boolean!
20+
deactivateInterceptor(
21+
id: ID!,
22+
proxyPort: Int!
23+
): Boolean!
1824
}
1925
2026
type Interceptor {
2127
id: ID!
22-
isActive: Boolean!
2328
version: String!
29+
30+
isActivable: Boolean!
31+
isActive(proxyPort: Int!): Boolean!
2432
}
2533
2634
scalar Json
2735
scalar Error
2836
`
2937

30-
const buildResolvers = (configPath: string) => {
31-
let interceptors = buildInterceptors(configPath);
32-
38+
const buildResolvers = (interceptors: _.Dictionary<Interceptor>) => {
3339
return {
3440
Query: {
3541
version: async () => (await import('../package.json')).version,
3642
interceptors: () => _.values(interceptors),
3743
},
3844

3945
Mutation: {
40-
activateInterceptor: (__: void, args: _.Dictionary<any>) => {
46+
activateInterceptor: async (__: void, args: _.Dictionary<any>) => {
47+
const { id, proxyPort, options } = args;
48+
await interceptors[id].activate(proxyPort, options);
49+
return interceptors[id].isActive(proxyPort);
50+
},
51+
deactivateInterceptor: async (__: void, args: _.Dictionary<any>) => {
52+
const { id, proxyPort, options } = args;
53+
await interceptors[id].deactivate(proxyPort, options);
54+
return !interceptors[id].isActive(proxyPort);
55+
}
56+
},
4157

58+
Interceptor: {
59+
isActivable: (interceptor: Interceptor) => {
60+
return interceptor.isActivable();
61+
},
62+
isActive: (interceptor: Interceptor, args: _.Dictionary<any>) => {
63+
return interceptor.isActive(args.proxyPort);
4264
}
4365
},
4466

@@ -75,10 +97,12 @@ export class HttpToolkitServer {
7597

7698
private graphql: GraphQLServer;
7799

78-
constructor(options: { configPath: string }) {
100+
constructor(config: HtkConfig) {
101+
let interceptors = buildInterceptors(config);
102+
79103
this.graphql = new GraphQLServer({
80104
typeDefs,
81-
resolvers: buildResolvers(options.configPath)
105+
resolvers: buildResolvers(interceptors)
82106
});
83107
}
84108

src/index.ts

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,66 @@
1-
import { Command, flags } from '@oclif/command'
2-
import { runHTK } from './run-htk';
1+
import * as util from 'util';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
import * as envPaths from 'env-paths';
5+
import { getStandalone, generateCACertificate } from 'mockttp';
36

4-
class HttpToolkitServer extends Command {
5-
static description = 'run the HTTP Toolkit server'
7+
import { HttpToolkitServer } from './htk-server';
68

7-
static flags = {
8-
version: flags.version({char: 'v'}),
9-
help: flags.help({char: 'h'}),
9+
const canAccess = util.promisify(fs.access);
10+
const mkDir = util.promisify(fs.mkdir);
11+
const writeFile = util.promisify(fs.writeFile);
1012

11-
config: flags.string({char: 'c', description: 'path in which to store config files'}),
12-
}
13+
const ensureDirectoryExists = (path: string) =>
14+
canAccess(path).catch(() => mkDir(path, { recursive: true }));
1315

14-
async run() {
15-
const { flags } = this.parse(HttpToolkitServer);
16+
async function generateHTTPSConfig(configPath: string) {
17+
const keyPath = path.join(configPath, 'ca.key');
18+
const certPath = path.join(configPath, 'ca.pem');
1619

17-
await runHTK({ configPath: flags.config });
18-
}
20+
await Promise.all([
21+
canAccess(keyPath, fs.constants.R_OK),
22+
canAccess(certPath, fs.constants.R_OK)
23+
]).catch(() => {
24+
const newCertPair = generateCACertificate({
25+
commonName: 'HTTP Toolkit CA - DO NOT TRUST'
26+
});
27+
28+
return Promise.all([
29+
writeFile(keyPath, newCertPair.key),
30+
writeFile(certPath, newCertPair.cert)
31+
]);
32+
});
33+
34+
return {
35+
keyPath,
36+
certPath,
37+
keyLength: 2048 // Reasonably secure keys please
38+
};
1939
}
2040

21-
export = HttpToolkitServer;
41+
export async function runHTK(options: {
42+
configPath?: string
43+
}) {
44+
const configPath = options.configPath || envPaths('httptoolkit', { suffix: '' }).config;
45+
46+
await ensureDirectoryExists(configPath);
47+
48+
const httpsConfig = await generateHTTPSConfig(configPath);
49+
50+
// Start a standalone server
51+
const standalone = getStandalone({
52+
serverDefaults: {
53+
cors: false,
54+
https: httpsConfig
55+
}
56+
});
57+
standalone.start();
58+
59+
// Start a HTK server
60+
const htkServer = new HttpToolkitServer({
61+
configPath
62+
});
63+
await htkServer.start();
64+
65+
console.log('Server started');
66+
}

src/interceptors/fresh-chrome.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,62 @@
1+
import { promisify } from 'util';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
15
import * as _ from 'lodash';
6+
import { generateSPKIFingerprint } from 'mockttp';
7+
8+
import { HtkConfig } from '../config';
29

310
import { getAvailableBrowsers, launchBrowser, BrowserInstance } from '../browsers';
411

5-
let browser: BrowserInstance | undefined;
12+
const readFile = promisify(fs.readFile);
13+
14+
let browsers: _.Dictionary<BrowserInstance> = {};
615

716
export class FreshChrome {
817
id = 'fresh-chrome';
918
version = '1.0.0';
1019

11-
constructor(private configPath: string) { }
20+
constructor(private config: HtkConfig) { }
1221

13-
get isActive() {
14-
return browser != null && !!browser.pid;
22+
isActive(proxyPort: number) {
23+
return browsers[proxyPort] != null && !!browsers[proxyPort].pid;
1524
}
1625

17-
async checkIfAvailable() {
18-
const browsers = await getAvailableBrowsers(this.configPath);
26+
async isActivable() {
27+
const browsers = await getAvailableBrowsers(this.config.configPath);
1928

2029
return _(browsers)
2130
.map(b => b.name)
2231
.includes('chrome')
2332

2433
}
2534

26-
async activate() {
27-
if (this.isActive) return;
35+
async activate(proxyPort: number) {
36+
if (this.isActive(proxyPort)) return;
2837

29-
browser = await launchBrowser('https://example.com', {
30-
browser: 'chrome'
31-
}, this.configPath);
32-
}
38+
const certificatePem = await readFile(path.join(this.config.configPath, 'ca.pem'), 'utf8');
39+
const spkiFingerprint = generateSPKIFingerprint(certificatePem);
3340

34-
async deactivate() {
35-
if (this.isActive) {
36-
browser!.process.kill();
41+
const browser = await launchBrowser('https://example.com', {
42+
browser: 'chrome',
43+
proxy: `https://localhost:${proxyPort}`,
44+
options: [
45+
`--ignore-certificate-errors-spki-list=${spkiFingerprint}`
46+
]
47+
}, this.config.configPath);
3748

38-
await new Promise((resolve) => browser!.process.on('exit', resolve));
49+
browsers[proxyPort] = browser;
50+
browser.process.once('exit', () => {
51+
delete browsers[proxyPort];
52+
});
53+
}
3954

40-
browser = undefined;
55+
async deactivate(proxyPort: number) {
56+
if (this.isActive(proxyPort)) {
57+
const browser = browsers[proxyPort];
58+
browser!.process.kill();
59+
await new Promise((resolve) => browser!.process.once('exit', resolve));
4160
}
4261
}
4362
};

src/interceptors/index.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import * as _ from 'lodash';
22

3+
import { HtkConfig } from '../config';
4+
35
import { FreshChrome } from './fresh-chrome';
46

57
export interface Interceptor {
68
id: string;
79
version: string;
8-
isActive: boolean;
910

10-
checkIfAvailable(): Promise<boolean>;
11-
activate(options?: any): Promise<void>;
12-
deactivate(): Promise<void>;
11+
isActivable(): Promise<boolean>;
12+
isActive(proxyPort: number): boolean;
13+
14+
activate(proxyPort: number, options?: any): Promise<void>;
15+
deactivate(proxyPort: number, options?: any): Promise<void>;
1316
}
1417

15-
export function buildInterceptors(configPath: string): _.Dictionary<Interceptor> {
18+
export function buildInterceptors(config: HtkConfig): _.Dictionary<Interceptor> {
1619
return _.keyBy([
17-
new FreshChrome(configPath)
20+
new FreshChrome(config)
1821
], (interceptor) => interceptor.id);
1922
}

src/run-htk.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

0 commit comments

Comments
 (0)