Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm-socket/lib/BaseIpcHandler.ts
1030 views
1
import { ChildProcess, spawn } from 'child_process';
2
import * as os from 'os';
3
import Log from '@secret-agent/commons/Logger';
4
import * as net from 'net';
5
import { unlink } from 'fs';
6
import Resolvable from '@secret-agent/commons/Resolvable';
7
import { IBoundLog } from '@secret-agent/interfaces/ILog';
8
import { CanceledPromiseError } from '@secret-agent/commons/interfaces/IPendingWaitEvent';
9
import { bindFunctions } from '@secret-agent/commons/utils';
10
import { createId, createIpcSocketPath } from '@secret-agent/commons/IpcUtils';
11
import * as Fs from 'fs';
12
import * as Path from 'path';
13
14
const ext = os.platform() === 'win32' ? '.exe' : '';
15
const libPath = Path.join(__dirname, '/../dist/', `connect${ext}`);
16
17
const distExists = Fs.existsSync(libPath);
18
19
const { log } = Log(module);
20
21
export default abstract class BaseIpcHandler {
22
public isClosing: boolean;
23
public get waitForConnected(): Promise<void> {
24
this.hasWaitListeners = true;
25
return this.waitForConnect.promise;
26
}
27
28
public get pid(): number | undefined {
29
return this.child?.pid;
30
}
31
32
protected abstract logger: IBoundLog;
33
protected options: IGoIpcOpts;
34
35
private hasWaitListeners = false;
36
private waitForConnect = new Resolvable<void>();
37
private child: ChildProcess;
38
private readonly ipcServer = new net.Server();
39
private ipcSocket: net.Socket;
40
private isExited = false;
41
42
private pendingMessage = '';
43
44
private readonly handlerName: string;
45
46
protected constructor(options: Partial<IGoIpcOpts>) {
47
this.options = this.getDefaultOptions(options);
48
49
if (!distExists) {
50
throw new Error(`Required files missing! The MitmSocket library was not found at ${libPath}`);
51
}
52
53
const mode = this.options.mode;
54
this.handlerName = `${mode[0].toUpperCase() + mode.slice(1)}IpcHandler`;
55
56
bindFunctions(this);
57
58
unlink(this.options.ipcSocketPath, () => {
59
this.ipcServer.listen(this.options.ipcSocketPath);
60
this.spawnChild();
61
});
62
this.ipcServer.once('connection', this.onIpcConnection.bind(this));
63
}
64
65
public close(): void {
66
const parentLogId = this.logger.info(`${this.handlerName}.Closing`);
67
if (this.isClosing || !this.child) return;
68
this.isClosing = true;
69
70
try {
71
// fix for node 13 throwing errors on closed sockets
72
this.child.stdin.on('error', () => {
73
// catch
74
});
75
// NOTE: windows writes to stdin
76
// MUST SEND SIGNALS BEFORE DISABLING PIPE!!
77
this.child.send('disconnect');
78
} catch (err) {
79
// don't log epipes
80
}
81
82
this.child.kill('SIGINT');
83
this.child.unref();
84
85
try {
86
this.onExit();
87
} catch (err) {
88
// don't log cleanup issue
89
}
90
91
if (!this.waitForConnect.isResolved && this.hasWaitListeners) {
92
this.waitForConnect.reject(new CanceledPromiseError('Canceling ipc connect'));
93
}
94
this.logger.stats(`${this.handlerName}.Closed`, {
95
parentLogId,
96
});
97
}
98
99
protected abstract onMessage(message: string): void;
100
protected abstract beforeExit(): void;
101
102
protected async sendIpcMessage(message: any): Promise<void> {
103
await this.waitForConnect.promise;
104
await new Promise<void>((resolve, reject) => {
105
this.ipcSocket.write(`${JSON.stringify(message)}\n`, err => {
106
if (err) reject(err);
107
else resolve();
108
});
109
});
110
}
111
112
private onIpcConnection(socket: net.Socket): void {
113
this.ipcSocket = socket;
114
this.ipcSocket.on('data', this.onIpcData.bind(this));
115
this.ipcSocket.on('error', err => {
116
// wait a sec to see if we're shutting down
117
setImmediate(error => {
118
if (!this.isClosing && !this.isExited)
119
this.logger.error(`${this.handlerName}.error`, { error });
120
}, err);
121
});
122
123
this.waitForConnect.resolve();
124
}
125
126
private onExit(): void {
127
if (this.isExited) return;
128
this.isExited = true;
129
this.beforeExit();
130
131
this.ipcServer.unref().close(() => {
132
unlink(this.options.ipcSocketPath, () => null);
133
});
134
if (this.ipcSocket) {
135
this.ipcSocket.unref().end();
136
}
137
}
138
139
private onError(error: Error): void {
140
if (this.isClosing) return;
141
this.logger.error(`${this.handlerName}.onError`, {
142
error,
143
});
144
}
145
146
private onIpcData(buffer: Buffer): void {
147
if (this.isClosing) return;
148
let end = buffer.indexOf('\n');
149
if (end === -1) {
150
this.pendingMessage += buffer.toString();
151
return;
152
}
153
const message = this.pendingMessage + buffer.toString(undefined, 0, end);
154
this.onMessage(message);
155
156
let start = end + 1;
157
end = buffer.indexOf('\n', start);
158
while (end !== -1) {
159
this.onMessage(buffer.toString(undefined, start, end));
160
start = end + 1;
161
end = buffer.indexOf('\n', start);
162
}
163
this.pendingMessage = buffer.toString(undefined, start);
164
}
165
166
private onChildProcessMessage(message: string): void {
167
if (this.isClosing) return;
168
this.logger.info(`${this.handlerName}.stdout: ${message}`);
169
}
170
171
private onChildProcessStderr(message: string): void {
172
if (this.isClosing) return;
173
this.logger.info(`${this.handlerName}.stderr: ${message}`);
174
}
175
176
private spawnChild(): void {
177
if (this.isClosing) return;
178
const options = this.options;
179
this.child = spawn(libPath, [JSON.stringify(options)], {
180
stdio: ['pipe', 'pipe', 'pipe'],
181
windowsHide: true,
182
cwd: options.storageDir,
183
});
184
const child = this.child;
185
child.on('exit', this.onExit);
186
child.on('error', this.onError);
187
child.stdout.setEncoding('utf8');
188
child.stderr.setEncoding('utf8');
189
child.stdout.on('data', this.onChildProcessMessage);
190
child.stderr.on('data', this.onChildProcessStderr);
191
}
192
193
private getDefaultOptions(options: Partial<IGoIpcOpts>): IGoIpcOpts {
194
options.debug ??= log.level === 'stats';
195
const mode = options.mode || 'proxy';
196
options.mode = mode;
197
198
if (options.ipcSocketPath === undefined) {
199
const id = createId();
200
options.ipcSocketPath = createIpcSocketPath(`sa-ipc-${mode}-${id}`);
201
}
202
return options as IGoIpcOpts;
203
}
204
}
205
206
export interface IGoIpcOpts {
207
mode?: 'certs' | 'proxy';
208
storageDir?: string;
209
ipcSocketPath?: string;
210
clientHelloId?: string;
211
tcpTtl?: number;
212
tcpWindowSize?: number;
213
rejectUnauthorized?: boolean;
214
debug?: boolean;
215
}
216
217