@kitware/wslink 2.5.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.
@@ -0,0 +1,296 @@
1
+ // Project not setup for typescript, manually compiling this file to chunker.js
2
+ // npx tsc chunking.ts --target esnext
3
+
4
+ const UINT32_LENGTH = 4;
5
+ const ID_LOCATION = 0;
6
+ const ID_LENGTH = UINT32_LENGTH;
7
+ const MESSAGE_OFFSET_LOCATION = ID_LOCATION + ID_LENGTH;
8
+ const MESSAGE_OFFSET_LENGTH = UINT32_LENGTH;
9
+ const MESSAGE_SIZE_LOCATION = MESSAGE_OFFSET_LOCATION + MESSAGE_OFFSET_LENGTH;
10
+ const MESSAGE_SIZE_LENGTH = UINT32_LENGTH;
11
+
12
+ const HEADER_LENGTH = ID_LENGTH + MESSAGE_OFFSET_LENGTH + MESSAGE_SIZE_LENGTH;
13
+
14
+ function encodeHeader(id: number, offset: number, size: number): Uint8Array {
15
+ const buffer = new ArrayBuffer(HEADER_LENGTH);
16
+ const header = new Uint8Array(buffer);
17
+ const view = new DataView(buffer);
18
+ view.setUint32(ID_LOCATION, id, true);
19
+ view.setUint32(MESSAGE_OFFSET_LOCATION, offset, true);
20
+ view.setUint32(MESSAGE_SIZE_LOCATION, size, true);
21
+
22
+ return header;
23
+ }
24
+
25
+ function decodeHeader(header: Uint8Array) {
26
+ const view = new DataView(header.buffer);
27
+ const id = view.getUint32(ID_LOCATION, true);
28
+ const offset = view.getUint32(MESSAGE_OFFSET_LOCATION, true);
29
+ const size = view.getUint32(MESSAGE_SIZE_LOCATION, true);
30
+
31
+ return { id, offset, size };
32
+ }
33
+
34
+ function* generateChunks(message: Uint8Array, maxSize: number) {
35
+ const totalSize = message.byteLength;
36
+ let maxContentSize: number;
37
+
38
+ if (maxSize === 0) {
39
+ maxContentSize = totalSize;
40
+ } else {
41
+ maxContentSize = Math.max(maxSize - HEADER_LENGTH, 1);
42
+ }
43
+
44
+ const id = new Uint32Array(1);
45
+ crypto.getRandomValues(id);
46
+
47
+ let offset = 0;
48
+
49
+ while (offset < totalSize) {
50
+ const contentSize = Math.min(maxContentSize, totalSize - offset);
51
+ const chunk = new Uint8Array(new ArrayBuffer(HEADER_LENGTH + contentSize));
52
+ const header = encodeHeader(id[0], offset, totalSize);
53
+ chunk.set(new Uint8Array(header.buffer), 0);
54
+ chunk.set(message.subarray(offset, offset + contentSize), HEADER_LENGTH);
55
+
56
+ yield chunk;
57
+
58
+ offset += contentSize;
59
+ }
60
+
61
+ return;
62
+ }
63
+
64
+ type PendingMessage = {
65
+ receivedSize: number;
66
+ content: Uint8Array;
67
+ decoder: any;
68
+ };
69
+
70
+ /*
71
+ This un-chunker is vulnerable to DOS.
72
+ If it receives a message with a header claiming a large incoming message
73
+ it will allocate the memory blindly even without actually receiving the content
74
+ Chunks for a given message can come in any order
75
+ Chunks across messages can be interleaved.
76
+ */
77
+ class UnChunker {
78
+ private pendingMessages: { [key: number]: PendingMessage };
79
+
80
+ constructor() {
81
+ this.pendingMessages = {};
82
+ }
83
+
84
+ releasePendingMessages() {
85
+ this.pendingMessages = {};
86
+ }
87
+
88
+ async processChunk(
89
+ chunk: Blob,
90
+ decoderFactory: () => any
91
+ ): Promise<unknown | undefined> {
92
+ const headerBlob = chunk.slice(0, HEADER_LENGTH);
93
+ const contentBlob = chunk.slice(HEADER_LENGTH);
94
+
95
+ const header = new Uint8Array(await headerBlob.arrayBuffer());
96
+ const { id, offset, size: totalSize } = decodeHeader(header);
97
+
98
+ let pendingMessage = this.pendingMessages[id];
99
+
100
+ if (!pendingMessage) {
101
+ pendingMessage = {
102
+ receivedSize: 0,
103
+ content: new Uint8Array(totalSize),
104
+ decoder: decoderFactory(),
105
+ };
106
+
107
+ this.pendingMessages[id] = pendingMessage;
108
+ }
109
+
110
+ // This should never happen, but still check it
111
+ if (totalSize !== pendingMessage.content.byteLength) {
112
+ delete this.pendingMessages[id];
113
+ throw new Error(
114
+ `Total size in chunk header for message ${id} does not match total size declared by previous chunk.`
115
+ );
116
+ }
117
+
118
+ const chunkContent = new Uint8Array(await contentBlob.arrayBuffer());
119
+ const content = pendingMessage.content;
120
+ content.set(chunkContent, offset);
121
+ pendingMessage.receivedSize += chunkContent.byteLength;
122
+
123
+ if (pendingMessage.receivedSize >= totalSize) {
124
+ delete this.pendingMessages[id];
125
+
126
+ try {
127
+ return pendingMessage["decoder"].decode(content);
128
+ } catch (e) {
129
+ console.error("Malformed message: ", content.slice(0, 100));
130
+ // debugger;
131
+ }
132
+ }
133
+
134
+ return undefined;
135
+ }
136
+ }
137
+
138
+ type StreamPendingMessage = {
139
+ receivedSize: number;
140
+ totalSize: number;
141
+ decoder: any;
142
+ };
143
+
144
+ // Makes sure messages are processed in order of arrival,
145
+ export class SequentialTaskQueue {
146
+ taskId: number;
147
+ pendingTaskId: number;
148
+ tasks: {
149
+ [id: number]: {
150
+ fn: (...args: any) => Promise<any>;
151
+ args: any[];
152
+ resolve: (value: any) => void;
153
+ reject: (err: any) => void;
154
+ };
155
+ };
156
+
157
+ constructor() {
158
+ this.taskId = 0;
159
+ this.pendingTaskId = -1;
160
+ this.tasks = {};
161
+ }
162
+
163
+ enqueue(fn: (...args: any) => Promise<any>, ...args: any[]) {
164
+ return new Promise((resolve, reject) => {
165
+ const taskId = this.taskId++;
166
+ this.tasks[taskId] = { fn, args, resolve, reject };
167
+ this._maybeExecuteNext();
168
+ });
169
+ }
170
+
171
+ _maybeExecuteNext() {
172
+ let pendingTask = this.tasks[this.pendingTaskId];
173
+
174
+ if (pendingTask) {
175
+ return;
176
+ }
177
+
178
+ const nextPendingTaskId = this.pendingTaskId + 1;
179
+
180
+ pendingTask = this.tasks[nextPendingTaskId];
181
+
182
+ if (!pendingTask) {
183
+ return;
184
+ }
185
+
186
+ this.pendingTaskId = nextPendingTaskId;
187
+
188
+ const { fn, args, resolve, reject } = pendingTask;
189
+
190
+ fn(...args)
191
+ .then((result) => {
192
+ resolve(result);
193
+ delete this.tasks[nextPendingTaskId];
194
+ this._maybeExecuteNext();
195
+ })
196
+ .catch((err) => {
197
+ reject(err);
198
+ delete this.tasks[nextPendingTaskId];
199
+ this._maybeExecuteNext();
200
+ });
201
+ }
202
+ }
203
+
204
+ /*
205
+ This un-chunker is more memory efficient
206
+ (each chunk is passed immediately to msgpack)
207
+ and it will only allocate memory when it receives content.
208
+ Chunks for a given message are expected to come sequentially
209
+ Chunks across messages can be interleaved.
210
+ */
211
+ class StreamUnChunker {
212
+ private pendingMessages: { [key: number]: StreamPendingMessage };
213
+
214
+ constructor() {
215
+ this.pendingMessages = {};
216
+ }
217
+
218
+ processChunk = async (
219
+ chunk: Blob,
220
+ decoderFactory: () => any
221
+ ): Promise<unknown | undefined> => {
222
+ const headerBlob = chunk.slice(0, HEADER_LENGTH);
223
+
224
+ const header = new Uint8Array(await headerBlob.arrayBuffer());
225
+ const { id, offset, size: totalSize } = decodeHeader(header);
226
+
227
+ const contentBlob = chunk.slice(HEADER_LENGTH);
228
+
229
+ let pendingMessage = this.pendingMessages[id];
230
+
231
+ if (!pendingMessage) {
232
+ pendingMessage = {
233
+ receivedSize: 0,
234
+ totalSize: totalSize,
235
+ decoder: decoderFactory(),
236
+ };
237
+
238
+ this.pendingMessages[id] = pendingMessage;
239
+ }
240
+
241
+ // This should never happen, but still check it
242
+ if (totalSize !== pendingMessage.totalSize) {
243
+ delete this.pendingMessages[id];
244
+ throw new Error(
245
+ `Total size in chunk header for message ${id} does not match total size declared by previous chunk.`
246
+ );
247
+ }
248
+
249
+ // This should never happen, but still check it
250
+ if (offset !== pendingMessage.receivedSize) {
251
+ delete this.pendingMessages[id];
252
+ throw new Error(
253
+ `Received an unexpected chunk for message ${id}.
254
+ Expected offset = ${pendingMessage.receivedSize},
255
+ Received offset = ${offset}.`
256
+ );
257
+ }
258
+
259
+ let result: unknown;
260
+ try {
261
+ result = await pendingMessage.decoder.decodeAsync(
262
+ contentBlob.stream() as any
263
+ );
264
+ } catch (e) {
265
+ if (e instanceof RangeError) {
266
+ // More data is needed, it should come in the next chunk
267
+ result = undefined;
268
+ }
269
+ }
270
+
271
+ pendingMessage.receivedSize += contentBlob.size;
272
+
273
+ /*
274
+ In principle feeding a stream to the unpacker could yield multiple outputs
275
+ for example unpacker.feed(b'0123') would yield b'0', b'1', etc
276
+ or concatenated packed payloads would yield two or more unpacked objects
277
+ but in our use case we expect a full message to be mapped to a single object
278
+ */
279
+ if (result && pendingMessage.receivedSize < totalSize) {
280
+ delete this.pendingMessages[id];
281
+ throw new Error(
282
+ `Received a parsable payload shorter than expected for message ${id}.
283
+ Expected size = ${totalSize},
284
+ Received size = ${pendingMessage.receivedSize}.`
285
+ );
286
+ }
287
+
288
+ if (pendingMessage.receivedSize >= totalSize) {
289
+ delete this.pendingMessages[id];
290
+ }
291
+
292
+ return result;
293
+ };
294
+ }
295
+
296
+ export { UnChunker, StreamUnChunker, generateChunks };
@@ -0,0 +1,70 @@
1
+ export interface SubscriberInfo {
2
+ topic: string;
3
+ callback: (args: any[]) => Promise<any>;
4
+ }
5
+
6
+ // Provides a generic RPC stub built on top of websocket.
7
+ export interface WebsocketSession {
8
+ // Issue a single-shot RPC.
9
+ call(
10
+ methodName: string,
11
+ args?: any[],
12
+ kwargs?: Record<string, any>
13
+ ): Promise<any>;
14
+ // Subscribe to one-way messages from the server.
15
+ subscribe(
16
+ topic: string,
17
+ callback: (args: any[]) => Promise<any>
18
+ ): {
19
+ // A promise to be resolved once subscription succeeds.
20
+ promise: Promise<SubscriberInfo>;
21
+ // Cancels the subscription.
22
+ unsubscribe: () => Promise<void>;
23
+ };
24
+ // Cancels the subscription.
25
+ unsubscribe(info: SubscriberInfo): Promise<void>;
26
+ close(): Promise<void>;
27
+
28
+ /**
29
+ * @param payload The binary data to send
30
+ * @returns The id assigned to the binary attachment
31
+ */
32
+ addAttachment(payload: Blob): string;
33
+ }
34
+
35
+ export interface IWebsocketConnectionInitialValues {
36
+ secret?: string;
37
+ connection?: any;
38
+ session?: any;
39
+ retry?: boolean;
40
+ }
41
+
42
+ // Represents a single established websocket connection.
43
+ export interface WebsocketConnection {
44
+ getSession(): WebsocketSession;
45
+ getUrl(): string | null;
46
+ destroy(): void;
47
+ }
48
+
49
+ /**
50
+ * Creates a new SmartConnect object with the given configuration.
51
+ */
52
+ export function newInstance(
53
+ initialValues: IWebsocketConnectionInitialValues
54
+ ): WebsocketConnection;
55
+
56
+ /**
57
+ * Decorates a given object (publicAPI+model) with WebsocketConnection characteristics.
58
+ */
59
+ export function extend(
60
+ publicAPI: object,
61
+ model: object,
62
+ initialValues?: IWebsocketConnectionInitialValues
63
+ ): void;
64
+
65
+ export declare const WebsocketConnection: {
66
+ newInstance: typeof newInstance;
67
+ extend: typeof extend;
68
+ };
69
+
70
+ export default WebsocketConnection;
@@ -0,0 +1,155 @@
1
+ // Helper borrowed from paraviewweb/src/Common/Core
2
+ import CompositeClosureHelper from "../CompositeClosureHelper";
3
+ import Session from "./session";
4
+
5
+ const DEFAULT_SECRET = "wslink-secret";
6
+
7
+ function getTransportObject(url) {
8
+ var idx = url.indexOf(":"),
9
+ protocol = url.substring(0, idx);
10
+ if (protocol === "ws" || protocol === "wss") {
11
+ return {
12
+ type: "websocket",
13
+ url,
14
+ };
15
+ }
16
+
17
+ throw new Error(
18
+ `Unknown protocol (${protocol}) for url (${url}). Unable to create transport object.`
19
+ );
20
+ }
21
+
22
+ function WebsocketConnection(publicAPI, model) {
23
+ // TODO Should we try to reconnect on error?
24
+
25
+ publicAPI.connect = () => {
26
+ // without a URL we can't do anything.
27
+ if (!model.urls) return null;
28
+ // concat allows a single url or a list.
29
+ var uriList = [].concat(model.urls),
30
+ transports = [];
31
+
32
+ for (let i = 0; i < uriList.length; i += 1) {
33
+ const url = uriList[i];
34
+ try {
35
+ const transport = getTransportObject(url);
36
+ transports.push(transport);
37
+ } catch (transportCreateError) {
38
+ console.error(transportCreateError);
39
+ publicAPI.fireConnectionError(publicAPI, transportCreateError);
40
+ return null;
41
+ }
42
+ }
43
+
44
+ if (model.connection) {
45
+ if (model.connection.url !== transports[0].url) {
46
+ model.connection.close();
47
+ } else if (
48
+ model.connection.readyState === 0 ||
49
+ model.connection.readyState === 1
50
+ ) {
51
+ // already connected.
52
+ return model.session;
53
+ }
54
+ }
55
+ try {
56
+ if (model.wsProxy) {
57
+ model.connection = WSLINK.createWebSocket(transports[0].url);
58
+ } else {
59
+ model.connection = new WebSocket(transports[0].url);
60
+ }
61
+ } catch (err) {
62
+ // If the server isn't running, we still don't enter here on Chrome -
63
+ // console shows a net::ERR_CONNECTION_REFUSED error inside WebSocket
64
+ console.error(err);
65
+ publicAPI.fireConnectionError(publicAPI, err);
66
+ return null;
67
+ }
68
+
69
+ model.connection.binaryType = "blob";
70
+ if (!model.secret) model.secret = DEFAULT_SECRET;
71
+ model.session = Session.newInstance({
72
+ ws: model.connection,
73
+ secret: model.secret,
74
+ });
75
+
76
+ model.connection.onopen = (event) => {
77
+ if (model.session) {
78
+ // sends handshake message - wait for reply before issuing ready()
79
+ model.session.onconnect(event).then(
80
+ () => {
81
+ publicAPI.fireConnectionReady(publicAPI);
82
+ },
83
+ (err) => {
84
+ console.error("Connection error", err);
85
+ publicAPI.fireConnectionError(publicAPI, err);
86
+ }
87
+ );
88
+ }
89
+ };
90
+
91
+ model.connection.onclose = (event) => {
92
+ publicAPI.fireConnectionClose(publicAPI, event);
93
+ model.connection = null;
94
+ // return !model.retry; // true => Stop retry
95
+ };
96
+ model.connection.onerror = (event) => {
97
+ publicAPI.fireConnectionError(publicAPI, event);
98
+ };
99
+ // handle messages in the session.
100
+ model.connection.onmessage = (event) => {
101
+ model.session.onmessage(event);
102
+ };
103
+ return model.session;
104
+ };
105
+
106
+ publicAPI.getSession = () => model.session;
107
+
108
+ publicAPI.getUrl = () =>
109
+ model.connection ? model.connection.url : undefined;
110
+
111
+ function cleanUp(timeout = 10) {
112
+ if (
113
+ model.connection &&
114
+ model.connection.readyState === 1 &&
115
+ model.session &&
116
+ timeout > 0
117
+ ) {
118
+ model.session.call("application.exit.later", [timeout]);
119
+ }
120
+ if (model.connection) {
121
+ model.connection.close();
122
+ }
123
+ model.connection = null;
124
+ }
125
+
126
+ publicAPI.destroy = CompositeClosureHelper.chain(cleanUp, publicAPI.destroy);
127
+ }
128
+
129
+ const DEFAULT_VALUES = {
130
+ secret: DEFAULT_SECRET,
131
+ connection: null,
132
+ session: null,
133
+ retry: false,
134
+ wsProxy: false, // Use WSLINK.WebSocket if true else native WebSocket
135
+ };
136
+
137
+ export function extend(publicAPI, model, initialValues = {}) {
138
+ Object.assign(model, DEFAULT_VALUES, initialValues);
139
+
140
+ CompositeClosureHelper.destroy(publicAPI, model);
141
+ CompositeClosureHelper.event(publicAPI, model, "ConnectionReady");
142
+ CompositeClosureHelper.event(publicAPI, model, "ConnectionClose");
143
+ CompositeClosureHelper.event(publicAPI, model, "ConnectionError");
144
+ CompositeClosureHelper.isA(publicAPI, model, "WebsocketConnection");
145
+
146
+ WebsocketConnection(publicAPI, model);
147
+ }
148
+
149
+ // ----------------------------------------------------------------------------
150
+
151
+ export const newInstance = CompositeClosureHelper.newInstance(extend);
152
+
153
+ // ----------------------------------------------------------------------------
154
+
155
+ export default { newInstance, extend };