socket-function 0.5.0 → 0.6.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.
@@ -1,10 +1,11 @@
1
- import { CallerContext, CallType, NetworkLocation } from "./SocketFunctionTypes";
1
+ import { CallerContext, CallType, NetworkLocation } from "../SocketFunctionTypes";
2
2
  import type * as ws from "ws";
3
3
  import type * as net from "net";
4
4
  import { performLocalCall } from "./callManager";
5
- import { convertErrorStackToError } from "./misc";
5
+ import { convertErrorStackToError, isNode } from "./misc";
6
6
  import { createWebsocket, getNodeId, getTLSSocket } from "./nodeAuthentication";
7
7
  import debugbreak from "debugbreak";
8
+ import http from "http";
8
9
 
9
10
  const retryInterval = 2000;
10
11
 
@@ -46,7 +47,7 @@ export async function callFactoryFromLocation(
46
47
  }
47
48
 
48
49
  export async function callFactoryFromWS(
49
- webSocket: ws.WebSocket
50
+ webSocket: ws.WebSocket & { nodeId?: string },
50
51
  ): Promise<CallFactory> {
51
52
  let socket = getTLSSocket(webSocket);
52
53
  let remoteAddress = socket.remoteAddress;
@@ -69,8 +70,19 @@ export async function callFactoryFromWS(
69
70
  return await createCallFactory(webSocket, location);
70
71
  }
71
72
 
73
+ export interface SenderInterface {
74
+ nodeId?: string;
75
+
76
+ send(data: string): void;
77
+
78
+ on(event: "open", listener: () => void): this;
79
+ on(event: "close", listener: (code: number, reason: Buffer) => void): this;
80
+ on(event: "error", listener: (err: Error) => void): this;
81
+ on(event: "message", listener: (data: ws.RawData, isBinary: boolean) => void): this;
82
+ }
83
+
72
84
  async function createCallFactory(
73
- webSocketBase: ws.WebSocket | undefined,
85
+ webSocketBase: SenderInterface | undefined,
74
86
  location: NetworkLocation,
75
87
  ): Promise<CallFactory> {
76
88
 
@@ -99,14 +111,18 @@ async function createCallFactory(
99
111
 
100
112
  const pendingNodeId = "PENDING";
101
113
  let callerContext: CallerContext = { location, nodeId: pendingNodeId };
102
- let webSocket!: ws.WebSocket;
114
+ let webSocket!: SenderInterface;
103
115
  if (!webSocketBase) {
104
116
  await tryToReconnect();
105
117
  } else {
106
118
  webSocket = webSocketBase;
107
119
  setupWebsocket(webSocketBase);
108
120
  }
109
- callerContext.nodeId = getNodeId(webSocket);
121
+ if (isNode()) {
122
+ callerContext.nodeId = getNodeId(webSocket);
123
+ } else {
124
+ callerContext.nodeId = location.address + ":" + location.listeningPorts[0];
125
+ }
110
126
 
111
127
  niceConnectionName = `${niceConnectionName} (${callerContext.nodeId})`;
112
128
 
@@ -121,7 +137,7 @@ async function createCallFactory(
121
137
  if (reconnectTimeout) {
122
138
  await Promise.race([
123
139
  reconnectingPromise,
124
- new Promise<ws.WebSocket>(resolve =>
140
+ new Promise<SenderInterface>(resolve =>
125
141
  setTimeout(() => {
126
142
  retriesEnabled = false;
127
143
  resolve(webSocket);
@@ -173,13 +189,13 @@ async function createCallFactory(
173
189
  setupWebsocket(newWebSocket);
174
190
 
175
191
  let connectError = await new Promise<string | undefined>(resolve => {
176
- newWebSocket.once("open", () => {
192
+ newWebSocket.on("open", () => {
177
193
  resolve(undefined);
178
194
  });
179
- newWebSocket.once("close", () => {
195
+ newWebSocket.on("close", () => {
180
196
  resolve("Connection closed for non-error reason?");
181
197
  });
182
- newWebSocket.once("error", e => {
198
+ newWebSocket.on("error", e => {
183
199
  resolve(String(e.stack));
184
200
  });
185
201
  });
@@ -187,14 +203,17 @@ async function createCallFactory(
187
203
  if (!connectError) {
188
204
  console.log(`Reconnected to ${location.address}:${port}`);
189
205
 
190
- let newNodeId = getNodeId(newWebSocket);
191
-
192
- let prevNodeId = callerContext.nodeId;
193
- if (prevNodeId === pendingNodeId) {
194
- callerContext.nodeId = newNodeId;
195
- } else {
196
- if (newNodeId !== prevNodeId) {
197
- throw new Error(`Connection lost to at ${niceConnectionName} ("${prevNodeId}"), but then re-established, however it is now "${newNodeId}"!`);
206
+ // NOTE: Clientside doesn't have access to peer certificates, so it can't know the nodeId of the server
207
+ // that way. However, it can
208
+ if (isNode()) {
209
+ let newNodeId = getNodeId(newWebSocket);
210
+ let prevNodeId = callerContext.nodeId;
211
+ if (prevNodeId === pendingNodeId) {
212
+ callerContext.nodeId = newNodeId;
213
+ } else {
214
+ if (newNodeId !== prevNodeId) {
215
+ throw new Error(`Connection lost to at ${niceConnectionName} ("${prevNodeId}"), but then re-established, however it is now "${newNodeId}"!`);
216
+ }
198
217
  }
199
218
  }
200
219
 
@@ -225,23 +244,30 @@ async function createCallFactory(
225
244
  })();
226
245
  }
227
246
 
228
- function setupWebsocket(webSocket: ws.WebSocket) {
247
+ function setupWebsocket(webSocket: SenderInterface) {
229
248
  webSocket.on("error", e => {
230
249
  console.log(`Websocket error for ${niceConnectionName}`, e);
231
250
  });
232
251
 
233
252
  webSocket.on("close", async () => {
234
253
  console.log(`Websocket closed ${niceConnectionName}`);
235
- await tryToReconnect();
254
+ if (retriesEnabled) {
255
+ await tryToReconnect();
256
+ }
236
257
  });
237
258
 
238
259
  webSocket.on("message", onMessage);
239
260
  }
240
261
 
241
262
 
242
- async function onMessage(message: ws.RawData) {
263
+ async function onMessage(message: ws.RawData | MessageEvent | string) {
243
264
  try {
244
- if (message instanceof Buffer) {
265
+ if (!isNode()) {
266
+ if (typeof message === "object" && "data" in message) {
267
+ message = message.data;
268
+ }
269
+ }
270
+ if (message instanceof Buffer || typeof message === "string") {
245
271
  let call = JSON.parse(message.toString()) as InternalCallType | InternalReturnType;
246
272
  if (call.isReturn) {
247
273
  let callbackObj = pendingCalls.get(call.seqNum);
File without changes
File without changes
@@ -2,10 +2,10 @@ import { arrayEqual } from "./misc";
2
2
  import { AnyFunction, Args, canHaveChildren } from "./types";
3
3
 
4
4
  export function lazy<T>(factory: () => T): () => T {
5
- let value: { value: T }|undefined = undefined;
5
+ let value: { value: T } | undefined = undefined;
6
6
 
7
7
  return () => {
8
- if(!value) {
8
+ if (!value) {
9
9
  value = { value: factory() };
10
10
  }
11
11
  return value.value;
@@ -0,0 +1,160 @@
1
+ import https from "https";
2
+ import http from "http";
3
+ import net from "net";
4
+ import tls from "tls";
5
+ import { CallerContext, CallType } from "../SocketFunctionTypes";
6
+ import { performLocalCall } from "./callManager";
7
+ import { getNodeIdRaw } from "./nodeAuthentication";
8
+ import debugbreak from "debugbreak";
9
+ import * as cookie from "cookie";
10
+
11
+ const nodeIdCookie = "node-id3";
12
+
13
+ let defaultHTTPCall: CallType | undefined;
14
+
15
+ export function setDefaultHTTPCall(call: CallType) {
16
+ defaultHTTPCall = call;
17
+ }
18
+
19
+ export function getNodeIdFromRequest(request: http.IncomingMessage): string | undefined {
20
+ let cookies = cookie.parse(request.headers.cookie ?? "");
21
+ return cookies[nodeIdCookie];
22
+ }
23
+
24
+ export async function httpCallHandler(request: http.IncomingMessage, response: http.ServerResponse) {
25
+ try {
26
+
27
+ let urlBase = request.url;
28
+ if (!urlBase) {
29
+ throw new Error("Missing URL");
30
+ }
31
+ if (urlBase === "/favicon.ico") {
32
+ response.end();
33
+ return;
34
+ }
35
+
36
+ let protocol = "https";
37
+ let url = protocol + "://" + request.headers.host + request.url;
38
+
39
+ console.log(`HTTP request (${request.method}) ${url}`);
40
+ let urlObj = new URL(url);
41
+
42
+ let payload = await new Promise<Buffer>((resolve, reject) => {
43
+ let data: Buffer[] = [];
44
+ request
45
+ .on("data", chunk => data.push(chunk))
46
+ .on("end", () => resolve(Buffer.concat(data)))
47
+ .on("error", (err) => reject(err))
48
+ ;
49
+ });
50
+
51
+ let socket = request.connection as tls.TLSSocket;
52
+
53
+ let address = socket.remoteAddress;
54
+ let port = socket.remotePort;
55
+ if (!address) {
56
+ throw new Error("Missing remote address");
57
+ }
58
+ if (!port) {
59
+ throw new Error("Missing remote port");
60
+ }
61
+
62
+ let nodeId = getNodeIdRaw(socket);
63
+ if (!nodeId) {
64
+ let headerNodeId = cookie.parse(request.headers.cookie || "")[nodeIdCookie];
65
+ if (typeof headerNodeId === "string") {
66
+ nodeId = headerNodeId;
67
+ }
68
+ }
69
+ if (!nodeId) {
70
+ nodeId = "HTTP_nodeId_" + Date.now() + "_" + Math.random();
71
+ response.setHeader("Set-Cookie", cookie.serialize(nodeIdCookie, nodeId, {
72
+ httpOnly: true,
73
+ path: "/",
74
+ secure: true,
75
+ domain: urlObj.hostname,
76
+ sameSite: "none"
77
+ }));
78
+
79
+ response.setHeader(nodeIdCookie, nodeId);
80
+ }
81
+
82
+ let caller: CallerContext = {
83
+ nodeId,
84
+ location: {
85
+ address,
86
+ localPort: port,
87
+ listeningPorts: [],
88
+ }
89
+ };
90
+
91
+ let classGuid = urlObj.searchParams.get("classGuid");
92
+ let functionName = urlObj.searchParams.get("functionName");
93
+ let args: string | unknown[] | null = urlObj.searchParams.get("args");
94
+
95
+ if (!classGuid) {
96
+ if (defaultHTTPCall) {
97
+ classGuid = defaultHTTPCall.classGuid;
98
+ functionName = defaultHTTPCall.functionName;
99
+ args = defaultHTTPCall.args;
100
+ } else {
101
+ throw new Error("Missing classGuid in URL query");
102
+ }
103
+ }
104
+ if (!functionName) {
105
+ throw new Error("Missing functionName in URL query");
106
+ }
107
+
108
+ if (!args) {
109
+ args = [];
110
+ } else {
111
+ if (typeof args === "string") {
112
+ args = JSON.parse(args) as unknown[];
113
+ }
114
+ }
115
+
116
+ if (payload.length > 0) {
117
+ args = JSON.parse(payload.toString())["args"] as unknown[];
118
+ }
119
+
120
+ let call: CallType = {
121
+ classGuid,
122
+ functionName,
123
+ args,
124
+ };
125
+
126
+ let result = await performLocalCall({
127
+ caller,
128
+ call
129
+ });
130
+
131
+ if (typeof result === "object" && result && result instanceof Buffer) {
132
+ let headers = (result as HTTPResultType)[resultHeaders];
133
+ if (headers) {
134
+ for (let headerName in headers) {
135
+ response.setHeader(headerName, headers[headerName]);
136
+ }
137
+ }
138
+ response.write(result);
139
+ } else {
140
+ response.write(JSON.stringify(result));
141
+ }
142
+ response.end();
143
+ } catch (e: any) {
144
+ console.error(`Request error`, e.stack);
145
+ response.writeHead(500, String(e.message));
146
+ response.end();
147
+ }
148
+ }
149
+
150
+
151
+ const resultHeaders = Symbol("resultHeaders");
152
+ type HTTPResultType = Buffer & { [resultHeaders]?: { [header: string]: string } };
153
+
154
+ export function setHTTPResultHeaders(
155
+ result: HTTPResultType,
156
+ headers: { [header: string]: string },
157
+ ): HTTPResultType {
158
+ result[resultHeaders] = headers;
159
+ return result;
160
+ }
@@ -1,14 +1,13 @@
1
- import { CallContextType, CallerContext, CallType, ClientHookContext, HookContext, NetworkLocation, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "./SocketFunctionTypes";
2
- import { _setSocketContext } from "./SocketFunction";
1
+ import { CallContextType, CallerContext, CallType, ClientHookContext, HookContext, NetworkLocation, SocketExposedInterface, SocketExposedInterfaceClass, SocketExposedShape, SocketFunctionClientHook, SocketFunctionHook, SocketRegistered } from "../SocketFunctionTypes";
2
+ import { _setSocketContext } from "../SocketFunction";
3
3
 
4
4
  let classes: {
5
5
  [classGuid: string]: {
6
- classType: SocketExposedInterfaceClass;
7
6
  controller: SocketExposedInterface;
8
7
  shape: SocketExposedShape;
9
8
  }
10
9
  } = {};
11
- let exposedClasses = new Set<SocketExposedInterfaceClass>();
10
+ let exposedClasses = new Set<string>();
12
11
 
13
12
  let globalHooks: SocketFunctionHook[] = [];
14
13
  let globalClientHooks: SocketFunctionClientHook[] = [];
@@ -26,7 +25,7 @@ export async function performLocalCall(
26
25
  throw new Error(`Class ${call.classGuid} not found`);
27
26
  }
28
27
 
29
- if (!exposedClasses.has(classDef.classType)) {
28
+ if (!exposedClasses.has(call.classGuid)) {
30
29
  throw new Error(`Class ${call.classGuid} not exposed`);
31
30
  }
32
31
 
@@ -55,20 +54,19 @@ export async function performLocalCall(
55
54
  return await result;
56
55
  }
57
56
 
58
- export function registerClass(classGuid: string, exposedClass: SocketExposedInterfaceClass, shape: SocketExposedShape) {
57
+ export function registerClass(classGuid: string, controller: SocketExposedInterface, shape: SocketExposedShape) {
59
58
  if (classes[classGuid]) {
60
59
  throw new Error(`Class ${classGuid} already registered`);
61
60
  }
62
61
 
63
62
  classes[classGuid] = {
64
- classType: exposedClass,
65
- controller: new exposedClass() as SocketExposedInterface,
63
+ controller,
66
64
  shape,
67
65
  };
68
66
  }
69
67
 
70
- export function exposeClass(exposedClass: SocketExposedInterfaceClass) {
71
- exposedClasses.add(exposedClass);
68
+ export function exposeClass(exposedClass: SocketRegistered) {
69
+ exposedClasses.add(exposedClass._classGuid);
72
70
  }
73
71
 
74
72
  export function registerGlobalHook(hook: SocketFunctionHook) {
@@ -19,9 +19,14 @@ export function arrayEqual(a: unknown[], b: unknown[]) {
19
19
  }
20
20
  return true;
21
21
  }
22
+ export function isNode() {
23
+ return typeof document === "undefined";
24
+ }
22
25
 
23
26
 
24
- // TODO: Find a better place for this...
25
- process.on("unhandledRejection", async (reason: any, promise) => {
26
- console.error(`Uncaught promise rejection: ${String(reason.stack || reason)}`);
27
- });
27
+ if (isNode()) {
28
+ // TODO: Find a better place for this...
29
+ process.on("unhandledRejection", async (reason: any, promise) => {
30
+ console.error(`Uncaught promise rejection: ${String(reason.stack || reason)}`);
31
+ });
32
+ }
@@ -1,5 +1,6 @@
1
1
  import ws from "ws";
2
2
  import tls from "tls";
3
+ import net from "net";
3
4
  import { getAppFolder } from "./storagePath";
4
5
  import fs from "fs";
5
6
  import child_process from "child_process";
@@ -7,12 +8,17 @@ import { cacheWeak, lazy } from "./caching";
7
8
  import https from "https";
8
9
  import debugbreak from "debugbreak";
9
10
  import crypto from "crypto";
10
- import { sha256Hash } from "./misc";
11
+ import { isNode, sha256Hash } from "./misc";
11
12
  import { getArgs } from "./args";
13
+ import { SenderInterface } from "./CallFactory";
12
14
 
13
15
  export const getCertKeyPair = lazy((): { key: Buffer; cert: Buffer } => {
14
16
  // TODO: Also get this working clientside...
15
17
  // - Probably using node-forge, maybe using this as an example: https://github.com/jfromaniello/selfsigned/blob/master/index.js
18
+ // - ALSO, get our nodeId set in our cookies, so HTTP requests can work as well
19
+ // - We will need to call some kind of endpoint to do this?
20
+ // - Then download the certs and try to get the user to install them, so chrome can use
21
+ // them? Otherwise there is no point of having certs clientside.
16
22
 
17
23
  // https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/
18
24
 
@@ -36,27 +42,70 @@ export function getTLSSocket(webSocket: ws.WebSocket) {
36
42
  return (webSocket as any)._socket as tls.TLSSocket;
37
43
  }
38
44
 
39
- export const getNodeId = cacheWeak(function (webSocket: ws.WebSocket): string {
45
+ export async function getOwnNodeId() {
46
+ if (!isNode()) {
47
+ throw new Error(`Clientside nodeIds are not exposed to the client`);
48
+ }
49
+
50
+ // This is BASICALLY just sha256Hash(getCertKeyPari().cert), however... I'm not 100% the format
51
+ // is the same, we would have to verify it. It isn't that important, other nodes know our nodeId,
52
+ // and clients don't really have a reason to use this anyway (they can't verify it, they can only
53
+ // really verify with a location).
54
+ throw new Error(`TODO: Implement getOwnNodeId`);
55
+ }
56
+
57
+ export const getNodeId = cacheWeak(function (webSocket: SenderInterface | ws.WebSocket & { nodeId?: string }): string {
58
+ if (!(webSocket instanceof ws.WebSocket)) {
59
+ if (!webSocket.nodeId) {
60
+ throw new Error("Sender isn't a WebSocket, and doesn't have a nodeId");
61
+ }
62
+ return webSocket.nodeId;
63
+ }
40
64
  let socket = getTLSSocket(webSocket);
65
+ let nodeId = getNodeIdRaw(socket);
66
+ if (!nodeId) {
67
+ if (webSocket.nodeId) {
68
+ return webSocket.nodeId;
69
+ }
70
+ throw new Error(`Peer certificate must use an RSA key or EC key (which should have a .pubkey property)`);
71
+ }
72
+ return nodeId;
73
+ });
74
+
75
+ export function getNodeIdRaw(socket: tls.TLSSocket) {
41
76
  let peerCert = socket.getPeerCertificate();
42
77
  if (!peerCert) {
43
78
  throw new Error("WebSocket connections must provided a peer certificate");
44
79
  }
45
80
  let pubkey = (peerCert as any).pubkey as Buffer | undefined;
46
81
  if (!pubkey) {
47
- throw new Error(`Peer certificate must use an RSA key or EC key (which should have a .pubkey property)`);
82
+ return undefined;
48
83
  }
49
84
  return sha256Hash(pubkey);
50
- });
85
+ }
51
86
 
52
- export function createWebsocket(address: string, port: number): ws.WebSocket {
53
- let { key, cert } = getCertKeyPair();
87
+ export function createWebsocket(address: string, port: number): SenderInterface {
54
88
  console.log(`Connecting to ${address}:${port}`);
55
- return new ws.WebSocket(`wss://${address}:${port}`, {
56
- cert,
57
- key,
58
- rejectUnauthorized: false,
59
- });
89
+ if (!isNode()) {
90
+ // NOTE: We assume an HTTP request has already been made, which will setup a nodeId cookie
91
+ // (And as this point we can't even use peer certificates if we wanted to, as this must be done
92
+ // directly in the browser)
93
+ let webSocket = new WebSocket(`wss://${address}:${port}`);
94
+ return Object.assign(webSocket, {
95
+ on(event: string, callback: any) {
96
+ // TODO: Use better type safety here
97
+ (webSocket as any)["on" + event] = callback;
98
+ return this as any;
99
+ },
100
+ });
101
+ } else {
102
+ let { key, cert } = getCertKeyPair();
103
+ return new ws.WebSocket(`wss://${address}:${port}`, {
104
+ cert,
105
+ key,
106
+ rejectUnauthorized: false,
107
+ });
108
+ }
60
109
  }
61
110
 
62
111
 
@@ -1,5 +1,5 @@
1
- import { callFactoryFromLocation, CallFactory } from "./CallInstance";
2
- import { NetworkLocation } from "./SocketFunctionTypes";
1
+ import { callFactoryFromLocation, CallFactory } from "./CallFactory";
2
+ import { NetworkLocation } from "../SocketFunctionTypes";
3
3
  import { MaybePromise } from "./types";
4
4
 
5
5
  // TODO: Add CallInstanceFactory.isClosed, so nodeCache can clean up old entries.
@@ -74,6 +74,6 @@ export function getCreateCallFactoryLocation(location: NetworkLocation): MaybePr
74
74
 
75
75
 
76
76
  // TODO: Give a special error if the nodeId has been seen, but is only one-way (from HTTP requests).
77
- export function getCallFactoryNodeId(nodeId: string): CallFactory|undefined {
77
+ export function getCallFactoryNodeId(nodeId: string): CallFactory | undefined {
78
78
  return nodeCache.get(nodeId)?.callFactory;
79
79
  }
@@ -1,5 +1,5 @@
1
1
  import { lazy } from "./caching";
2
- import { SocketExposedInterface } from "./SocketFunctionTypes";
2
+ import { SocketExposedInterface } from "../SocketFunctionTypes";
3
3
 
4
4
  //todonext
5
5
  // We need some sort of cache, but... maybe not here?
@@ -12,13 +12,13 @@ type CallProxyType = {
12
12
  let proxyCache = new Map<string, CallProxyType>();
13
13
  export function getCallProxy(id: string, callback: (controllerName: string, functionName: string, args: unknown[]) => Promise<unknown>): CallProxyType {
14
14
  let value = proxyCache.get(id);
15
- if(!value) {
15
+ if (!value) {
16
16
  let controllerCache = new Map<string, CallProxyType[""]>();
17
17
  value = new Proxy(Object.create(null), {
18
18
  get(target, controllerName) {
19
- if(typeof controllerName !== "string") return undefined;
19
+ if (typeof controllerName !== "string") return undefined;
20
20
  let controller = controllerCache.get(controllerName);
21
- if(!controller) {
21
+ if (!controller) {
22
22
  controller = new Proxy(Object.create(null), {
23
23
  get(target, functionName) {
24
24
  if (typeof functionName !== "string") return undefined;