socket-function 0.17.0 → 0.19.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.17.0",
3
+ "version": "0.19.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -3,7 +3,7 @@ import debugbreak from "debugbreak";
3
3
  import fs from "fs";
4
4
  import { SocketFunction } from "../SocketFunction";
5
5
  import { getCurrentHTTPRequest, setHTTPResultHeaders } from "../src/callHTTPHandler";
6
- import { formatNumberSuffixed, isNodeTrue, sha256Hash, sha256HashPromise } from "../src/misc";
6
+ import { formatNumberSuffixed, isNode, isNodeTrue, sha256Hash, sha256HashPromise } from "../src/misc";
7
7
  import zlib from "zlib";
8
8
  import { cacheLimited } from "../src/caching";
9
9
  import { formatNumber } from "../src/formatting/format";
@@ -35,6 +35,15 @@ declare global {
35
35
  // Times are both unique (two modules evaluated at the same Date.now() will have different values).
36
36
  evalStartTime?: number;
37
37
  evalEndTime?: number;
38
+
39
+ /** (Presently only called by require.js)
40
+ * Called on require calls, to allow providers to create custom exports depending on the caller.
41
+ * - Mostly used to allow functions to know the calling module.
42
+ */
43
+ remapExports?: (exports: { [key: string]: unknown }, callerModule: NodeJS.Module) => { [key: string]: unknown };
44
+
45
+ /** Only set if clientside (and allowed clientside) */
46
+ source?: string;
38
47
  }
39
48
  }
40
49
  interface Window {
@@ -43,6 +52,16 @@ declare global {
43
52
  var suppressUnexpectedModuleWarning: number | undefined;
44
53
  }
45
54
 
55
+ /** Imports it, serverside, delayed. For dynamic imports, which we need to include once, but don't want to include
56
+ * immediately (due to cyclic issues), and isn't included initially.
57
+ */
58
+ export function lazyImport(getModule: () => Promise<unknown>) {
59
+ if (!isNode()) return;
60
+ // Import it, asynchronously, so it isn't preloaded, but it is available for clientside imports.
61
+ // NOTE: We track delayed imports (somewhere), and don't preload them by default in RequireController.
62
+ void Promise.resolve().then(() => getModule());
63
+ }
64
+
46
65
  export interface SerializedModule {
47
66
  originalId: string;
48
67
  filename: string;
@@ -240,7 +240,7 @@
240
240
  return requestsResolvedPaths.map(x => getModule(x));
241
241
  } finally {
242
242
  time = Date.now() - time;
243
- console.log(`%cimport(${requests.join(", ")}) finished evaluate ${time}ms (${moduleCount} modules) at ${Date.now() - startTime}ms`, "color: blue");
243
+ console.log(`%cimport(${requests.join(", ")}) finished evaluate ${time}ms (${moduleCount} modules) at ${Date.now() - startTime}ms`, "color: lightblue");
244
244
  }
245
245
  }
246
246
 
@@ -326,8 +326,6 @@
326
326
  if (property === unloadedModule) return true;
327
327
  if (property === "default") return exportsOverride;
328
328
 
329
- serializedModule;
330
-
331
329
  console.warn(`Accessed non-whitelisted module %c${childId}%c, specifically property %c${String(property)}%c.\n\tAdd %cmodule.allowclient = true%c to the file to allow access.\n\t(IF it is a 3rd party library, use the global "setFlag" helper (in the file you imported the module) to set properties on other modules (it can even recursively set properties)).\n\n\tFrom ${module.id}`,
332
330
  "color: red", "color: unset",
333
331
  "color: red", "color: unset",
@@ -343,12 +341,19 @@
343
341
  return exportsOverride;
344
342
  }
345
343
 
346
- let childModule = getModule(resolvedPath);
347
- module.children.push(childModule);
344
+ let providerModule = getModule(resolvedPath);
345
+ module.children.push(providerModule);
348
346
  if (exportsOverride !== undefined) {
349
- childModule.exports = exportsOverride;
347
+ providerModule.exports = exportsOverride;
348
+ }
349
+
350
+ let exports = providerModule.exports;
351
+ let remapExports = providerModule.remapExports;
352
+ if (remapExports && typeof remapExports === "function") {
353
+ exports = remapExports(exports, module);
350
354
  }
351
- return childModule.exports;
355
+
356
+ return exports;
352
357
  };
353
358
  }
354
359
 
@@ -445,6 +450,7 @@
445
450
  }
446
451
 
447
452
  module.size = source.length;
453
+ module.source = source;
448
454
 
449
455
  let moduleFnc = wrapSafe(module.id, source);
450
456
 
@@ -589,8 +589,6 @@ export async function createCallFactory(
589
589
  } else if (err.includes("The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.")) {
590
590
  console.error(`WebSocket data was dropped by the browser due to exceeding the Blob limit. Either you are about to run out of memory, or you hit the much lower Incognito Blob limit. This will likely break the application. To reset the memory you must close all tabs of this site. This is a bug/feature in chrome.`);
591
591
  } else {
592
- debugbreak(2);
593
- debugger;
594
592
  console.error(e.stack);
595
593
  }
596
594
  }
@@ -655,23 +653,14 @@ const compressObj = measureWrap(async function wireCallCompress(obj: unknown): P
655
653
  return result;
656
654
  });
657
655
  const decompressObj = measureWrap(async function wireCallDecompress(obj: Buffer): Promise<unknown> {
658
- try {
659
- let buffer = await unzipBase(obj);
660
- let lengthBuffer = buffer.slice(0, 8);
661
- let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
662
- let buffers: Buffer[] = [];
663
- let offset = 8;
664
- for (let length of lengths) {
665
- buffers.push(buffer.slice(offset, offset + length));
666
- offset += length;
667
- }
668
-
669
- return await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
670
- } catch (e) {
671
- // We were encountering issues with the checksum failing when unzipping. Presumably if the data
672
- // is bad deserialize will also fail. I can't repro it anymore though...
673
- debugbreak(2);
674
- debugger;
675
- throw e;
656
+ let buffer = await unzipBase(obj);
657
+ let lengthBuffer = buffer.slice(0, 8);
658
+ let lengths = new Float64Array(lengthBuffer.buffer, lengthBuffer.byteOffset, lengthBuffer.byteLength / 8);
659
+ let buffers: Buffer[] = [];
660
+ let offset = 8;
661
+ for (let length of lengths) {
662
+ buffers.push(buffer.slice(offset, offset + length));
663
+ offset += length;
676
664
  }
665
+ return await SocketFunction.WIRE_SERIALIZER.deserialize(buffers);
677
666
  });
@@ -298,10 +298,6 @@ export class JSONLACKS {
298
298
  if (type === "ref") {
299
299
  let id = obj.id as string;
300
300
  if (!JSONLACKS.IGNORE_MISSING_REFERENCES && !references.has(id)) {
301
- if (!config?.discardMissingReferences) {
302
- debugbreak(2);
303
- debugger;
304
- }
305
301
  throw new Error(`Reference to undefined id "${id}"`);
306
302
  }
307
303
  return references.get(id);
package/src/batching.ts CHANGED
@@ -226,7 +226,7 @@ export async function runInfinitePollCallAtStart(
226
226
  try {
227
227
  await fnc();
228
228
  } catch (e: any) {
229
- console.error(`Error in infinite poll ${fnc.name} (continuing poll loop)\n${e.stack}`);
229
+ console.error(`Error in infinite poll ${fnc.name || fnc.toString().slice(0, 100)} (continuing poll loop)\n${e.stack}`);
230
230
  }
231
231
  }
232
232
  })();
@@ -69,7 +69,7 @@ export function isDataImmutable(call: CallType) {
69
69
  export function registerClass(classGuid: string, controller: SocketExposedInterface, shape: SocketExposedShape, config?: {
70
70
  noFunctionMeasure?: boolean;
71
71
  }) {
72
- if (!isHotReloading?.() && classes[classGuid]) {
72
+ if (!globalThis.isHotReloading?.() && classes[classGuid]) {
73
73
  throw new Error(`Class ${classGuid} already registered`);
74
74
  }
75
75
 
@@ -9,7 +9,7 @@ function ansiRGB(r: number, g: number, b: number, text: string): string {
9
9
  }
10
10
 
11
11
  const lightness = 68;
12
- export const blue = (text: string) => `\x1b[34m${text}\x1b[0m`;
12
+ export const blue = ansiHSL.bind(null, 235, 100, lightness);
13
13
  export const red = ansiHSL.bind(null, 0, 100, lightness);
14
14
  export const green = (text: string) => `\x1b[32m${text}\x1b[0m`;
15
15
  export const yellow = (text: string) => `\x1b[33m${text}\x1b[0m`;
package/src/https.ts CHANGED
@@ -11,7 +11,7 @@ export function httpsRequest(
11
11
  method = "GET",
12
12
  sendSessionCookies = true,
13
13
  config?: {
14
- headers?: { [key: string]: string },
14
+ headers?: { [key: string]: string | undefined },
15
15
  }
16
16
  ): Promise<Buffer> {
17
17
  if (isNode()) {
@@ -66,8 +66,9 @@ export function httpsRequest(
66
66
  var request = new XMLHttpRequest();
67
67
  request.open(method, url, true);
68
68
  if (config?.headers) {
69
- for (let key in config.headers) {
70
- request.setRequestHeader(key, config.headers[key]);
69
+ for (let [key, value] of Object.entries(config.headers)) {
70
+ if (value === undefined) continue;
71
+ request.setRequestHeader(key, value);
71
72
  }
72
73
  }
73
74
  request.responseType = "arraybuffer";
package/src/misc.ts CHANGED
@@ -1,4 +1,3 @@
1
- import * as crypto from "crypto";
2
1
  import { canHaveChildren, MaybePromise } from "./types";
3
2
  import { formatNumber } from "./formatting/format";
4
3
 
@@ -18,16 +17,17 @@ export function convertErrorStackToError(error: string): Error {
18
17
  return errorObj;
19
18
  }
20
19
 
20
+
21
21
  export function sha256Hash(buffer: Buffer | string): string {
22
- return crypto.createHash("sha256").update(buffer).digest("hex");
22
+ return require("crypto").createHash("sha256").update(buffer).digest("hex");
23
23
  }
24
24
  export function sha256HashBuffer(buffer: Buffer | string): Buffer {
25
- return crypto.createHash("sha256").update(buffer).digest();
25
+ return require("crypto").createHash("sha256").update(buffer).digest();
26
26
  }
27
27
  /** Async, but works both clientside and serverside. */
28
28
  export async function sha256HashPromise(buffer: Buffer) {
29
29
  if (isNode()) {
30
- return crypto.createHash("sha256").update(buffer).digest("hex");
30
+ return require("crypto").createHash("sha256").update(buffer).digest("hex");
31
31
  } else {
32
32
  let buf = await window.crypto.subtle.digest("SHA-256", buffer);
33
33
  return Buffer.from(buf).toString("hex");
@@ -35,7 +35,7 @@ export async function sha256HashPromise(buffer: Buffer) {
35
35
  }
36
36
  export async function sha256BufferPromise(buffer: Buffer): Promise<Buffer> {
37
37
  if (isNode()) {
38
- return crypto.createHash("sha256").update(buffer).digest();
38
+ return require("crypto").createHash("sha256").update(buffer).digest();
39
39
  } else {
40
40
  let buf = await window.crypto.subtle.digest("SHA-256", buffer);
41
41
  return Buffer.from(buf);
package/src/sniTest.ts CHANGED
@@ -6,6 +6,7 @@ let tlsExtensionLookup: { [type: number]: string } = {
6
6
  2: "client_certificate_url",
7
7
  3: "trusted_ca_keys",
8
8
  4: "truncated_hmac",
9
+ // Likely an OCSP status request
9
10
  5: "status_request",
10
11
  6: "user_mapping",
11
12
  7: "client_authz",
@@ -84,18 +85,19 @@ let tlsExtensionLookup: { [type: number]: string } = {
84
85
  65281: "renegotiation_info" // 0xFF01
85
86
  };
86
87
 
88
+ // yarn testsni
87
89
  async function main() {
88
90
  const packet = Buffer.from(
89
- `FgMBBwgBAAcEAwNWbpKnOzI9CRp2ESvA5QzCXg5FYncEObckUkNoG3+/DyAwI4HL0havBXKvPlJtZJBgtZ+I/FqBlKGek8NGJcgdqQAgiooTARMCEwPAK8AvwCzAMMypzKjAE8AUAJwAnQAvADUBAAabSkoAAAAbAAMCAAIACgAMAArq6mOZAB0AFwAYAAUABQEAAAAAAC0AAgEB/wEAAQAAIwAAAAsAAgEAABIAAAANABIAEAQDCAQEAQUDCAUFAQgGBgEAFwAA/g0AugAAAQABCwAgIRJsXQHGN5cbIp9ucgXJ824RS/6hqIikNiMKw7/gsDAAkK8j0YYXYsSnpX4siEVnB/AxzQCPaGMoT4y1i63f4mzV0Sa6puc4sZqkxY7TyQMJLrGPL00HaJZh6ReT/ggPbCwg5f5/2hUCdZSx4aUpYMb7lAx9VVJjhrquZIRRVrk8yV0OjEivreTN02u1mgIgMY/0P8RzOea+iQluEF9ASOYxgNIU20xoKK608ktQrTafSQAQAAsACQhodHRwLzEuMQAzBO8E7erqAAEAY5kEwB8fuK+qumVLi67+BmQ5uQ556Opa122UZFmnfuoKQY1tiXkzM7Z+wmWOLTg7l3Ncu8ECL/Z9G6ZLP4N5OKGjiHE307sEvNRqdEnFYoQMVotF6hNbt3F4GrEBa6CuCEohg9HKg3RtIfMFYNc1lDC4rJCLwNETijo5MlOFnYVGpWOMcBZbdLsGv5JxN7idRUGvxsttaya3GYxSI1yFW9dZaysTsYXA2IzPp7M950GkJzBK5Zy3wBiXoZYGYKJGy5EA1XIE88Y6X2VNghPIatHHjdav+PpsLZCyUowEsAN49gh/yRrBNZxo6gNbfOIds/UpjlYtIEo1lch2ygWzVLS55qxm35IQUVKQjEQiPDNUI6g9RZZD+VSH5vB1ouMwTqbDAAN9EsUxigYmQggjz1jGFEm+DecEQTkXoQOIwWsAq+M39Eq7M3CwYVQ6yDQI+rQD9xxin8I9PrdVoXGaTVwnY9GFKFCYZyewSGeuPGSaPcdKwvBb/IgfFVRt2aU24XNmFsCkgLSzHEkBg1uYqjd+uIY7zTWo5WIevniL4SCIvRyvTQB7DICAZ+BxSUxio9x19OWQCitnoXdoGGwuTIQqSYuimFBedKa2b4Yk3nkPIRqa3jldbGMRwQmpaQFnsOJml/pEr3sQIuEcQ2shaIuqW+Zo86yamflqpCmCbQWNiipRYwQ/AmG7oOeS1iJomPiRJDwVNaoDZzI2ApYIcQckNPNrI9qOrGwww6x1T9EwKadz/1OcHQcbWtej5QkLm5Ss/9paxqYsu1kuohlvPOSd4txqKOGNvOhzxqqOweZ7p8l/H/JhnMkEUCZYvOW4JhthXsYMfnWn8rciE7KuMskf65mqI1tI5SZqzxAfH4MidyUJRNwR6mwWUgzMi7kB3eM3IPUbCDpUIpQBipt2SyenIMXNH9ZjnYtdCwVDe7m6mshYAJVV7oJDw5yn2pYH5nsZ+gM93vNWBvZVQjZZmRdZQ2YqhKaUyQgpOEhlE1w7XAXJp+um2kA5qElw9sNbxvOziNSPLyi17xgy1CSBSzB+o8JAfZamxUHFXEGSRYoR8qekMnlJeVRXlojEoaxMsspFX3itMOqIHNUL9Nltswaqggu7s+NQfshoLEg5gkfM/tok4sISLZlX7ACgcWZBQudMPTyn3FsQIbzNgXBzWTdiCZg9iZttoJtX8FygYbzEF9kFJyc5fIBA4NACVZhOf+onlMcFEbHCu6ilqMuzwQxf6vgV`
91
+ `FgMBAgABAAH8AwNYhO4jReWM/LOl/teBLLZOLN4z0u+jB3uEYW+9WwzwgSCWYyBiHRCFg/brbyJiNFv2n72FvXHnMHmZvjIBqPC8AgA+EwITAxMBwCzAMACfzKnMqMyqwCvALwCewCTAKABrwCPAJwBnwArAFAA5wAnAEwAzAJ0AnAA9ADwANQAvAP8BAAF1AAsABAMAAQIACgAWABQAHQAXAB4AGQAYAQABAQECAQMBBAAjAAAAFgAAABcAAAANACoAKAQDBQMGAwgHCAgICQgKCAsIBAgFCAYEAQUBBgEDAwMBAwIEAgUCBgIAKwAFBAMEAwMALQACAQEAMwAmACQAHQAgsMkHWnjEPL3IeGuvapE9aEOBVv+o8cDH07vYf4W0ykQAFQDcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==`
90
92
  .replace(/\s/g, "")
91
93
  , "base64"
92
94
  );
93
95
 
94
96
  let data = parseTLSHello(packet);
95
- let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
97
+ //let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
96
98
  console.log(`Packet size ${packet.byteLength}, missing bytes ${data.missingBytes}`);
97
99
  for (let ext of data.extensions) {
98
- console.log(`Extension: ${tlsExtensionLookup[ext.type] || ext.type}, bytes length: ${ext.data.length}`);
100
+ console.log(`Extension: ${tlsExtensionLookup[ext.type] || ext.type}, bytes length: ${ext.data.length}, ${ext.data.toString("hex")}`);
99
101
  }
100
102
  }
101
103
  main().catch(e => console.error(e)).finally(() => process.exit());
@@ -180,6 +180,7 @@ export async function startSocketServer(
180
180
  });
181
181
 
182
182
  let realServer = net.createServer(socket => {
183
+ const remoteAddress = socket.remoteAddress;
183
184
  function handleTLSHello(buffer: Buffer, packetCount: number): void | "more" {
184
185
  // All HTTPS requests start with 22, and no HTTP requests start with 22,
185
186
  // so we just need to read the first byte.
@@ -196,7 +197,7 @@ export async function startSocketServer(
196
197
  console.log(`Received TCP connection with SNI ${JSON.stringify(sni)}`);
197
198
  }
198
199
  if (!sni) {
199
- console.warn(`No SNI found in TLS hello, using main server. Packets ${packetCount}`);
200
+ console.warn(`No SNI found in TLS hello from ${remoteAddress}, using main server. Packets ${packetCount}`);
200
201
  console.log(buffer.toString("base64"));
201
202
  }
202
203
  server = sniServers.get(sni) || mainHTTPSServer;
@@ -221,7 +222,7 @@ export async function startSocketServer(
221
222
  }
222
223
  getNextData();
223
224
  socket.on("error", (e) => {
224
- console.error(`Exposed socket error, ${e.stack}`);
225
+ console.error(`Socket error for ${remoteAddress}, ${e.stack}`);
225
226
  });
226
227
  });
227
228
 
package/test.ts CHANGED
@@ -8,23 +8,23 @@ import { forwardPort } from "./src/forwardPort";
8
8
 
9
9
  // Usage example:
10
10
  async function main() {
11
- const externalPort = 8088;
11
+ const externalPort = 11300;
12
12
  const internalPort = externalPort;
13
13
 
14
14
  await forwardPort({ externalPort, internalPort });
15
15
 
16
16
  // Listen on the external port
17
- const server = http.createServer((req, res) => {
18
- console.log("Request received");
19
- res.end("Hello, world!");
20
- });
21
- server.listen(externalPort, "0.0.0.0");
22
-
23
- {
24
- const externalIP = await getExternalIP();
25
- let test = await fetch(`http://${externalIP}:${externalPort}`);
26
- console.log(await test.text());
27
- }
17
+ // const server = http.createServer((req, res) => {
18
+ // console.log("Request received");
19
+ // res.end("Hello, world!");
20
+ // });
21
+ // server.listen(externalPort, "0.0.0.0");
22
+
23
+ // {
24
+ // const externalIP = await getExternalIP();
25
+ // let test = await fetch(`http://${externalIP}:${externalPort}`);
26
+ // console.log(await test.text());
27
+ // }
28
28
 
29
29
 
30
30
  //await createPortMapping({ externalPort, internalPort, gateWayIP, internalIP, });
@@ -1,187 +1,212 @@
1
- import { SocketFunction } from "../SocketFunction";
2
- import { blue, green, red, yellow } from "../src/formatting/logColors";
3
- import { isNode } from "../src/misc";
4
-
5
- module.allowclient = true;
6
-
7
- const UPDATE_INTERVAL = 1000 * 60 * 10;
8
- // More frequent, to ensure we don't run into major issues with sleep (coming back from sleep,
9
- // having the interval not be fired immediately, and having the time be off for a few minutes).
10
- const UPDATE_SUB_INTERVAL = 1000 * 10;
11
- // Smearing is important, otherwise some performance timing (especially on load) can easily be off
12
- // by a few hundred milliseconds. The current smear parameters will mean even with 1s of offset
13
- // we only add 10ms every 100ms, so worst case scenario some timing that takes 0ms will take 10ms.
14
- const UPDATE_SMEAR_TICK_DURATION = 100;
15
- const UPDATE_SMEAR_TICK_COUNT = 100;
16
- const UPDATE_VERIFY_COUNT = 3;
17
-
18
- let trueTimeOffset = 0;
19
- let didFirstTimeSync = false;
20
- let onFirstTimeSync!: () => void;
21
- let firstTimeSyncPromise = new Promise<void>((resolve) => {
22
- onFirstTimeSync = resolve;
23
- });
24
-
25
- const baseGetTime = Date.now;
26
- export function getTrueTime() {
27
- return baseGetTime() + trueTimeOffset;
28
- }
29
- export function getTrueTimeOffset() {
30
- return trueTimeOffset;
31
- }
32
- export function waitForFirstTimeSync() {
33
- return firstTimeSyncPromise;
34
- }
35
- export function shimDateNow() {
36
- Date.now = getTrueTime;
37
- }
38
-
39
- export function setGetTimeOffsetBase(base: () => Promise<number>) {
40
- getTimeOffsetBase = base;
41
- }
42
-
43
-
44
- async function defaultGetTimeOffset(): Promise<number> {
45
- if (!isNode()) {
46
- let sendTime = baseGetTime();
47
- let serverTrueTime = await TimeController.nodes[SocketFunction.browserNodeId()].getTrueTime();
48
- let systemTime = baseGetTime();
49
- let predictedServerToClientLatency = (systemTime - sendTime) / 2;
50
- let trueTimeRightNow = serverTrueTime + predictedServerToClientLatency;
51
- return trueTimeRightNow - systemTime;
52
- }
53
- const dgram = await import("dgram");
54
- const NTP_SERVER = "time.google.com";
55
- const NTP_PORT = 123;
56
- const NTP_PACKET_SIZE = 48;
57
- const NTP_EPOCH_OFFSET = 2208988800000; // Number of milliseconds between 1900-01-01 and 1970-01-01
58
- return new Promise((resolve, reject) => {
59
- const client = dgram.createSocket("udp4");
60
- const message = Buffer.alloc(NTP_PACKET_SIZE);
61
-
62
- // Set the first byte to represent NTP client request (LI = 0, VN = 3, Mode = 3)
63
- message[0] = 0x1B;
64
-
65
- const sendTime = baseGetTime();
66
-
67
- client.send(message, 0, message.length, NTP_PORT, NTP_SERVER);
68
- client.on("error", (err) => {
69
- client.close();
70
- reject(err);
71
- });
72
-
73
- client.on("message", (msg) => {
74
- const receiveTime = baseGetTime();
75
-
76
- // Extract the transmit timestamp from the server response
77
- const transmitTimestampSeconds = msg.readUInt32BE(40);
78
- const transmitTimestampFraction = msg.readUInt32BE(44);
79
- const transmitTimestamp = (transmitTimestampSeconds * 1000) + (transmitTimestampFraction * 1000 / 0x100000000) - NTP_EPOCH_OFFSET;
80
-
81
- const predictedServerToClientLatency = (receiveTime - sendTime) / 2;
82
-
83
- // Calculate the offset
84
- const systemTime = baseGetTime();
85
- const actualTime = transmitTimestamp + predictedServerToClientLatency;
86
- const offset = actualTime - systemTime;
87
-
88
- client.close();
89
- resolve(offset);
90
- });
91
- });
92
- }
93
-
94
- let getTimeOffsetBase: () => Promise<number> = defaultGetTimeOffset;
95
- let updatingOffset = false;
96
- async function updateTimeOffset() {
97
- if (updatingOffset) return;
98
- updatingOffset = true;
99
- try {
100
- let offsets: number[] = [];
101
- for (let i = 0; i < UPDATE_VERIFY_COUNT; i++) {
102
- try {
103
- offsets.push(await getTimeOffsetBase());
104
- } catch (e) {
105
- console.error("Error getting time offset:", e);
106
- }
107
- }
108
- // If we have no offsets, it likely means every call errored out (probably because the network is down).
109
- // This is fine, just don't update (DO register the first sync as being done, otherwise calling code
110
- // might be waiting forever).
111
- if (offsets.length > 0) {
112
- // Pick the middle offset
113
- offsets.sort((a, b) => a - b);
114
- let offset = offsets[Math.floor(offsets.length / 2)];
115
-
116
- // Smear it slowly
117
- let currentSmearCount = UPDATE_SMEAR_TICK_COUNT;
118
- // Update the initial time all at once, otherwise initial requests to other servers might
119
- // be rejected (because they could use the system time, which could be off by a few seconds).
120
- if (!didFirstTimeSync) {
121
- currentSmearCount = 1;
122
- }
123
-
124
- let prevOffset = trueTimeOffset;
125
- let offsetRound = Math.abs(Math.round(offset));
126
- let offsetColored = (
127
- Math.abs(offset) > 600 && red(offsetRound + "ms")
128
- || Math.abs(offset) > 300 && yellow(offsetRound + "ms")
129
- || green(offsetRound + "ms")
130
- );
131
- if (Math.abs(offset) > 500) {
132
- console.log(`${blue("Synchronized time")}, local clock was ${offset > 0 ? "behind" : "ahead"} by ${offsetColored} @ ${blue(Date.now() + "")}`);
133
- }
134
- for (let i = 0; i < currentSmearCount; i++) {
135
- let fraction = (i + 1) / currentSmearCount;
136
- trueTimeOffset = prevOffset * (1 - fraction) + offset * fraction;
137
- if (i < currentSmearCount - 1) {
138
- await new Promise((resolve) => setTimeout(resolve, UPDATE_SMEAR_TICK_DURATION));
139
- }
140
- }
141
- }
142
-
143
- if (!didFirstTimeSync) {
144
- didFirstTimeSync = true;
145
- onFirstTimeSync();
146
- }
147
- } finally {
148
- updatingOffset = false;
149
- }
150
- }
151
-
152
- let nextUpdateTime = 0;
153
- setInterval(() => {
154
- if (baseGetTime() < nextUpdateTime) return;
155
- nextUpdateTime = baseGetTime() + UPDATE_INTERVAL;
156
- updateTimeOffset().catch((e) => {
157
- console.warn("Error updating time offset:", e);
158
- });
159
- }, UPDATE_SUB_INTERVAL);
160
- setImmediate(() => {
161
- updateTimeOffset().catch((e) => {
162
- console.error("Error updating initial offset:", e);
163
- });
164
- });
165
-
166
-
167
- class TimeControllerBase {
168
- public async getTrueTime() {
169
- await waitForFirstTimeSync();
170
- return getTrueTime();
171
- }
172
- }
173
-
174
- const TimeController = SocketFunction.register(
175
- "TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976",
176
- new TimeControllerBase(),
177
- () => ({
178
- getTrueTime: {},
179
- }),
180
- () => ({
181
- }),
182
- {
183
- // NOTE: Autoexpose, because our exposed endpoints are incredibly lightweight
184
- // (just a ping), and don't expose really expose any data.
185
- // noAutoExpose: true
186
- }
1
+ import { SocketFunction } from "../SocketFunction";
2
+ import { blue, green, red, yellow } from "../src/formatting/logColors";
3
+ import { isNode } from "../src/misc";
4
+
5
+ module.allowclient = true;
6
+
7
+ const UPDATE_INTERVAL = 1000 * 60 * 10;
8
+ // More frequent, to ensure we don't run into major issues with sleep (coming back from sleep,
9
+ // having the interval not be fired immediately, and having the time be off for a few minutes).
10
+ const UPDATE_SUB_INTERVAL = 1000 * 10;
11
+ // Smearing is important, otherwise some performance timing (especially on load) can easily be off
12
+ // by a few hundred milliseconds. The current smear parameters will mean even with 1s of offset
13
+ // we only add 10ms every 100ms, so worst case scenario some timing that takes 0ms will take 10ms.
14
+ const UPDATE_SMEAR_TICK_DURATION = 100;
15
+ const UPDATE_SMEAR_TICK_COUNT = 100;
16
+ const UPDATE_VERIFY_COUNT = 3;
17
+
18
+ // Time can never go backwards, but we can run at a slower rate until the output time allows
19
+ // the real time to catch up with it.
20
+ const MINIMUM_TIME_RATE = 0.5;
21
+
22
+ let trueTimeOffset = 0;
23
+ let didFirstTimeSync = false;
24
+ let onFirstTimeSync!: () => void;
25
+ let firstTimeSyncPromise = new Promise<void>((resolve) => {
26
+ onFirstTimeSync = resolve;
27
+ });
28
+
29
+ const baseGetTime = Date.now;
30
+ let lastTime = 0;
31
+ let lastBaseTime = 0;
32
+ export function getTrueTime() {
33
+ let baseTime = baseGetTime();
34
+ let time = baseTime + trueTimeOffset;
35
+ // Only adjust time once we have a time offset. Otherwise systems with a really bad clock
36
+ // might take days be correct. It is better for the time to jump once at startup, rather
37
+ // than be off by days, for days at a time.
38
+ if (lastTime && trueTimeOffset) {
39
+ if (time < lastTime) {
40
+ let diff = baseTime - lastBaseTime;
41
+ if (diff >= 0) {
42
+ // Some time passed, so we have a baseline for how much to increase the time by.
43
+ // This allows the real time to catch up with our time naturally.
44
+ time = lastTime + diff * MINIMUM_TIME_RATE;
45
+ } else {
46
+ // The issue is the system time going backwards. In this case, allow the time to change
47
+ }
48
+ }
49
+ }
50
+ lastTime = time;
51
+ lastBaseTime = baseTime;
52
+ return time;
53
+ }
54
+ export function getTrueTimeOffset() {
55
+ return trueTimeOffset;
56
+ }
57
+ export function waitForFirstTimeSync() {
58
+ return firstTimeSyncPromise;
59
+ }
60
+ export function shimDateNow() {
61
+ Date.now = getTrueTime;
62
+ }
63
+
64
+ export function setGetTimeOffsetBase(base: () => Promise<number>) {
65
+ getTimeOffsetBase = base;
66
+ }
67
+
68
+
69
+ async function defaultGetTimeOffset(): Promise<number> {
70
+ if (!isNode()) {
71
+ let sendTime = baseGetTime();
72
+ let serverTrueTime = await TimeController.nodes[SocketFunction.browserNodeId()].getTrueTime();
73
+ let systemTime = baseGetTime();
74
+ let predictedServerToClientLatency = (systemTime - sendTime) / 2;
75
+ let trueTimeRightNow = serverTrueTime + predictedServerToClientLatency;
76
+ return trueTimeRightNow - systemTime;
77
+ }
78
+ const dgram = await import("dgram");
79
+ const NTP_SERVER = "time.google.com";
80
+ const NTP_PORT = 123;
81
+ const NTP_PACKET_SIZE = 48;
82
+ const NTP_EPOCH_OFFSET = 2208988800000; // Number of milliseconds between 1900-01-01 and 1970-01-01
83
+ return new Promise((resolve, reject) => {
84
+ const client = dgram.createSocket("udp4");
85
+ const message = Buffer.alloc(NTP_PACKET_SIZE);
86
+
87
+ // Set the first byte to represent NTP client request (LI = 0, VN = 3, Mode = 3)
88
+ message[0] = 0x1B;
89
+
90
+ const sendTime = baseGetTime();
91
+
92
+ client.send(message, 0, message.length, NTP_PORT, NTP_SERVER);
93
+ client.on("error", (err) => {
94
+ client.close();
95
+ reject(err);
96
+ });
97
+
98
+ client.on("message", (msg) => {
99
+ const receiveTime = baseGetTime();
100
+
101
+ // Extract the transmit timestamp from the server response
102
+ const transmitTimestampSeconds = msg.readUInt32BE(40);
103
+ const transmitTimestampFraction = msg.readUInt32BE(44);
104
+ const transmitTimestamp = (transmitTimestampSeconds * 1000) + (transmitTimestampFraction * 1000 / 0x100000000) - NTP_EPOCH_OFFSET;
105
+
106
+ const predictedServerToClientLatency = (receiveTime - sendTime) / 2;
107
+
108
+ // Calculate the offset
109
+ const systemTime = baseGetTime();
110
+ const actualTime = transmitTimestamp + predictedServerToClientLatency;
111
+ const offset = actualTime - systemTime;
112
+
113
+ client.close();
114
+ resolve(offset);
115
+ });
116
+ });
117
+ }
118
+
119
+ let getTimeOffsetBase: () => Promise<number> = defaultGetTimeOffset;
120
+ let updatingOffset = false;
121
+ async function updateTimeOffset() {
122
+ if (updatingOffset) return;
123
+ updatingOffset = true;
124
+ try {
125
+ let offsets: number[] = [];
126
+ for (let i = 0; i < UPDATE_VERIFY_COUNT; i++) {
127
+ try {
128
+ offsets.push(await getTimeOffsetBase());
129
+ } catch (e) {
130
+ console.error("Error getting time offset:", e);
131
+ }
132
+ }
133
+ // If we have no offsets, it likely means every call errored out (probably because the network is down).
134
+ // This is fine, just don't update (DO register the first sync as being done, otherwise calling code
135
+ // might be waiting forever).
136
+ if (offsets.length > 0) {
137
+ // Pick the middle offset
138
+ offsets.sort((a, b) => a - b);
139
+ let offset = offsets[Math.floor(offsets.length / 2)];
140
+
141
+ // Smear it slowly
142
+ let currentSmearCount = UPDATE_SMEAR_TICK_COUNT;
143
+ // Update the initial time all at once, otherwise initial requests to other servers might
144
+ // be rejected (because they could use the system time, which could be off by a few seconds).
145
+ if (!didFirstTimeSync) {
146
+ currentSmearCount = 1;
147
+ }
148
+
149
+ let prevOffset = trueTimeOffset;
150
+ let offsetRound = Math.abs(Math.round(offset));
151
+ let offsetColored = (
152
+ Math.abs(offset) > 600 && red(offsetRound + "ms")
153
+ || Math.abs(offset) > 300 && yellow(offsetRound + "ms")
154
+ || green(offsetRound + "ms")
155
+ );
156
+ if (Math.abs(offset) > 500) {
157
+ console.log(`${blue("Synchronized time")}, local clock was ${offset > 0 ? "behind" : "ahead"} by ${offsetColored} @ ${blue(Date.now() + "")}`);
158
+ }
159
+ for (let i = 0; i < currentSmearCount; i++) {
160
+ let fraction = (i + 1) / currentSmearCount;
161
+ trueTimeOffset = prevOffset * (1 - fraction) + offset * fraction;
162
+ if (i < currentSmearCount - 1) {
163
+ await new Promise((resolve) => setTimeout(resolve, UPDATE_SMEAR_TICK_DURATION));
164
+ }
165
+ }
166
+ }
167
+
168
+ if (!didFirstTimeSync) {
169
+ didFirstTimeSync = true;
170
+ onFirstTimeSync();
171
+ }
172
+ } finally {
173
+ updatingOffset = false;
174
+ }
175
+ }
176
+
177
+ let nextUpdateTime = 0;
178
+ setInterval(() => {
179
+ if (baseGetTime() < nextUpdateTime) return;
180
+ nextUpdateTime = baseGetTime() + UPDATE_INTERVAL;
181
+ updateTimeOffset().catch((e) => {
182
+ console.warn("Error updating time offset:", e);
183
+ });
184
+ }, UPDATE_SUB_INTERVAL);
185
+ setImmediate(() => {
186
+ updateTimeOffset().catch((e) => {
187
+ console.error("Error updating initial offset:", e);
188
+ });
189
+ });
190
+
191
+
192
+ class TimeControllerBase {
193
+ public async getTrueTime() {
194
+ await waitForFirstTimeSync();
195
+ return getTrueTime();
196
+ }
197
+ }
198
+
199
+ const TimeController = SocketFunction.register(
200
+ "TimeController-ddf4753e-fc8a-413f-8cc2-b927dd449976",
201
+ new TimeControllerBase(),
202
+ () => ({
203
+ getTrueTime: {},
204
+ }),
205
+ () => ({
206
+ }),
207
+ {
208
+ // NOTE: Autoexpose, because our exposed endpoints are incredibly lightweight
209
+ // (just a ping), and don't expose really expose any data.
210
+ // noAutoExpose: true
211
+ }
187
212
  );