socket-function 0.13.0 → 0.15.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/SocketFunction.ts +19 -1
- package/SocketFunctionTypes.ts +1 -0
- package/hot/HotReloadController.ts +19 -2
- package/package.json +3 -3
- package/require/RequireController.ts +10 -0
- package/require/require.js +6 -0
- package/src/callManager.ts +1 -1
- package/src/formatting/format.ts +7 -1
- package/src/formatting/logColors.ts +1 -0
- package/src/forwardPort.ts +157 -0
- package/src/https.ts +115 -0
- package/src/misc.ts +52 -40
- package/src/networking.ts +44 -0
- package/src/profiling/measure.ts +3 -1
- package/src/sniTest.ts +101 -0
- package/src/tlsParsing.ts +8 -2
- package/src/webSocketServer.ts +72 -31
- package/test.ts +33 -0
- package/time/trueTimeShim.ts +3 -1
package/SocketFunction.ts
CHANGED
|
@@ -25,6 +25,18 @@ setImmediate(async () => {
|
|
|
25
25
|
|
|
26
26
|
setFlag(require, "cbor-x", "allowclient", true);
|
|
27
27
|
let cborxInstance = new cborx.Encoder({ structuredClone: true });
|
|
28
|
+
if (isNode()) {
|
|
29
|
+
// Do not crash on unhandled errors. SocketFunction is made to run a webserver,
|
|
30
|
+
// which will run perfectly after 99.9% of errors. Crashing the process is
|
|
31
|
+
// not a good alternative to proper error log and notifications. Do you guys
|
|
32
|
+
// not get automated emails when unexpected errors are logged? I do.
|
|
33
|
+
process.on("unhandledRejection", (e) => {
|
|
34
|
+
console.error("Unhandled rejection", e);
|
|
35
|
+
});
|
|
36
|
+
process.on("uncaughtException", (e) => {
|
|
37
|
+
console.error("Uncaught exception", e);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
28
40
|
|
|
29
41
|
module.allowclient = true;
|
|
30
42
|
|
|
@@ -72,6 +84,7 @@ export class SocketFunction {
|
|
|
72
84
|
return caller;
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
private static getShapeHotReloadable = new Map<string, () => SocketExposedShape<SocketExposedInterface>>();
|
|
75
88
|
// NOTE: We use callbacks we don't run into issues with cyclic dependencies
|
|
76
89
|
// (ex, using a hook in a controller where the hook also calls the controller).
|
|
77
90
|
public static register<
|
|
@@ -107,7 +120,9 @@ export class SocketFunction {
|
|
|
107
120
|
|
|
108
121
|
for (let value of Object.values(shape)) {
|
|
109
122
|
if (!value) continue;
|
|
110
|
-
|
|
123
|
+
if (!value.noClientHooks) {
|
|
124
|
+
value.clientHooks = [...(defaultHooks?.clientHooks || []), ...(value.clientHooks || [])];
|
|
125
|
+
}
|
|
111
126
|
if (value.noDefaultHooks) {
|
|
112
127
|
value.hooks = [...(value.hooks || [])];
|
|
113
128
|
} else {
|
|
@@ -118,6 +133,9 @@ export class SocketFunction {
|
|
|
118
133
|
return shape as any as SocketExposedShape;
|
|
119
134
|
});
|
|
120
135
|
|
|
136
|
+
// Wait, so any constants referenced by the base shapeFnc will be fully resolved
|
|
137
|
+
// by now. This is IMPORTANT, as it allows permissions functions to be moved
|
|
138
|
+
// to a common module, instead of all being inline.
|
|
121
139
|
void Promise.resolve().then(() => {
|
|
122
140
|
registerClass(classGuid, instance as SocketExposedInterface, getShape(), {
|
|
123
141
|
noFunctionMeasure: config?.noFunctionMeasure,
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -44,12 +44,17 @@ declare global {
|
|
|
44
44
|
noserverhotreload?: boolean;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
var isHotReloading: (() => boolean) | undefined;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
let isHotReloadingValue = false;
|
|
50
51
|
export function isHotReloading() {
|
|
51
52
|
return isHotReloadingValue;
|
|
52
53
|
}
|
|
54
|
+
globalThis.isHotReloading = isHotReloading;
|
|
55
|
+
export function hotReloadingGuard(): true {
|
|
56
|
+
return !isHotReloading() as any;
|
|
57
|
+
}
|
|
53
58
|
export function setExternalHotReloading(value: boolean) {
|
|
54
59
|
isHotReloadingValue = value;
|
|
55
60
|
}
|
|
@@ -86,9 +91,14 @@ const hotReloadModule = cache((module: NodeJS.Module) => {
|
|
|
86
91
|
console.error(red(`Error hot reloading ${module.id}`));
|
|
87
92
|
console.error(e);
|
|
88
93
|
} finally {
|
|
89
|
-
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
isHotReloadingValue = false;
|
|
96
|
+
}, 1000);
|
|
90
97
|
}
|
|
91
98
|
}
|
|
99
|
+
for (let callback of hotReloadCallbacks) {
|
|
100
|
+
callback([module]);
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
103
|
triggerClientSideReload({
|
|
94
104
|
files: [module.filename],
|
|
@@ -149,7 +159,14 @@ class HotReloadControllerBase {
|
|
|
149
159
|
for (let module of modules) {
|
|
150
160
|
module.loaded = false;
|
|
151
161
|
}
|
|
152
|
-
|
|
162
|
+
isHotReloadingValue = true;
|
|
163
|
+
try {
|
|
164
|
+
await Promise.all(modules.map(module => module.load(module.filename)));
|
|
165
|
+
} finally {
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
isHotReloadingValue = false;
|
|
168
|
+
}, 1000);
|
|
169
|
+
}
|
|
153
170
|
|
|
154
171
|
for (let callback of hotReloadCallbacks) {
|
|
155
172
|
callback(modules);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"@types/pako": "^2.0.3",
|
|
9
9
|
"@types/ws": "^8.5.3",
|
|
10
10
|
"cbor-x": "^1.5.6",
|
|
11
|
-
"cookie": "^0.5.0",
|
|
12
11
|
"mobx": "^6.6.2",
|
|
13
12
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
14
13
|
"pako": "^2.1.0",
|
|
@@ -21,7 +20,8 @@
|
|
|
21
20
|
},
|
|
22
21
|
"scripts": {
|
|
23
22
|
"test": "yarn typenode ./test/server.ts",
|
|
24
|
-
"type": "yarn tsc --noEmit"
|
|
23
|
+
"type": "yarn tsc --noEmit",
|
|
24
|
+
"testsni": "yarn typenode ./src/sniTest.ts"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/cookie": "^0.5.1",
|
|
@@ -69,6 +69,10 @@ export interface SerializedModule {
|
|
|
69
69
|
|
|
70
70
|
size?: number;
|
|
71
71
|
version?: number;
|
|
72
|
+
|
|
73
|
+
flags?: {
|
|
74
|
+
[flag: string]: true;
|
|
75
|
+
};
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
let nextModuleSeqNum = 1;
|
|
@@ -203,7 +207,13 @@ class RequireControllerBase {
|
|
|
203
207
|
size: module.size,
|
|
204
208
|
version: module.version,
|
|
205
209
|
asyncRequests: module.asyncRequires,
|
|
210
|
+
flags: {},
|
|
206
211
|
};
|
|
212
|
+
for (let [flag, value] of Object.entries(module)) {
|
|
213
|
+
if (value === true) {
|
|
214
|
+
modules[module.filename].flags![flag] = value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
207
217
|
let moduleObj = modules[module.filename];
|
|
208
218
|
if (moduleObj.allowclient) {
|
|
209
219
|
moduleObj.source = module.moduleContents;
|
package/require/require.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
|
|
4
4
|
let startTime = Date.now();
|
|
5
5
|
|
|
6
|
+
Symbol.dispose = Symbol.dispose || Symbol("dispose");
|
|
7
|
+
Symbol.asyncDispose = Symbol.asyncDispose || Symbol("asyncDispose");
|
|
8
|
+
|
|
6
9
|
// Globals
|
|
7
10
|
Object.assign(window, {
|
|
8
11
|
process: {
|
|
@@ -397,6 +400,9 @@
|
|
|
397
400
|
module.exports = {};
|
|
398
401
|
module.exports.default = module.exports;
|
|
399
402
|
module.children = [];
|
|
403
|
+
for (let key in serializedModule.flags || {}) {
|
|
404
|
+
module[key] = true;
|
|
405
|
+
}
|
|
400
406
|
|
|
401
407
|
module.load = load;
|
|
402
408
|
|
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 (classes[classGuid]) {
|
|
72
|
+
if (!isHotReloading?.() && classes[classGuid]) {
|
|
73
73
|
throw new Error(`Class ${classGuid} already registered`);
|
|
74
74
|
}
|
|
75
75
|
|
package/src/formatting/format.ts
CHANGED
|
@@ -154,9 +154,15 @@ export function formatNumber(count: number | undefined, maxAbsoluteValue?: numbe
|
|
|
154
154
|
} else if (maxAbsoluteValue < 1000 * 1000 * 1000 * extraFactor) {
|
|
155
155
|
suffix = "M";
|
|
156
156
|
divisor = 1000 * 1000;
|
|
157
|
-
} else {
|
|
157
|
+
} else if (maxAbsoluteValue < 1000 * 1000 * 1000 * 1000 * extraFactor) {
|
|
158
158
|
suffix = "B";
|
|
159
159
|
divisor = 1000 * 1000 * 1000;
|
|
160
|
+
} else if (maxAbsoluteValue < 1000 * 1000 * 1000 * 1000 * 1000 * extraFactor) {
|
|
161
|
+
suffix = "T";
|
|
162
|
+
divisor = 1000 * 1000 * 1000 * 1000;
|
|
163
|
+
} else {
|
|
164
|
+
suffix = "Q";
|
|
165
|
+
divisor = 1000 * 1000 * 1000 * 1000 * 1000;
|
|
160
166
|
}
|
|
161
167
|
count /= divisor;
|
|
162
168
|
maxAbsoluteValue /= divisor;
|
|
@@ -14,5 +14,6 @@ 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`;
|
|
16
16
|
export const white = ansiHSL.bind(null, 0, 0, 80);
|
|
17
|
+
export const gray = ansiHSL.bind(null, 0, 0, 50);
|
|
17
18
|
|
|
18
19
|
export const magenta = (text: string) => `\x1b[35m${text}\x1b[0m`;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as dgram from "dgram";
|
|
2
|
+
import os from "os";
|
|
3
|
+
|
|
4
|
+
const SSDP_DISCOVER_MX = 2;
|
|
5
|
+
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`;
|
|
6
|
+
|
|
7
|
+
export async function forwardPort(config: {
|
|
8
|
+
externalPort: number;
|
|
9
|
+
internalPort: number;
|
|
10
|
+
}) {
|
|
11
|
+
const { externalPort, internalPort } = config;
|
|
12
|
+
|
|
13
|
+
const localObj = getLocalInterfaceAddress();
|
|
14
|
+
if (!localObj) throw new Error("Could not find the local address / gateway");
|
|
15
|
+
|
|
16
|
+
const { internalIP, gatewayIP } = localObj;
|
|
17
|
+
let gateway = await discoverGateway(internalIP);
|
|
18
|
+
let controlURLs = await getControlPaths(gateway);
|
|
19
|
+
let controlPort = Number(new URL(gateway).port);
|
|
20
|
+
|
|
21
|
+
for (let controlURL of controlURLs) {
|
|
22
|
+
try {
|
|
23
|
+
await createPortMapping({
|
|
24
|
+
externalPort, internalPort,
|
|
25
|
+
gatewayIP,
|
|
26
|
+
controlPort,
|
|
27
|
+
controlPath: controlURL,
|
|
28
|
+
internalIP,
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.error("Failed to create port mapping, could not find controlURL");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getLocalInterfaceAddress(): { internalIP: string; gatewayIP: string; } | undefined {
|
|
39
|
+
const interfaces = os.networkInterfaces() as any;
|
|
40
|
+
for (const name of Object.keys(interfaces)) {
|
|
41
|
+
for (const iface of interfaces[name]) {
|
|
42
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
43
|
+
// TOOD: Correctly resolve the cidr?
|
|
44
|
+
let gatewayIP = iface.cidr.split(".").slice(0, 3).join(".") + ".1";
|
|
45
|
+
// TOOD: We try discovery on all gateways, so we can know for sure which one it is
|
|
46
|
+
// (and maybe even port forward all gateway, if multiple respond?)
|
|
47
|
+
if (gatewayIP.startsWith("10.0.0") || gatewayIP.startsWith("10.0.1") || gatewayIP.startsWith("192.168.0")) {
|
|
48
|
+
return { internalIP: iface.address, gatewayIP };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function discoverGateway(localAddress: string): Promise<string> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const socket = dgram.createSocket("udp4");
|
|
59
|
+
let isResolved = false;
|
|
60
|
+
|
|
61
|
+
if (!localAddress) {
|
|
62
|
+
reject(new Error("Could not find a suitable local address"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
socket.on("message", (msg) => {
|
|
67
|
+
const response = msg.toString();
|
|
68
|
+
const location = response.match(/LOCATION: (.*)\r\n/i);
|
|
69
|
+
if (location && location[1]) {
|
|
70
|
+
isResolved = true;
|
|
71
|
+
socket.close();
|
|
72
|
+
resolve(location[1]);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
socket.on("error", (err) => {
|
|
77
|
+
socket.close();
|
|
78
|
+
if (!isResolved) {
|
|
79
|
+
reject(err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
socket.on("listening", () => {
|
|
84
|
+
socket.addMembership("239.255.255.250", localAddress);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
socket.bind({ address: localAddress }, () => {
|
|
88
|
+
socket.setBroadcast(true);
|
|
89
|
+
socket.send(SSDP_DISCOVER_MSG, 0, SSDP_DISCOVER_MSG.length, 1900, "239.255.255.250");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
if (!isResolved) {
|
|
94
|
+
socket.close();
|
|
95
|
+
reject(new Error(`SSDP discovery timeout. Search on ${localAddress}`));
|
|
96
|
+
}
|
|
97
|
+
}, SSDP_DISCOVER_MX * 1000);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function getControlPaths(gateway: string) {
|
|
102
|
+
let xml = await (await fetch(gateway)).text();
|
|
103
|
+
const controlURLRegex = /<controlURL>(.*?)<\/controlURL>/g;
|
|
104
|
+
const matches = [];
|
|
105
|
+
let match;
|
|
106
|
+
while ((match = controlURLRegex.exec(xml)) !== null) {
|
|
107
|
+
matches.push(match[1]);
|
|
108
|
+
}
|
|
109
|
+
matches.reverse();
|
|
110
|
+
return matches;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function createPortMapping(config: {
|
|
114
|
+
externalPort: number;
|
|
115
|
+
internalPort: number;
|
|
116
|
+
gatewayIP: string;
|
|
117
|
+
controlPort: number;
|
|
118
|
+
controlPath: string;
|
|
119
|
+
internalIP: string;
|
|
120
|
+
|
|
121
|
+
}): Promise<void> {
|
|
122
|
+
const { externalPort, internalPort, internalIP, controlPath, controlPort, gatewayIP } = config;
|
|
123
|
+
const action = "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"";
|
|
124
|
+
|
|
125
|
+
const soapBody = `
|
|
126
|
+
<?xml version="1.0"?>
|
|
127
|
+
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
|
128
|
+
<s:Body>
|
|
129
|
+
<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
|
130
|
+
<NewRemoteHost></NewRemoteHost>
|
|
131
|
+
<NewExternalPort>${externalPort}</NewExternalPort>
|
|
132
|
+
<NewProtocol>TCP</NewProtocol>
|
|
133
|
+
<NewInternalPort>${internalPort}</NewInternalPort>
|
|
134
|
+
<NewInternalClient>${internalIP}</NewInternalClient>
|
|
135
|
+
<NewEnabled>1</NewEnabled>
|
|
136
|
+
<NewPortMappingDescription>My Port Mapping</NewPortMappingDescription>
|
|
137
|
+
<NewLeaseDuration>0</NewLeaseDuration>
|
|
138
|
+
</u:AddPortMapping>
|
|
139
|
+
</s:Body>
|
|
140
|
+
</s:Envelope>
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const res = await fetch(`http://${gatewayIP}:${controlPort}${controlPath}`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "text/xml; charset=\"utf-8\"",
|
|
147
|
+
"SOAPAction": action,
|
|
148
|
+
"Content-Length": Buffer.byteLength(soapBody) + "",
|
|
149
|
+
},
|
|
150
|
+
body: soapBody
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (res.status !== 200) {
|
|
154
|
+
const data = await res.text();
|
|
155
|
+
throw new Error(`Failed to create port mapping: ${data}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/src/https.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as https from "https";
|
|
2
|
+
import * as http from "http";
|
|
3
|
+
import { isNode } from "./misc";
|
|
4
|
+
|
|
5
|
+
const textEncoder = new TextEncoder();
|
|
6
|
+
const textDecoder = new TextDecoder();
|
|
7
|
+
|
|
8
|
+
export function httpsRequest(
|
|
9
|
+
url: string,
|
|
10
|
+
payload?: Buffer | Buffer[],
|
|
11
|
+
method = "GET",
|
|
12
|
+
sendSessionCookies = true,
|
|
13
|
+
config?: {
|
|
14
|
+
headers?: { [key: string]: string },
|
|
15
|
+
}
|
|
16
|
+
): Promise<Buffer> {
|
|
17
|
+
if (isNode()) {
|
|
18
|
+
return (async () => {
|
|
19
|
+
let urlObj = new URL(url);
|
|
20
|
+
|
|
21
|
+
let requestor = url.startsWith("https") ? https : http;
|
|
22
|
+
let port = url.startsWith("https") ? 443 : 80;
|
|
23
|
+
if (urlObj.port) {
|
|
24
|
+
port = +urlObj.port;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
28
|
+
let httpRequest = requestor.request(
|
|
29
|
+
urlObj + "",
|
|
30
|
+
{
|
|
31
|
+
method,
|
|
32
|
+
headers: config?.headers,
|
|
33
|
+
},
|
|
34
|
+
async httpResponse => {
|
|
35
|
+
let data: Buffer[] = [];
|
|
36
|
+
httpResponse.on("data", chunk => {
|
|
37
|
+
data.push(chunk);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await new Promise<void>(resolve => {
|
|
41
|
+
httpResponse.on("end", () => {
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!httpResponse.statusCode?.toString().startsWith("2")) {
|
|
47
|
+
reject(new Error(`Error for ${url}, ${httpResponse.statusCode} ${httpResponse.statusMessage}\n` + Buffer.concat(data).toString()));
|
|
48
|
+
} else {
|
|
49
|
+
resolve(Buffer.concat(data));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
httpRequest.on("error", reject);
|
|
54
|
+
|
|
55
|
+
if (payload) {
|
|
56
|
+
if (Array.isArray(payload)) {
|
|
57
|
+
payload = Buffer.concat(payload);
|
|
58
|
+
}
|
|
59
|
+
httpRequest.write(payload);
|
|
60
|
+
}
|
|
61
|
+
httpRequest.end();
|
|
62
|
+
});
|
|
63
|
+
})();
|
|
64
|
+
|
|
65
|
+
} else {
|
|
66
|
+
var request = new XMLHttpRequest();
|
|
67
|
+
request.open(method, url, true);
|
|
68
|
+
if (config?.headers) {
|
|
69
|
+
for (let key in config.headers) {
|
|
70
|
+
request.setRequestHeader(key, config.headers[key]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
request.responseType = "arraybuffer";
|
|
74
|
+
request.withCredentials = sendSessionCookies;
|
|
75
|
+
if (payload) {
|
|
76
|
+
if (Array.isArray(payload)) {
|
|
77
|
+
payload = Buffer.concat(payload);
|
|
78
|
+
}
|
|
79
|
+
request.send(payload);
|
|
80
|
+
} else {
|
|
81
|
+
request.send();
|
|
82
|
+
}
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
request.onload = () => {
|
|
85
|
+
if (request.status !== 200) {
|
|
86
|
+
try {
|
|
87
|
+
// It should be an error.stack. But if it isn't... just throw the status text...
|
|
88
|
+
let responseText = textDecoder.decode(request.response);
|
|
89
|
+
let message = responseText.split("\n")[0];
|
|
90
|
+
|
|
91
|
+
let error = new Error(`For ${url}, ` + message);
|
|
92
|
+
error.stack = `For ${url}, ` + responseText;
|
|
93
|
+
|
|
94
|
+
reject(error);
|
|
95
|
+
|
|
96
|
+
} catch (e: any) {
|
|
97
|
+
reject(new Error(`For ${url}, ` + request.statusText));
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
resolve(Buffer.from(request.response));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
request.onerror = (e) => {
|
|
105
|
+
reject(new Error(`Network error for request at ${url}`));
|
|
106
|
+
};
|
|
107
|
+
request.ontimeout = (e) => {
|
|
108
|
+
reject(new Error(`Network timeout for request at ${url}`));
|
|
109
|
+
};
|
|
110
|
+
request.onabort = (e) => {
|
|
111
|
+
reject(new Error(`Network abort for request at ${url}`));
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/misc.ts
CHANGED
|
@@ -126,13 +126,6 @@ export function getStringKeys<T extends {}>(obj: T): ((keyof T) & string)[] {
|
|
|
126
126
|
return Object.keys(obj) as any;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
if (isNode()) {
|
|
130
|
-
// TODO: Find a better place for this...
|
|
131
|
-
process.on("unhandledRejection", async (reason: any, promise) => {
|
|
132
|
-
console.error(`Uncaught promise rejection: ${String(reason.stack || reason)}`);
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
129
|
export function keyBy<T, K>(arr: T[], getKey: (value: T) => K): Map<K, T> {
|
|
137
130
|
let map = new Map<K, T>();
|
|
138
131
|
for (let item of arr) {
|
|
@@ -159,43 +152,44 @@ export function deepCloneJSON<T>(obj: T): T {
|
|
|
159
152
|
return JSON.parse(JSON.stringify(obj));
|
|
160
153
|
}
|
|
161
154
|
|
|
155
|
+
export class PromiseObj<T = void> {
|
|
156
|
+
public promise: Promise<T>;
|
|
157
|
+
public value: { value?: T; error?: string } | undefined;
|
|
158
|
+
/** Resolve called does not mean the value is ready, as it may be resolved with a promise. */
|
|
159
|
+
public resolveCalled?: boolean;
|
|
162
160
|
|
|
161
|
+
public resolve(value: T | Promise<T>) {
|
|
162
|
+
this.resolveCalled = true;
|
|
163
|
+
if (typeof value === "object" && value !== null && value instanceof Promise) {
|
|
164
|
+
value.then(
|
|
165
|
+
value => this.value = { value },
|
|
166
|
+
error => this.value = { error },
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
this.value = { value };
|
|
170
|
+
}
|
|
171
|
+
this.baseResolve(value);
|
|
172
|
+
}
|
|
173
|
+
public reject(error: any) {
|
|
174
|
+
this.baseReject(error);
|
|
175
|
+
}
|
|
163
176
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
private baseResolve!: (value: T | Promise<T>) => void;
|
|
178
|
+
private baseReject!: (error: any) => void;
|
|
179
|
+
constructor() {
|
|
180
|
+
this.promise = new Promise<T>((resolve, reject) => {
|
|
181
|
+
this.baseResolve = resolve;
|
|
182
|
+
this.baseReject = reject;
|
|
183
|
+
});
|
|
184
|
+
this.promise.then(
|
|
185
|
+
value => this.value = { value },
|
|
186
|
+
error => this.value = { error }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
171
189
|
}
|
|
172
190
|
|
|
173
191
|
export function promiseObj<T = void>(): PromiseObj<T> {
|
|
174
|
-
|
|
175
|
-
let reject!: (error: any) => void;
|
|
176
|
-
let promise = new Promise<T>((_resolve, _reject) => {
|
|
177
|
-
resolve = _resolve;
|
|
178
|
-
reject = _reject;
|
|
179
|
-
});
|
|
180
|
-
let obj: PromiseObj<T> = {
|
|
181
|
-
resolve(value: T | Promise<T>) {
|
|
182
|
-
obj.resolveCalled = true;
|
|
183
|
-
if (typeof value === "object" && value !== null && value instanceof Promise) {
|
|
184
|
-
value.then(
|
|
185
|
-
value => obj.value = { value },
|
|
186
|
-
error => obj.value = { error },
|
|
187
|
-
);
|
|
188
|
-
} else {
|
|
189
|
-
obj.value = { value };
|
|
190
|
-
}
|
|
191
|
-
resolve(value);
|
|
192
|
-
},
|
|
193
|
-
reject,
|
|
194
|
-
promise,
|
|
195
|
-
value: undefined
|
|
196
|
-
};
|
|
197
|
-
promise.then(value => obj.value = { value }, error => obj.value = { error });
|
|
198
|
-
return obj;
|
|
192
|
+
return new PromiseObj<T>();
|
|
199
193
|
}
|
|
200
194
|
|
|
201
195
|
|
|
@@ -291,13 +285,29 @@ export function entries<Obj extends { [key: string]: unknown }>(obj: Obj): [keyo
|
|
|
291
285
|
return Object.entries(obj) as any;
|
|
292
286
|
}
|
|
293
287
|
|
|
288
|
+
export function keys<Obj extends { [key: string]: unknown }>(obj: Obj): (keyof Obj)[] {
|
|
289
|
+
return Object.keys(obj) as any;
|
|
290
|
+
}
|
|
291
|
+
|
|
294
292
|
export function sort<T>(arr: T[], sortKey: (obj: T) => unknown) {
|
|
295
293
|
if (arr.length <= 1) return arr;
|
|
296
294
|
arr.sort((a, b) => compare(sortKey(a), sortKey(b)));
|
|
297
295
|
return arr;
|
|
298
296
|
}
|
|
299
297
|
|
|
300
|
-
|
|
298
|
+
export function binarySearchBasic<T, V>(array: T[], getVal: (val: T) => V, searchValue: V): number {
|
|
299
|
+
return binarySearchIndex(array.length, i => compare(getVal(array[i]), searchValue));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Searches indexes, allowing you to query structures that aren't arrays. To search an array, use:
|
|
304
|
+
* `binarySearchIndex(array.length, i => compare(array[i], searchValue))`
|
|
305
|
+
*
|
|
306
|
+
* NOTE: If there are duplicates, returns the first match.
|
|
307
|
+
*
|
|
308
|
+
* NOTE: If the value can't be found, returns the bitwise negation of the index where it should be inserted.
|
|
309
|
+
* - If you just want the index which is >=, use `if(index < 0) index = ~index;`
|
|
310
|
+
*/
|
|
301
311
|
export function binarySearchIndex(listCount: number, compare: (lhsIndex: number) => number): number {
|
|
302
312
|
if (listCount === 0) {
|
|
303
313
|
return ~0;
|
|
@@ -324,6 +334,8 @@ export function compare(lhs: unknown, rhs: unknown): number {
|
|
|
324
334
|
return compare(typeof lhs, typeof rhs);
|
|
325
335
|
}
|
|
326
336
|
if (lhs === rhs) return 0;
|
|
337
|
+
if (lhs === null && rhs !== null) return -1;
|
|
338
|
+
if (lhs !== null && rhs === null) return 1;
|
|
327
339
|
if (typeof lhs === "number") {
|
|
328
340
|
if (Number.isNaN(lhs)) {
|
|
329
341
|
if (Number.isNaN(rhs)) return 0;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import net from "net";
|
|
2
|
+
import { lazy } from "./caching";
|
|
3
|
+
import { httpsRequest } from "./https";
|
|
4
|
+
import { measureWrap } from "./profiling/measure";
|
|
5
|
+
|
|
6
|
+
export const testTCPIsListening = measureWrap(async function testTCPIsListening(host: string, port: number): Promise<boolean> {
|
|
7
|
+
// We need to establish a TCP connection, then close it? Yeah... so it is
|
|
8
|
+
// not even a SocketFunction call, because it can't be, because that woule be TLS,
|
|
9
|
+
// which we can't do with an ip!
|
|
10
|
+
let socket = net.connect({ host, port });
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
socket.on("connect", () => {
|
|
13
|
+
socket.end();
|
|
14
|
+
resolve(true);
|
|
15
|
+
});
|
|
16
|
+
socket.on("error", () => {
|
|
17
|
+
resolve(false);
|
|
18
|
+
});
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
socket.end();
|
|
21
|
+
resolve(false);
|
|
22
|
+
}, 1000 * 60);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const ipServers = [
|
|
28
|
+
"http://quentinbrooks.com:4283",
|
|
29
|
+
"https://ipinfo.io/ip",
|
|
30
|
+
"https://api.ipify.org"
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const getExternalIP = lazy(measureWrap(async function getExternalIP(): Promise<string> {
|
|
34
|
+
for (let server of ipServers) {
|
|
35
|
+
try {
|
|
36
|
+
return (await httpsRequest(server)).toString();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.warn(`Failed to get external ip from ${server}: ${e}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Failed to get external ip from any server`);
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
export const getPublicIP = getExternalIP;
|
package/src/profiling/measure.ts
CHANGED
|
@@ -62,12 +62,14 @@ export function measureWrap<T extends (...args: any[]) => any>(fnc: T, name?: st
|
|
|
62
62
|
return fnc;
|
|
63
63
|
}
|
|
64
64
|
let usedName = name || fnc.name || fnc.toString().slice(0, 100).replaceAll(/\s/g, " ");
|
|
65
|
-
|
|
65
|
+
let output = nameFunction(usedName, (function (this: any, ...args: unknown[]): unknown {
|
|
66
66
|
if (outstandingProfiles.length === 0) {
|
|
67
67
|
return fnc.apply(this, args);
|
|
68
68
|
}
|
|
69
69
|
return getOwnTime(usedName, () => fnc.apply(this, args), recordOwnTime);
|
|
70
70
|
})) as T;
|
|
71
|
+
(output as any).originalFnc = fnc;
|
|
72
|
+
return output;
|
|
71
73
|
}
|
|
72
74
|
export function measureBlock<T extends (...args: any[]) => any>(fnc: T, name?: string): ReturnType<T> {
|
|
73
75
|
return measureWrap(fnc, name)();
|
package/src/sniTest.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
2
|
+
|
|
3
|
+
let tlsExtensionLookup: { [type: number]: string } = {
|
|
4
|
+
0: "server_name",
|
|
5
|
+
1: "max_fragment_length",
|
|
6
|
+
2: "client_certificate_url",
|
|
7
|
+
3: "trusted_ca_keys",
|
|
8
|
+
4: "truncated_hmac",
|
|
9
|
+
5: "status_request",
|
|
10
|
+
6: "user_mapping",
|
|
11
|
+
7: "client_authz",
|
|
12
|
+
8: "server_authz",
|
|
13
|
+
9: "cert_type",
|
|
14
|
+
10: "supported_groups",
|
|
15
|
+
11: "ec_point_formats",
|
|
16
|
+
12: "srp",
|
|
17
|
+
13: "signature_algorithms",
|
|
18
|
+
14: "use_srtp",
|
|
19
|
+
15: "heartbeat",
|
|
20
|
+
16: "application_layer_protocol_negotiation",
|
|
21
|
+
17: "status_request_v2",
|
|
22
|
+
18: "signed_certificate_timestamp",
|
|
23
|
+
19: "client_certificate_type",
|
|
24
|
+
20: "server_certificate_type",
|
|
25
|
+
21: "padding",
|
|
26
|
+
22: "encrypt_then_mac",
|
|
27
|
+
23: "extended_master_secret",
|
|
28
|
+
24: "token_binding",
|
|
29
|
+
25: "cached_info",
|
|
30
|
+
26: "tls_lts",
|
|
31
|
+
27: "compress_certificate",
|
|
32
|
+
28: "record_size_limit",
|
|
33
|
+
29: "pwd_protect",
|
|
34
|
+
30: "pwd_clear",
|
|
35
|
+
31: "password_salt",
|
|
36
|
+
32: "ticket_pinning",
|
|
37
|
+
33: "tls_cert_with_extern_psk",
|
|
38
|
+
34: "delegated_credential",
|
|
39
|
+
35: "session_ticket",
|
|
40
|
+
36: "TLMSP",
|
|
41
|
+
37: "TLMSP_proxying",
|
|
42
|
+
38: "TLMSP_delegate",
|
|
43
|
+
39: "supported_ekt_ciphers",
|
|
44
|
+
40: "Reserved",
|
|
45
|
+
41: "pre_shared_key",
|
|
46
|
+
42: "early_data",
|
|
47
|
+
43: "supported_versions",
|
|
48
|
+
44: "cookie",
|
|
49
|
+
45: "psk_key_exchange_modes",
|
|
50
|
+
46: "Reserved",
|
|
51
|
+
47: "certificate_authorities",
|
|
52
|
+
48: "oid_filters",
|
|
53
|
+
49: "post_handshake_auth",
|
|
54
|
+
50: "signature_algorithms_cert",
|
|
55
|
+
51: "key_share",
|
|
56
|
+
52: "transparency_info",
|
|
57
|
+
53: "connection_id",
|
|
58
|
+
54: "connection_id",
|
|
59
|
+
55: "external_id_hash",
|
|
60
|
+
56: "external_session_id",
|
|
61
|
+
57: "quic_transport_parameters",
|
|
62
|
+
58: "ticket_request",
|
|
63
|
+
59: "dnssec_chain",
|
|
64
|
+
60: "sequence_number_encryption_algorithms",
|
|
65
|
+
61: "rrc",
|
|
66
|
+
2570: "encrypted_client_hello?",
|
|
67
|
+
17513: "application_settings",
|
|
68
|
+
6682: "generated_random_extensions_and_sustain_extensibility",
|
|
69
|
+
10794: "generated_random_extensions_and_sustain_extensibility",
|
|
70
|
+
14906: "generated_random_extensions_and_sustain_extensibility",
|
|
71
|
+
19018: "generated_random_extensions_and_sustain_extensibility",
|
|
72
|
+
23130: "generated_random_extensions_and_sustain_extensibility",
|
|
73
|
+
27242: "generated_random_extensions_and_sustain_extensibility",
|
|
74
|
+
31354: "generated_random_extensions_and_sustain_extensibility",
|
|
75
|
+
35466: "generated_random_extensions_and_sustain_extensibility",
|
|
76
|
+
39578: "generated_random_extensions_and_sustain_extensibility",
|
|
77
|
+
43690: "generated_random_extensions_and_sustain_extensibility",
|
|
78
|
+
47802: "generated_random_extensions_and_sustain_extensibility",
|
|
79
|
+
51914: "generated_random_extensions_and_sustain_extensibility",
|
|
80
|
+
56026: "generated_random_extensions_and_sustain_extensibility",
|
|
81
|
+
60138: "generated_random_extensions_and_sustain_extensibility",
|
|
82
|
+
64250: "generated_random_extensions_and_sustain_extensibility",
|
|
83
|
+
65037: "encrypted_client_hello",
|
|
84
|
+
65281: "renegotiation_info" // 0xFF01
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
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`
|
|
90
|
+
.replace(/\s/g, "")
|
|
91
|
+
, "base64"
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
let data = parseTLSHello(packet);
|
|
95
|
+
let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
|
|
96
|
+
console.log(`Packet size ${packet.byteLength}, missing bytes ${data.missingBytes}`);
|
|
97
|
+
for (let ext of data.extensions) {
|
|
98
|
+
console.log(`Extension: ${tlsExtensionLookup[ext.type] || ext.type}, bytes length: ${ext.data.length}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
main().catch(e => console.error(e)).finally(() => process.exit());
|
package/src/tlsParsing.ts
CHANGED
|
@@ -45,7 +45,6 @@ export function parseTLSHello(buffer: Buffer): {
|
|
|
45
45
|
let sessionIdLength = buffer[pos++];
|
|
46
46
|
pos += sessionIdLength;
|
|
47
47
|
|
|
48
|
-
|
|
49
48
|
let cipherSuiteLength = readShort();
|
|
50
49
|
pos += cipherSuiteLength;
|
|
51
50
|
|
|
@@ -53,11 +52,17 @@ export function parseTLSHello(buffer: Buffer): {
|
|
|
53
52
|
pos += compressionLength;
|
|
54
53
|
|
|
55
54
|
let extensionsLength = readShort();
|
|
56
|
-
output.missingBytes = contentLength - (pos + extensionsLength);
|
|
57
55
|
let extensionsEnd = pos + extensionsLength;
|
|
58
56
|
while (pos < extensionsEnd) {
|
|
59
57
|
let extensionType = readShort();
|
|
60
58
|
let length = readShort();
|
|
59
|
+
// Break if we only have part of the extension
|
|
60
|
+
if (Number.isNaN(extensionType) || Number.isNaN(length)) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (pos + length > buffer.length) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
61
66
|
|
|
62
67
|
output.extensions.push({
|
|
63
68
|
type: extensionType, data: viewSliceBuffer(buffer, pos, length)
|
|
@@ -65,6 +70,7 @@ export function parseTLSHello(buffer: Buffer): {
|
|
|
65
70
|
|
|
66
71
|
pos += length;
|
|
67
72
|
}
|
|
73
|
+
output.missingBytes = contentLength - pos;
|
|
68
74
|
} catch { }
|
|
69
75
|
|
|
70
76
|
return output;
|
package/src/webSocketServer.ts
CHANGED
|
@@ -11,12 +11,14 @@ import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
|
11
11
|
import debugbreak from "debugbreak";
|
|
12
12
|
import { getNodeId } from "./nodeCache";
|
|
13
13
|
import crypto from "crypto";
|
|
14
|
-
import { Watchable } from "./misc";
|
|
15
|
-
import { delay, runInfinitePoll } from "./batching";
|
|
16
|
-
import { magenta } from "./formatting/logColors";
|
|
14
|
+
import { Watchable, timeInHour } from "./misc";
|
|
15
|
+
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "./batching";
|
|
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
|
+
import { getExternalIP, testTCPIsListening } from "./networking";
|
|
21
|
+
import { forwardPort } from "./forwardPort";
|
|
20
22
|
|
|
21
23
|
export type SocketServerConfig = (
|
|
22
24
|
https.ServerOptions & {
|
|
@@ -30,6 +32,12 @@ export type SocketServerConfig = (
|
|
|
30
32
|
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
31
33
|
// causes the server to only accept local connections.
|
|
32
34
|
public?: boolean;
|
|
35
|
+
/** Tries forwarding ports (using UPnP), if we detect they aren't externally reachable.
|
|
36
|
+
* - This causes an extra request and delay during startup, so should only be used
|
|
37
|
+
* during development.
|
|
38
|
+
* - Ignored if public is false
|
|
39
|
+
*/
|
|
40
|
+
autoForwardPort?: boolean;
|
|
33
41
|
ip?: string;
|
|
34
42
|
|
|
35
43
|
// NOTE: Any same origin accesses are allowed (header.origin === header.host)
|
|
@@ -187,6 +195,10 @@ export async function startSocketServer(
|
|
|
187
195
|
if (!SocketFunction.silent) {
|
|
188
196
|
console.log(`Received TCP connection with SNI ${JSON.stringify(sni)}`);
|
|
189
197
|
}
|
|
198
|
+
if (!sni) {
|
|
199
|
+
console.warn(`No SNI found in TLS hello, using main server. Packets ${packetCount}`);
|
|
200
|
+
console.log(buffer.toString("base64"));
|
|
201
|
+
}
|
|
190
202
|
server = sniServers.get(sni) || mainHTTPSServer;
|
|
191
203
|
}
|
|
192
204
|
|
|
@@ -214,49 +226,78 @@ export async function startSocketServer(
|
|
|
214
226
|
});
|
|
215
227
|
|
|
216
228
|
|
|
217
|
-
let listenPromise = new Promise<void>((resolve, error) => {
|
|
218
|
-
realServer.on("listening", () => {
|
|
219
|
-
resolve();
|
|
220
|
-
});
|
|
221
|
-
realServer.on("error", e => {
|
|
222
|
-
error(e);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
229
|
let host = config.public ? "0.0.0.0" : "127.0.0.1";
|
|
227
230
|
if (config.ip) {
|
|
228
231
|
host = config.ip;
|
|
229
232
|
}
|
|
230
233
|
|
|
231
234
|
let port = config.port;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
235
|
+
if (!SocketFunction.silent) {
|
|
236
|
+
console.log(yellow(`Trying to listening on ${host}:${port}`));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let listeningPromise = waitUntilListening();
|
|
240
|
+
listeningPromise.catch(e => { });
|
|
241
|
+
|
|
242
|
+
// Return true if we are listening, false if the address is in use, and throws on other errors
|
|
243
|
+
async function waitUntilListening() {
|
|
244
|
+
return await new Promise<boolean>((resolve, reject) => {
|
|
245
|
+
realServer.once("error", e => {
|
|
246
|
+
reject(e);
|
|
247
|
+
});
|
|
248
|
+
realServer.once("listening", () => {
|
|
249
|
+
resolve(false);
|
|
250
|
+
});
|
|
244
251
|
});
|
|
245
252
|
}
|
|
253
|
+
|
|
246
254
|
if (config.useAvailablePortIfPortInUse && port) {
|
|
247
|
-
|
|
255
|
+
realServer.listen(port, host);
|
|
256
|
+
let isListening = await new Promise<boolean>((resolve, reject) => {
|
|
257
|
+
if (realServer.listening) {
|
|
258
|
+
resolve(true);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
realServer.once("error", e => {
|
|
262
|
+
if (e.message.includes("EADDRINUSE")) {
|
|
263
|
+
resolve(true);
|
|
264
|
+
} else {
|
|
265
|
+
reject(e);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
realServer.once("listening", () => {
|
|
269
|
+
resolve(false);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
if (!isListening) {
|
|
248
273
|
port = 0;
|
|
274
|
+
realServer.listen(port, host);
|
|
275
|
+
listeningPromise = waitUntilListening();
|
|
249
276
|
}
|
|
277
|
+
} else {
|
|
278
|
+
realServer.listen(port, host);
|
|
250
279
|
}
|
|
251
280
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
realServer.listen(port, host);
|
|
281
|
+
await listeningPromise;
|
|
282
|
+
port = (realServer.address() as net.AddressInfo).port;
|
|
256
283
|
|
|
257
|
-
|
|
284
|
+
if (config.autoForwardPort && config.public) {
|
|
285
|
+
// let externalIP = await getExternalIP();
|
|
286
|
+
// let isListening = await testTCPIsListening(externalIP, port);
|
|
287
|
+
// if (!isListening) {
|
|
288
|
+
// console.log(magenta(`Port ${port} is not externally reachable, trying to forward it`));
|
|
289
|
+
// await forwardPort({ externalPort: port, internalPort: port });
|
|
290
|
+
// }
|
|
291
|
+
// Even if they are listening, they might not stay listening. Forward every 8 hours
|
|
292
|
+
// (including at the start, in case the forward is about to expire).
|
|
293
|
+
async function forward() {
|
|
294
|
+
await forwardPort({ externalPort: port, internalPort: port });
|
|
295
|
+
console.log(magenta(`Forwarded port ${port} to our machine`));
|
|
296
|
+
}
|
|
297
|
+
// Every hour, in case our network configuration changes
|
|
298
|
+
runInfinitePollCallAtStart(timeInHour * 1, forward).catch(e => console.error(red(`Error in port forwarding ${e.stack}`)));
|
|
299
|
+
}
|
|
258
300
|
|
|
259
|
-
port = (realServer.address() as net.AddressInfo).port;
|
|
260
301
|
let nodeId = getNodeId(getCommonName(config.cert), port);
|
|
261
302
|
console.log(green(`Started Listening on ${nodeId} after ${formatTime(process.uptime() * 1000)}`));
|
|
262
303
|
|
package/test.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getExternalIP } from "./src/networking";
|
|
2
|
+
import http from "http";
|
|
3
|
+
import * as dgram from "dgram";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import debugbreak from "debugbreak";
|
|
6
|
+
import { forwardPort } from "./src/forwardPort";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// Usage example:
|
|
10
|
+
async function main() {
|
|
11
|
+
const externalPort = 8088;
|
|
12
|
+
const internalPort = externalPort;
|
|
13
|
+
|
|
14
|
+
await forwardPort({ externalPort, internalPort });
|
|
15
|
+
|
|
16
|
+
// Listen on the external port
|
|
17
|
+
const server = http.createServer((req, res) => {
|
|
18
|
+
console.log("Request received");
|
|
19
|
+
res.end("Hello, world!");
|
|
20
|
+
});
|
|
21
|
+
server.listen(externalPort, "0.0.0.0");
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
const externalIP = await getExternalIP();
|
|
25
|
+
let test = await fetch(`http://${externalIP}:${externalPort}`);
|
|
26
|
+
console.log(await test.text());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
//await createPortMapping({ externalPort, internalPort, gateWayIP, internalIP, });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch(e => console.error(e));
|
package/time/trueTimeShim.ts
CHANGED
|
@@ -128,7 +128,9 @@ async function updateTimeOffset() {
|
|
|
128
128
|
|| Math.abs(offset) > 300 && yellow(offsetRound + "ms")
|
|
129
129
|
|| green(offsetRound + "ms")
|
|
130
130
|
);
|
|
131
|
-
|
|
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
|
+
}
|
|
132
134
|
for (let i = 0; i < currentSmearCount; i++) {
|
|
133
135
|
let fraction = (i + 1) / currentSmearCount;
|
|
134
136
|
trueTimeOffset = prevOffset * (1 - fraction) + offset * fraction;
|