socket-function 0.105.0 → 0.107.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.105.0",
3
+ "version": "0.107.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -171,7 +171,7 @@ export function requireMain() {
171
171
  }} */
172
172
  let serializedModules: { [id: string]: SerializedModule | undefined } = Object.create(null);
173
173
 
174
- type ModuleType = {
174
+ type ModuleType = NodeJS.Module & {
175
175
  id: string;
176
176
  filename: string;
177
177
  exports: unknown;
@@ -181,10 +181,12 @@ export function requireMain() {
181
181
  loaded: boolean;
182
182
  isPreloading: boolean;
183
183
  evalStartTime: number;
184
- evalEndTime: number;
184
+ evalEndTime: number | undefined;
185
+ evaluateStarted: boolean;
185
186
  source: string;
186
187
  allowclient: boolean;
187
188
  size: number;
189
+ import: (request: string, asyncIsFine?: boolean) => unknown;
188
190
  };
189
191
 
190
192
  let moduleCache: { [id: string]: ModuleType } = Object.create(null);
@@ -660,9 +662,9 @@ export function requireMain() {
660
662
 
661
663
  let currentModuleEvaluationStack: string[] = [];
662
664
  // See https://nodejs.org/api/modules.html
663
- function getModule(resolvedId: string, preload?: "preload") {
665
+ function getModule(resolvedId: string, preload?: "preload"): ModuleType {
664
666
  if (resolvedId === "") {
665
- return {};
667
+ return {} as ModuleType;
666
668
  }
667
669
  if (resolvedId in moduleCache) {
668
670
  let module = moduleCache[resolvedId];
@@ -685,16 +687,17 @@ export function requireMain() {
685
687
  console.warn(`Failed to find module ${resolvedId}. The server should have given an error about this.`, serializedModules);
686
688
  }
687
689
 
688
- let module = Object.create(null);
690
+ let module = Object.create(null) as ModuleType;
689
691
  moduleCache[resolvedId] = module;
690
692
  module.id = resolvedId;
691
- module.filename = serializedModule?.filename;
693
+ module.filename = serializedModule?.filename || "";
692
694
  module.exports = {};
693
- module.exports.default = module.exports;
695
+ // Default default of exports to the exports itself
696
+ (module.exports as any).default = module.exports;
694
697
  module.children = [];
695
698
  for (let key in serializedModule?.flags || {}) {
696
699
  if (key === "loaded") continue;
697
- module[key] = true;
700
+ (module as any)[key] = true;
698
701
  }
699
702
 
700
703
  module.load = load;
@@ -717,10 +720,11 @@ export function requireMain() {
717
720
  const serializedModule = serializedModules[resolvedId];
718
721
  if (!serializedModule) return;
719
722
  if (!module.loaded) {
723
+ module.evaluateStarted = false;
720
724
  if (alreadyHave) {
721
725
  delete alreadyHave.seqNums[serializedModule.seqNum];
722
726
  }
723
- // NOTE: There is almost never recovery from module downloading errors, so just don't catch them
727
+ // NOTE: There is almost never a way to recover from module downloading errors, so just don't catch them
724
728
  return Promise.resolve()
725
729
  .then(() => rootRequire(resolvedId, true))
726
730
  .then(async () => {
@@ -729,8 +733,11 @@ export function requireMain() {
729
733
  });
730
734
  }
731
735
 
736
+ // Skip double loads
737
+ if (module.evaluateStarted) return;
738
+
732
739
  module.requires = serializedModule.requests;
733
- module.require = createRequire(module, serializedModule);
740
+ module.require = createRequire(module, serializedModule) as any;
734
741
  // TODO: Once typescript supports dynamic import, map import() to importDynamic, so it
735
742
  // uses our import function, instead of the built in one.
736
743
  // (As apparently we can't just override import on a per module basis, because
@@ -763,6 +770,7 @@ export function requireMain() {
763
770
  let time = Date.now();
764
771
  currentModuleEvaluationStack.push(module.filename);
765
772
  try {
773
+ module.evaluateStarted = true;
766
774
  module.isPreloading = true;
767
775
  module.evalStartTime = nextTime();
768
776
  module.evalEndTime = undefined;
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
+ }