Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm-socket/index.ts
1028 views
1
// eslint-disable-next-line max-classes-per-file
2
import * as net from 'net';
3
import { unlink } from 'fs';
4
import Log from '@secret-agent/commons/Logger';
5
import { TypedEventEmitter } from '@secret-agent/commons/eventUtils';
6
import Resolvable from '@secret-agent/commons/Resolvable';
7
import { createIpcSocketPath } from '@secret-agent/commons/IpcUtils';
8
import IHttpSocketConnectOptions from '@secret-agent/interfaces/IHttpSocketConnectOptions';
9
import IHttpSocketWrapper from '@secret-agent/interfaces/IHttpSocketWrapper';
10
import EventSubscriber from '@secret-agent/commons/EventSubscriber';
11
import MitmSocketSession from './lib/MitmSocketSession';
12
13
const { log } = Log(module);
14
15
let idCounter = 0;
16
17
export default class MitmSocket
18
extends TypedEventEmitter<{
19
connect: void;
20
dial: void;
21
eof: void;
22
close: void;
23
}>
24
implements IHttpSocketWrapper {
25
public get isWebsocket(): boolean {
26
return this.connectOpts.isWebsocket === true;
27
}
28
29
public readonly socketPath: string;
30
public alpn = 'http/1.1';
31
public socket: net.Socket;
32
public dnsResolvedIp: string;
33
public remoteAddress: string;
34
public localAddress: string;
35
public serverName: string;
36
37
public id = (idCounter += 1);
38
39
public createTime: Date;
40
public dnsLookupTime: Date;
41
public ipcConnectionTime: Date;
42
public connectTime: Date;
43
public errorTime: Date;
44
public closeTime: Date;
45
46
public isConnected = false;
47
public isReused = false;
48
public isClosing = false;
49
public closedPromise = new Resolvable<Date>();
50
public connectError?: string;
51
public receivedEOF = false;
52
53
private server: net.Server;
54
private connectPromise: Resolvable<void>;
55
private socketReadyPromise = new Resolvable<void>();
56
private eventSubscriber = new EventSubscriber();
57
private readonly callStack: string;
58
59
constructor(readonly sessionId: string, readonly connectOpts: IHttpSocketConnectOptions) {
60
super();
61
this.callStack = new Error().stack.replace('Error:', '').trim();
62
this.serverName = connectOpts.servername;
63
this.logger = log.createChild(module, { sessionId });
64
this.connectOpts.isSsl ??= true;
65
66
this.socketPath = createIpcSocketPath(`sa-${sessionId}-${this.id}`);
67
68
// start listening
69
this.server = new net.Server().unref();
70
this.eventSubscriber.on(this.server, 'connection', this.onConnected.bind(this));
71
this.eventSubscriber.on(this.server, 'error', error => {
72
if (this.isClosing) return;
73
this.logger.warn('IpcSocketServerError', { error });
74
});
75
76
unlink(this.socketPath, () => {
77
this.server.listen(this.socketPath);
78
});
79
80
this.createTime = new Date();
81
}
82
83
public isReusable(): boolean {
84
if (!this.socket || this.isClosing || !this.isConnected) return false;
85
return this.socket.writable && !this.socket.destroyed;
86
}
87
88
public setProxyUrl(url: string): void {
89
this.connectOpts.proxyUrl = url;
90
}
91
92
public isHttp2(): boolean {
93
return this.alpn === 'h2';
94
}
95
96
public close(): void {
97
if (this.isClosing) return;
98
99
const parentLogId = this.logger.info(`MitmSocket.Closing`);
100
this.isClosing = true;
101
this.closeTime = new Date();
102
if (!this.connectPromise?.isResolved) {
103
this.connectPromise?.reject(
104
buildConnectError(
105
this.connectError ?? `Failed to connect to ${this.serverName}`,
106
this.callStack,
107
),
108
);
109
}
110
this.emit('close');
111
this.cleanupSocket();
112
this.closedPromise.resolve(this.closeTime);
113
this.logger.stats(`MitmSocket.Closed`, {
114
parentLogId,
115
});
116
this.eventSubscriber.close('error');
117
}
118
119
public onConnected(socket: net.Socket): void {
120
this.ipcConnectionTime = new Date();
121
this.socket = socket;
122
this.eventSubscriber.on(socket, 'error', error => {
123
this.logger.warn('MitmSocket.SocketError', {
124
sessionId: this.sessionId,
125
error,
126
socketPath: this.socketPath,
127
host: this.connectOpts?.host,
128
});
129
if ((error as any)?.code === 'ENOENT') {
130
this.errorTime = new Date();
131
this.close();
132
}
133
this.isConnected = false;
134
});
135
this.eventSubscriber.on(socket, 'end', this.onSocketClose.bind(this, 'end'));
136
this.eventSubscriber.on(socket, 'close', this.onSocketClose.bind(this, 'close'));
137
this.socketReadyPromise.resolve();
138
}
139
140
public async connect(session: MitmSocketSession, connectTimeoutMillis = 30e3): Promise<void> {
141
if (!this.server.listening) {
142
await new Promise(resolve => this.eventSubscriber.once(this.server, 'listening', resolve));
143
}
144
145
this.connectPromise = new Resolvable<void>(
146
connectTimeoutMillis,
147
`Timeout connecting to ${this.serverName ?? 'host'} at ${this.connectOpts.host}:${
148
this.connectOpts.port
149
}`,
150
);
151
152
await session.requestSocket(this);
153
154
await Promise.all([this.connectPromise.promise, this.socketReadyPromise.promise]);
155
}
156
157
public onMessage(message: any): void {
158
const status = message?.status;
159
if (status === 'connected') {
160
this.connectTime = new Date();
161
this.isConnected = true;
162
if (message.alpn) this.alpn = message.alpn;
163
this.remoteAddress = message.remoteAddress;
164
this.localAddress = message.localAddress;
165
this.emit('connect');
166
this.connectPromise.resolve();
167
} else if (status === 'error') {
168
this.onError(message.error);
169
} else if (status === 'eof') {
170
this.receivedEOF = true;
171
setImmediate(() => {
172
if (this.isClosing) return;
173
this.emit('eof');
174
});
175
} else if (status === 'closing') {
176
this.close();
177
}
178
}
179
180
public onExit(): void {
181
this.triggerConnectErrorIfNeeded(true);
182
this.close();
183
}
184
185
private triggerConnectErrorIfNeeded(isExiting = false): void {
186
if (this.connectPromise?.isResolved) return;
187
if (isExiting && !this.connectError) {
188
this.connectPromise.resolve();
189
return;
190
}
191
this.connectPromise?.reject(
192
buildConnectError(
193
this.connectError ?? `Socket process exited during connect`,
194
this.callStack,
195
),
196
);
197
}
198
199
private onError(message: string): void {
200
this.errorTime = new Date();
201
this.logger.info('MitmSocket.error', { message, host: this.connectOpts.host });
202
if (
203
message.includes('panic: runtime error:') ||
204
message.includes('tlsConn.Handshake error') ||
205
message.includes('connection refused') ||
206
message.includes('no such host') ||
207
message.includes('Dial (proxy/remote)') ||
208
message.includes('PROXY_ERR')
209
) {
210
this.connectError = message.trim();
211
if (this.connectError.includes('Error:')) {
212
this.connectError = this.connectError.split('Error:').pop().trim();
213
}
214
215
this.triggerConnectErrorIfNeeded(false);
216
}
217
this.close();
218
}
219
220
private cleanupSocket(): void {
221
if (this.socket) {
222
this.socket.unref();
223
const closeError = this.connectError
224
? buildConnectError(this.connectError, this.callStack)
225
: undefined;
226
this.socket.destroy(closeError);
227
}
228
this.server.removeAllListeners();
229
this.server.unref().close();
230
this.isConnected = false;
231
unlink(this.socketPath, () => null);
232
delete this.socket;
233
}
234
235
private onSocketClose(): void {
236
this.close();
237
}
238
}
239
240
class Socks5ProxyConnectError extends Error {}
241
class HttpProxyConnectError extends Error {}
242
class SocketConnectError extends Error {}
243
244
function buildConnectError(connectError = 'Error connecting to host', callStack: string): Error {
245
let error: Error;
246
if (connectError.includes('SOCKS5_PROXY_ERR')) {
247
error = new Socks5ProxyConnectError(connectError.replace('SOCKS5_PROXY_ERR', '').trim());
248
} else if (connectError.includes('HTTP_PROXY_ERR')) {
249
error = new HttpProxyConnectError(connectError.replace('HTTP_PROXY_ERR', '').trim());
250
} else {
251
error = new SocketConnectError(connectError.trim());
252
}
253
254
error.stack += `\n${'------DIAL'.padEnd(50, '-')}\n `;
255
error.stack += callStack;
256
return error;
257
}
258
259