@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.
- package/dist/io.js +3 -0
- package/dist/io.js.map +1 -0
- package/dist/plugin/buffer-serializer.js +18 -1
- package/dist/plugin/buffer-serializer.js.map +1 -1
- package/dist/plugin/media.js +4 -3
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +2 -0
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +83 -14
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +7 -5
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-websocket.js +0 -2
- package/dist/plugin/plugin-remote-websocket.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +12 -10
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +4 -2
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/rpc.js +18 -27
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +95 -2
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +20 -21
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/server-settings.js +67 -15
- package/dist/server-settings.js.map +1 -1
- package/dist/services/cors.js +17 -0
- package/dist/services/cors.js.map +1 -0
- package/package.json +6 -4
- package/python/plugin-remote.py +22 -17
- package/python/rpc.py +0 -6
- package/src/io.ts +25 -0
- package/src/plugin/buffer-serializer.ts +19 -0
- package/src/plugin/media.ts +5 -4
- package/src/plugin/plugin-host-api.ts +2 -0
- package/src/plugin/plugin-host.ts +70 -17
- package/src/plugin/plugin-http.ts +11 -8
- package/src/plugin/plugin-remote-websocket.ts +0 -2
- package/src/plugin/plugin-remote-worker.ts +12 -10
- package/src/plugin/plugin-remote.ts +4 -2
- package/src/rpc.ts +22 -36
- package/src/runtime.ts +86 -3
- package/src/scrypted-server-main.ts +24 -27
- package/src/server-settings.ts +54 -16
- package/src/services/cors.ts +19 -0
- package/dist/addresses.js +0 -12
- package/dist/addresses.js.map +0 -1
- 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
|
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(
|
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(
|
176
|
+
this.remote.ioEvent(id, 'message', message)
|
144
177
|
});
|
145
178
|
socket.on('close', reason => {
|
146
|
-
this.remote.ioEvent(
|
179
|
+
this.remote.ioEvent(id, 'close');
|
147
180
|
});
|
148
181
|
|
149
|
-
await handler.onConnection(endpointRequest, `io://${
|
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.
|
315
|
-
|
316
|
-
|
317
|
-
}
|
318
|
-
};
|
347
|
+
this.peer.params.updateStats = (stats: any) => {
|
348
|
+
this.stats = stats;
|
349
|
+
}
|
319
350
|
}
|
320
351
|
|
321
|
-
async createRpcIoPeer(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
|
-
|
327
|
-
|
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
|
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
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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.
|
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.
|
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<
|
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(
|
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
|
|