@moddable/pebbleproxy 0.1.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.
Files changed (2) hide show
  1. package/package.json +17 -0
  2. package/proxy.js +479 -0
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@moddable/pebbleproxy",
3
+ "version": "0.1.0",
4
+ "description": "Proxy network protocols for PebbleOS",
5
+ "license": "LGPLv3",
6
+ "main": "proxy.js",
7
+ "exports": {
8
+ ".": "./proxy.js"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/Moddable-OpenSource/moddable"
16
+ }
17
+ }
package/proxy.js ADDED
@@ -0,0 +1,479 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Moddable Tech, Inc.
3
+ *
4
+ * This file is part of the Moddable SDK Runtime.
5
+ *
6
+ * The Moddable SDK Runtime is free software: you can redistribute it and/or modify
7
+ * it under the terms of the GNU Lesser General Public License as published by
8
+ * the Free Software Foundation, either version 3 of the License, or
9
+ * (at your option) any later version.
10
+ *
11
+ * The Moddable SDK Runtime is distributed in the hope that it will be useful,
12
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ * GNU Lesser General Public License for more details.
15
+ *
16
+ * You should have received a copy of the GNU Lesser General Public License
17
+ * along with the Moddable SDK Runtime. If not, see <http://www.gnu.org/licenses/>.
18
+ *
19
+ */
20
+
21
+ console.log("moddable proxies launched");
22
+
23
+ const HTTP_BASE = 15000;
24
+ const WS_BASE = 15050;
25
+
26
+ const gHTTPRequests = new Map;
27
+ const gWSRequests = new Map;
28
+
29
+ function eventReceived(e) {
30
+ if (state.log) {
31
+ console.log("moddable appmessage received");
32
+
33
+ for (let key in e.payload) {
34
+ console.log(key);
35
+ let value = e.payload[key];
36
+ if (Array.isArray(value)) {
37
+ let tt = Uint8Array.from(value);
38
+ tt = String.fromCharCode(...tt);
39
+ console.log("binary: " + tt);
40
+ }
41
+ else
42
+ console.log((typeof value) + ": " + value);
43
+ }
44
+ }
45
+
46
+ let id = e.payload[HTTP_BASE + 1];
47
+ if (undefined !== id) {
48
+ try {
49
+ httpMessage(id, e);
50
+ }
51
+ catch (error) {
52
+ console.log("moddable pxoxy http exception" + error);
53
+ }
54
+ return true;
55
+ }
56
+
57
+ id = e.payload[WS_BASE + 1];
58
+ if (undefined !== id) {
59
+ try {
60
+ wsMessage(id, e);
61
+ }
62
+ catch (error) {
63
+ console.log("moddable pxoxy ws exception" + error);
64
+ }
65
+ return true;
66
+ }
67
+
68
+ return false;
69
+ };
70
+
71
+ function httpMessage(id, e) {
72
+ if (state.log)
73
+ console.log(" connection: " + id);
74
+
75
+ if (!gHTTPRequests.has(id))
76
+ gHTTPRequests.set(id, {id, state: "configure", kind: "http"});
77
+ const request = gHTTPRequests.get(id);
78
+
79
+ switch (request.state) {
80
+ case "configure": {
81
+ const [protocol, method, host, port, path, bufferSize, headersMask] = arrayToString(e.payload[HTTP_BASE + 2]).split(":");
82
+ request.bufferSize = parseInt(bufferSize);
83
+ if ("/" === path)
84
+ request.path = "";
85
+ else
86
+ request.path = path || "";
87
+ request.port = port;
88
+ request.host = host;
89
+ request.protocol = protocol;
90
+ request.method = method;
91
+ request.headersMask = headersMask ? headersMask.split(",") : "*";
92
+
93
+ request.state = "recieveHeaders";
94
+ request.headers = "";
95
+ } break;
96
+
97
+ case "recieveHeaders":
98
+ if (e.payload[HTTP_BASE + 3]) {
99
+ request.headers += arrayToString(e.payload[HTTP_BASE + 3]);
100
+ break;
101
+ }
102
+ request.requestBody = new Uint8Array(0);
103
+ request.state = "receiveBody";
104
+ // deliberate fall through
105
+
106
+ case "receiveBody":
107
+ if (e.payload[HTTP_BASE + 4]) {
108
+ const fragment = arrayToUint8Array(e.payload[HTTP_BASE + 4]);
109
+ const requestBody = new Uint8Array(fragment.length + request.requestBody.length);
110
+ requestBody.set(request.requestBody);
111
+ requestBody.set(fragment, request.requestBody.length);;
112
+ request.requestBody = requestBody;
113
+ break;
114
+ }
115
+ request.state = "makeRequest";
116
+ // deliberate fall through
117
+
118
+ case "makeRequest": {
119
+ if (!e.payload[HTTP_BASE + 5])
120
+ throw new Error("expected property missing");
121
+
122
+ if (state.log) {
123
+ console.log("make the request")
124
+ console.log(` method: ${request.method}`);
125
+ console.log(` protocol: ${request.protocol}`);
126
+ console.log(` host: ${request.host}`);
127
+ console.log(` port: ${request.port}`);
128
+ console.log(` path: ${request.path}`);
129
+ console.log(` bufferSize: ${request.bufferSize}`);
130
+ console.log(` headersMask: ${request.headersMask}`);
131
+ console.log(` requestBody: ${request.requestBody.length} bytes`);
132
+ request.headers.split("\n").forEach(line => console.log(" " + line));
133
+ }
134
+
135
+ request.xhr = new XMLHttpRequest;
136
+ const url = `${request.protocol}://${request.host}${request.port ? ":" + request.port : ""}/${request.path}`;
137
+ if (state.log)
138
+ console.log(` url: ${url}`);
139
+ request.xhr.open(request.method, url, true);
140
+ request.xhr.responseType = 'arraybuffer';
141
+
142
+ request.headers.split("\n").forEach(line => {
143
+ const [key, value] = line.split(":");
144
+ request.xhr.setRequestHeader(key, value);
145
+ });
146
+
147
+ request.xhr.onload = function () {
148
+ request.messages = [];
149
+
150
+ request.messages.push({
151
+ [HTTP_BASE + 1]: request.id,
152
+ [HTTP_BASE + 6]: request.xhr.status,
153
+ [HTTP_BASE + 11]: request.xhr.statusText
154
+ });
155
+
156
+ const headers = request.xhr.getAllResponseHeaders().split("\r\n").filter(header => {
157
+ if ("*" === request.headersMask)
158
+ return true; // no mask, return all
159
+ header = header.split(":");
160
+ return request.headersMask.includes(header[0].trim().toLowerCase());
161
+ }).map(header => {
162
+ header = header.split(":");
163
+ header[0] = header[0].trim().toLowerCase();
164
+ header[1] = header[1].trim();
165
+ return header.join(":");
166
+ }).join("\n");
167
+ for (let position = 0, fragmentSize = request.bufferSize - 32 /* @@ */; position < headers.length; position += fragmentSize) {
168
+ const fragment = headers.slice(position, position + fragmentSize);
169
+ request.messages.push({
170
+ [HTTP_BASE + 1]: request.id,
171
+ [HTTP_BASE + 7]: fragment
172
+ });
173
+ }
174
+
175
+ for (let position = 0, response = new Uint8Array(request.xhr.response), fragmentSize = request.bufferSize - 32 /* @@ */; position < response.byteLength; position += fragmentSize) {
176
+ const fragment = response.slice(position, position + fragmentSize);
177
+ request.messages.push({
178
+ [HTTP_BASE + 1]: request.id,
179
+ [HTTP_BASE + 8]: Array.from(fragment) // sendAppMessage won't accept ArrayBuffer or Uint8Array. only Array.
180
+ });
181
+ }
182
+ request.messages.push({
183
+ [HTTP_BASE + 1]: request.id,
184
+ [HTTP_BASE + 9]: 0 // done. success.
185
+ });
186
+ request.state = "sendMessages";
187
+
188
+ sendRequestMessage(request);
189
+ }
190
+ request.xhr.onerror = function () {
191
+ console.log("ON error!!!!")
192
+ Pebble.sendAppMessage({
193
+ [HTTP_BASE + 1]: request.id,
194
+ [HTTP_BASE + 9]: -1 // done. failure.
195
+ });
196
+ }
197
+ if (request.requestBody.length)
198
+ request.xhr.send(request.requestBody.buffer);
199
+ else
200
+ request.xhr.send();
201
+ request.state = "waitResponse";
202
+ } break;
203
+
204
+ default:
205
+ console.log("unexpected state " + request.state + "\n");
206
+ break;
207
+ }
208
+ }
209
+
210
+ function wsMessage(id, e) {
211
+ if (!gWSRequests.has(id))
212
+ gWSRequests.set(id, {id, state: "configure", kind: "ws"});
213
+ const request = gWSRequests.get(id);
214
+
215
+ switch (request.state) {
216
+ case "configure": {
217
+ const [protocol, subprotocol, host, port, path, bufferSize] = arrayToString(e.payload[WS_BASE + 2]).split(":");
218
+ request.bufferSize = parseInt(bufferSize);
219
+ if ("/" === path)
220
+ request.path = "";
221
+ else
222
+ request.path = path || "";
223
+ request.port = port;
224
+ request.host = host;
225
+ request.protocol = protocol;
226
+ request.subprotocol = subprotocol ? subprotocol.split(",") : [];
227
+
228
+ request.state = "waitHandshake";
229
+ request.headers = ""; //@@ to do
230
+ }
231
+
232
+ // deliberate fall through
233
+
234
+ case "connecting": {
235
+ if (state.log) {
236
+ console.log("websocket connect")
237
+ console.log(` protocol: ${request.protocol}`);
238
+ console.log(` host: ${request.host}`);
239
+ console.log(` port: ${request.port}`);
240
+ console.log(` path: ${request.path}`);
241
+ console.log(` subprotocol: ${request.subprotocol}`);
242
+ console.log(` bufferSize: ${request.bufferSize}`);
243
+ }
244
+
245
+ const url = `${request.protocol}://${request.host}${request.port ? ":" + request.port : ""}/${request.path}`;
246
+ if (state.log)
247
+ console.log(` url: ${url}`);
248
+
249
+ //@@ use of subprotocol gives exception in pypkjs request.ws = request.subprotocol ? new WebSocket(url, request.subprotocol) : new WebSocket(url);
250
+ request.ws = new WebSocket(url);
251
+ request.ws.binaryType = "arraybuffer";
252
+
253
+ request.ws.onopen = event => {
254
+ if (state.log)
255
+ console.log("websocket connected to host");
256
+ request.state = "connected";
257
+ request.messages = [];
258
+ request.messages.sending = false;
259
+ Pebble.sendAppMessage({
260
+ [WS_BASE + 1]: request.id,
261
+ [WS_BASE + 2]: 0 // connected. success.
262
+ });
263
+ };
264
+ request.ws.onerror = event => {
265
+ if (state.log)
266
+ console.log("websocket connection failed");
267
+ request.state = "error";
268
+ Pebble.sendAppMessage({
269
+ [WS_BASE + 1]: request.id,
270
+ [WS_BASE + 3]: -1 // disconnected error.
271
+ });
272
+ };
273
+ request.ws.onclose = event => {
274
+ if (state.log)
275
+ console.log("websocket connection closed");
276
+ request.state = "closed";
277
+ let reason = event.reason ? stringToArray(event.reason) : [];
278
+ let bytes = new Uint8Array(2 + reason.length);
279
+ let code = event.code ? event.code : 0;
280
+ bytes[0] = event.code >> 8;
281
+ bytes[1] = event.code;
282
+ if (state.log)
283
+ console.log(`close code ${code} reason ${arrayToString(reason)}`);
284
+ if (reason.byteLength)
285
+ bytes.set(arrayToUint8Array(reason.slice(2)), 2);
286
+ Pebble.sendAppMessage({
287
+ [WS_BASE + 1]: request.id,
288
+ [WS_BASE + 3]: 0, // disconnected clean.
289
+ [WS_BASE + 10]: Array.from(bytes) // sendAppMessage wants an Array
290
+ });
291
+ };
292
+ request.ws.onmessage = event => {
293
+ let data = event.data; // either ArrayBuffer or String
294
+ if (data instanceof ArrayBuffer)
295
+ data = new Uint8Array(data);
296
+ const binary = "string" !== typeof data;
297
+ if (binary)
298
+ data = Array.from(data); // sendAppMessage wants an Array
299
+ else
300
+ data = stringToArray(data); // sendAppMessage wants an Array
301
+
302
+ for (let position = 0, fragmentSize = request.bufferSize - 64 /* @@ */; position < data.length; position += fragmentSize) {
303
+ const fragment = data.slice(position, position + fragmentSize);
304
+ const more = (position + fragment.length) < data.length;
305
+ const part = (binary ? 4 : 6) + (more ? 1 : 0);
306
+
307
+ request.messages.push({
308
+ [WS_BASE + 1]: request.id,
309
+ [WS_BASE + part]: data
310
+ });
311
+ }
312
+
313
+ if (!request.messages.sending)
314
+ sendRequestMessage(request);
315
+ };
316
+ } break;
317
+
318
+ case "connected": {
319
+ let binary;
320
+ if (!request.pendingWrite)
321
+ request.pendingWrite = [];
322
+
323
+ if (e.payload[WS_BASE + 4]) { // binary no more
324
+ request.pendingWrite.push(e.payload[WS_BASE + 4]);
325
+ binary = true;
326
+ }
327
+ else if (e.payload[WS_BASE + 5]) // binary more
328
+ request.pendingWrite.push(e.payload[WS_BASE + 5]);
329
+ else if (e.payload[WS_BASE + 6]) { // text no more
330
+ request.pendingWrite.push(e.payload[WS_BASE + 6]);
331
+ binary = false;
332
+ }
333
+ else if (e.payload[WS_BASE + 7]) // text more
334
+ request.pendingWrite.push(e.payload[WS_BASE + 7]);
335
+ else if (e.payload[WS_BASE + 8]) { // close
336
+ request.state = "closing";
337
+ const bytes = arrayToUint8Array(e.payload[WS_BASE + 8]);
338
+ let code, reason;
339
+ if (bytes.byteLength >= 2) {
340
+ code = (new DataView(bytes.buffer)).getInt16(0, false);
341
+ if (state.log)
342
+ console.log(`code ${code}`);
343
+ if (bytes.byteLength > 2) {
344
+ reason = arrayToString(e.payload[WS_BASE + 8].slice(2));
345
+ if (state.log)
346
+ console.log(`reason ${reason}`);
347
+ }
348
+ }
349
+ // if (undefined === code)
350
+ request.ws.close();
351
+ // else if (undefined === reason)
352
+ // request.ws.close(code);
353
+ // else
354
+ // request.ws.close(code, reason);
355
+ return;
356
+ }
357
+ else {
358
+ console.log("no payload found!");
359
+ throw new Error("surrender");
360
+ }
361
+
362
+ if (undefined !== binary) {
363
+ let total = 0;
364
+ request.pendingWrite.forEach(fragment => total += fragment.length);
365
+ let msg = new Uint8Array(total);
366
+ for (let i = 0, offset = 0; i < request.pendingWrite.length; offset += request.pendingWrite[i++].length)
367
+ msg.set(arrayToUint8Array(request.pendingWrite[i]), offset);
368
+ if (binary)
369
+ request.ws.send(msg);
370
+ else
371
+ request.ws.send(arrayToString(msg));
372
+ delete request.pendingWrite;
373
+ }
374
+ } break;
375
+
376
+ default:
377
+ console.log("unexpected state " + request.state + "\n");
378
+ break;
379
+ }
380
+ }
381
+
382
+ function sendRequestMessage(request) {
383
+ if ("http" === request.kind)
384
+ sendRequestMessageHTTP(request);
385
+ else if ("ws" === request.kind)
386
+ sendRequestMessageWS(request);
387
+ else
388
+ throw new Error("unexpected request kind: " + request.kind);
389
+ }
390
+
391
+ function sendRequestMessageHTTP(request)
392
+ {
393
+ Pebble.sendAppMessage(
394
+ request.messages.shift(),
395
+ function () {
396
+ if (request.messages.length)
397
+ sendRequestMessage(request);
398
+ else
399
+ request.state = "done";
400
+ },
401
+ function () {
402
+ console.log("message send FAILED");
403
+
404
+ Pebble.sendAppMessage({
405
+ [HTTP_BASE + 1]: request.id,
406
+ [HTTP_BASE + 9]: -1 // done. failure.
407
+ });
408
+ }
409
+ );
410
+ }
411
+
412
+ function sendRequestMessageWS(request) {
413
+ Pebble.sendAppMessage(
414
+ request.messages.shift(),
415
+ function () {
416
+ if (request.messages.length)
417
+ sendRequestMessage(request);
418
+ else
419
+ request.messages.sending = false;
420
+ },
421
+ function (e) {
422
+ console.log("message send FAILED " + JSON.stringify(e));
423
+
424
+ Pebble.sendAppMessage({
425
+ [WS_BASE + 1]: request.id,
426
+ [WS_BASE + 3]: -1 // done. failure.
427
+ });
428
+ }
429
+ );
430
+ request.messages.sending = true;
431
+ }
432
+
433
+ function arrayToString(a) {
434
+ return String.fromCharCode(...a);
435
+ }
436
+
437
+ function arrayToUint8Array(a) {
438
+ return Uint8Array.from(a);
439
+ }
440
+
441
+ function stringToArray(str) {
442
+ const result = [];
443
+
444
+ for (let i = 0; i < str.length; i++) {
445
+ const charCode = str.charCodeAt(i);
446
+
447
+ if (charCode < 0x80)
448
+ result.push(charCode);
449
+ else if (charCode < 0x800)
450
+ result.push( 0xc0 | (charCode >> 6),
451
+ 0x80 | (charCode & 0x3f));
452
+ else if (charCode < 0xd800 || charCode >= 0xe000)
453
+ result.push( 0xe0 | (charCode >> 12),
454
+ 0x80 | ((charCode >> 6) & 0x3f),
455
+ 0x80 | (charCode & 0x3f));
456
+ else {
457
+ i++;
458
+ if (i >= str.length)
459
+ throw new Error('Unmatched surrogate pair');
460
+
461
+ const surrogate1 = charCode, surrogate2 = str.charCodeAt(i);
462
+ const codePoint = 0x10000 + ((surrogate1 - 0xd800) << 10) + (surrogate2 - 0xdc00);
463
+
464
+ result.push( 0xf0 | (codePoint >> 18),
465
+ 0x80 | ((codePoint >> 12) & 0x3f),
466
+ 0x80 | ((codePoint >> 6) & 0x3f),
467
+ 0x80 | (codePoint & 0x3f));
468
+ }
469
+ }
470
+
471
+ return result;
472
+ }
473
+
474
+ const state = {
475
+ eventReceived,
476
+ log: false
477
+ };
478
+
479
+ module.exports = state;