Skip to content

Commit 52ecbf7

Browse files
committed
Create a PoC electron interceptor
1 parent c8a4a82 commit 52ecbf7

File tree

7 files changed

+321
-19
lines changed

7 files changed

+321
-19
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
declare module 'chrome-remote-interface' {
2+
function CDP(options: {
3+
port?: number
4+
}): Promise<CDP.CdpClient>;
5+
6+
namespace CDP {
7+
interface Stack {
8+
callFrames: Array<CallFrame>;
9+
}
10+
11+
interface CallFrame {
12+
callFrameId: string;
13+
}
14+
15+
export interface CdpClient {
16+
Runtime: {
17+
runIfWaitingForDebugger(): void;
18+
enable(): Promise<void>;
19+
evaluate(options: { expression: string }): Promise<{
20+
result?: unknown,
21+
exceptionDetails?: unknown
22+
}>
23+
};
24+
25+
Debugger: {
26+
enable(): Promise<void>;
27+
paused(callback: (stack: Stack) => void): void;
28+
resume(): void;
29+
evaluateOnCallFrame(options: {
30+
callFrameId: string,
31+
expression: string
32+
}): Promise<{
33+
result?: unknown,
34+
exceptionDetails?: unknown
35+
}>
36+
};
37+
38+
on(event: 'disconnect', callback: () => void): void;
39+
once(event: 'disconnect', callback: () => void): void;
40+
41+
close(): Promise<void>;
42+
}
43+
}
44+
45+
export = CDP;
46+
}

overrides/js/prepend-electron.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Injected into Electron via the debug protocol before any user code is run.
3+
*/
4+
5+
// Wrap all normal node HTTP APIs
6+
require('./prepend-node');
7+
8+
module.exports = function reconfigureElectron(params) {
9+
let electronWrapped = false;
10+
11+
// Reconfigure electron slightly too
12+
const wrapModule = require('./wrap-require');
13+
wrapModule('electron', function wrapElectron (loadedModule) {
14+
if (
15+
electronWrapped ||
16+
!loadedModule.app ||
17+
!loadedModule.app.commandLine
18+
) return;
19+
20+
electronWrapped = true;
21+
22+
console.log('wrapping');
23+
const app = loadedModule.app;
24+
25+
app.commandLine.appendSwitch('proxy-server', process.env.HTTP_PROXY);
26+
app.commandLine.appendSwitch('proxy-bypass-list', '<-loopback>');
27+
28+
app.commandLine.appendSwitch(
29+
'ignore-certificate-errors-spki-list', params.spkiFingerprint
30+
);
31+
32+
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
33+
if (
34+
certificate.issuerCert &&
35+
certificate.issuerCert.data === params.newlineEncodedCertData
36+
) {
37+
event.preventDefault();
38+
callback(true);
39+
} else {
40+
callback(false);
41+
}
42+
});
43+
}, true);
44+
45+
wrapModule('crypto', function wrapCrypto () {
46+
const NativeSecureContext = process.binding('crypto').SecureContext;
47+
const addRootCerts = NativeSecureContext.prototype.addRootCerts;
48+
NativeSecureContext.prototype.addRootCerts = function() {
49+
const ret = addRootCerts.apply(this,arguments);
50+
this.addCACert(params.newlineEncodedCertData);
51+
return ret;
52+
};
53+
}, true);
54+
};

package-lock.json

Lines changed: 75 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@sentry/node": "^5.6.2",
3737
"@types/command-exists": "^1.2.0",
3838
"async-mutex": "^0.1.3",
39+
"chrome-remote-interface": "^0.28.0",
3940
"command-exists": "^1.2.8",
4041
"env-paths": "^1.0.0",
4142
"global-agent": "^2.0.0",
@@ -46,6 +47,7 @@
4647
"mockttp": "^0.19.1",
4748
"node-forge": "^0.9.0",
4849
"node-gsettings-wrapper": "^0.5.0",
50+
"portfinder": "^1.0.25",
4951
"registry-js": "^1.4.0",
5052
"rimraf": "^2.6.2",
5153
"tslib": "^1.9.3",

src/interceptors/electron.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as _ from 'lodash';
2+
import { spawn } from 'child_process';
3+
import * as util from 'util';
4+
import * as fs from 'fs';
5+
6+
import { getPortPromise as getPort } from 'portfinder';
7+
import { generateSPKIFingerprint } from 'mockttp';
8+
import ChromeRemoteInterface = require('chrome-remote-interface');
9+
10+
import { Interceptor } from '.';
11+
12+
import { HtkConfig } from '../config';
13+
import { delay } from '../util';
14+
import { getTerminalEnvVars } from './terminal/terminal-env-overrides';
15+
16+
const readFile = util.promisify(fs.readFile);
17+
18+
export class ElectronInterceptor implements Interceptor {
19+
readonly id = 'electron';
20+
readonly version = '1.0.0';
21+
22+
private debugClients: {
23+
[port: string]: Array<ChromeRemoteInterface.CdpClient>
24+
} = {};
25+
26+
constructor(private config: HtkConfig) { }
27+
28+
private certData = readFile(this.config.https.certPath, 'utf8')
29+
30+
async isActivable(): Promise<boolean> {
31+
return true;
32+
}
33+
34+
isActive(proxyPort: number | string) {
35+
return !!this.debugClients[proxyPort] &&
36+
!!this.debugClients[proxyPort].length;
37+
}
38+
39+
async activate(proxyPort: number, options: {
40+
pathToApplication: string
41+
}): Promise<void | {}> {
42+
const debugPort = await getPort({ port: proxyPort });
43+
44+
spawn(options.pathToApplication, [`--inspect-brk=${debugPort}`], {
45+
stdio: 'inherit',
46+
env: Object.assign({},
47+
process.env,
48+
getTerminalEnvVars(proxyPort, this.config.https, process.env)
49+
)
50+
});
51+
52+
let debugClient: ChromeRemoteInterface.CdpClient | undefined;
53+
let retries = 10;
54+
while (!debugClient && retries >= 0) {
55+
try {
56+
debugClient = await ChromeRemoteInterface({ port: debugPort });
57+
} catch (error) {
58+
if (error.code !== 'ECONNREFUSED' || retries === 0) {
59+
throw error;
60+
}
61+
62+
retries = retries - 1;
63+
await delay(500);
64+
}
65+
}
66+
if (!debugClient) throw new Error('Could not initialize CDP client');
67+
68+
this.debugClients[proxyPort] = this.debugClients[proxyPort] || [];
69+
this.debugClients[proxyPort].push(debugClient);
70+
71+
const callFramePromise = new Promise<string>((resolve) => {
72+
debugClient!.Debugger.paused((stack) => {
73+
resolve(stack.callFrames[0].callFrameId);
74+
});
75+
});
76+
77+
debugClient.Runtime.runIfWaitingForDebugger();
78+
await debugClient.Runtime.enable();
79+
await debugClient.Debugger.enable();
80+
81+
const callFrameId = await callFramePromise;
82+
83+
// Patch in our various module overrides:
84+
await debugClient.Debugger.evaluateOnCallFrame({
85+
expression: `require("${
86+
// Inside the Electron process, load our electron-intercepting JS
87+
require.resolve('../../overrides/js/prepend-electron.js')
88+
}")({
89+
newlineEncodedCertData: "${(await this.certData).replace(/\r\n|\r|\n/g, '\\n')}",
90+
spkiFingerprint: "${generateSPKIFingerprint(await this.certData)}"
91+
})`,
92+
callFrameId
93+
});
94+
95+
debugClient.Debugger.resume();
96+
debugClient.once('disconnect', () => {
97+
_.remove(this.debugClients[proxyPort], c => c === debugClient);
98+
});
99+
}
100+
101+
async deactivate(proxyPort: number | string): Promise<void> {
102+
if (!this.isActive(proxyPort)) return;
103+
104+
await Promise.all(
105+
this.debugClients[proxyPort].map(async (debugClient) => {
106+
// Politely signal self to shutdown cleanly
107+
await debugClient.Runtime.evaluate({
108+
expression: 'process.kill(process.pid, "SIGTERM")'
109+
});
110+
111+
// Wait up to 1s for a clean shutdown & disconnect
112+
const cleanShutdown = await Promise.race([
113+
new Promise((resolve) =>
114+
debugClient.once('disconnect', () => resolve(true))
115+
),
116+
delay(1000).then(() => false)
117+
]);
118+
119+
if (!cleanShutdown) {
120+
// Didn't shutdown? Inject a hard exit.
121+
await debugClient.Runtime.evaluate({
122+
expression: 'process.exit(0)'
123+
}).catch(() => {}) // Ignore errors (there's an inherent race here)
124+
};
125+
})
126+
);
127+
}
128+
129+
async deactivateAll(): Promise<void> {
130+
await Promise.all<void>(
131+
Object.keys(this.debugClients).map(port => this.deactivate(port))
132+
);
133+
}
134+
135+
}

0 commit comments

Comments
 (0)