socket-function 0.17.0 → 0.18.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.18.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;
@@ -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);
@@ -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/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
 
@@ -1,187 +1,187 @@
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
+ 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
+ }
187
187
  );