socket-function 1.2.8 → 1.2.10
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/SocketFunction.d.ts +0 -1
- package/bin/upreal.js +4 -0
- package/index.d.ts +25 -15
- package/package.json +6 -2
- package/require/RequireController.d.ts +0 -1
- package/require/extMapper.d.ts +0 -1
- package/src/CallFactory.d.ts +0 -1
- package/src/JSONLACKS/JSONLACKS.d.ts +0 -1
- package/src/Zip.d.ts +0 -1
- package/src/bits.d.ts +0 -1
- package/src/buffers.d.ts +0 -1
- package/src/callHTTPHandler.d.ts +0 -1
- package/src/certStore.d.ts +0 -1
- package/src/forwardPort.d.ts +20 -0
- package/src/forwardPort.ts +131 -8
- package/src/https.d.ts +0 -1
- package/src/lz4/LZ4.d.ts +0 -1
- package/src/misc.d.ts +0 -1
- package/src/tlsParsing.d.ts +0 -1
- package/src/upreal.d.ts +1 -0
- package/src/upreal.ts +197 -0
- package/src/webSocketServer.d.ts +0 -1
- package/src/webSocketServer.ts +87 -32
- package/test.ts +10 -154
package/SocketFunction.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/// <reference path="require/RequireController.d.ts" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
-
/// <reference types="node" />
|
|
4
3
|
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, SocketRegisterType } from "./SocketFunctionTypes";
|
|
5
4
|
import { SocketServerConfig } from "./src/webSocketServer";
|
|
6
5
|
import { Args, MaybePromise } from "./src/types";
|
package/bin/upreal.js
ADDED
package/index.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ declare module "socket-function/SetProcessVariables" {
|
|
|
9
9
|
declare module "socket-function/SocketFunction" {
|
|
10
10
|
/// <reference path="require/RequireController.d.ts" />
|
|
11
11
|
/// <reference types="node" />
|
|
12
|
-
/// <reference types="node" />
|
|
13
12
|
import { SocketExposedInterface, SocketFunctionHook, SocketFunctionClientHook, SocketExposedShape, SocketRegistered, CallerContext, FullCallType, SocketRegisterType } from "socket-function/SocketFunctionTypes";
|
|
14
13
|
import { SocketServerConfig } from "socket-function/src/webSocketServer";
|
|
15
14
|
import { Args, MaybePromise } from "socket-function/src/types";
|
|
@@ -315,7 +314,6 @@ declare module "socket-function/require/CSSShim" {
|
|
|
315
314
|
declare module "socket-function/require/RequireController" {
|
|
316
315
|
/// <reference path="../../typenode/index.d.ts" />
|
|
317
316
|
/// <reference types="node" />
|
|
318
|
-
/// <reference types="node" />
|
|
319
317
|
declare global {
|
|
320
318
|
namespace NodeJS {
|
|
321
319
|
interface Module {
|
|
@@ -445,7 +443,6 @@ declare module "socket-function/require/compileFlags" {
|
|
|
445
443
|
}
|
|
446
444
|
|
|
447
445
|
declare module "socket-function/require/extMapper" {
|
|
448
|
-
/// <reference types="node" />
|
|
449
446
|
/// <reference types="node" />
|
|
450
447
|
export declare function getExtContentType(ext: string): string;
|
|
451
448
|
export declare function getContentTypeFromBuffer(buffer: Buffer): string | undefined;
|
|
@@ -471,7 +468,6 @@ declare module "socket-function/require/require" {
|
|
|
471
468
|
}
|
|
472
469
|
|
|
473
470
|
declare module "socket-function/src/CallFactory" {
|
|
474
|
-
/// <reference types="node" />
|
|
475
471
|
/// <reference types="node" />
|
|
476
472
|
/// <reference types="node" />
|
|
477
473
|
import { CallType } from "socket-function/SocketFunctionTypes";
|
|
@@ -530,7 +526,6 @@ declare module "socket-function/src/CallFactory" {
|
|
|
530
526
|
}
|
|
531
527
|
|
|
532
528
|
declare module "socket-function/src/JSONLACKS/JSONLACKS" {
|
|
533
|
-
/// <reference types="node" />
|
|
534
529
|
/// <reference types="node" />
|
|
535
530
|
export interface JSONLACKS_ParseConfig {
|
|
536
531
|
extended?: boolean;
|
|
@@ -566,7 +561,6 @@ declare module "socket-function/src/JSONLACKS/JSONLACKS.generated.js" {
|
|
|
566
561
|
}
|
|
567
562
|
|
|
568
563
|
declare module "socket-function/src/Zip" {
|
|
569
|
-
/// <reference types="node" />
|
|
570
564
|
/// <reference types="node" />
|
|
571
565
|
import { MaybePromise } from "socket-function/src/types";
|
|
572
566
|
export declare class Zip {
|
|
@@ -654,7 +648,6 @@ declare module "socket-function/src/batching" {
|
|
|
654
648
|
}
|
|
655
649
|
|
|
656
650
|
declare module "socket-function/src/bits" {
|
|
657
|
-
/// <reference types="node" />
|
|
658
651
|
/// <reference types="node" />
|
|
659
652
|
/** Subtracts the smallest possible value from a number (a double). This makes it possible to convert an exclusive range end
|
|
660
653
|
* to an inclusive range end, which is sometimes required (as in, < x is the same as <= minusEpsilon(x)).
|
|
@@ -681,7 +674,6 @@ declare module "socket-function/src/bits" {
|
|
|
681
674
|
}
|
|
682
675
|
|
|
683
676
|
declare module "socket-function/src/buffers" {
|
|
684
|
-
/// <reference types="node" />
|
|
685
677
|
/// <reference types="node" />
|
|
686
678
|
export type ArrayBufferViewTypes = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array | Float64Array | Float32Array | Uint8ClampedArray;
|
|
687
679
|
export type BufferType = ArrayBuffer | SharedArrayBuffer | ArrayBufferViewTypes;
|
|
@@ -759,7 +751,6 @@ declare module "socket-function/src/caching" {
|
|
|
759
751
|
}
|
|
760
752
|
|
|
761
753
|
declare module "socket-function/src/callHTTPHandler" {
|
|
762
|
-
/// <reference types="node" />
|
|
763
754
|
/// <reference types="node" />
|
|
764
755
|
/// <reference types="node" />
|
|
765
756
|
import http from "http";
|
|
@@ -814,7 +805,6 @@ declare module "socket-function/src/callManager" {
|
|
|
814
805
|
}
|
|
815
806
|
|
|
816
807
|
declare module "socket-function/src/certStore" {
|
|
817
|
-
/// <reference types="node" />
|
|
818
808
|
/// <reference types="node" />
|
|
819
809
|
/** Must be populated before the server starts */
|
|
820
810
|
export declare function trustCertificate(cert: string | Buffer): void;
|
|
@@ -942,11 +932,31 @@ declare module "socket-function/src/formatting/logColors" {
|
|
|
942
932
|
}
|
|
943
933
|
|
|
944
934
|
declare module "socket-function/src/forwardPort" {
|
|
935
|
+
export interface PortMapping {
|
|
936
|
+
externalPort: number;
|
|
937
|
+
internalPort: number;
|
|
938
|
+
protocol: string;
|
|
939
|
+
/** The LAN client the mapping forwards to (NewInternalClient). */
|
|
940
|
+
internalClient: string;
|
|
941
|
+
/** Empty string means "any" remote host (the usual case). */
|
|
942
|
+
remoteHost: string;
|
|
943
|
+
enabled: boolean;
|
|
944
|
+
description: string;
|
|
945
|
+
/** Remaining lease in seconds; 0 means a permanent (static) mapping. */
|
|
946
|
+
leaseDuration: number;
|
|
947
|
+
}
|
|
948
|
+
/** Queries the router for every existing UPnP port mapping by walking
|
|
949
|
+
* GetGenericPortMappingEntry from index 0 until the gateway reports the index
|
|
950
|
+
* is out of range (SOAP error 713 / a non-200 response). */
|
|
951
|
+
export declare function listPortMappings(): Promise<PortMapping[]>;
|
|
945
952
|
export declare function forwardPort(config: {
|
|
946
953
|
externalPort: number;
|
|
947
954
|
internalPort: number;
|
|
948
955
|
duration?: number;
|
|
949
956
|
}): Promise<void>;
|
|
957
|
+
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
958
|
+
* mapping points at us or at a different machine on the network. */
|
|
959
|
+
export declare function getLocalInternalIP(): string | undefined;
|
|
950
960
|
|
|
951
961
|
}
|
|
952
962
|
|
|
@@ -955,7 +965,6 @@ declare module "socket-function/src/getUniqueTime" {
|
|
|
955
965
|
}
|
|
956
966
|
|
|
957
967
|
declare module "socket-function/src/https" {
|
|
958
|
-
/// <reference types="node" />
|
|
959
968
|
/// <reference types="node" />
|
|
960
969
|
export declare function httpsRequest(url: string, payload?: Buffer | Buffer[], method?: string, sendSessionCookies?: boolean, config?: {
|
|
961
970
|
headers?: {
|
|
@@ -967,7 +976,6 @@ declare module "socket-function/src/https" {
|
|
|
967
976
|
}
|
|
968
977
|
|
|
969
978
|
declare module "socket-function/src/lz4/LZ4" {
|
|
970
|
-
/// <reference types="node" />
|
|
971
979
|
/// <reference types="node" />
|
|
972
980
|
export declare class LZ4 {
|
|
973
981
|
static compress(data: Buffer): Buffer;
|
|
@@ -1035,7 +1043,6 @@ declare module "socket-function/src/lz4/lz4_wasm_nodejs_bg.wasm" {
|
|
|
1035
1043
|
}
|
|
1036
1044
|
|
|
1037
1045
|
declare module "socket-function/src/misc" {
|
|
1038
|
-
/// <reference types="node" />
|
|
1039
1046
|
/// <reference types="node" />
|
|
1040
1047
|
import { MaybePromise } from "socket-function/src/types";
|
|
1041
1048
|
export declare const timeInSecond = 1000;
|
|
@@ -1465,7 +1472,6 @@ declare module "socket-function/src/storagePath" {
|
|
|
1465
1472
|
}
|
|
1466
1473
|
|
|
1467
1474
|
declare module "socket-function/src/tlsParsing" {
|
|
1468
|
-
/// <reference types="node" />
|
|
1469
1475
|
/// <reference types="node" />
|
|
1470
1476
|
export declare function parseTLSHello(buffer: Buffer): {
|
|
1471
1477
|
extensions: {
|
|
@@ -1489,8 +1495,12 @@ declare module "socket-function/src/types" {
|
|
|
1489
1495
|
|
|
1490
1496
|
}
|
|
1491
1497
|
|
|
1498
|
+
declare module "socket-function/src/upreal" {
|
|
1499
|
+
export {};
|
|
1500
|
+
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1492
1503
|
declare module "socket-function/src/webSocketServer" {
|
|
1493
|
-
/// <reference types="node" />
|
|
1494
1504
|
/// <reference types="node" />
|
|
1495
1505
|
/// <reference types="node" />
|
|
1496
1506
|
import https from "https";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
"preact": "10.24.3",
|
|
13
13
|
"typedev": "^0.4.0",
|
|
14
14
|
"typenode": "^6.0.1",
|
|
15
|
-
"ws": "^8.
|
|
15
|
+
"ws": "^8.21.0"
|
|
16
16
|
},
|
|
17
17
|
"optionalDependencies": {
|
|
18
18
|
"lmdb": "^3.5.1"
|
|
19
19
|
},
|
|
20
20
|
"types": "index.d.ts",
|
|
21
|
+
"bin": {
|
|
22
|
+
"upreal": "./bin/upreal.js"
|
|
23
|
+
},
|
|
21
24
|
"scripts": {
|
|
25
|
+
"upreal": "yarn typenode ./src/upreal.ts",
|
|
22
26
|
"test": "yarn typenode ./test.ts",
|
|
23
27
|
"test-all": "node run-test.js",
|
|
24
28
|
"type": "yarn tsc --noEmit",
|
package/require/extMapper.d.ts
CHANGED
package/src/CallFactory.d.ts
CHANGED
package/src/Zip.d.ts
CHANGED
package/src/bits.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
2
|
/** Subtracts the smallest possible value from a number (a double). This makes it possible to convert an exclusive range end
|
|
4
3
|
* to an inclusive range end, which is sometimes required (as in, < x is the same as <= minusEpsilon(x)).
|
|
5
4
|
*/
|
package/src/buffers.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
2
|
export type ArrayBufferViewTypes = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array | Float64Array | Float32Array | Uint8ClampedArray;
|
|
4
3
|
export type BufferType = ArrayBuffer | SharedArrayBuffer | ArrayBufferViewTypes;
|
|
5
4
|
export declare function cloneBuffer(data: Buffer): Buffer;
|
package/src/callHTTPHandler.d.ts
CHANGED
package/src/certStore.d.ts
CHANGED
package/src/forwardPort.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
+
export interface PortMapping {
|
|
2
|
+
externalPort: number;
|
|
3
|
+
internalPort: number;
|
|
4
|
+
protocol: string;
|
|
5
|
+
/** The LAN client the mapping forwards to (NewInternalClient). */
|
|
6
|
+
internalClient: string;
|
|
7
|
+
/** Empty string means "any" remote host (the usual case). */
|
|
8
|
+
remoteHost: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
description: string;
|
|
11
|
+
/** Remaining lease in seconds; 0 means a permanent (static) mapping. */
|
|
12
|
+
leaseDuration: number;
|
|
13
|
+
}
|
|
14
|
+
/** Queries the router for every existing UPnP port mapping by walking
|
|
15
|
+
* GetGenericPortMappingEntry from index 0 until the gateway reports the index
|
|
16
|
+
* is out of range (SOAP error 713 / a non-200 response). */
|
|
17
|
+
export declare function listPortMappings(): Promise<PortMapping[]>;
|
|
1
18
|
export declare function forwardPort(config: {
|
|
2
19
|
externalPort: number;
|
|
3
20
|
internalPort: number;
|
|
4
21
|
duration?: number;
|
|
5
22
|
}): Promise<void>;
|
|
23
|
+
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
24
|
+
* mapping points at us or at a different machine on the network. */
|
|
25
|
+
export declare function getLocalInternalIP(): string | undefined;
|
package/src/forwardPort.ts
CHANGED
|
@@ -6,6 +6,72 @@ import { timeInHour } from "./misc";
|
|
|
6
6
|
const SSDP_DISCOVER_MX = 2;
|
|
7
7
|
const SSDP_DISCOVER_MSG = `M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: "ssdp:discover"\r\nMX: ${SSDP_DISCOVER_MX}\r\nST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n\r\n`;
|
|
8
8
|
|
|
9
|
+
/** Resolves the UPnP Internet Gateway Device we can talk to, along with the local
|
|
10
|
+
* addressing needed to build SOAP requests against it. Shared by every operation
|
|
11
|
+
* that needs to reach the router's control endpoint. */
|
|
12
|
+
async function resolveGateway(): Promise<{
|
|
13
|
+
internalIP: string;
|
|
14
|
+
gatewayIP: string;
|
|
15
|
+
controlPort: number;
|
|
16
|
+
controlURLs: string[];
|
|
17
|
+
}> {
|
|
18
|
+
const localObj = getLocalInterfaceAddress();
|
|
19
|
+
if (!localObj) throw new Error("Could not find the local address / gateway");
|
|
20
|
+
|
|
21
|
+
const { internalIP, gatewayIP } = localObj;
|
|
22
|
+
console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
|
|
23
|
+
let gateway = await discoverGateway(internalIP);
|
|
24
|
+
let controlURLs = await getControlPaths(gateway);
|
|
25
|
+
let controlPort = Number(new URL(gateway).port);
|
|
26
|
+
|
|
27
|
+
return { internalIP, gatewayIP, controlPort, controlURLs };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PortMapping {
|
|
31
|
+
externalPort: number;
|
|
32
|
+
internalPort: number;
|
|
33
|
+
protocol: string;
|
|
34
|
+
/** The LAN client the mapping forwards to (NewInternalClient). */
|
|
35
|
+
internalClient: string;
|
|
36
|
+
/** Empty string means "any" remote host (the usual case). */
|
|
37
|
+
remoteHost: string;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
description: string;
|
|
40
|
+
/** Remaining lease in seconds; 0 means a permanent (static) mapping. */
|
|
41
|
+
leaseDuration: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Queries the router for every existing UPnP port mapping by walking
|
|
45
|
+
* GetGenericPortMappingEntry from index 0 until the gateway reports the index
|
|
46
|
+
* is out of range (SOAP error 713 / a non-200 response). */
|
|
47
|
+
export async function listPortMappings(): Promise<PortMapping[]> {
|
|
48
|
+
if (os.platform() === "linux") return [];
|
|
49
|
+
|
|
50
|
+
const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
|
|
51
|
+
|
|
52
|
+
let lastError: unknown;
|
|
53
|
+
for (let controlURL of controlURLs) {
|
|
54
|
+
try {
|
|
55
|
+
const mappings: PortMapping[] = [];
|
|
56
|
+
for (let index = 0; ; index++) {
|
|
57
|
+
const entry = await getGenericPortMappingEntry({
|
|
58
|
+
gatewayIP,
|
|
59
|
+
controlPort,
|
|
60
|
+
controlPath: controlURL,
|
|
61
|
+
index,
|
|
62
|
+
});
|
|
63
|
+
if (!entry) break;
|
|
64
|
+
mappings.push(entry);
|
|
65
|
+
}
|
|
66
|
+
return mappings;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
lastError = e;
|
|
69
|
+
console.error(`Failed to list port mappings using controlURL ${controlURL}`, e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to list port mappings, could not find a working controlURL. Last error: ${(lastError as Error)?.stack ?? lastError}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
9
75
|
export async function forwardPort(config: {
|
|
10
76
|
externalPort: number;
|
|
11
77
|
internalPort: number;
|
|
@@ -18,14 +84,7 @@ export async function forwardPort(config: {
|
|
|
18
84
|
const { externalPort, internalPort } = config;
|
|
19
85
|
let duration = config.duration ?? timeInHour;
|
|
20
86
|
|
|
21
|
-
const
|
|
22
|
-
if (!localObj) throw new Error("Could not find the local address / gateway");
|
|
23
|
-
|
|
24
|
-
const { internalIP, gatewayIP } = localObj;
|
|
25
|
-
console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
|
|
26
|
-
let gateway = await discoverGateway(internalIP);
|
|
27
|
-
let controlURLs = await getControlPaths(gateway);
|
|
28
|
-
let controlPort = Number(new URL(gateway).port);
|
|
87
|
+
const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
|
|
29
88
|
|
|
30
89
|
for (let controlURL of controlURLs) {
|
|
31
90
|
try {
|
|
@@ -49,6 +108,12 @@ export async function forwardPort(config: {
|
|
|
49
108
|
}
|
|
50
109
|
}
|
|
51
110
|
|
|
111
|
+
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
112
|
+
* mapping points at us or at a different machine on the network. */
|
|
113
|
+
export function getLocalInternalIP(): string | undefined {
|
|
114
|
+
return getLocalInterfaceAddress()?.internalIP;
|
|
115
|
+
}
|
|
116
|
+
|
|
52
117
|
function getLocalInterfaceAddress(): { internalIP: string; gatewayIP: string; } | undefined {
|
|
53
118
|
let looksLikeRouter = (ip: string) => ip.startsWith("10.0.0") || ip.startsWith("10.0.1") || ip.startsWith("192.168.0");
|
|
54
119
|
|
|
@@ -216,3 +281,61 @@ async function createPortMapping(config: {
|
|
|
216
281
|
throw new Error(`Failed to create port mapping: ${data}`);
|
|
217
282
|
}
|
|
218
283
|
}
|
|
284
|
+
|
|
285
|
+
// UPnP returns this error code when we walk past the last mapping index, which is how
|
|
286
|
+
// we detect the end of the list rather than a real failure.
|
|
287
|
+
const UPNP_ARRAY_INDEX_INVALID = 713;
|
|
288
|
+
|
|
289
|
+
/** Fetches a single port mapping by its index. Returns undefined once the index is past
|
|
290
|
+
* the end of the table (the router's signal that there are no more entries). */
|
|
291
|
+
async function getGenericPortMappingEntry(config: {
|
|
292
|
+
gatewayIP: string;
|
|
293
|
+
controlPort: number;
|
|
294
|
+
controlPath: string;
|
|
295
|
+
index: number;
|
|
296
|
+
}): Promise<PortMapping | undefined> {
|
|
297
|
+
const { gatewayIP, controlPort, controlPath, index } = config;
|
|
298
|
+
const action = "\"urn:schemas-upnp-org:service:WANIPConnection:1#GetGenericPortMappingEntry\"";
|
|
299
|
+
|
|
300
|
+
const soapBody = `
|
|
301
|
+
<?xml version="1.0"?>
|
|
302
|
+
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
303
|
+
<s:Body>
|
|
304
|
+
<u:GetGenericPortMappingEntry xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
|
305
|
+
<NewPortMappingIndex>${index}</NewPortMappingIndex>
|
|
306
|
+
</u:GetGenericPortMappingEntry>
|
|
307
|
+
</s:Body>
|
|
308
|
+
</s:Envelope>
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
const res = await fetch(`http://${gatewayIP}:${controlPort}${controlPath}`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: {
|
|
314
|
+
"Content-Type": "text/xml; charset=\"utf-8\"",
|
|
315
|
+
"SOAPAction": action,
|
|
316
|
+
"Content-Length": Buffer.byteLength(soapBody) + "",
|
|
317
|
+
},
|
|
318
|
+
body: soapBody
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const data = await res.text();
|
|
322
|
+
if (res.status !== 200) {
|
|
323
|
+
let errorCode = Number(data.match(/<errorCode>(\d+)<\/errorCode>/)?.[1]);
|
|
324
|
+
if (errorCode === UPNP_ARRAY_INDEX_INVALID) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
throw new Error(`Failed to get port mapping entry ${index}: ${res.status} ${data}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let getField = (name: string) => data.match(new RegExp(`<${name}>(.*?)</${name}>`, "s"))?.[1] ?? "";
|
|
331
|
+
return {
|
|
332
|
+
externalPort: Number(getField("NewExternalPort")),
|
|
333
|
+
internalPort: Number(getField("NewInternalPort")),
|
|
334
|
+
protocol: getField("NewProtocol"),
|
|
335
|
+
internalClient: getField("NewInternalClient"),
|
|
336
|
+
remoteHost: getField("NewRemoteHost"),
|
|
337
|
+
enabled: getField("NewEnabled") === "1",
|
|
338
|
+
description: getField("NewPortMappingDescription"),
|
|
339
|
+
leaseDuration: Number(getField("NewLeaseDuration")),
|
|
340
|
+
};
|
|
341
|
+
}
|
package/src/https.d.ts
CHANGED
package/src/lz4/LZ4.d.ts
CHANGED
package/src/misc.d.ts
CHANGED
package/src/tlsParsing.d.ts
CHANGED
package/src/upreal.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/upreal.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as https from "https";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
|
|
6
|
+
// Dependency sections in package.json that upreal is willing to update, in the order we search them.
|
|
7
|
+
const DEP_SECTIONS = [
|
|
8
|
+
"dependencies",
|
|
9
|
+
"devDependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
"peerDependencies",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
let packageName = process.argv.slice(2).find(arg => !arg.startsWith("-"));
|
|
16
|
+
if (!packageName) {
|
|
17
|
+
console.error("Usage: yarn upreal <package-name>");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let projectRoot = process.cwd();
|
|
23
|
+
let packageJsonPath = path.join(projectRoot, "package.json");
|
|
24
|
+
let lockPath = path.join(projectRoot, "yarn.lock");
|
|
25
|
+
|
|
26
|
+
let packageJsonRaw = fs.readFileSync(packageJsonPath, "utf8");
|
|
27
|
+
let packageJson = JSON.parse(packageJsonRaw) as {
|
|
28
|
+
[section: string]: { [name: string]: string } | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let currentRange = findCurrentRange(packageJson, packageName);
|
|
32
|
+
if (currentRange === undefined) {
|
|
33
|
+
console.error(`Package "${packageName}" is not listed in ${DEP_SECTIONS.join(", ")} of package.json`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`Current range for ${packageName}: ${currentRange}`);
|
|
39
|
+
|
|
40
|
+
let latest = await getLatestVersion(packageName);
|
|
41
|
+
console.log(`Latest version of ${packageName}: ${latest}`);
|
|
42
|
+
|
|
43
|
+
let newRange = applyPrefix(currentRange, latest);
|
|
44
|
+
if (newRange === currentRange) {
|
|
45
|
+
console.log(`package.json range unchanged (${currentRange}); will still re-resolve the lockfile.`);
|
|
46
|
+
} else {
|
|
47
|
+
packageJsonRaw = replaceRange(packageJsonRaw, packageName, currentRange, newRange);
|
|
48
|
+
fs.writeFileSync(packageJsonPath, packageJsonRaw);
|
|
49
|
+
console.log(`Updated package.json: ${packageName} ${currentRange} -> ${newRange}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Drop every lockfile entry for this package so `yarn install` re-resolves each of its ranges (ours and any
|
|
53
|
+
// transitive ones) to the newest version they can reach. This is what makes "everything that could be updated"
|
|
54
|
+
// update in one shot, rather than yarn pinning the previously-locked versions.
|
|
55
|
+
if (fs.existsSync(lockPath)) {
|
|
56
|
+
let lockRaw = fs.readFileSync(lockPath, "utf8");
|
|
57
|
+
let { lock, removed } = removeLockEntries(lockRaw, packageName);
|
|
58
|
+
if (removed > 0) {
|
|
59
|
+
fs.writeFileSync(lockPath, lock);
|
|
60
|
+
console.log(`Removed ${removed} lockfile entr${removed === 1 ? "y" : "ies"} for ${packageName} so they re-resolve.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`Running yarn install...`);
|
|
65
|
+
await runYarnInstall(projectRoot);
|
|
66
|
+
console.log(`Done. ${packageName} is now at ${newRange}.`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findCurrentRange(
|
|
70
|
+
packageJson: { [section: string]: { [name: string]: string } | undefined },
|
|
71
|
+
packageName: string
|
|
72
|
+
): string | undefined {
|
|
73
|
+
for (let section of DEP_SECTIONS) {
|
|
74
|
+
let deps = packageJson[section];
|
|
75
|
+
if (deps && packageName in deps) {
|
|
76
|
+
return deps[packageName];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getLatestVersion(packageName: string): Promise<string> {
|
|
83
|
+
// Scoped names ("@scope/name") must have their slash encoded in the registry path.
|
|
84
|
+
let encodedName = packageName.startsWith("@")
|
|
85
|
+
? "@" + encodeURIComponent(packageName.slice(1))
|
|
86
|
+
: encodeURIComponent(packageName);
|
|
87
|
+
let url = `https://registry.npmjs.org/${encodedName}/latest`;
|
|
88
|
+
|
|
89
|
+
let body = await new Promise<string>((resolve, reject) => {
|
|
90
|
+
https.get(url, res => {
|
|
91
|
+
if (res.statusCode !== 200) {
|
|
92
|
+
res.resume();
|
|
93
|
+
reject(new Error(`Registry returned ${res.statusCode} for ${url}`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let chunks: Buffer[] = [];
|
|
97
|
+
res.on("data", chunk => chunks.push(chunk));
|
|
98
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
99
|
+
}).on("error", reject);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let parsed = JSON.parse(body) as { version?: string };
|
|
103
|
+
if (!parsed.version) {
|
|
104
|
+
throw new Error(`Registry response for ${packageName} had no version field`);
|
|
105
|
+
}
|
|
106
|
+
return parsed.version;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Keep whatever range operator the user already chose (^, ~, exact, or *) and point it at the new version.
|
|
110
|
+
function applyPrefix(currentRange: string, latest: string): string {
|
|
111
|
+
if (currentRange === "*" || currentRange === "" || currentRange === "latest") {
|
|
112
|
+
return currentRange;
|
|
113
|
+
}
|
|
114
|
+
if (currentRange.startsWith("^")) {
|
|
115
|
+
return "^" + latest;
|
|
116
|
+
}
|
|
117
|
+
if (currentRange.startsWith("~")) {
|
|
118
|
+
return "~" + latest;
|
|
119
|
+
}
|
|
120
|
+
return latest;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function replaceRange(packageJsonRaw: string, packageName: string, oldRange: string, newRange: string): string {
|
|
124
|
+
// Rewrite the value in place (rather than JSON.stringify) so the file keeps its exact formatting and tab style.
|
|
125
|
+
let keyPattern = new RegExp(`("${escapeRegExp(packageName)}"\\s*:\\s*")${escapeRegExp(oldRange)}(")`);
|
|
126
|
+
if (!keyPattern.test(packageJsonRaw)) {
|
|
127
|
+
throw new Error(`Could not locate "${packageName}": "${oldRange}" in package.json to update`);
|
|
128
|
+
}
|
|
129
|
+
return packageJsonRaw.replace(keyPattern, `$1${newRange}$2`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Remove all yarn.lock v1 blocks that resolve the given package (across every range variant of it).
|
|
133
|
+
function removeLockEntries(lockRaw: string, packageName: string): { lock: string; removed: number } {
|
|
134
|
+
// yarn.lock v1 blocks are separated by blank lines; the first line of each block is the comma-separated
|
|
135
|
+
// list of specs it satisfies, e.g. `"pkg@^1.0.0", "pkg@~1.2.0":`.
|
|
136
|
+
let blocks = lockRaw.split(/\r?\n\r?\n/);
|
|
137
|
+
let removed = 0;
|
|
138
|
+
let kept: string[] = [];
|
|
139
|
+
for (let block of blocks) {
|
|
140
|
+
if (blockResolvesPackage(block, packageName)) {
|
|
141
|
+
removed++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
kept.push(block);
|
|
145
|
+
}
|
|
146
|
+
return { lock: kept.join("\n\n"), removed };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function blockResolvesPackage(block: string, packageName: string): boolean {
|
|
150
|
+
let headerLine = block.split(/\r?\n/).find(line => line.trim().length > 0 && !line.startsWith("#"));
|
|
151
|
+
if (!headerLine || !headerLine.trimEnd().endsWith(":")) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// Strip the trailing colon, then split on commas into individual quoted-or-bare specs.
|
|
155
|
+
let specsPart = headerLine.trimEnd().replace(/:$/, "");
|
|
156
|
+
let specs = specsPart.split(",").map(spec => spec.trim().replace(/^"|"$/g, ""));
|
|
157
|
+
for (let spec of specs) {
|
|
158
|
+
if (specNamePackage(spec) === packageName) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Extract the package name from a lock spec like `@scope/name@^1.0.0` or `name@~1.2.0`.
|
|
166
|
+
function specNamePackage(spec: string): string {
|
|
167
|
+
let atIndex = spec.lastIndexOf("@");
|
|
168
|
+
// A leading "@" (scoped package) is part of the name, not the name/range separator.
|
|
169
|
+
if (atIndex <= 0) {
|
|
170
|
+
return spec;
|
|
171
|
+
}
|
|
172
|
+
return spec.slice(0, atIndex);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function escapeRegExp(value: string): string {
|
|
176
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function runYarnInstall(cwd: string): Promise<void> {
|
|
180
|
+
await new Promise<void>((resolve, reject) => {
|
|
181
|
+
// yarn is a .cmd shim on Windows, so it must be spawned through the shell.
|
|
182
|
+
let child = spawn("yarn", ["install"], { cwd, stdio: "inherit", shell: true });
|
|
183
|
+
child.on("error", reject);
|
|
184
|
+
child.on("exit", code => {
|
|
185
|
+
if (code === 0) {
|
|
186
|
+
resolve();
|
|
187
|
+
} else {
|
|
188
|
+
reject(new Error(`yarn install exited with code ${code}`));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
main().catch(e => {
|
|
195
|
+
console.error(e.stack ?? e);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
});
|
package/src/webSocketServer.d.ts
CHANGED
package/src/webSocketServer.ts
CHANGED
|
@@ -12,13 +12,14 @@ import debugbreak from "debugbreak";
|
|
|
12
12
|
import { getNodeId } from "./nodeCache";
|
|
13
13
|
import crypto from "crypto";
|
|
14
14
|
import { Watchable, getRootDomain, timeInHour, timeInMinute } from "./misc";
|
|
15
|
-
import { delay, runInfinitePoll
|
|
15
|
+
import { delay, runInfinitePoll } from "./batching";
|
|
16
16
|
import { magenta, red } from "./formatting/logColors";
|
|
17
17
|
import { yellow } from "./formatting/logColors";
|
|
18
18
|
import { green } from "./formatting/logColors";
|
|
19
19
|
import { formatTime } from "./formatting/format";
|
|
20
20
|
import { getExternalIP, testTCPIsListening } from "./networking";
|
|
21
|
-
import { forwardPort } from "./forwardPort";
|
|
21
|
+
import { forwardPort, listPortMappings, getLocalInternalIP, PortMapping } from "./forwardPort";
|
|
22
|
+
import os from "os";
|
|
22
23
|
|
|
23
24
|
// When a requested port is taken and useAvailablePortIfPortInUse is set, we scan
|
|
24
25
|
// upwards from this base instead of binding a random OS-assigned port, so restarts
|
|
@@ -348,49 +349,103 @@ export async function startSocketServer(
|
|
|
348
349
|
});
|
|
349
350
|
}
|
|
350
351
|
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
352
|
+
// Frees the currently-bound listening socket so we can rebind on a different port
|
|
353
|
+
// (used when a locally-free port turns out to be unusable for forwarding).
|
|
354
|
+
async function releasePort(): Promise<void> {
|
|
355
|
+
await new Promise<void>((resolve, reject) => {
|
|
356
|
+
if (!realServer.listening) {
|
|
357
|
+
resolve();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
realServer.close(e => e ? reject(e) : resolve());
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Forwarding maps the external port to an equal internal port, so a candidate is only
|
|
365
|
+
// usable if we can also own its external mapping on the router. Skipped on linux,
|
|
366
|
+
// where forwardPort is a no-op anyway.
|
|
367
|
+
const doForward = !!(config.autoForwardPort && config.public && os.platform() !== "linux");
|
|
368
|
+
|
|
369
|
+
// Ensures the router's external mapping for `externalPort` belongs to us. Returns true
|
|
370
|
+
// if it's ours to keep (existing-and-ours → refresh the lease; free → create and
|
|
371
|
+
// confirm we won it), false if another machine owns it and we should try a new port.
|
|
372
|
+
async function claimPortForward(externalPort: number): Promise<boolean> {
|
|
373
|
+
const ourIP = getLocalInternalIP();
|
|
374
|
+
const matches = (m: PortMapping) => m.externalPort === externalPort && m.protocol.toUpperCase() === "TCP";
|
|
375
|
+
|
|
376
|
+
const existing = (await listPortMappings()).find(matches);
|
|
377
|
+
if (existing) {
|
|
378
|
+
if (ourIP && existing.internalClient === ourIP) {
|
|
379
|
+
await forwardPort({ externalPort, internalPort: externalPort });
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
console.log(magenta(`External port ${externalPort} is already forwarded to ${existing.internalClient}, trying another port`));
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Free right now — create the mapping, then re-read it to make sure another host
|
|
387
|
+
// didn't grab the same external port in the race between our list and our create.
|
|
388
|
+
await forwardPort({ externalPort, internalPort: externalPort });
|
|
389
|
+
const ours = (await listPortMappings()).find(matches);
|
|
390
|
+
if (!ours || (ourIP && ours.internalClient !== ourIP)) {
|
|
391
|
+
console.log(magenta(`Failed to claim external port ${externalPort} (now ${ours ? `forwarded to ${ours.internalClient}` : "still unmapped"}), trying another port`));
|
|
392
|
+
return false;
|
|
359
393
|
}
|
|
394
|
+
return true;
|
|
360
395
|
}
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
|
|
396
|
+
|
|
397
|
+
// Candidate ports: an explicitly requested port first (a falsy port, the default 0,
|
|
398
|
+
// means "no preference"), then a consistent upward scan so restarts are predictable.
|
|
399
|
+
function* candidatePorts(): Generator<number> {
|
|
400
|
+
if (port) yield port;
|
|
364
401
|
for (let candidate = PORT_SCAN_START; candidate < PORT_SCAN_START + PORT_SCAN_COUNT; candidate++) {
|
|
365
402
|
if (candidate === port) continue;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
403
|
+
yield candidate;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let bound = false;
|
|
408
|
+
for (const candidate of candidatePorts()) {
|
|
409
|
+
if (!await tryListen(candidate)) {
|
|
410
|
+
if (candidate === config.port && !config.useAvailablePortIfPortInUse) {
|
|
411
|
+
throw new Error(`Port ${candidate} is already in use (set useAvailablePortIfPortInUse to fall back to another port)`);
|
|
370
412
|
}
|
|
413
|
+
continue;
|
|
371
414
|
}
|
|
372
|
-
|
|
373
|
-
|
|
415
|
+
// Locally bound. If we also forward, the external mapping must be ours too, or we
|
|
416
|
+
// release this port and keep scanning.
|
|
417
|
+
if (doForward) {
|
|
418
|
+
let claimed: boolean;
|
|
419
|
+
try {
|
|
420
|
+
claimed = await claimPortForward(candidate);
|
|
421
|
+
} catch (e) {
|
|
422
|
+
// UPnP unavailable (no gateway / discovery failed): fall back to best-effort
|
|
423
|
+
// forwarding rather than refusing to start.
|
|
424
|
+
console.error(red(`Could not verify forwarding for port ${candidate}, continuing best-effort: ${(e as Error).stack}`));
|
|
425
|
+
claimed = true;
|
|
426
|
+
}
|
|
427
|
+
if (!claimed) {
|
|
428
|
+
await releasePort();
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
374
431
|
}
|
|
432
|
+
port = candidate;
|
|
433
|
+
bound = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (!bound) {
|
|
437
|
+
throw new Error(`Could not find an available port in range ${PORT_SCAN_START}-${PORT_SCAN_START + PORT_SCAN_COUNT - 1} (requested ${config.port})`);
|
|
375
438
|
}
|
|
376
439
|
|
|
377
440
|
port = (realServer.address() as net.AddressInfo).port;
|
|
378
441
|
|
|
379
|
-
if (
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
// if (!isListening) {
|
|
383
|
-
// console.log(magenta(`Port ${port} is not externally reachable, trying to forward it`));
|
|
384
|
-
// await forwardPort({ externalPort: port, internalPort: port });
|
|
385
|
-
// }
|
|
386
|
-
// Even if they are listening, they might not stay listening. Forward every 8 hours
|
|
387
|
-
// (including at the start, in case the forward is about to expire).
|
|
388
|
-
async function forward() {
|
|
442
|
+
if (doForward) {
|
|
443
|
+
// The mapping is claimed above; keep refreshing the lease so it doesn't expire.
|
|
444
|
+
async function refreshForward() {
|
|
389
445
|
await forwardPort({ externalPort: port, internalPort: port });
|
|
390
|
-
console.log(magenta(`
|
|
446
|
+
console.log(magenta(`Refreshed port forward ${port} to our machine`));
|
|
391
447
|
}
|
|
392
|
-
|
|
393
|
-
runInfinitePollCallAtStart(timeInMinute * 30, forward).catch(e => console.error(red(`Error in port forwarding ${e.stack}`)));
|
|
448
|
+
runInfinitePoll(timeInMinute * 30, refreshForward);
|
|
394
449
|
}
|
|
395
450
|
|
|
396
451
|
let nodeId = getNodeId(getCommonName(config.cert), port);
|
package/test.ts
CHANGED
|
@@ -1,161 +1,17 @@
|
|
|
1
|
-
import { computeTweenedOffset, getTimeComponentsDetailed, waitForFirstTimeSync } from "./time/trueTimeShim";
|
|
2
1
|
import * as fs from "fs";
|
|
3
2
|
import * as path from "path";
|
|
3
|
+
import { listPortMappings } from "./src/forwardPort";
|
|
4
4
|
|
|
5
|
-
const SAMPLE_COUNT = 1000;
|
|
6
|
-
const SAMPLE_INTERVAL_MS = 10;
|
|
7
|
-
|
|
8
|
-
// Generate a simple numeric ID for this test run
|
|
9
|
-
const TEST_RUN_ID = Math.floor(Math.random() * 10000);
|
|
10
|
-
|
|
11
|
-
async function sampleMode() {
|
|
12
|
-
await waitForFirstTimeSync();
|
|
13
|
-
|
|
14
|
-
type Sample = {
|
|
15
|
-
id: number;
|
|
16
|
-
systemTime: number;
|
|
17
|
-
offset: number;
|
|
18
|
-
fromOffset: number;
|
|
19
|
-
toOffset: number;
|
|
20
|
-
fromTime: number;
|
|
21
|
-
toTime: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const samples: Sample[] = [];
|
|
25
|
-
|
|
26
|
-
console.log(`[Test ID: ${TEST_RUN_ID}] Sampling ${SAMPLE_COUNT} time components...`);
|
|
27
|
-
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
|
28
|
-
const detailed = getTimeComponentsDetailed();
|
|
29
|
-
|
|
30
|
-
// Calculate the tween using the same systemTime
|
|
31
|
-
const offset = computeTweenedOffset(detailed);
|
|
32
|
-
|
|
33
|
-
samples.push({
|
|
34
|
-
id: TEST_RUN_ID,
|
|
35
|
-
systemTime: detailed.systemTime,
|
|
36
|
-
offset,
|
|
37
|
-
fromOffset: detailed.fromOffset,
|
|
38
|
-
toOffset: detailed.toOffset,
|
|
39
|
-
fromTime: detailed.fromTime,
|
|
40
|
-
toTime: detailed.toTime,
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (SAMPLE_INTERVAL_MS > 0) {
|
|
44
|
-
await new Promise(resolve => setTimeout(resolve, SAMPLE_INTERVAL_MS));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Write to file with unique name
|
|
49
|
-
const distDir = path.join(__dirname, "dist");
|
|
50
|
-
if (!fs.existsSync(distDir)) {
|
|
51
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const filename = path.join(distDir, `time-samples-${Date.now()}-${process.pid}.json`);
|
|
55
|
-
fs.writeFileSync(filename, JSON.stringify(samples, null, 2));
|
|
56
|
-
|
|
57
|
-
console.log(`[Test ID: ${TEST_RUN_ID}] Wrote ${samples.length} samples to ${filename}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function verifyMode() {
|
|
61
|
-
const distDir = path.join(__dirname, "dist");
|
|
62
|
-
if (!fs.existsSync(distDir)) {
|
|
63
|
-
console.error("No dist directory found. Run sampling mode first.");
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const files = fs.readdirSync(distDir)
|
|
68
|
-
.filter(f => f.startsWith("time-samples-") && f.endsWith(".json"))
|
|
69
|
-
.map(f => path.join(distDir, f));
|
|
70
|
-
|
|
71
|
-
if (files.length === 0) {
|
|
72
|
-
console.error("No sample files found. Run sampling mode first.");
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log(`Found ${files.length} sample files`);
|
|
77
|
-
|
|
78
|
-
type Sample = {
|
|
79
|
-
id: number;
|
|
80
|
-
systemTime: number;
|
|
81
|
-
offset: number;
|
|
82
|
-
fromOffset: number;
|
|
83
|
-
toOffset: number;
|
|
84
|
-
fromTime: number;
|
|
85
|
-
toTime: number;
|
|
86
|
-
file: string;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Load all samples from all files
|
|
90
|
-
const allSamples: Sample[] = [];
|
|
91
|
-
|
|
92
|
-
for (const file of files) {
|
|
93
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
94
|
-
const samples = JSON.parse(content);
|
|
95
|
-
for (const sample of samples) {
|
|
96
|
-
allSamples.push({ ...sample, file: path.basename(file) });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
console.log(`Loaded ${allSamples.length} total samples`);
|
|
101
|
-
|
|
102
|
-
// Sort by systemTime
|
|
103
|
-
allSamples.sort((a, b) => a.systemTime - b.systemTime);
|
|
104
|
-
|
|
105
|
-
// Verify that systemTime + offset is monotonically increasing
|
|
106
|
-
let errors = 0;
|
|
107
|
-
let lastTrueTime = -Infinity;
|
|
108
|
-
|
|
109
|
-
for (let i = 0; i < allSamples.length; i++) {
|
|
110
|
-
const sample = allSamples[i];
|
|
111
|
-
const trueTime = sample.systemTime + sample.offset;
|
|
112
|
-
|
|
113
|
-
if (trueTime < lastTrueTime) {
|
|
114
|
-
errors++;
|
|
115
|
-
const prev = allSamples[i - 1];
|
|
116
|
-
const prevTrueTime = prev.systemTime + prev.offset;
|
|
117
|
-
|
|
118
|
-
console.error(`\nERROR at index ${i}: trueTime went backwards!`);
|
|
119
|
-
console.error(` Previous [ID: ${prev.id}, file: ${prev.file}]:`);
|
|
120
|
-
console.error(` systemTime: ${prev.systemTime}`);
|
|
121
|
-
console.error(` offset: ${prev.offset}`);
|
|
122
|
-
console.error(` trueTime: ${prevTrueTime}`);
|
|
123
|
-
console.error(` tween: ${prev.fromOffset} -> ${prev.toOffset}`);
|
|
124
|
-
console.error(` timeWindow: ${prev.fromTime} -> ${prev.toTime}`);
|
|
125
|
-
console.error(` Current [ID: ${sample.id}, file: ${sample.file}]:`);
|
|
126
|
-
console.error(` systemTime: ${sample.systemTime}`);
|
|
127
|
-
console.error(` offset: ${sample.offset}`);
|
|
128
|
-
console.error(` trueTime: ${trueTime}`);
|
|
129
|
-
console.error(` tween: ${sample.fromOffset} -> ${sample.toOffset}`);
|
|
130
|
-
console.error(` timeWindow: ${sample.fromTime} -> ${sample.toTime}`);
|
|
131
|
-
console.error(` Difference: ${trueTime - lastTrueTime}ms`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
lastTrueTime = trueTime;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (errors === 0) {
|
|
138
|
-
console.log(`✓ SUCCESS: All ${allSamples.length} samples are correctly ordered!`);
|
|
139
|
-
console.log(` Time range: ${allSamples[0].systemTime} to ${allSamples[allSamples.length - 1].systemTime}`);
|
|
140
|
-
console.log(` Duration: ${allSamples[allSamples.length - 1].systemTime - allSamples[0].systemTime}ms`);
|
|
141
|
-
} else {
|
|
142
|
-
console.error(`✗ FAILED: Found ${errors} ordering violations`);
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
5
|
|
|
147
6
|
async function main() {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
} else {
|
|
157
|
-
await sampleMode();
|
|
158
|
-
}
|
|
7
|
+
let mappings = await listPortMappings();
|
|
8
|
+
console.log(`Found ${mappings.length} port mapping(s):`);
|
|
9
|
+
for (let mapping of mappings) {
|
|
10
|
+
let host = mapping.remoteHost || "*";
|
|
11
|
+
let lease = mapping.leaseDuration === 0 ? "permanent" : `${mapping.leaseDuration}s left`;
|
|
12
|
+
console.log(` ${mapping.protocol} ${host}:${mapping.externalPort} -> ${mapping.internalClient}:${mapping.internalPort} (${mapping.enabled ? "enabled" : "disabled"}, ${lease})${mapping.description ? ` "${mapping.description}"` : ""}`);
|
|
13
|
+
}
|
|
14
|
+
console.table(mappings);
|
|
159
15
|
}
|
|
160
16
|
|
|
161
|
-
main().catch(e => console.error(e)).finally(() => process.exit(0));
|
|
17
|
+
main().catch(e => console.error(e)).finally(() => process.exit(0));
|