@scrypted/server 0.0.181 → 0.1.1

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.

Potentially problematic release.


This version of @scrypted/server might be problematic. Click here for more details.

Files changed (48) hide show
  1. package/dist/io.js +3 -0
  2. package/dist/io.js.map +1 -0
  3. package/dist/plugin/buffer-serializer.js +18 -1
  4. package/dist/plugin/buffer-serializer.js.map +1 -1
  5. package/dist/plugin/media.js +4 -3
  6. package/dist/plugin/media.js.map +1 -1
  7. package/dist/plugin/plugin-host-api.js +2 -0
  8. package/dist/plugin/plugin-host-api.js.map +1 -1
  9. package/dist/plugin/plugin-host.js +83 -14
  10. package/dist/plugin/plugin-host.js.map +1 -1
  11. package/dist/plugin/plugin-http.js +7 -5
  12. package/dist/plugin/plugin-http.js.map +1 -1
  13. package/dist/plugin/plugin-remote-websocket.js +0 -2
  14. package/dist/plugin/plugin-remote-websocket.js.map +1 -1
  15. package/dist/plugin/plugin-remote-worker.js +12 -10
  16. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  17. package/dist/plugin/plugin-remote.js +4 -2
  18. package/dist/plugin/plugin-remote.js.map +1 -1
  19. package/dist/rpc.js +18 -27
  20. package/dist/rpc.js.map +1 -1
  21. package/dist/runtime.js +95 -2
  22. package/dist/runtime.js.map +1 -1
  23. package/dist/scrypted-server-main.js +20 -21
  24. package/dist/scrypted-server-main.js.map +1 -1
  25. package/dist/server-settings.js +67 -15
  26. package/dist/server-settings.js.map +1 -1
  27. package/dist/services/cors.js +17 -0
  28. package/dist/services/cors.js.map +1 -0
  29. package/package.json +6 -4
  30. package/python/plugin-remote.py +22 -17
  31. package/python/rpc.py +0 -6
  32. package/src/io.ts +25 -0
  33. package/src/plugin/buffer-serializer.ts +19 -0
  34. package/src/plugin/media.ts +5 -4
  35. package/src/plugin/plugin-host-api.ts +2 -0
  36. package/src/plugin/plugin-host.ts +70 -17
  37. package/src/plugin/plugin-http.ts +11 -8
  38. package/src/plugin/plugin-remote-websocket.ts +0 -2
  39. package/src/plugin/plugin-remote-worker.ts +12 -10
  40. package/src/plugin/plugin-remote.ts +4 -2
  41. package/src/rpc.ts +22 -36
  42. package/src/runtime.ts +86 -3
  43. package/src/scrypted-server-main.ts +24 -27
  44. package/src/server-settings.ts +54 -16
  45. package/src/services/cors.ts +19 -0
  46. package/dist/addresses.js +0 -12
  47. package/dist/addresses.js.map +0 -1
  48. package/src/addresses.ts +0 -5
@@ -1,17 +1,19 @@
1
1
  import { Device, EngineIOHandler } from '@scrypted/types';
2
2
  import AdmZip from 'adm-zip';
3
3
  import crypto from 'crypto';
4
- import io, { Socket } from 'engine.io';
4
+ import * as io from 'engine.io';
5
5
  import fs from 'fs';
6
6
  import mkdirp from 'mkdirp';
7
7
  import path from 'path';
8
8
  import rimraf from 'rimraf';
9
- import WebSocket from 'ws';
9
+ import WebSocket, { once } from 'ws';
10
10
  import { Plugin } from '../db-types';
11
+ import { IOServer, IOServerSocket } from '../io';
11
12
  import { Logger } from '../logger';
12
13
  import { RpcPeer } from '../rpc';
13
14
  import { ScryptedRuntime } from '../runtime';
14
15
  import { sleep } from '../sleep';
16
+ import { SidebandBufferSerializer } from './buffer-serializer';
15
17
  import { MediaManagerHostImpl } from './media';
16
18
  import { PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
17
19
  import { ConsoleServer, createConsoleServer } from './plugin-console';
@@ -34,8 +36,16 @@ export class PluginHost {
34
36
  module: Promise<any>;
35
37
  scrypted: ScryptedRuntime;
36
38
  remote: PluginRemote;
37
- io = io(undefined, {
39
+ io: IOServer = new io.Server({
38
40
  pingTimeout: 120000,
41
+ perMessageDeflate: true,
42
+ cors: (req, callback) => {
43
+ const header = this.scrypted.getAccessControlAllowOrigin(req.headers);
44
+ callback(undefined, {
45
+ origin: header,
46
+ credentials: true,
47
+ })
48
+ },
39
49
  });
40
50
  ws: { [id: string]: WebSocket } = {};
41
51
  api: PluginHostAPI;
@@ -123,6 +133,26 @@ export class PluginHost {
123
133
  try {
124
134
  try {
125
135
  if (socket.request.url.indexOf('/api') !== -1) {
136
+ if (socket.request.url.indexOf('/public') !== -1) {
137
+ socket.send(JSON.stringify({
138
+ // @ts-expect-error
139
+ id: socket.id,
140
+ }));
141
+ const timeout = new Promise((_, rj) => setTimeout(() => rj(new Error('timeout')), 10000));
142
+ try {
143
+ await Promise.race([
144
+ once(socket, '/api/activate'),
145
+ timeout,
146
+ ]);
147
+ // client will send a start request when it's ready to process events.
148
+ await once(socket, 'message');
149
+ }
150
+ catch (e) {
151
+ socket.close();
152
+ return;
153
+ }
154
+ }
155
+
126
156
  await this.createRpcIoPeer(socket);
127
157
  return;
128
158
  }
@@ -139,14 +169,17 @@ export class PluginHost {
139
169
 
140
170
  const handler = this.scrypted.getDevice<EngineIOHandler>(pluginDevice._id);
141
171
 
172
+ // @ts-expect-error
173
+ const id = socket.id;
174
+
142
175
  socket.on('message', message => {
143
- this.remote.ioEvent(socket.id, 'message', message)
176
+ this.remote.ioEvent(id, 'message', message)
144
177
  });
145
178
  socket.on('close', reason => {
146
- this.remote.ioEvent(socket.id, 'close');
179
+ this.remote.ioEvent(id, 'close');
147
180
  });
148
181
 
149
- await handler.onConnection(endpointRequest, `io://${socket.id}`);
182
+ await handler.onConnection(endpointRequest, `io://${id}`);
150
183
  }
151
184
  catch (e) {
152
185
  console.error('engine.io plugin error', e);
@@ -311,22 +344,40 @@ export class PluginHost {
311
344
  disconnect();
312
345
  });
313
346
 
314
- this.peer.onOob = (oob: any) => {
315
- if (oob.type === 'stats') {
316
- this.stats = oob;
317
- }
318
- };
347
+ this.peer.params.updateStats = (stats: any) => {
348
+ this.stats = stats;
349
+ }
319
350
  }
320
351
 
321
- async createRpcIoPeer(socket: Socket) {
352
+ async createRpcIoPeer(socket: IOServerSocket) {
322
353
  let connected = true;
323
- const rpcPeer = new RpcPeer(`api/${this.pluginId}`, 'web', (message, reject) => {
324
- if (!connected)
354
+ const rpcPeer = new RpcPeer(`api/${this.pluginId}`, 'web', (message, reject, serializationContext) => {
355
+ if (!connected) {
325
356
  reject?.(new Error('peer disconnected'));
326
- else
327
- socket.send(JSON.stringify(message))
357
+ return;
358
+ }
359
+ const buffers = serializationContext?.buffers;
360
+ if (buffers) {
361
+ for (const buffer of buffers) {
362
+ socket.send(buffer);
363
+ }
364
+ }
365
+ socket.send(JSON.stringify(message))
366
+ });
367
+ let pendingSerializationContext: any = {};
368
+ socket.on('message', data => {
369
+ if (data.constructor === Buffer || data.constructor === ArrayBuffer) {
370
+ pendingSerializationContext = pendingSerializationContext || {
371
+ buffers: [],
372
+ };
373
+ const buffers: Buffer[] = pendingSerializationContext.buffers;
374
+ buffers.push(Buffer.from(data));
375
+ return;
376
+ }
377
+ const messageSerializationContext = pendingSerializationContext;
378
+ pendingSerializationContext = undefined;
379
+ rpcPeer.handleMessage(JSON.parse(data as string), messageSerializationContext);
328
380
  });
329
- socket.on('message', data => rpcPeer.handleMessage(JSON.parse(data as string)));
330
381
  // wrap the host api with a connection specific api that can be torn down on disconnect
331
382
  const api = new PluginAPIProxy(this.api, await this.peer.getParam('mediaManager'));
332
383
  const kill = () => {
@@ -336,6 +387,8 @@ export class PluginHost {
336
387
  }
337
388
  socket.on('close', kill);
338
389
  socket.on('error', kill);
390
+
391
+ rpcPeer.addSerializer(Buffer, 'Buffer', new SidebandBufferSerializer());
339
392
  return setupPluginRemote(rpcPeer, api, null, () => this.scrypted.stateManager.getSystemState());
340
393
  }
341
394
  }
@@ -1,29 +1,32 @@
1
- import { Request, Response, Router } from 'express';
2
- import bodyParser from 'body-parser';
3
1
  import { HttpRequest } from '@scrypted/types';
4
- import WebSocket, { Server as WebSocketServer } from "ws";
2
+ import bodyParser from 'body-parser';
3
+ import { Request, Response, Router } from 'express';
5
4
  import { ServerResponse } from 'http';
5
+ import WebSocket, { Server as WebSocketServer } from "ws";
6
6
 
7
7
  export abstract class PluginHttp<T> {
8
8
  wss = new WebSocketServer({ noServer: true });
9
9
 
10
10
  constructor(public app: Router) {
11
- app.all(['/endpoint/@:owner/:pkg/public/engine.io/*', '/endpoint/:pkg/public/engine.io/*'], (req, res) => {
11
+ }
12
+
13
+ addMiddleware() {
14
+ this.app.all(['/endpoint/@:owner/:pkg/public/engine.io/*', '/endpoint/:pkg/public/engine.io/*'], (req, res) => {
12
15
  this.endpointHandler(req, res, true, true, this.handleEngineIOEndpoint.bind(this))
13
16
  });
14
17
 
15
- app.all(['/endpoint/@:owner/:pkg/engine.io/*', '/endpoint/@:owner/:pkg/engine.io/*'], (req, res) => {
18
+ this.app.all(['/endpoint/@:owner/:pkg/engine.io/*', '/endpoint/@:owner/:pkg/engine.io/*'], (req, res) => {
16
19
  this.endpointHandler(req, res, false, true, this.handleEngineIOEndpoint.bind(this))
17
20
  });
18
21
 
19
22
  // stringify all http endpoints
20
- app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], bodyParser.text() as any);
23
+ this.app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], bodyParser.text() as any);
21
24
 
22
- app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg/public', '/endpoint/:pkg/public/*'], (req, res) => {
25
+ this.app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg/public', '/endpoint/:pkg/public/*'], (req, res) => {
23
26
  this.endpointHandler(req, res, true, false, this.handleRequestEndpoint.bind(this))
24
27
  });
25
28
 
26
- app.all(['/endpoint/@:owner/:pkg', '/endpoint/@:owner/:pkg/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], (req, res) => {
29
+ this.app.all(['/endpoint/@:owner/:pkg', '/endpoint/@:owner/:pkg/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], (req, res) => {
27
30
  this.endpointHandler(req, res, false, false, this.handleRequestEndpoint.bind(this))
28
31
  });
29
32
  }
@@ -10,7 +10,6 @@ interface WebSocketEventListener {
10
10
  (evt: WebSocketEvent): void;
11
11
  }
12
12
 
13
- // @ts-ignore
14
13
  class WebSocketEventTarget {
15
14
  events: { [type: string]: WebSocketEventListener[] } = {};
16
15
 
@@ -71,7 +70,6 @@ export interface WebSocketMethods {
71
70
 
72
71
  export function createWebSocketClass(__websocketConnect: WebSocketConnect): any {
73
72
 
74
- // @ts-ignore
75
73
  class WebSocket extends WebSocketEventTarget {
76
74
  _url: string;
77
75
  _protocols: string[];
@@ -149,16 +149,18 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
149
149
  }, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
150
150
  }
151
151
 
152
- let lastCpuUsage: NodeJS.CpuUsage;
153
- setInterval(() => {
154
- const cpuUsage = process.cpuUsage(lastCpuUsage);
155
- lastCpuUsage = cpuUsage;
156
- peer.sendOob({
157
- type: 'stats',
158
- cpu: cpuUsage,
159
- memoryUsage: process.memoryUsage(),
160
- });
161
- }, 10000);
152
+ peer.getParam('updateStats').then((updateStats: (stats: any) => void) => {
153
+ let lastCpuUsage: NodeJS.CpuUsage;
154
+ setInterval(() => {
155
+ const cpuUsage = process.cpuUsage(lastCpuUsage);
156
+ lastCpuUsage = cpuUsage;
157
+ updateStats({
158
+ type: 'stats',
159
+ cpu: cpuUsage,
160
+ memoryUsage: process.memoryUsage(),
161
+ });
162
+ }, 10000);
163
+ });
162
164
 
163
165
  let replPort: Promise<number>;
164
166
 
@@ -300,7 +300,8 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
300
300
  // core plugin to web (node to browser).
301
301
  // always add the BufferSerializer, so serialization is gauranteed to work.
302
302
  // but in plugin-host, mark Buffer as transport safe.
303
- peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
303
+ if (!peer.constructorSerializerMap.get(Buffer))
304
+ peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
304
305
  const getRemote = await peer.getParam('getRemote');
305
306
  const remote = await getRemote(api, pluginId);
306
307
 
@@ -355,7 +356,8 @@ export interface PluginRemoteAttachOptions {
355
356
  export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
356
357
  const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole, getPluginConsole } = options || {};
357
358
 
358
- peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
359
+ if (!peer.constructorSerializerMap.get(Buffer))
360
+ peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
359
361
 
360
362
  let done: (scrypted: ScryptedStatic) => void;
361
363
  const retPromise = new Promise<ScryptedStatic>(resolve => done = resolve);
package/src/rpc.ts CHANGED
@@ -43,10 +43,6 @@ interface RpcResult extends RpcMessage {
43
43
  result?: any;
44
44
  }
45
45
 
46
- interface RpcOob extends RpcMessage {
47
- oob: any;
48
- }
49
-
50
46
  interface RpcRemoteProxyValue {
51
47
  __remote_proxy_id: string | undefined;
52
48
  __remote_proxy_finalizer_id: string | undefined;
@@ -127,8 +123,9 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
127
123
  // undefined is not JSON serializable.
128
124
  const method = target() || null;
129
125
  const args: any[] = [];
126
+ const serializationContext: any = {};
130
127
  for (const arg of (argArray || [])) {
131
- args.push(this.peer.serialize(arg));
128
+ args.push(this.peer.serialize(arg, serializationContext));
132
129
  }
133
130
 
134
131
  const rpcApply: RpcApply = {
@@ -147,7 +144,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
147
144
 
148
145
  return this.peer.createPendingResult((id, reject) => {
149
146
  rpcApply.id = id;
150
- this.peer.send(rpcApply, reject);
147
+ this.peer.send(rpcApply, reject, serializationContext);
151
148
  })
152
149
  }
153
150
  }
@@ -193,8 +190,8 @@ catch (e) {
193
190
  }
194
191
 
195
192
  export interface RpcSerializer {
196
- serialize(value: any): any;
197
- deserialize(serialized: any): any;
193
+ serialize(value: any, serializationContext?: any): any;
194
+ deserialize(serialized: any, serializationContext?: any): any;
198
195
  }
199
196
 
200
197
  interface LocalProxiedEntry {
@@ -204,7 +201,6 @@ interface LocalProxiedEntry {
204
201
 
205
202
  export class RpcPeer {
206
203
  idCounter = 1;
207
- onOob?: (oob: any) => void;
208
204
  params: { [name: string]: any } = {};
209
205
  pendingResults: { [id: string]: Deferred } = {};
210
206
  proxyCounter = 1;
@@ -213,7 +209,7 @@ export class RpcPeer {
213
209
  remoteWeakProxies: { [id: string]: WeakRef<any> } = {};
214
210
  finalizers = new FinalizationRegistry(entry => this.finalize(entry as LocalProxiedEntry));
215
211
  nameDeserializerMap = new Map<string, RpcSerializer>();
216
- constructorSerializerMap = new Map<string, string>();
212
+ constructorSerializerMap = new Map<any, string>();
217
213
  transportSafeArgumentTypes = RpcPeer.getDefaultTransportSafeArgumentTypes();
218
214
 
219
215
  static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
@@ -251,7 +247,7 @@ export class RpcPeer {
251
247
  static readonly PROPERTY_PROXY_PROPERTIES = '__proxy_props';
252
248
  static readonly PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children';
253
249
 
254
- constructor(public selfName: string, public peerName: string, public send: (message: RpcMessage, reject?: (e: Error) => void) => void) {
250
+ constructor(public selfName: string, public peerName: string, public send: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
255
251
  }
256
252
 
257
253
  createPendingResult(cb: (id: string, reject: (e: Error) => void) => void): Promise<any> {
@@ -310,13 +306,6 @@ export class RpcPeer {
310
306
  });
311
307
  }
312
308
 
313
- sendOob(oob: any) {
314
- this.send({
315
- type: 'oob',
316
- oob,
317
- } as RpcOob)
318
- }
319
-
320
309
  evalLocal<T>(script: string, filename?: string, coercedParams?: { [name: string]: any }): T {
321
310
  const params = Object.assign({}, this.params, coercedParams);
322
311
  let compile: CompileFunction;
@@ -339,7 +328,7 @@ export class RpcPeer {
339
328
  result.message = (e as Error).message || 'no message';
340
329
  }
341
330
 
342
- deserialize(value: any): any {
331
+ deserialize(value: any, deserializationContext: any): any {
343
332
  if (!value)
344
333
  return value;
345
334
 
@@ -347,7 +336,7 @@ export class RpcPeer {
347
336
  if (copySerializeChildren) {
348
337
  const ret: any = {};
349
338
  for (const [key, val] of Object.entries(value)) {
350
- ret[key] = this.deserialize(val);
339
+ ret[key] = this.deserialize(val, deserializationContext);
351
340
  }
352
341
  return ret;
353
342
  }
@@ -370,17 +359,17 @@ export class RpcPeer {
370
359
 
371
360
  const deserializer = this.nameDeserializerMap.get(__remote_constructor_name);
372
361
  if (deserializer) {
373
- return deserializer.deserialize(__serialized_value);
362
+ return deserializer.deserialize(__serialized_value, deserializationContext);
374
363
  }
375
364
 
376
365
  return value;
377
366
  }
378
367
 
379
- serialize(value: any): any {
368
+ serialize(value: any, serializationContext: any): any {
380
369
  if (value?.[RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN] === true) {
381
370
  const ret: any = {};
382
371
  for (const [key, val] of Object.entries(value)) {
383
- ret[key] = this.serialize(val);
372
+ ret[key] = this.serialize(val, serializationContext);
384
373
  }
385
374
  return ret;
386
375
  }
@@ -419,7 +408,7 @@ export class RpcPeer {
419
408
  const serializer = this.nameDeserializerMap.get(serializerMapName);
420
409
  if (!serializer)
421
410
  throw new Error('serializer not found for ' + serializerMapName);
422
- const serialized = serializer.serialize(value);
411
+ const serialized = serializer.serialize(value, serializationContext);
423
412
  const ret: RpcRemoteProxyValue = {
424
413
  __remote_proxy_id: undefined,
425
414
  __remote_proxy_finalizer_id: undefined,
@@ -464,17 +453,18 @@ export class RpcPeer {
464
453
  return proxy;
465
454
  }
466
455
 
467
- async handleMessage(message: RpcMessage) {
456
+ async handleMessage(message: RpcMessage, deserializationContext?: any) {
468
457
  try {
469
458
  switch (message.type) {
470
459
  case 'param': {
471
460
  const rpcParam = message as RpcParam;
461
+ const serializationContext: any = {};
472
462
  const result: RpcResult = {
473
463
  type: 'result',
474
464
  id: rpcParam.id,
475
- result: this.serialize(this.params[rpcParam.param])
465
+ result: this.serialize(this.params[rpcParam.param], serializationContext)
476
466
  };
477
- this.send(result);
467
+ this.send(result, undefined, serializationContext);
478
468
  break;
479
469
  }
480
470
  case 'apply': {
@@ -483,6 +473,7 @@ export class RpcPeer {
483
473
  type: 'result',
484
474
  id: rpcApply.id || '',
485
475
  };
476
+ const serializationContext: any = {};
486
477
 
487
478
  try {
488
479
  const target = this.localProxyMap[rpcApply.proxyId];
@@ -491,7 +482,7 @@ export class RpcPeer {
491
482
 
492
483
  const args = [];
493
484
  for (const arg of (rpcApply.args || [])) {
494
- args.push(this.deserialize(arg));
485
+ args.push(this.deserialize(arg, deserializationContext));
495
486
  }
496
487
 
497
488
  let value: any;
@@ -505,7 +496,7 @@ export class RpcPeer {
505
496
  value = await target(...args);
506
497
  }
507
498
 
508
- result.result = this.serialize(value);
499
+ result.result = this.serialize(value, serializationContext);
509
500
  }
510
501
  catch (e) {
511
502
  // console.error('failure', rpcApply.method, e);
@@ -513,7 +504,7 @@ export class RpcPeer {
513
504
  }
514
505
 
515
506
  if (!rpcApply.oneway)
516
- this.send(result);
507
+ this.send(result, undefined, serializationContext);
517
508
  break;
518
509
  }
519
510
  case 'result': {
@@ -530,7 +521,7 @@ export class RpcPeer {
530
521
  deferred.reject(e);
531
522
  return;
532
523
  }
533
- deferred.resolve(this.deserialize(rpcResult.result));
524
+ deferred.resolve(this.deserialize(rpcResult.result, deserializationContext));
534
525
  break;
535
526
  }
536
527
  case 'finalize': {
@@ -547,11 +538,6 @@ export class RpcPeer {
547
538
  }
548
539
  break;
549
540
  }
550
- case 'oob': {
551
- const rpcOob = message as RpcOob;
552
- this.onOob?.(rpcOob.oob);
553
- break;
554
- }
555
541
  default:
556
542
  throw new Error(`unknown rpc message type ${message.type}`);
557
543
  }
package/src/runtime.ts CHANGED
@@ -6,7 +6,7 @@ import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
6
6
  import { getState, ScryptedStateManager, setState } from './state';
7
7
  import { Request, Response } from 'express';
8
8
  import { createResponseInterface } from './http-interfaces';
9
- import http, { ServerResponse } from 'http';
9
+ import http, { ServerResponse, IncomingHttpHeaders } from 'http';
10
10
  import https from 'https';
11
11
  import express from 'express';
12
12
  import { LogEntry, Logger, makeAlertId } from './logger';
@@ -25,13 +25,15 @@ import semver from 'semver';
25
25
  import { ServiceControl } from './services/service-control';
26
26
  import { Alerts } from './services/alerts';
27
27
  import { Info } from './services/info';
28
- import io from 'engine.io';
28
+ import * as io from 'engine.io';
29
29
  import { spawn as ptySpawn } from 'node-pty';
30
30
  import rimraf from 'rimraf';
31
31
  import { getPluginVolume } from './plugin/plugin-volume';
32
32
  import { PluginHttp } from './plugin/plugin-http';
33
33
  import AdmZip from 'adm-zip';
34
34
  import path from 'path';
35
+ import { CORSControl, CORSServer } from './services/cors';
36
+ import { IOServer, IOServerSocket } from './io';
35
37
 
36
38
  interface DeviceProxyPair {
37
39
  handler: PluginDeviceProxyHandler;
@@ -56,9 +58,18 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
56
58
  devicesLogger = this.logger.getLogger('device', 'Devices');
57
59
  wss = new WebSocketServer({ noServer: true });
58
60
  wsAtomic = 0;
59
- shellio = io(undefined, {
61
+ shellio: IOServer = new io.Server({
60
62
  pingTimeout: 120000,
63
+ perMessageDeflate: true,
64
+ cors: (req, callback) => {
65
+ const header = this.getAccessControlAllowOrigin(req.headers);
66
+ callback(undefined, {
67
+ origin: header,
68
+ credentials: true,
69
+ })
70
+ },
61
71
  });
72
+ cors: CORSServer[] = [];
62
73
 
63
74
  constructor(datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
64
75
  super(app);
@@ -67,6 +78,48 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
67
78
 
68
79
  app.disable('x-powered-by');
69
80
 
81
+ this.app.options(['/endpoint/@:owner/:pkg/engine.io/api/activate', '/endpoint/@:owner/:pkg/engine.io/api/activate'], (req, res) => {
82
+ this.addAccessControlHeaders(req, res);
83
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
84
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
85
+ res.send(200);
86
+ });
87
+
88
+ this.app.post(['/endpoint/@:owner/:pkg/engine.io/api/activate', '/endpoint/@:owner/:pkg/engine.io/api/activate'], (req, res) => {
89
+ const { username } = (req as any);
90
+ if (!username) {
91
+ res.status(401);
92
+ res.send('Not Authorized');
93
+ return;
94
+ }
95
+
96
+ const { owner, pkg } = req.params;
97
+ let endpoint = pkg;
98
+ if (owner)
99
+ endpoint = `@${owner}/${endpoint}`;
100
+
101
+ const { id } = req.body;
102
+ try {
103
+ const host = this.plugins?.[endpoint];
104
+ if (!host)
105
+ throw new Error('invalid plugin');
106
+ // @ts-expect-error
107
+ const socket: IOServerSocket = host.io.clients[id];
108
+ if (!socket)
109
+ throw new Error('invalid socket');
110
+ socket.emit('/api/activate');
111
+ res.send({
112
+ id,
113
+ })
114
+ }
115
+ catch (e) {
116
+ res.status(500);
117
+ res.end();
118
+ }
119
+ });
120
+
121
+ this.addMiddleware();
122
+
70
123
  app.get('/web/oauth/callback', (req, res) => {
71
124
  this.oauthCallback(req, res);
72
125
  });
@@ -122,6 +175,34 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
122
175
  }, 60 * 60 * 1000);
123
176
  }
124
177
 
178
+ addAccessControlHeaders(req: http.IncomingMessage, res: http.ServerResponse) {
179
+ res.setHeader('Vary', 'Origin,Referer');
180
+ const header = this.getAccessControlAllowOrigin(req.headers);
181
+ if (header)
182
+ res.setHeader('Access-Control-Allow-Origin', header);
183
+ }
184
+
185
+ getAccessControlAllowOrigin(headers: http.IncomingHttpHeaders) {
186
+ let { origin, referer } = headers;
187
+ if (!origin && referer) {
188
+ try {
189
+ const u = new URL(headers.referer)
190
+ origin = u.origin;
191
+ }
192
+ catch (e) {
193
+ return;
194
+ }
195
+ }
196
+ if (!origin)
197
+ return;
198
+ const servers: string[] = process.env.SCRYPTED_ACCESS_CONTROL_ALLOW_ORIGINS?.split(',') || [];
199
+ servers.push(...Object.values(this.cors).map(entry => entry.server));
200
+ if (!servers.includes(origin))
201
+ return;
202
+
203
+ return origin;
204
+ }
205
+
125
206
  getDeviceLogger(device: PluginDevice): Logger {
126
207
  return this.devicesLogger.getLogger(device._id, getState(device, ScryptedInterfaceProperty.name));
127
208
  }
@@ -311,6 +392,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
311
392
  return this.logger;
312
393
  case 'alerts':
313
394
  return new Alerts(this);
395
+ case 'cors':
396
+ return new CORSControl(this);
314
397
  }
315
398
  }
316
399