socket-function 0.106.0 → 0.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/callManager.ts +1 -1
- package/src/misc.ts +4 -2
- package/src/webSocketServer.ts +362 -356
package/package.json
CHANGED
package/src/callManager.ts
CHANGED
|
@@ -44,7 +44,7 @@ export async function performLocalCall(
|
|
|
44
44
|
let controller = classDef.controller;
|
|
45
45
|
let functionShape = classDef.shape[call.functionName];
|
|
46
46
|
if (!functionShape) {
|
|
47
|
-
throw new Error(`Function ${call.functionName} not exposed`);
|
|
47
|
+
throw new Error(`Function ${call.functionName} not exposed in ${call.classGuid}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (!controller[call.functionName]) {
|
package/src/misc.ts
CHANGED
|
@@ -446,9 +446,11 @@ export function timeoutToUndefinedSilent<T>(time: number, p: Promise<T>) {
|
|
|
446
446
|
});
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
export function errorToWarning<T>(promise: Promise<T>):
|
|
449
|
+
export function errorToWarning<T>(promise: Promise<T>): void {
|
|
450
|
+
// Return the promise, so they can wait if they want, but have the return type be void,
|
|
451
|
+
// so that they don't have to await it.
|
|
450
452
|
return promise.catch(e => {
|
|
451
453
|
console.warn(e.stack);
|
|
452
454
|
return undefined as any;
|
|
453
|
-
});
|
|
455
|
+
}) as any;
|
|
454
456
|
}
|
package/src/webSocketServer.ts
CHANGED
|
@@ -1,356 +1,362 @@
|
|
|
1
|
-
import https from "https";
|
|
2
|
-
import http from "http";
|
|
3
|
-
import net from "net";
|
|
4
|
-
import tls from "tls";
|
|
5
|
-
import * as ws from "ws";
|
|
6
|
-
import { getNodeIdsFromRequest, httpCallHandler } from "./callHTTPHandler";
|
|
7
|
-
import { SocketFunction } from "../SocketFunction";
|
|
8
|
-
import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
9
|
-
import { createCallFactory } from "./CallFactory";
|
|
10
|
-
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
11
|
-
import debugbreak from "debugbreak";
|
|
12
|
-
import { getNodeId } from "./nodeCache";
|
|
13
|
-
import crypto from "crypto";
|
|
14
|
-
import { Watchable, getRootDomain, timeInHour, timeInMinute } from "./misc";
|
|
15
|
-
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "./batching";
|
|
16
|
-
import { magenta, red } from "./formatting/logColors";
|
|
17
|
-
import { yellow } from "./formatting/logColors";
|
|
18
|
-
import { green } from "./formatting/logColors";
|
|
19
|
-
import { formatTime } from "./formatting/format";
|
|
20
|
-
import { getExternalIP, testTCPIsListening } from "./networking";
|
|
21
|
-
import { forwardPort } from "./forwardPort";
|
|
22
|
-
|
|
23
|
-
export type SocketServerConfig = (
|
|
24
|
-
https.ServerOptions & {
|
|
25
|
-
key: string | Buffer;
|
|
26
|
-
cert: string | Buffer;
|
|
27
|
-
|
|
28
|
-
port: number;
|
|
29
|
-
/** You can also set `port: 0` if you don't care what port you want at all. */
|
|
30
|
-
useAvailablePortIfPortInUse?: boolean;
|
|
31
|
-
|
|
32
|
-
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
33
|
-
// causes the server to only accept local connections.
|
|
34
|
-
public?: boolean;
|
|
35
|
-
/** Tries forwarding ports (using UPnP), if we detect they aren't externally reachable.
|
|
36
|
-
* - This causes an extra request and delay during startup, so should only be used
|
|
37
|
-
* during development.
|
|
38
|
-
* - Ignored if public is false (in which case we mount on 127.0.0.1, so port forwarding
|
|
39
|
-
* wouldn't matter anyways).
|
|
40
|
-
*/
|
|
41
|
-
autoForwardPort?: boolean;
|
|
42
|
-
ip?: string;
|
|
43
|
-
|
|
44
|
-
// NOTE: Any same origin accesses are allowed (header.origin === header.host)
|
|
45
|
-
// For example, to allow "letx.ca" to access the server (when the hosted domain
|
|
46
|
-
// may be, "querysub.com", for example), use ["letx.ca"]
|
|
47
|
-
allowHostnames?: string[];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
httpsServerLast.
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
httpsServer.on("
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
// NOTE:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
realServer.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1
|
+
import https from "https";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import net from "net";
|
|
4
|
+
import tls from "tls";
|
|
5
|
+
import * as ws from "ws";
|
|
6
|
+
import { getNodeIdsFromRequest, httpCallHandler } from "./callHTTPHandler";
|
|
7
|
+
import { SocketFunction } from "../SocketFunction";
|
|
8
|
+
import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
9
|
+
import { createCallFactory } from "./CallFactory";
|
|
10
|
+
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
11
|
+
import debugbreak from "debugbreak";
|
|
12
|
+
import { getNodeId } from "./nodeCache";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import { Watchable, getRootDomain, timeInHour, timeInMinute } from "./misc";
|
|
15
|
+
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "./batching";
|
|
16
|
+
import { magenta, red } from "./formatting/logColors";
|
|
17
|
+
import { yellow } from "./formatting/logColors";
|
|
18
|
+
import { green } from "./formatting/logColors";
|
|
19
|
+
import { formatTime } from "./formatting/format";
|
|
20
|
+
import { getExternalIP, testTCPIsListening } from "./networking";
|
|
21
|
+
import { forwardPort } from "./forwardPort";
|
|
22
|
+
|
|
23
|
+
export type SocketServerConfig = (
|
|
24
|
+
https.ServerOptions & {
|
|
25
|
+
key: string | Buffer;
|
|
26
|
+
cert: string | Buffer;
|
|
27
|
+
|
|
28
|
+
port: number;
|
|
29
|
+
/** You can also set `port: 0` if you don't care what port you want at all. */
|
|
30
|
+
useAvailablePortIfPortInUse?: boolean;
|
|
31
|
+
|
|
32
|
+
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
33
|
+
// causes the server to only accept local connections.
|
|
34
|
+
public?: boolean;
|
|
35
|
+
/** Tries forwarding ports (using UPnP), if we detect they aren't externally reachable.
|
|
36
|
+
* - This causes an extra request and delay during startup, so should only be used
|
|
37
|
+
* during development.
|
|
38
|
+
* - Ignored if public is false (in which case we mount on 127.0.0.1, so port forwarding
|
|
39
|
+
* wouldn't matter anyways).
|
|
40
|
+
*/
|
|
41
|
+
autoForwardPort?: boolean;
|
|
42
|
+
ip?: string;
|
|
43
|
+
|
|
44
|
+
// NOTE: Any same origin accesses are allowed (header.origin === header.host)
|
|
45
|
+
// For example, to allow "letx.ca" to access the server (when the hosted domain
|
|
46
|
+
// may be, "querysub.com", for example), use ["letx.ca"]
|
|
47
|
+
allowHostnames?: string[];
|
|
48
|
+
// If a hostname is in allowHostnames or allowHostnameFnc returns true, it is allowed
|
|
49
|
+
allowHostnameFnc?: (hostname: string) => boolean;
|
|
50
|
+
|
|
51
|
+
/** If the SNI matches this domain, we use a different key/cert.
|
|
52
|
+
* We remove subdomains until we find a match
|
|
53
|
+
*/
|
|
54
|
+
SNICerts?: {
|
|
55
|
+
[domain: string]: Watchable<https.ServerOptions>;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export async function startSocketServer(
|
|
61
|
+
config: SocketServerConfig
|
|
62
|
+
): Promise<string> {
|
|
63
|
+
|
|
64
|
+
const webSocketServer = new ws.Server({
|
|
65
|
+
noServer: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function setupHTTPSServer(watchOptions: Watchable<https.ServerOptions>) {
|
|
69
|
+
let httpsServerLast: https.Server | undefined;
|
|
70
|
+
let onHttpServer: (server: https.Server) => void;
|
|
71
|
+
let httpServerPromise = new Promise<https.Server>(r => onHttpServer = r);
|
|
72
|
+
let lastOptions!: https.ServerOptions;
|
|
73
|
+
await watchOptions(value => {
|
|
74
|
+
// NOTE: If this is called a lot... STOP CALLING IT A LOT! Calling setSecureContext
|
|
75
|
+
// so frequently likely leaks memory!
|
|
76
|
+
console.log(`Updating websocket server options`);
|
|
77
|
+
lastOptions = {
|
|
78
|
+
...value,
|
|
79
|
+
ca: getTrustedCertificates(),
|
|
80
|
+
// Attempt to disable sessions, because they make SNI significantly harder to parse.
|
|
81
|
+
secureOptions: require("node:constants").SSL_OP_NO_TICKET,
|
|
82
|
+
};
|
|
83
|
+
if (!httpsServerLast) {
|
|
84
|
+
httpsServerLast = https.createServer(lastOptions);
|
|
85
|
+
// NOTE: This MIGHT be different than the keep alive option? Probably not, but also...
|
|
86
|
+
// something weird is happening with connections...
|
|
87
|
+
httpsServerLast.keepAliveTimeout = 0;
|
|
88
|
+
} else {
|
|
89
|
+
httpsServerLast.setSecureContext(lastOptions);
|
|
90
|
+
}
|
|
91
|
+
onHttpServer(httpsServerLast);
|
|
92
|
+
});
|
|
93
|
+
let httpsServer = await httpServerPromise;
|
|
94
|
+
|
|
95
|
+
let allowedHostnames = new Set<string>();
|
|
96
|
+
for (let hostname of config.allowHostnames || []) {
|
|
97
|
+
allowedHostnames.add(hostname);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
watchTrustedCertificates(() => {
|
|
101
|
+
// NOTE: If this is called a lot... STOP CALLING IT A LOT! Calling setSecureContext
|
|
102
|
+
// so frequently likely leaks memory!
|
|
103
|
+
console.log(`Updating websocket server trusted certificates`);
|
|
104
|
+
lastOptions.ca = getTrustedCertificates();
|
|
105
|
+
httpsServer.setSecureContext(lastOptions);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
httpsServer.on("connection", socket => {
|
|
109
|
+
let debug = (socket as any).remoteAddress + ":" + (socket as any).remotePort;
|
|
110
|
+
if (!SocketFunction.silent) {
|
|
111
|
+
console.log(`HTTP server connection established ${debug}`);
|
|
112
|
+
}
|
|
113
|
+
socket.on("error", e => {
|
|
114
|
+
if (!SocketFunction.silent) {
|
|
115
|
+
console.log(`HTTP server socket error for ${debug}, ${e.message}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
socket.on("close", () => {
|
|
119
|
+
if (!SocketFunction.silent) {
|
|
120
|
+
console.log(`HTTP server socket closed for ${debug}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
httpsServer.on("error", e => {
|
|
125
|
+
console.error(`Connection attempt error ${e.message}`);
|
|
126
|
+
});
|
|
127
|
+
httpsServer.on("tlsClientError", e => {
|
|
128
|
+
// NOTE: This happens a lot when we have tabs open that connected to an old
|
|
129
|
+
// server (with old certs, that the browser will reject?)
|
|
130
|
+
if (!SocketFunction.silent) {
|
|
131
|
+
console.error(`TLS client error ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
httpsServer.on("request", httpCallHandler);
|
|
136
|
+
|
|
137
|
+
httpsServer.on("upgrade", (request, socket, upgradeHead) => {
|
|
138
|
+
let originHeader = request.headers["origin"];
|
|
139
|
+
if (originHeader) {
|
|
140
|
+
try {
|
|
141
|
+
let host = getRootDomain(new URL("ws://" + request.headers["host"]).hostname);
|
|
142
|
+
let origin = getRootDomain(new URL(originHeader).hostname);
|
|
143
|
+
let allowed = host === origin || allowedHostnames.has(origin);
|
|
144
|
+
if (!allowed && config.allowHostnameFnc) {
|
|
145
|
+
allowed = config.allowHostnameFnc(origin);
|
|
146
|
+
}
|
|
147
|
+
if (!allowed) {
|
|
148
|
+
throw new Error(`Invalid cross domain request, ${JSON.stringify(host)} !== ${JSON.stringify(origin)} (also not in config.allowedHostnames ${JSON.stringify(config.allowHostnames)})`);
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
// Destroy the socket, so we don't lock up the client
|
|
152
|
+
socket.destroy();
|
|
153
|
+
// NOTE: Just log, because invalid requests are guaranteed to happen, and
|
|
154
|
+
// there's no point wasting time looking at them.
|
|
155
|
+
console.log(e);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
webSocketServer.handleUpgrade(request, socket, upgradeHead, (ws) => {
|
|
160
|
+
const { nodeId, localNodeId } = getNodeIdsFromRequest(request);
|
|
161
|
+
createCallFactory(ws, nodeId, localNodeId).catch(e => {
|
|
162
|
+
console.error(`Error in creating call factory, ${e.stack}`);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
return httpsServer;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// TODO: Only allow unauthorized for ip certificates, and then for domains use the domain as the nodeId,
|
|
170
|
+
// so it is easy to read, and consistent.
|
|
171
|
+
let options: https.ServerOptions = {
|
|
172
|
+
...config,
|
|
173
|
+
// Keep alive causes problems with our HTTP requests. AND... almost all of our data uses
|
|
174
|
+
// our websockets, so... we really don't need to keep alive our HTTP requests
|
|
175
|
+
// (and our images go through cloudflare, so we don't even need keep alive for that)
|
|
176
|
+
keepAlive: false,
|
|
177
|
+
keepAliveInitialDelay: 0,
|
|
178
|
+
noDelay: true,
|
|
179
|
+
};
|
|
180
|
+
if (!config.cert) {
|
|
181
|
+
throw new Error("No cert specified");
|
|
182
|
+
}
|
|
183
|
+
if (!config.key) {
|
|
184
|
+
throw new Error("No key specified");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const mainHTTPSServer = await setupHTTPSServer(callback => callback(options));
|
|
188
|
+
let sniServers = new Map<string, https.Server>();
|
|
189
|
+
for (let [domain, obj] of Object.entries(config.SNICerts || {})) {
|
|
190
|
+
sniServers.set(domain, await setupHTTPSServer(obj));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let httpServer = http.createServer({ keepAlive: false, }, async function (req, res) {
|
|
194
|
+
let url = new URL("http://" + req.headers.host + req.url);
|
|
195
|
+
url.protocol = "https:";
|
|
196
|
+
//url.hostname = opts.hostname;
|
|
197
|
+
url.hostname = req.headers.host || "";
|
|
198
|
+
res.writeHead(301, { Location: url + "" });
|
|
199
|
+
res.end();
|
|
200
|
+
});
|
|
201
|
+
httpServer.keepAliveTimeout = 0;
|
|
202
|
+
httpServer.on("error", e => {
|
|
203
|
+
console.error(`HTTP error ${e.stack}`);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
let realServer = net.createServer(socket => {
|
|
207
|
+
const debug = socket.remoteAddress + ":" + socket.remotePort;
|
|
208
|
+
if (!SocketFunction.silent) {
|
|
209
|
+
console.log(`Received TCP connection from ${debug}`);
|
|
210
|
+
}
|
|
211
|
+
function handleTLSHello(buffer: Buffer, packetCount: number): void | "more" {
|
|
212
|
+
if (!SocketFunction.silent) {
|
|
213
|
+
console.log(`Received TCP header packet from ${debug}, have ${buffer.length} bytes so far, ${packetCount} packets`);
|
|
214
|
+
}
|
|
215
|
+
// All HTTPS requests start with 22, and no HTTP requests start with 22,
|
|
216
|
+
// so we just need to read the first byte.
|
|
217
|
+
let server: https.Server | http.Server;
|
|
218
|
+
if (buffer[0] !== 22) {
|
|
219
|
+
server = httpServer;
|
|
220
|
+
} else {
|
|
221
|
+
let data = parseTLSHello(buffer);
|
|
222
|
+
if (data.missingBytes > 0) {
|
|
223
|
+
return "more";
|
|
224
|
+
}
|
|
225
|
+
let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
|
|
226
|
+
if (!SocketFunction.silent) {
|
|
227
|
+
console.log(`Received TCP connection with SNI ${JSON.stringify(sni)}. Have handlers for: ${Array.from(sniServers.keys()).join(", ")}`);
|
|
228
|
+
}
|
|
229
|
+
if (!sni) {
|
|
230
|
+
console.warn(`No SNI found in TLS hello from ${debug}, using main server. Packets ${packetCount}`);
|
|
231
|
+
console.log(buffer.toString("base64"));
|
|
232
|
+
}
|
|
233
|
+
let originalSNI = sni;
|
|
234
|
+
if (sni) {
|
|
235
|
+
// Remove subdomains until we can find a domain
|
|
236
|
+
while (!sniServers.has(sni)) {
|
|
237
|
+
let parts = sni.split(".");
|
|
238
|
+
if (parts.length <= 2) break;
|
|
239
|
+
sni = parts.slice(1).join(".");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!sniServers.has(sni)) {
|
|
244
|
+
console.warn(`No SNI server found for ${originalSNI}, using main server. SNI candidates ${Array.from(sniServers.keys()).join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
server = sniServers.get(sni) || mainHTTPSServer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// NOTE: Messages aren't dequeued until the current handler finishes, so we don't need to pause the socket or anything.
|
|
250
|
+
server.emit("connection", socket);
|
|
251
|
+
socket.unshift(buffer);
|
|
252
|
+
}
|
|
253
|
+
let buffers: Buffer[] = [];
|
|
254
|
+
function getNextData() {
|
|
255
|
+
// NOTE: ONCE is used, so we only look at the first buffer, and then after that
|
|
256
|
+
// we pipe. This should be very efficient, as pipe has insane throughput
|
|
257
|
+
// (100s of MB/s, easily, even on a terrible machine).
|
|
258
|
+
socket.once("data", buffer => {
|
|
259
|
+
buffers.push(buffer);
|
|
260
|
+
let result = handleTLSHello(Buffer.concat(buffers), buffers.length);
|
|
261
|
+
if (result === "more") {
|
|
262
|
+
getNextData();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
getNextData();
|
|
267
|
+
socket.on("error", (e) => {
|
|
268
|
+
console.error(`TCP socket error for ${debug}, ${e.stack}`);
|
|
269
|
+
});
|
|
270
|
+
socket.on("close", () => {
|
|
271
|
+
if (!SocketFunction.silent) {
|
|
272
|
+
console.log(`TCP socket closed for ${debug}`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
let host = config.public ? "0.0.0.0" : "127.0.0.1";
|
|
279
|
+
if (config.ip) {
|
|
280
|
+
host = config.ip;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let port = config.port;
|
|
284
|
+
if (!SocketFunction.silent) {
|
|
285
|
+
console.log(yellow(`Trying to listening on ${host}:${port}`));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let listeningPromise = waitUntilListening();
|
|
289
|
+
listeningPromise.catch(e => { });
|
|
290
|
+
|
|
291
|
+
// Return true if we are listening, false if the address is in use, and throws on other errors
|
|
292
|
+
async function waitUntilListening() {
|
|
293
|
+
return await new Promise<boolean>((resolve, reject) => {
|
|
294
|
+
realServer.once("error", e => {
|
|
295
|
+
reject(e);
|
|
296
|
+
});
|
|
297
|
+
realServer.once("listening", () => {
|
|
298
|
+
resolve(false);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (config.useAvailablePortIfPortInUse && port) {
|
|
304
|
+
realServer.listen(port, host);
|
|
305
|
+
let isListening = await new Promise<boolean>((resolve, reject) => {
|
|
306
|
+
if (realServer.listening) {
|
|
307
|
+
resolve(true);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
realServer.once("error", e => {
|
|
311
|
+
if (e.message.includes("EADDRINUSE")) {
|
|
312
|
+
resolve(true);
|
|
313
|
+
} else {
|
|
314
|
+
reject(e);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
realServer.once("listening", () => {
|
|
318
|
+
resolve(false);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
if (!isListening) {
|
|
322
|
+
port = 0;
|
|
323
|
+
realServer.listen(port, host);
|
|
324
|
+
listeningPromise = waitUntilListening();
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
realServer.listen(port, host);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await listeningPromise;
|
|
331
|
+
port = (realServer.address() as net.AddressInfo).port;
|
|
332
|
+
|
|
333
|
+
if (config.autoForwardPort && config.public) {
|
|
334
|
+
// let externalIP = await getExternalIP();
|
|
335
|
+
// let isListening = await testTCPIsListening(externalIP, port);
|
|
336
|
+
// if (!isListening) {
|
|
337
|
+
// console.log(magenta(`Port ${port} is not externally reachable, trying to forward it`));
|
|
338
|
+
// await forwardPort({ externalPort: port, internalPort: port });
|
|
339
|
+
// }
|
|
340
|
+
// Even if they are listening, they might not stay listening. Forward every 8 hours
|
|
341
|
+
// (including at the start, in case the forward is about to expire).
|
|
342
|
+
async function forward() {
|
|
343
|
+
await forwardPort({ externalPort: port, internalPort: port });
|
|
344
|
+
console.log(magenta(`Forwarded port ${port} to our machine`));
|
|
345
|
+
}
|
|
346
|
+
// Every hour, in case our network configuration changes
|
|
347
|
+
runInfinitePollCallAtStart(timeInMinute * 30, forward).catch(e => console.error(red(`Error in port forwarding ${e.stack}`)));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
351
|
+
console.log(green(`Started Listening on ${nodeId} (${host}) after ${formatTime(process.uptime() * 1000)}`));
|
|
352
|
+
|
|
353
|
+
return nodeId;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function getCommonName(cert: Buffer | string) {
|
|
357
|
+
let subject = new crypto.X509Certificate(cert).subject;
|
|
358
|
+
let subjectKVPs = new Map(subject.split(",").map(x => x.trim().split("=")).map(x => [x[0], x.slice(1).join("=")]));
|
|
359
|
+
let commonName = subjectKVPs.get("CN");
|
|
360
|
+
if (!commonName) throw new Error(`No common name in subject: ${subject}`);
|
|
361
|
+
return commonName;
|
|
362
|
+
}
|