socket-function 0.8.33 → 0.8.35
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 +3 -3
- package/SocketFunctionTypes.ts +2 -17
- package/hot/HotReloadController.ts +2 -0
- package/index.d.ts +1 -0
- package/package.json +3 -3
- package/require/RequireController.ts +1 -1
- package/src/CallFactory.ts +5 -13
- package/src/callHTTPHandler.ts +3 -3
- package/src/callManager.ts +4 -4
- package/src/certStore.ts +7 -33
- package/src/nodeAuthentication.ts +4 -82
- package/src/tlsParsing.ts +97 -0
- package/src/webSocketServer.ts +84 -73
package/SocketFunction.ts
CHANGED
|
@@ -28,7 +28,6 @@ export class SocketFunction {
|
|
|
28
28
|
type: "gzip";
|
|
29
29
|
};
|
|
30
30
|
public static httpETagCache = false;
|
|
31
|
-
public static rejectUnauthorized = true;
|
|
32
31
|
|
|
33
32
|
public static register<
|
|
34
33
|
ClassInstance extends object,
|
|
@@ -129,11 +128,12 @@ export class SocketFunction {
|
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
public static mountedNodeId: string = "NOTMOUNTED";
|
|
132
|
-
public static async mount(config: SocketServerConfig) {
|
|
131
|
+
public static async mount(config: SocketServerConfig & { nodeId: string }) {
|
|
133
132
|
if (this.mountedNodeId !== "NOTMOUNTED") {
|
|
134
133
|
throw new Error("SocketFunction already mounted, mounting twice in one thread is not allowed.");
|
|
135
134
|
}
|
|
136
|
-
this.mountedNodeId =
|
|
135
|
+
this.mountedNodeId = config.nodeId;
|
|
136
|
+
await startSocketServer(config);
|
|
137
137
|
return this.mountedNodeId;
|
|
138
138
|
}
|
|
139
139
|
|
package/SocketFunctionTypes.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
module.allowclient = true;
|
|
2
2
|
|
|
3
|
-
import debugbreak from "debugbreak";
|
|
4
|
-
import * as tls from "tls";
|
|
5
|
-
import { SenderInterface } from "./src/CallFactory";
|
|
6
|
-
import { isNode } from "./src/misc";
|
|
7
|
-
import { CertInfo, getNodeIdFromCert } from "./src/nodeAuthentication";
|
|
8
|
-
import { getClientNodeId } from "./src/nodeCache";
|
|
9
3
|
import { getCallObj } from "./src/nodeProxy";
|
|
10
4
|
import { Args, MaybePromise } from "./src/types";
|
|
11
5
|
|
|
@@ -52,14 +46,14 @@ export interface SocketFunctionHook<ExposedType extends SocketExposedInterface =
|
|
|
52
46
|
(config: HookContext<ExposedType, CallContext>): MaybePromise<void>;
|
|
53
47
|
}
|
|
54
48
|
export type HookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
|
|
55
|
-
call:
|
|
49
|
+
call: FullCallType;
|
|
56
50
|
context: SocketRegistered<ExposedType, CallContext>["context"];
|
|
57
51
|
// If the result is overriden, we continue evaluating hooks BUT DO NOT perform the final call
|
|
58
52
|
overrideResult?: unknown;
|
|
59
53
|
};
|
|
60
54
|
|
|
61
55
|
export type ClientHookContext<ExposedType extends SocketExposedInterface = SocketExposedInterface, CallContext extends CallContextType = CallContextType> = {
|
|
62
|
-
call:
|
|
56
|
+
call: FullCallType;
|
|
63
57
|
/** If the calls takes longer than this (for ANY reason), we return with an error.
|
|
64
58
|
* - Different from reconnectTimeout, which only errors if we lose the connection.
|
|
65
59
|
*/
|
|
@@ -101,15 +95,6 @@ export type CallerContextBase = {
|
|
|
101
95
|
// a more permanent identity, you must derive it from certInfo yourself.
|
|
102
96
|
nodeId: string;
|
|
103
97
|
|
|
104
|
-
/** Gives further info on the node. When we set this, we always make sure it has a verified
|
|
105
|
-
* issuer. It may be set by app code, which should make sure the issuer is verified (not
|
|
106
|
-
* necessarily by the machine, but just in some sense, 'verified', to secure the common name
|
|
107
|
-
* of the cert and prevent anyone from using the same common name as someone else).
|
|
108
|
-
* IF set, is directly used to derive nodeId (by nodeAuthentication.ts)
|
|
109
|
-
*/
|
|
110
|
-
certInfo: CertInfo | undefined;
|
|
111
|
-
updateCertInfo?: (certInfo: CertInfo, callbackPort: number | undefined) => void;
|
|
112
|
-
|
|
113
98
|
// The nodeId they contacted. This is useful to determine their intention (otherwise
|
|
114
99
|
// requests can be redirected to us and would accept them, even though they are being
|
|
115
100
|
// blatantly MITMed).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference path="../typenode/index.d.ts" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "socket-function",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.35",
|
|
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",
|
|
@@ -10,11 +10,10 @@
|
|
|
10
10
|
"@types/node-forge": "^1.3.1",
|
|
11
11
|
"@types/ws": "^8.5.3",
|
|
12
12
|
"cookie": "^0.5.0",
|
|
13
|
-
"debugbreak": "^0.6.5",
|
|
14
13
|
"mobx": "^6.6.2",
|
|
15
14
|
"node-forge": "https://github.com/sliftist/forge#name",
|
|
16
15
|
"preact": "^10.10.6",
|
|
17
|
-
"typenode": "^4.9.4",
|
|
16
|
+
"typenode": "^4.9.4-a",
|
|
18
17
|
"ws": "^8.8.0"
|
|
19
18
|
},
|
|
20
19
|
"scripts": {
|
|
@@ -22,6 +21,7 @@
|
|
|
22
21
|
"type": "yarn tsc --noEmit"
|
|
23
22
|
},
|
|
24
23
|
"devDependencies": {
|
|
24
|
+
"debugbreak": "^0.6.5",
|
|
25
25
|
"typedev": "^0.1.0"
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -216,7 +216,7 @@ class RequireControllerBase {
|
|
|
216
216
|
//require(rootPath);
|
|
217
217
|
let clientModule = require.cache[resolvedPath];
|
|
218
218
|
if (!clientModule) {
|
|
219
|
-
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) was not included serverside
|
|
219
|
+
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${JSON.stringify(resolvedPath)}) was not included serverside. Resolve root ${JSON.stringify(this.rootResolvePath)} (set by call to setRequireBootRequire), resolve search paths: ${JSON.stringify(searchPaths)})}`);
|
|
220
220
|
}
|
|
221
221
|
if (!clientModule.allowclient) {
|
|
222
222
|
clientModule = createNotFoundModule(`Module ${pathRequest} (resolved to ${resolvedPath}) is not allowed clientside (set module.allowclient in it, or call setFlag when it is imported).`);
|
package/src/CallFactory.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { CallerContext, CallerContextBase, CallType } from "../SocketFunctionTypes";
|
|
1
|
+
import { CallerContext, CallerContextBase, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
2
2
|
import * as ws from "ws";
|
|
3
3
|
import { performLocalCall } from "./callManager";
|
|
4
4
|
import { convertErrorStackToError, formatNumberSuffixed, isNode } from "./misc";
|
|
5
|
-
import { createWebsocketFactory,
|
|
5
|
+
import { createWebsocketFactory, getTLSSocket } from "./nodeAuthentication";
|
|
6
6
|
import { SocketFunction } from "../SocketFunction";
|
|
7
7
|
import { gzip } from "zlib";
|
|
8
8
|
import * as tls from "tls";
|
|
@@ -10,7 +10,7 @@ import { getClientNodeId, getNodeIdLocation, registerNodeClient } from "./nodeCa
|
|
|
10
10
|
|
|
11
11
|
const retryInterval = 2000;
|
|
12
12
|
|
|
13
|
-
type InternalCallType =
|
|
13
|
+
type InternalCallType = FullCallType & {
|
|
14
14
|
seqNum: number;
|
|
15
15
|
isReturn: false;
|
|
16
16
|
compress: boolean;
|
|
@@ -77,16 +77,7 @@ export async function createCallFactory(
|
|
|
77
77
|
|
|
78
78
|
let callerContext: CallerContextBase = {
|
|
79
79
|
nodeId,
|
|
80
|
-
localNodeId
|
|
81
|
-
certInfo: webSocketBase?.socket?.getPeerCertificate(true),
|
|
82
|
-
updateCertInfo: (certRaw, port) => {
|
|
83
|
-
let nodeId = getNodeIdFromCert(certRaw, port);
|
|
84
|
-
if (!nodeId) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
callerContext.nodeId = nodeId;
|
|
88
|
-
callerContext.certInfo = certRaw;
|
|
89
|
-
}
|
|
80
|
+
localNodeId
|
|
90
81
|
};
|
|
91
82
|
|
|
92
83
|
let callFactory: CallFactory = {
|
|
@@ -99,6 +90,7 @@ export async function createCallFactory(
|
|
|
99
90
|
|
|
100
91
|
let seqNum = nextSeqNum++;
|
|
101
92
|
let fullCall: InternalCallType = {
|
|
93
|
+
nodeId,
|
|
102
94
|
isReturn: false,
|
|
103
95
|
args: call.args,
|
|
104
96
|
classGuid: call.classGuid,
|
package/src/callHTTPHandler.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from "http";
|
|
2
2
|
import tls from "tls";
|
|
3
|
-
import { CallerContext, CallType } from "../SocketFunctionTypes";
|
|
3
|
+
import { CallerContext, CallType, FullCallType } from "../SocketFunctionTypes";
|
|
4
4
|
import { isDataImmutable, performLocalCall } from "./callManager";
|
|
5
5
|
import { SocketFunction } from "../SocketFunction";
|
|
6
6
|
import { gzip } from "zlib";
|
|
@@ -79,7 +79,6 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
79
79
|
|
|
80
80
|
let caller: CallerContext = {
|
|
81
81
|
nodeId,
|
|
82
|
-
certInfo: undefined,
|
|
83
82
|
localNodeId,
|
|
84
83
|
};
|
|
85
84
|
|
|
@@ -112,7 +111,8 @@ export async function httpCallHandler(request: http.IncomingMessage, response: h
|
|
|
112
111
|
args = JSON.parse(payload.toString())["args"] as unknown[];
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
let call:
|
|
114
|
+
let call: FullCallType = {
|
|
115
|
+
nodeId,
|
|
116
116
|
classGuid,
|
|
117
117
|
functionName,
|
|
118
118
|
args,
|
package/src/callManager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CallContextType, CallerContext, CallType, ClientHookContext, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
1
|
+
import { CallContextType, CallerContext, CallType, ClientHookContext, FullCallType, HookContext, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
|
|
2
2
|
import { _setSocketContext } from "../SocketFunction";
|
|
3
3
|
|
|
4
4
|
let classes: {
|
|
@@ -14,7 +14,7 @@ let globalClientHooks: SocketFunctionClientHook[] = [];
|
|
|
14
14
|
|
|
15
15
|
export async function performLocalCall(
|
|
16
16
|
config: {
|
|
17
|
-
call:
|
|
17
|
+
call: FullCallType;
|
|
18
18
|
caller: CallerContext;
|
|
19
19
|
}
|
|
20
20
|
): Promise<unknown> {
|
|
@@ -92,7 +92,7 @@ export function unregisterGlobalClientHook(hook: SocketFunctionClientHook) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export async function runClientHooks(
|
|
95
|
-
callType:
|
|
95
|
+
callType: FullCallType,
|
|
96
96
|
hooks: SocketExposedShape[""],
|
|
97
97
|
): Promise<ClientHookContext> {
|
|
98
98
|
let context: ClientHookContext = { call: callType };
|
|
@@ -106,7 +106,7 @@ export async function runClientHooks(
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async function runServerHooks(
|
|
109
|
-
callType:
|
|
109
|
+
callType: FullCallType,
|
|
110
110
|
context: SocketRegistered["context"],
|
|
111
111
|
hooks: SocketExposedShape[""],
|
|
112
112
|
): Promise<HookContext> {
|
package/src/certStore.ts
CHANGED
|
@@ -1,50 +1,24 @@
|
|
|
1
|
-
import * as os from "os";
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as fsSync from "fs";
|
|
4
|
-
import * as child_process from "child_process";
|
|
5
1
|
import * as tls from "tls";
|
|
6
|
-
import { SocketFunction } from "../SocketFunction";
|
|
7
|
-
import { isNode, isNodeTrue, sha256Hash } from "./misc";
|
|
8
|
-
import { lazy } from "./caching";
|
|
9
2
|
|
|
10
3
|
let trustedCerts = new Set<string>();
|
|
11
|
-
let loadedTrustedCerts = false;
|
|
12
4
|
let watchCallbacks = new Set<(certs: string[]) => void>();
|
|
13
5
|
|
|
14
|
-
let storePath = isNodeTrue() && process.argv[1].replaceAll("\\", "/").split("/").slice(0, -1).join("/") + "/certstore/";
|
|
15
|
-
if (isNode()) {
|
|
16
|
-
if (!fsSync.existsSync(storePath)) {
|
|
17
|
-
fsSync.mkdirSync(storePath);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
6
|
/** Must be populated before the server starts */
|
|
22
|
-
export
|
|
7
|
+
export function trustCertificate(cert: string | Buffer) {
|
|
8
|
+
cert = cert.toString();
|
|
23
9
|
if (trustedCerts.has(cert)) return;
|
|
24
10
|
trustedCerts.add(cert);
|
|
25
|
-
|
|
26
|
-
let certs = getTrustedUserCertificates();
|
|
11
|
+
let certs = getTrustedCertificates();
|
|
27
12
|
for (let callback of watchCallbacks) {
|
|
28
13
|
callback(certs);
|
|
29
14
|
}
|
|
30
15
|
}
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
for (let file of files) {
|
|
34
|
-
let cert = await fs.readFile(storePath + file, "utf8");
|
|
35
|
-
trustedCerts.add(cert);
|
|
36
|
-
}
|
|
37
|
-
loadedTrustedCerts = true;
|
|
38
|
-
});
|
|
39
|
-
export function getTrustedUserCertificates(): string[] {
|
|
40
|
-
if (!loadedTrustedCerts) {
|
|
41
|
-
throw new Error("Must call loadTrustedUserCertificates (and await it) before calling getTrustedUserCertificates");
|
|
42
|
-
}
|
|
43
|
-
return Array.from(trustedCerts);
|
|
16
|
+
export function getTrustedCertificates(): string[] {
|
|
17
|
+
return tls.rootCertificates.concat(Array.from(trustedCerts));
|
|
44
18
|
}
|
|
45
19
|
|
|
46
|
-
export function
|
|
20
|
+
export function watchTrustedCertificates(callback: (certs: string[]) => void) {
|
|
47
21
|
watchCallbacks.add(callback);
|
|
48
|
-
callback(
|
|
22
|
+
callback(getTrustedCertificates());
|
|
49
23
|
return () => watchCallbacks.delete(callback);
|
|
50
24
|
}
|
|
@@ -1,88 +1,15 @@
|
|
|
1
1
|
import ws from "ws";
|
|
2
2
|
import tls from "tls";
|
|
3
|
-
import
|
|
4
|
-
import { getAppFolder } from "./storagePath";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import child_process from "child_process";
|
|
7
|
-
import { cacheWeak, lazy } from "./caching";
|
|
8
|
-
import https from "https";
|
|
9
|
-
import debugbreak from "debugbreak";
|
|
10
|
-
import crypto from "crypto";
|
|
11
|
-
import { isNode, sha256Hash } from "./misc";
|
|
12
|
-
import { getArgs } from "./args";
|
|
3
|
+
import { isNode } from "./misc";
|
|
13
4
|
import { SenderInterface } from "./CallFactory";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { getClientNodeId, getNodeId, getNodeIdLocation } from "./nodeCache";
|
|
5
|
+
import { getTrustedCertificates } from "./certStore";
|
|
6
|
+
import { getNodeIdLocation } from "./nodeCache";
|
|
17
7
|
|
|
18
|
-
export type CertInfo = { raw: Buffer | string; issuerCertificate: { raw: Buffer | string } };
|
|
19
|
-
|
|
20
|
-
let certKeyPairOverride: { key: Buffer; cert: Buffer } | undefined;
|
|
21
|
-
export function getCertKeyPair(): { key: Buffer; cert: Buffer } {
|
|
22
|
-
if (certKeyPairOverride) return certKeyPairOverride;
|
|
23
|
-
return getCertKeyPairBase();
|
|
24
|
-
}
|
|
25
|
-
const getCertKeyPairBase = lazy((): { key: Buffer; cert: Buffer } => {
|
|
26
|
-
// https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/
|
|
27
|
-
|
|
28
|
-
let folder = getAppFolder();
|
|
29
|
-
let identityPrefix = getArgs().identity || "";
|
|
30
|
-
let keyPath = folder + identityPrefix + "key.pem";
|
|
31
|
-
let certPath = folder + identityPrefix + "cert.pem";
|
|
32
|
-
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
|
33
|
-
child_process.execSync(`openssl genrsa -out "${keyPath}"`);
|
|
34
|
-
child_process.execSync(`openssl req -new -key "${keyPath}" -out csr.pem -subj "/CN=notused"`);
|
|
35
|
-
child_process.execSync(`openssl x509 -req -days 9999 -in csr.pem -signkey "${keyPath}" -out "${certPath}"`);
|
|
36
|
-
fs.rmSync("csr.pem");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let key = fs.readFileSync(keyPath);
|
|
40
|
-
let cert = fs.readFileSync(certPath);
|
|
41
|
-
return { key, cert };
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
export function overrideCertKeyPair<T>(certKey: { key: Buffer; cert: Buffer; }, code: () => T): T {
|
|
45
|
-
if (!isNode()) {
|
|
46
|
-
throw new Error(`Cannot override cert/key pair in browser`);
|
|
47
|
-
}
|
|
48
|
-
let prevOverride = certKeyPairOverride;
|
|
49
|
-
certKeyPairOverride = certKey;
|
|
50
|
-
try {
|
|
51
|
-
return code();
|
|
52
|
-
} finally {
|
|
53
|
-
certKeyPairOverride = prevOverride;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
8
|
|
|
57
9
|
export function getTLSSocket(webSocket: ws.WebSocket) {
|
|
58
10
|
return (webSocket as any)._socket as tls.TLSSocket;
|
|
59
11
|
}
|
|
60
12
|
|
|
61
|
-
export async function getOwnNodeId() {
|
|
62
|
-
if (!isNode()) {
|
|
63
|
-
throw new Error(`Clientside nodeIds are not exposed to the client`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// This is BASICALLY just sha256Hash(getCertKeyPari().cert), however... I'm not 100% the format
|
|
67
|
-
// is the same, we would have to verify it. It isn't that important, other nodes know our nodeId,
|
|
68
|
-
// and clients don't really have a reason to use this anyway (they can't verify it, they can only
|
|
69
|
-
// really verify with a location).
|
|
70
|
-
throw new Error(`TODO: Implement getOwnNodeId`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function getNodeIdFromCert(certRaw: { raw: Buffer | string } | undefined, callbackPort: number | undefined) {
|
|
74
|
-
if (!certRaw?.raw) return undefined;
|
|
75
|
-
let cert = new crypto.X509Certificate(certRaw.raw);
|
|
76
|
-
if (!callbackPort) {
|
|
77
|
-
return getClientNodeId(cert.subject);
|
|
78
|
-
}
|
|
79
|
-
let subject = cert.subject;
|
|
80
|
-
if (subject.startsWith("CN=")) {
|
|
81
|
-
subject = subject.slice("CN=".length);
|
|
82
|
-
}
|
|
83
|
-
return getNodeId(subject, callbackPort);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
13
|
/** NOTE: We create a factory, which embeds the key/cert information. Otherwise retries might use
|
|
87
14
|
* a different key/cert context.
|
|
88
15
|
*/
|
|
@@ -98,8 +25,6 @@ export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
|
|
|
98
25
|
return new WebSocket(`wss://${address}:${port}`);
|
|
99
26
|
};
|
|
100
27
|
} else {
|
|
101
|
-
let { key, cert } = getCertKeyPair();
|
|
102
|
-
let rejectUnauthorized = SocketFunction.rejectUnauthorized;
|
|
103
28
|
return (nodeId: string) => {
|
|
104
29
|
let location = getNodeIdLocation(nodeId);
|
|
105
30
|
if (!location) throw new Error(`Cannot connect to ${nodeId}, no address known`);
|
|
@@ -107,10 +32,7 @@ export function createWebsocketFactory(): (nodeId: string) => SenderInterface {
|
|
|
107
32
|
|
|
108
33
|
console.log(`Connecting to ${address}:${port}`);
|
|
109
34
|
let webSocket = new ws.WebSocket(`wss://${address}:${port}`, {
|
|
110
|
-
|
|
111
|
-
key,
|
|
112
|
-
rejectUnauthorized,
|
|
113
|
-
ca: tls.rootCertificates.concat(getTrustedUserCertificates()),
|
|
35
|
+
ca: tls.rootCertificates.concat(getTrustedCertificates()),
|
|
114
36
|
});
|
|
115
37
|
let result = Object.assign(webSocket, { socket: undefined as tls.TLSSocket | undefined });
|
|
116
38
|
webSocket.once("upgrade", e => {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export function parseTLSHello(buffer: Buffer): {
|
|
2
|
+
extensions: {
|
|
3
|
+
type: number;
|
|
4
|
+
data: Buffer;
|
|
5
|
+
}[];
|
|
6
|
+
} {
|
|
7
|
+
let output: {
|
|
8
|
+
extensions: {
|
|
9
|
+
type: number;
|
|
10
|
+
data: Buffer;
|
|
11
|
+
}[];
|
|
12
|
+
} = {
|
|
13
|
+
extensions: []
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
let pos = 0;
|
|
18
|
+
|
|
19
|
+
function readShort() {
|
|
20
|
+
let high = buffer[pos++];
|
|
21
|
+
let low = buffer[pos++];
|
|
22
|
+
return high * 256 + low;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let type = buffer[pos++];
|
|
26
|
+
let version = readShort();
|
|
27
|
+
|
|
28
|
+
var contentLength = readShort();
|
|
29
|
+
|
|
30
|
+
let clientMessageType = buffer[pos++];
|
|
31
|
+
// High length byte (how would this be used if contentLength is only 2 bytes?)
|
|
32
|
+
pos++;
|
|
33
|
+
let clientMessageLength = readShort();
|
|
34
|
+
|
|
35
|
+
// Client version
|
|
36
|
+
let clientVersion = readShort();
|
|
37
|
+
|
|
38
|
+
// Client random
|
|
39
|
+
pos += 32;
|
|
40
|
+
|
|
41
|
+
// Session id
|
|
42
|
+
let sessionIdLength = buffer[pos++];
|
|
43
|
+
pos += sessionIdLength;
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
let cipherSuiteLength = readShort();
|
|
47
|
+
pos += cipherSuiteLength;
|
|
48
|
+
|
|
49
|
+
let compressionLength = buffer[pos++];
|
|
50
|
+
pos += compressionLength;
|
|
51
|
+
|
|
52
|
+
let extensionsLength = readShort();
|
|
53
|
+
let extensionsEnd = pos + extensionsLength;
|
|
54
|
+
while (pos < extensionsEnd) {
|
|
55
|
+
let extensionType = readShort();
|
|
56
|
+
let length = readShort();
|
|
57
|
+
|
|
58
|
+
output.extensions.push({
|
|
59
|
+
type: extensionType, data: viewSliceBuffer(buffer, pos, length)
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pos += length;
|
|
63
|
+
}
|
|
64
|
+
} catch { }
|
|
65
|
+
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const SNIType = 0x0;
|
|
70
|
+
export function parseSNIExtension(data: Buffer): string[] {
|
|
71
|
+
let pos = 0;
|
|
72
|
+
function readShort() {
|
|
73
|
+
let high = data[pos++];
|
|
74
|
+
let low = data[pos++];
|
|
75
|
+
return high * 256 + low;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let snis: string[] = [];
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
while (pos < data.length) {
|
|
82
|
+
let len = readShort();
|
|
83
|
+
let end = pos + len;
|
|
84
|
+
let type = data[pos++];
|
|
85
|
+
let len2 = readShort();
|
|
86
|
+
snis.push(viewSliceBuffer(data, pos, len2).toString());
|
|
87
|
+
pos = end;
|
|
88
|
+
}
|
|
89
|
+
} catch { }
|
|
90
|
+
|
|
91
|
+
return snis;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function viewSliceBuffer(data: Buffer, index: number, count?: number) {
|
|
95
|
+
count = count ?? (data.length - index);
|
|
96
|
+
return Buffer.from(data.buffer, data.byteOffset + index, count);
|
|
97
|
+
}
|
package/src/webSocketServer.ts
CHANGED
|
@@ -3,97 +3,106 @@ import http from "http";
|
|
|
3
3
|
import net from "net";
|
|
4
4
|
import tls from "tls";
|
|
5
5
|
import * as ws from "ws";
|
|
6
|
-
import { getCertKeyPair, getNodeIdFromCert } from "./nodeAuthentication";
|
|
7
6
|
import { getNodeIdsFromRequest, httpCallHandler } from "./callHTTPHandler";
|
|
8
7
|
import { SocketFunction } from "../SocketFunction";
|
|
9
|
-
import {
|
|
8
|
+
import { getTrustedCertificates, watchTrustedCertificates } from "./certStore";
|
|
10
9
|
import { createCallFactory } from "./CallFactory";
|
|
10
|
+
import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
|
|
11
|
+
import debugbreak from "debugbreak";
|
|
11
12
|
|
|
12
13
|
export type SocketServerConfig = (
|
|
13
|
-
{
|
|
14
|
+
https.ServerOptions & {
|
|
15
|
+
nodeId?: string;
|
|
16
|
+
|
|
17
|
+
key: string | Buffer;
|
|
18
|
+
cert: string | Buffer;
|
|
19
|
+
|
|
14
20
|
port: number;
|
|
21
|
+
|
|
15
22
|
// public sets ip to "0.0.0.0", otherwise it defaults to "127.0.0.1", which
|
|
16
23
|
// causes the server to only accept local connections.
|
|
17
24
|
public?: boolean;
|
|
18
25
|
ip?: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
|
|
27
|
+
/** If the SNI matches this domain, we use a different key/cert. */
|
|
28
|
+
SNICerts?: {
|
|
29
|
+
[domain: string]: https.ServerOptions;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
22
32
|
);
|
|
23
33
|
|
|
24
34
|
export async function startSocketServer(
|
|
25
35
|
config: SocketServerConfig
|
|
26
|
-
): Promise<
|
|
27
|
-
let isSecure = "cert" in config || "key" in config || "pfx" in config;
|
|
28
|
-
if (!isSecure) {
|
|
29
|
-
let { key, cert } = getCertKeyPair();
|
|
30
|
-
config.key = key;
|
|
31
|
-
config.cert = cert;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
await loadTrustedUserCertificates();
|
|
35
|
-
|
|
36
|
-
// TODO: Only allow unauthorized for ip certificates, and then for domains use the domain as the nodeId,
|
|
37
|
-
// so it is easy to read, and consistent.
|
|
38
|
-
let options: https.ServerOptions = {
|
|
39
|
-
...config,
|
|
40
|
-
rejectUnauthorized: SocketFunction.rejectUnauthorized,
|
|
41
|
-
requestCert: true,
|
|
42
|
-
};
|
|
36
|
+
): Promise<void> {
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
options.ca = tls.rootCertificates.concat(getTrustedUserCertificates());
|
|
47
|
-
httpsServer.setSecureContext(options);
|
|
38
|
+
const webSocketServer = new ws.Server({
|
|
39
|
+
noServer: true,
|
|
48
40
|
});
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
socket.on("close", () => {
|
|
56
|
-
console.log("Client socket closed");
|
|
42
|
+
function setupHTTPSServer(options: https.ServerOptions) {
|
|
43
|
+
let httpsServer = https.createServer(options);
|
|
44
|
+
watchTrustedCertificates(() => {
|
|
45
|
+
options.ca = getTrustedCertificates();
|
|
46
|
+
httpsServer.setSecureContext(options);
|
|
57
47
|
});
|
|
58
|
-
});
|
|
59
|
-
httpsServer.on("error", e => {
|
|
60
|
-
console.error(`Connection attempt error ${e.message}`);
|
|
61
|
-
});
|
|
62
|
-
httpsServer.on("tlsClientError", e => {
|
|
63
|
-
console.error(`TLS client error ${e.message}`);
|
|
64
|
-
});
|
|
65
48
|
|
|
49
|
+
httpsServer.on("connection", socket => {
|
|
50
|
+
console.log("Client connection established");
|
|
51
|
+
socket.on("error", e => {
|
|
52
|
+
console.log(`Client socket error ${e.message}`);
|
|
53
|
+
});
|
|
54
|
+
socket.on("close", () => {
|
|
55
|
+
console.log("Client socket closed");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
httpsServer.on("error", e => {
|
|
59
|
+
console.error(`Connection attempt error ${e.message}`);
|
|
60
|
+
});
|
|
61
|
+
httpsServer.on("tlsClientError", e => {
|
|
62
|
+
console.error(`TLS client error ${e.message}`);
|
|
63
|
+
});
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
httpsServer.on("request", httpCallHandler);
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
socket.on("error", e => {
|
|
74
|
-
console.log(`Client socket error ${e.message}`);
|
|
75
|
-
});
|
|
67
|
+
httpsServer.on("upgrade", (request, socket, upgradeHead) => {
|
|
68
|
+
socket.on("error", e => {
|
|
69
|
+
console.log(`Client socket error ${e.message}`);
|
|
70
|
+
});
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
let originHeader = request.headers["origin"];
|
|
73
|
+
if (originHeader) {
|
|
74
|
+
try {
|
|
75
|
+
let host = new URL("ws://" + request.headers["host"]).hostname;
|
|
76
|
+
let origin = new URL(originHeader).hostname;
|
|
77
|
+
if (host !== origin) {
|
|
78
|
+
throw new Error(`Invalid cross thread request, ${JSON.stringify(host)} !== ${JSON.stringify(origin)}`);
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error(e);
|
|
82
|
+
return;
|
|
84
83
|
}
|
|
85
|
-
} catch (e) {
|
|
86
|
-
console.error(e);
|
|
87
|
-
return;
|
|
88
84
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
webSocketServer.handleUpgrade(request, socket, upgradeHead, (ws) => {
|
|
86
|
+
const { nodeId, localNodeId } = getNodeIdsFromRequest(request);
|
|
87
|
+
createCallFactory(ws, nodeId, localNodeId).catch(e => {
|
|
88
|
+
console.error(`Error in creating call factory, ${e.stack}`);
|
|
89
|
+
});
|
|
94
90
|
});
|
|
95
91
|
});
|
|
96
|
-
|
|
92
|
+
return httpsServer;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// TODO: Only allow unauthorized for ip certificates, and then for domains use the domain as the nodeId,
|
|
96
|
+
// so it is easy to read, and consistent.
|
|
97
|
+
let options: https.ServerOptions = {
|
|
98
|
+
...config,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const mainHTTPSServer = setupHTTPSServer(options);
|
|
102
|
+
let sniServers = new Map<string, https.Server>();
|
|
103
|
+
for (let [domain, obj] of Object.entries(config.SNICerts || {})) {
|
|
104
|
+
sniServers.set(domain, setupHTTPSServer(obj));
|
|
105
|
+
}
|
|
97
106
|
|
|
98
107
|
let httpServer = http.createServer({}, async function (req, res) {
|
|
99
108
|
let url = new URL("http://" + req.headers.host + req.url);
|
|
@@ -114,7 +123,15 @@ export async function startSocketServer(
|
|
|
114
123
|
socket.once("data", buffer => {
|
|
115
124
|
// All HTTPS requests start with 22, and no HTTP requests start with 22,
|
|
116
125
|
// so we just need to read the first byte.
|
|
117
|
-
|
|
126
|
+
|
|
127
|
+
let server: https.Server | http.Server;
|
|
128
|
+
if (buffer[0] !== 22) {
|
|
129
|
+
server = httpServer;
|
|
130
|
+
} else {
|
|
131
|
+
let data = parseTLSHello(buffer);
|
|
132
|
+
let sni = data.extensions.filter(x => x.type === SNIType).flatMap(x => parseSNIExtension(x.data))[0];
|
|
133
|
+
server = sniServers.get(sni) || mainHTTPSServer;
|
|
134
|
+
}
|
|
118
135
|
|
|
119
136
|
// NOTE: Messages aren't dequeued until the current handler finishes, so we don't need to pause the socket or anything.
|
|
120
137
|
server.emit("connection", socket);
|
|
@@ -148,11 +165,5 @@ export async function startSocketServer(
|
|
|
148
165
|
|
|
149
166
|
let port = (realServer.address() as net.AddressInfo).port;
|
|
150
167
|
|
|
151
|
-
console.log(`Started Listening on ${host}:${port}`);
|
|
152
|
-
|
|
153
|
-
let serverNodeId = getNodeIdFromCert({ raw: config.cert as Buffer | string }, port);
|
|
154
|
-
if (!serverNodeId) {
|
|
155
|
-
throw new Error(`Something is wrong with our cert, we don't have a nodeId?`);
|
|
156
|
-
}
|
|
157
|
-
return serverNodeId;
|
|
168
|
+
console.log(`Started Listening on ${config.nodeId || host}:${port}`);
|
|
158
169
|
}
|