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 +1 -1
- package/require/RequireController.ts +20 -1
- package/require/require.js +13 -7
- package/src/CallFactory.ts +9 -20
- package/src/JSONLACKS/JSONLACKS.ts +0 -4
- package/src/batching.ts +1 -1
- package/src/callManager.ts +1 -1
- package/src/formatting/logColors.ts +1 -1
- package/src/https.ts +4 -3
- package/src/misc.ts +5 -5
- package/src/sniTest.ts +5 -3
- package/src/webSocketServer.ts +3 -2
- package/test.ts +12 -12
- package/time/trueTimeShim.ts +211 -186
package/package.json
CHANGED
|
@@ -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;
|
package/require/require.js
CHANGED
|
@@ -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:
|
|
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
|
|
347
|
-
module.children.push(
|
|
344
|
+
let providerModule = getModule(resolvedPath);
|
|
345
|
+
module.children.push(providerModule);
|
|
348
346
|
if (exportsOverride !== undefined) {
|
|
349
|
-
|
|
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
|
-
|
|
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
|
|
package/src/CallFactory.ts
CHANGED
|
@@ -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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
})();
|
package/src/callManager.ts
CHANGED
|
@@ -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 = (
|
|
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
|
|
70
|
-
|
|
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
|
-
`
|
|
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());
|
package/src/webSocketServer.ts
CHANGED
|
@@ -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(`
|
|
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 =
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
21
|
-
server.listen(externalPort, "0.0.0.0");
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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, });
|
package/time/trueTimeShim.ts
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
()
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
);
|