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