@scrypted/server 0.1.15 → 0.1.16
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/http-interfaces.js +11 -0
- package/dist/http-interfaces.js.map +1 -1
- package/dist/plugin/media.js +57 -34
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-host.js +6 -4
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +1 -1
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +118 -7
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +11 -81
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/node-fork-worker.js +11 -3
- package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
- package/dist/plugin/socket-serializer.js +17 -0
- package/dist/plugin/socket-serializer.js.map +1 -0
- package/dist/rpc.js +1 -1
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +3 -1
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-plugin-main.js +4 -1
- package/dist/scrypted-plugin-main.js.map +1 -1
- package/dist/scrypted-server-main.js +45 -8
- package/dist/scrypted-server-main.js.map +1 -1
- package/package.json +2 -2
- package/src/http-interfaces.ts +13 -0
- package/src/plugin/media.ts +66 -34
- package/src/plugin/plugin-api.ts +1 -0
- package/src/plugin/plugin-host.ts +6 -4
- package/src/plugin/plugin-http.ts +2 -2
- package/src/plugin/plugin-remote-worker.ts +138 -10
- package/src/plugin/plugin-remote.ts +14 -88
- package/src/plugin/runtime/node-fork-worker.ts +11 -3
- package/src/plugin/runtime/runtime-worker.ts +1 -1
- package/src/plugin/socket-serializer.ts +15 -0
- package/src/rpc.ts +1 -1
- package/src/runtime.ts +4 -1
- package/src/scrypted-plugin-main.ts +4 -1
- package/src/scrypted-server-main.ts +51 -9
@@ -1,16 +1,22 @@
|
|
1
1
|
import { DeviceManager, ScryptedNativeId, ScryptedStatic, SystemManager } from '@scrypted/types';
|
2
|
+
import AdmZip from 'adm-zip';
|
2
3
|
import { Console } from 'console';
|
4
|
+
import fs from 'fs';
|
5
|
+
import { Volume } from 'memfs';
|
3
6
|
import net from 'net';
|
7
|
+
import path from 'path';
|
4
8
|
import { install as installSourceMapSupport } from 'source-map-support';
|
5
9
|
import { PassThrough } from 'stream';
|
6
10
|
import { RpcMessage, RpcPeer } from '../rpc';
|
7
11
|
import { MediaManagerImpl } from './media';
|
8
|
-
import { PluginAPI } from './plugin-api';
|
12
|
+
import { PluginAPI, PluginRemoteLoadZipOptions } from './plugin-api';
|
9
13
|
import { installOptionalDependencies } from './plugin-npm-dependencies';
|
10
|
-
import { attachPluginRemote, PluginReader } from './plugin-remote';
|
14
|
+
import { attachPluginRemote, PluginReader, setupPluginRemote } from './plugin-remote';
|
11
15
|
import { createREPLServer } from './plugin-repl';
|
16
|
+
import { NodeThreadWorker } from './runtime/node-thread-worker';
|
17
|
+
const { link } = require('linkfs');
|
12
18
|
|
13
|
-
export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void) => void) {
|
19
|
+
export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
|
14
20
|
const peer = new RpcPeer('unknown', 'host', peerSend);
|
15
21
|
|
16
22
|
let systemManager: SystemManager;
|
@@ -66,13 +72,18 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
66
72
|
return pluginsPromise;
|
67
73
|
}
|
68
74
|
|
75
|
+
const deviceConsoles = new Map<string, Console>();
|
69
76
|
const getDeviceConsole = (nativeId?: ScryptedNativeId) => {
|
70
77
|
// the the plugin console is simply the default console
|
71
78
|
// and gets read from stderr/stdout.
|
72
79
|
if (!nativeId)
|
73
80
|
return console;
|
74
81
|
|
75
|
-
|
82
|
+
let ret = deviceConsoles.get(nativeId);
|
83
|
+
if (ret)
|
84
|
+
return ret;
|
85
|
+
|
86
|
+
ret = getConsole(async (stdout, stderr) => {
|
76
87
|
const connect = async () => {
|
77
88
|
const plugins = await getPlugins();
|
78
89
|
const port = await plugins.getRemoteServicePort(peer.selfName, 'console-writer');
|
@@ -93,10 +104,25 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
93
104
|
};
|
94
105
|
connect();
|
95
106
|
}, undefined, undefined);
|
107
|
+
|
108
|
+
deviceConsoles.set(nativeId, ret);
|
109
|
+
return ret;
|
96
110
|
}
|
97
111
|
|
112
|
+
const mixinConsoles = new Map<string, Map<string, Console>>();
|
113
|
+
|
98
114
|
const getMixinConsole = (mixinId: string, nativeId: ScryptedNativeId) => {
|
99
|
-
|
115
|
+
let nativeIdConsoles = mixinConsoles.get(nativeId);
|
116
|
+
if (!nativeIdConsoles) {
|
117
|
+
nativeIdConsoles = new Map();
|
118
|
+
mixinConsoles.set(nativeId, nativeIdConsoles);
|
119
|
+
}
|
120
|
+
|
121
|
+
let ret = nativeIdConsoles.get(mixinId);
|
122
|
+
if (ret)
|
123
|
+
return ret;
|
124
|
+
|
125
|
+
ret = getConsole(async (stdout, stderr) => {
|
100
126
|
if (!mixinId) {
|
101
127
|
return;
|
102
128
|
}
|
@@ -147,6 +173,9 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
147
173
|
}
|
148
174
|
tryConnect();
|
149
175
|
}, getDeviceConsole(nativeId), `[${systemManager.getDeviceById(mixinId)?.name}]`);
|
176
|
+
|
177
|
+
nativeIdConsoles.set(mixinId, ret);
|
178
|
+
return ret;
|
150
179
|
}
|
151
180
|
|
152
181
|
peer.getParam('updateStats').then((updateStats: (stats: any) => void) => {
|
@@ -183,10 +212,6 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
183
212
|
api = _api;
|
184
213
|
peer.selfName = pluginId;
|
185
214
|
},
|
186
|
-
onPluginReady: async (scrypted, params, plugin) => {
|
187
|
-
replPort = createREPLServer(scrypted, params, plugin);
|
188
|
-
postInstallSourceMapSupport(scrypted);
|
189
|
-
},
|
190
215
|
getPluginConsole,
|
191
216
|
getDeviceConsole,
|
192
217
|
getMixinConsole,
|
@@ -198,7 +223,59 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
198
223
|
}
|
199
224
|
throw new Error(`unknown service ${name}`);
|
200
225
|
},
|
201
|
-
async onLoadZip(
|
226
|
+
async onLoadZip(scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
|
227
|
+
let volume: any;
|
228
|
+
let pluginReader: PluginReader;
|
229
|
+
if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
|
230
|
+
volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
|
231
|
+
pluginReader = name => {
|
232
|
+
const filename = path.join(zipOptions.unzippedPath, name);
|
233
|
+
if (!fs.existsSync(filename))
|
234
|
+
return;
|
235
|
+
return fs.readFileSync(filename);
|
236
|
+
};
|
237
|
+
}
|
238
|
+
else {
|
239
|
+
const admZip = new AdmZip(zipData);
|
240
|
+
volume = new Volume();
|
241
|
+
for (const entry of admZip.getEntries()) {
|
242
|
+
if (entry.isDirectory)
|
243
|
+
continue;
|
244
|
+
if (!entry.entryName.startsWith('fs/'))
|
245
|
+
continue;
|
246
|
+
const name = entry.entryName.substring('fs/'.length);
|
247
|
+
volume.mkdirpSync(path.dirname(name));
|
248
|
+
const data = entry.getData();
|
249
|
+
volume.writeFileSync(name, data);
|
250
|
+
}
|
251
|
+
|
252
|
+
pluginReader = name => {
|
253
|
+
const entry = admZip.getEntry(name);
|
254
|
+
if (!entry)
|
255
|
+
return;
|
256
|
+
return entry.getData();
|
257
|
+
}
|
258
|
+
}
|
259
|
+
zipData = undefined;
|
260
|
+
|
261
|
+
const pluginConsole = getPluginConsole?.();
|
262
|
+
params.console = pluginConsole;
|
263
|
+
params.require = (name: string) => {
|
264
|
+
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
|
265
|
+
return volume;
|
266
|
+
}
|
267
|
+
if (name === 'realfs') {
|
268
|
+
return require('fs');
|
269
|
+
}
|
270
|
+
const module = require(name);
|
271
|
+
return module;
|
272
|
+
};
|
273
|
+
const window: any = {};
|
274
|
+
const exports: any = window;
|
275
|
+
window.exports = exports;
|
276
|
+
params.window = window;
|
277
|
+
params.exports = exports;
|
278
|
+
|
202
279
|
const entry = pluginReader('main.nodejs.js.map')
|
203
280
|
const map = entry?.toString();
|
204
281
|
|
@@ -234,6 +311,57 @@ export function startPluginRemote(pluginId: string, peerSend: (message: RpcMessa
|
|
234
311
|
};
|
235
312
|
|
236
313
|
await installOptionalDependencies(getPluginConsole(), packageJson);
|
314
|
+
|
315
|
+
const main = pluginReader('main.nodejs.js');
|
316
|
+
pluginReader = undefined;
|
317
|
+
const script = main.toString();
|
318
|
+
|
319
|
+
scrypted.fork = async () => {
|
320
|
+
const ntw = new NodeThreadWorker(pluginId, {
|
321
|
+
env: process.env,
|
322
|
+
pluginDebug: undefined,
|
323
|
+
});
|
324
|
+
const threadPeer = new RpcPeer('main', 'thread', (message, reject) => ntw.send(message, reject));
|
325
|
+
threadPeer.params.updateStats = (stats: any) => {
|
326
|
+
// todo: merge.
|
327
|
+
// this.stats = stats;
|
328
|
+
}
|
329
|
+
ntw.setupRpcPeer(threadPeer);
|
330
|
+
|
331
|
+
const remote = await setupPluginRemote(threadPeer, api, pluginId, () => systemManager.getSystemState());
|
332
|
+
const forkOptions = Object.assign({}, zipOptions);
|
333
|
+
forkOptions.fork = true;
|
334
|
+
return remote.loadZip(packageJson, zipData, forkOptions)
|
335
|
+
}
|
336
|
+
|
337
|
+
try {
|
338
|
+
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
339
|
+
pluginConsole?.log('plugin successfully loaded');
|
340
|
+
|
341
|
+
if (zipOptions?.fork) {
|
342
|
+
const fork = exports.fork;
|
343
|
+
const ret = await fork();
|
344
|
+
ret[RpcPeer.PROPERTY_JSON_DISABLE_SERIALIZATION] = true;
|
345
|
+
return ret;
|
346
|
+
}
|
347
|
+
|
348
|
+
let pluginInstance = exports.default;
|
349
|
+
// support exporting a plugin class, plugin main function,
|
350
|
+
// or a plugin instance
|
351
|
+
if (pluginInstance.toString().startsWith('class '))
|
352
|
+
pluginInstance = new pluginInstance();
|
353
|
+
if (typeof pluginInstance === 'function')
|
354
|
+
pluginInstance = await pluginInstance();
|
355
|
+
|
356
|
+
replPort = createREPLServer(scrypted, params, pluginInstance);
|
357
|
+
postInstallSourceMapSupport(scrypted);
|
358
|
+
|
359
|
+
return pluginInstance;
|
360
|
+
}
|
361
|
+
catch (e) {
|
362
|
+
pluginConsole?.error('plugin failed to start', e);
|
363
|
+
throw e;
|
364
|
+
}
|
237
365
|
}
|
238
366
|
}).then(scrypted => {
|
239
367
|
systemManager = scrypted.systemManager;
|
@@ -1,15 +1,10 @@
|
|
1
|
-
import
|
2
|
-
import { Volume } from 'memfs';
|
3
|
-
import path from 'path';
|
4
|
-
import { ScryptedNativeId, DeviceManager, Logger, Device, DeviceManifest, DeviceState, EndpointManager, SystemDeviceState, ScryptedStatic, SystemManager, MediaManager, ScryptedMimeTypes, ScryptedInterface, ScryptedInterfaceProperty, HttpRequest } from '@scrypted/types'
|
5
|
-
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
6
|
-
import { SystemManagerImpl } from './system';
|
1
|
+
import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, HttpRequest, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
|
7
2
|
import { RpcPeer, RPCResultError } from '../rpc';
|
8
3
|
import { BufferSerializer } from './buffer-serializer';
|
4
|
+
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
9
5
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketMethods } from './plugin-remote-websocket';
|
10
|
-
import fs from 'fs';
|
11
6
|
import { checkProperty } from './plugin-state-check';
|
12
|
-
|
7
|
+
import { SystemManagerImpl } from './system';
|
13
8
|
|
14
9
|
class DeviceLogger implements Logger {
|
15
10
|
nativeId: ScryptedNativeId;
|
@@ -347,13 +342,12 @@ export interface PluginRemoteAttachOptions {
|
|
347
342
|
getDeviceConsole?: (nativeId?: ScryptedNativeId) => Console;
|
348
343
|
getPluginConsole?: () => Console;
|
349
344
|
getMixinConsole?: (id: string, nativeId?: ScryptedNativeId) => Console;
|
350
|
-
onLoadZip?: (
|
345
|
+
onLoadZip?: (scrypted: ScryptedStatic, params: any, packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) => Promise<any>;
|
351
346
|
onGetRemote?: (api: PluginAPI, pluginId: string) => Promise<void>;
|
352
|
-
onPluginReady?: (scrypted: ScryptedStatic, params: any, plugin: any) => Promise<void>;
|
353
347
|
}
|
354
348
|
|
355
349
|
export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOptions): Promise<ScryptedStatic> {
|
356
|
-
const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole
|
350
|
+
const { createMediaManager, getServicePort, getDeviceConsole, getMixinConsole } = options || {};
|
357
351
|
|
358
352
|
if (!peer.constructorSerializerMap.get(Buffer))
|
359
353
|
peer.addSerializer(Buffer, 'Buffer', new BufferSerializer());
|
@@ -367,7 +361,11 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
367
361
|
const systemManager = new SystemManagerImpl();
|
368
362
|
const deviceManager = new DeviceManagerImpl(systemManager, getDeviceConsole, getMixinConsole);
|
369
363
|
const endpointManager = new EndpointManagerImpl();
|
370
|
-
const
|
364
|
+
const hostMediaManager = await api.getMediaManager();
|
365
|
+
if (!hostMediaManager) {
|
366
|
+
peer.params['createMediaManager'] = async () => createMediaManager(systemManager, deviceManager);
|
367
|
+
}
|
368
|
+
const mediaManager = hostMediaManager || await createMediaManager(systemManager, deviceManager);
|
371
369
|
peer.params['mediaManager'] = mediaManager;
|
372
370
|
const ioSockets: { [id: string]: WebSocketConnectCallbacks } = {};
|
373
371
|
|
@@ -382,6 +380,7 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
382
380
|
endpointManager,
|
383
381
|
mediaManager,
|
384
382
|
log,
|
383
|
+
pluginHostAPI: api,
|
385
384
|
}
|
386
385
|
|
387
386
|
delete peer.params.getRemote;
|
@@ -472,50 +471,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
472
471
|
},
|
473
472
|
|
474
473
|
async loadZip(packageJson: any, zipData: Buffer | string, zipOptions?: PluginRemoteLoadZipOptions) {
|
475
|
-
const pluginConsole = getPluginConsole?.();
|
476
|
-
|
477
|
-
let volume: any;
|
478
|
-
let pluginReader: PluginReader;
|
479
|
-
if (zipOptions?.unzippedPath && fs.existsSync(zipOptions?.unzippedPath)) {
|
480
|
-
volume = link(fs, ['', path.join(zipOptions.unzippedPath, 'fs')]);
|
481
|
-
pluginReader = name => {
|
482
|
-
const filename = path.join(zipOptions.unzippedPath, name);
|
483
|
-
if (!fs.existsSync(filename))
|
484
|
-
return;
|
485
|
-
return fs.readFileSync(filename);
|
486
|
-
};
|
487
|
-
}
|
488
|
-
else {
|
489
|
-
const admZip = new AdmZip(zipData);
|
490
|
-
volume = new Volume();
|
491
|
-
for (const entry of admZip.getEntries()) {
|
492
|
-
if (entry.isDirectory)
|
493
|
-
continue;
|
494
|
-
if (!entry.entryName.startsWith('fs/'))
|
495
|
-
continue;
|
496
|
-
const name = entry.entryName.substring('fs/'.length);
|
497
|
-
volume.mkdirpSync(path.dirname(name));
|
498
|
-
const data = entry.getData();
|
499
|
-
volume.writeFileSync(name, data);
|
500
|
-
}
|
501
|
-
|
502
|
-
pluginReader = name => {
|
503
|
-
const entry = admZip.getEntry(name);
|
504
|
-
if (!entry)
|
505
|
-
return;
|
506
|
-
return entry.getData();
|
507
|
-
}
|
508
|
-
}
|
509
|
-
zipData = undefined;
|
510
|
-
|
511
|
-
await options?.onLoadZip?.(pluginReader, packageJson);
|
512
|
-
const main = pluginReader('main.nodejs.js');
|
513
|
-
pluginReader = undefined;
|
514
|
-
const script = main.toString();
|
515
|
-
const window: any = {};
|
516
|
-
const exports: any = window;
|
517
|
-
window.exports = exports;
|
518
|
-
|
519
474
|
|
520
475
|
function websocketConnect(url: string, protocols: any, callbacks: WebSocketConnectCallbacks) {
|
521
476
|
if (url.startsWith('io://') || url.startsWith('ws://')) {
|
@@ -535,18 +490,6 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
535
490
|
|
536
491
|
const params: any = {
|
537
492
|
__filename: undefined,
|
538
|
-
exports,
|
539
|
-
window,
|
540
|
-
require: (name: string) => {
|
541
|
-
if (name === 'fakefs' || (name === 'fs' && !packageJson.scrypted.realfs)) {
|
542
|
-
return volume;
|
543
|
-
}
|
544
|
-
if (name === 'realfs') {
|
545
|
-
return require('fs');
|
546
|
-
}
|
547
|
-
const module = require(name);
|
548
|
-
return module;
|
549
|
-
},
|
550
493
|
deviceManager,
|
551
494
|
systemManager,
|
552
495
|
mediaManager,
|
@@ -555,29 +498,12 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
555
498
|
localStorage,
|
556
499
|
pluginHostAPI: api,
|
557
500
|
WebSocket: createWebSocketClass(websocketConnect),
|
501
|
+
pluginRuntimeAPI: ret,
|
558
502
|
};
|
559
503
|
|
560
|
-
params.
|
504
|
+
params.pluginRuntimeAPI = ret;
|
561
505
|
|
562
|
-
|
563
|
-
peer.evalLocal(script, zipOptions?.filename || '/plugin/main.nodejs.js', params);
|
564
|
-
pluginConsole?.log('plugin successfully loaded');
|
565
|
-
|
566
|
-
let pluginInstance = exports.default;
|
567
|
-
// support exporting a plugin class, plugin main function,
|
568
|
-
// or a plugin instance
|
569
|
-
if (pluginInstance.toString().startsWith('class '))
|
570
|
-
pluginInstance = new pluginInstance();
|
571
|
-
if (typeof pluginInstance === 'function')
|
572
|
-
pluginInstance = await pluginInstance();
|
573
|
-
|
574
|
-
await options?.onPluginReady?.(ret, params, pluginInstance);
|
575
|
-
return pluginInstance;
|
576
|
-
}
|
577
|
-
catch (e) {
|
578
|
-
pluginConsole?.error('plugin failed to start', e);
|
579
|
-
throw e;
|
580
|
-
}
|
506
|
+
return options.onLoadZip(ret, params, packageJson, zipData, zipOptions);
|
581
507
|
},
|
582
508
|
}
|
583
509
|
|
@@ -4,6 +4,8 @@ import path from 'path';
|
|
4
4
|
import { RpcMessage, RpcPeer } from "../../rpc";
|
5
5
|
import { ChildProcessWorker } from "./child-process-worker";
|
6
6
|
import { getPluginNodePath } from "../plugin-npm-dependencies";
|
7
|
+
import { SidebandSocketSerializer } from "../socket-serializer";
|
8
|
+
import net from "net";
|
7
9
|
|
8
10
|
export class NodeForkWorker extends ChildProcessWorker {
|
9
11
|
|
@@ -31,7 +33,12 @@ export class NodeForkWorker extends ChildProcessWorker {
|
|
31
33
|
|
32
34
|
setupRpcPeer(peer: RpcPeer): void {
|
33
35
|
this.worker.on('message', (message, sendHandle) => {
|
34
|
-
if (sendHandle) {
|
36
|
+
if ((message as any).type && sendHandle) {
|
37
|
+
peer.handleMessage(message as any, {
|
38
|
+
sendHandle,
|
39
|
+
});
|
40
|
+
}
|
41
|
+
else if (sendHandle) {
|
35
42
|
this.emit('rpc', message, sendHandle);
|
36
43
|
}
|
37
44
|
else {
|
@@ -39,13 +46,14 @@ export class NodeForkWorker extends ChildProcessWorker {
|
|
39
46
|
}
|
40
47
|
});
|
41
48
|
peer.transportSafeArgumentTypes.add(Buffer.name);
|
49
|
+
peer.addSerializer(net.Socket, net.Socket.name, new SidebandSocketSerializer());
|
42
50
|
}
|
43
51
|
|
44
|
-
send(message: RpcMessage, reject?: (e: Error) => void): void {
|
52
|
+
send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void {
|
45
53
|
try {
|
46
54
|
if (!this.worker)
|
47
55
|
throw new Error('worked has been killed');
|
48
|
-
this.worker.send(message,
|
56
|
+
this.worker.send(message, serializationContext?.sendHandle, e => {
|
49
57
|
if (e && reject)
|
50
58
|
reject(e);
|
51
59
|
});
|
@@ -23,7 +23,7 @@ export interface RuntimeWorker {
|
|
23
23
|
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
24
24
|
once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
25
25
|
|
26
|
-
send(message: RpcMessage, reject?: (e: Error) => void): void;
|
26
|
+
send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void;
|
27
27
|
|
28
28
|
setupRpcPeer(peer: RpcPeer): void;
|
29
29
|
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { RpcSerializer } from "../rpc";
|
2
|
+
|
3
|
+
export class SidebandSocketSerializer implements RpcSerializer {
|
4
|
+
serialize(value: any, serializationContext?: any) {
|
5
|
+
if (!serializationContext)
|
6
|
+
throw new Error('socket serialization context unavailable');
|
7
|
+
serializationContext.sendHandle = value;
|
8
|
+
}
|
9
|
+
|
10
|
+
deserialize(serialized: any, serializationContext?: any) {
|
11
|
+
if (!serializationContext)
|
12
|
+
throw new Error('socket deserialization context unavailable');
|
13
|
+
return serializationContext.sendHandle;
|
14
|
+
}
|
15
|
+
}
|
package/src/rpc.ts
CHANGED
@@ -138,7 +138,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
|
|
138
138
|
|
139
139
|
if (this.proxyOneWayMethods?.includes?.(method)) {
|
140
140
|
rpcApply.oneway = true;
|
141
|
-
this.peer.send(rpcApply);
|
141
|
+
this.peer.send(rpcApply, undefined, serializationContext);
|
142
142
|
return Promise.resolve();
|
143
143
|
}
|
144
144
|
|
package/src/runtime.ts
CHANGED
@@ -282,8 +282,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
282
282
|
this.shellio.handleRequest(req, res);
|
283
283
|
}
|
284
284
|
|
285
|
-
async getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
|
285
|
+
async getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
|
286
286
|
const ret = await this.getPluginForEndpoint(endpoint);
|
287
|
+
if (req.url.indexOf('/engine.io/api') !== -1)
|
288
|
+
return ret;
|
289
|
+
|
287
290
|
const { pluginDevice } = ret;
|
288
291
|
|
289
292
|
// check if upgrade requests can be handled. must be websocket.
|
@@ -2,6 +2,8 @@ import { startPluginRemote } from "./plugin/plugin-remote-worker";
|
|
2
2
|
import { RpcMessage } from "./rpc";
|
3
3
|
import worker_threads from "worker_threads";
|
4
4
|
import v8 from 'v8';
|
5
|
+
import net from 'net';
|
6
|
+
import { SidebandSocketSerializer } from "./plugin/socket-serializer";
|
5
7
|
|
6
8
|
if (process.argv[2] === 'child-thread') {
|
7
9
|
const peer = startPluginRemote(process.argv[3], (message, reject) => {
|
@@ -16,7 +18,7 @@ if (process.argv[2] === 'child-thread') {
|
|
16
18
|
worker_threads.parentPort.on('message', message => peer.handleMessage(v8.deserialize(message)));
|
17
19
|
}
|
18
20
|
else {
|
19
|
-
const peer = startPluginRemote(process.argv[3], (message, reject) => process.send(message,
|
21
|
+
const peer = startPluginRemote(process.argv[3], (message, reject, serializationContext) => process.send(message, serializationContext?.sendHandle, {
|
20
22
|
swallowErrors: !reject,
|
21
23
|
}, e => {
|
22
24
|
if (e)
|
@@ -24,6 +26,7 @@ else {
|
|
24
26
|
}));
|
25
27
|
|
26
28
|
peer.transportSafeArgumentTypes.add(Buffer.name);
|
29
|
+
peer.addSerializer(net.Socket, net.Socket.name, new SidebandSocketSerializer());
|
27
30
|
process.on('message', message => peer.handleMessage(message as RpcMessage));
|
28
31
|
process.on('disconnect', () => {
|
29
32
|
console.error('peer host disconnected, exiting.');
|
@@ -155,7 +155,30 @@ async function start() {
|
|
155
155
|
// use a hash of the private key as the cookie secret.
|
156
156
|
app.use(cookieParser(crypto.createHash('sha256').update(certSetting.value.serviceKey).digest().toString('hex')));
|
157
157
|
|
158
|
-
|
158
|
+
// trap to add access control headers.
|
159
|
+
app.use((req, res, next) => {
|
160
|
+
if (!req.headers.upgrade)
|
161
|
+
scrypted.addAccessControlHeaders(req, res);
|
162
|
+
next();
|
163
|
+
})
|
164
|
+
|
165
|
+
app.options('*', (req, res) => {
|
166
|
+
// add more?
|
167
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
168
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
|
169
|
+
res.send(200);
|
170
|
+
});
|
171
|
+
|
172
|
+
const authSalt = crypto.randomBytes(16);
|
173
|
+
const createAuthorizationToken = (login_user_token: string) => {
|
174
|
+
const salted = login_user_token + authSalt;
|
175
|
+
const hash = crypto.createHash('sha256');
|
176
|
+
hash.update(salted);
|
177
|
+
const sha = hash.digest().toString('hex');
|
178
|
+
return `Bearer ${sha}#${login_user_token}`;
|
179
|
+
}
|
180
|
+
|
181
|
+
app.use(async (req, res, next) => {
|
159
182
|
// this is a trap for all auth.
|
160
183
|
// only basic auth will fail with 401. it is up to the endpoints to manage
|
161
184
|
// lack of login from cookie auth.
|
@@ -165,10 +188,8 @@ async function start() {
|
|
165
188
|
const userTokenParts = login_user_token.split('#');
|
166
189
|
const username = userTokenParts[0];
|
167
190
|
const timestamp = parseInt(userTokenParts[1]);
|
168
|
-
if (timestamp + 86400000 < Date.now())
|
169
|
-
console.warn('login expired');
|
191
|
+
if (timestamp + 86400000 < Date.now())
|
170
192
|
return next();
|
171
|
-
}
|
172
193
|
|
173
194
|
// this database lookup on every web request is not necessary, the cookie
|
174
195
|
// itself is the auth, and is signed. furthermore, this is currently
|
@@ -182,7 +203,27 @@ async function start() {
|
|
182
203
|
// }
|
183
204
|
|
184
205
|
res.locals.username = username;
|
185
|
-
|
206
|
+
}
|
207
|
+
else if (req.headers.authorization?.startsWith('Bearer ')) {
|
208
|
+
const splits = req.headers.authorization.substring('Bearer '.length).split('#');
|
209
|
+
const login_user_token = splits[1] + '#' + splits[2];
|
210
|
+
if (login_user_token) {
|
211
|
+
const check = splits[0];
|
212
|
+
|
213
|
+
const salted = login_user_token + authSalt;
|
214
|
+
const hash = crypto.createHash('sha256');
|
215
|
+
hash.update(salted);
|
216
|
+
const sha = hash.digest().toString('hex');
|
217
|
+
|
218
|
+
if (check === sha) {
|
219
|
+
const splits2 = login_user_token.split('#');
|
220
|
+
const username = splits2[0];
|
221
|
+
const timestamp = parseInt(splits2[1]);
|
222
|
+
if (timestamp + 86400000 < Date.now())
|
223
|
+
return next();
|
224
|
+
res.locals.username = username;
|
225
|
+
}
|
226
|
+
}
|
186
227
|
}
|
187
228
|
next();
|
188
229
|
});
|
@@ -355,7 +396,7 @@ async function start() {
|
|
355
396
|
});
|
356
397
|
|
357
398
|
const getLoginUserToken = (reqSecure: boolean) => {
|
358
|
-
return reqSecure ? 'login_user_token' : '
|
399
|
+
return reqSecure ? 'login_user_token' : 'login_user_token_insecure';
|
359
400
|
};
|
360
401
|
|
361
402
|
const getSignedLoginUserToken = (req: Request<any>): string => {
|
@@ -370,15 +411,12 @@ async function start() {
|
|
370
411
|
let hasLogin = await db.getCount(ScryptedUser) > 0;
|
371
412
|
|
372
413
|
app.options('/login', (req, res) => {
|
373
|
-
scrypted.addAccessControlHeaders(req, res);
|
374
414
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
375
415
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
|
376
416
|
res.send(200);
|
377
417
|
});
|
378
418
|
|
379
419
|
app.post('/login', async (req, res) => {
|
380
|
-
scrypted.addAccessControlHeaders(req, res);
|
381
|
-
|
382
420
|
const { username, password, change_password } = req.body;
|
383
421
|
const timestamp = Date.now();
|
384
422
|
const maxAge = 86400000;
|
@@ -422,6 +460,7 @@ async function start() {
|
|
422
460
|
}
|
423
461
|
|
424
462
|
res.send({
|
463
|
+
authorization: createAuthorizationToken(login_user_token),
|
425
464
|
username,
|
426
465
|
expiration: maxAge,
|
427
466
|
addresses,
|
@@ -456,6 +495,7 @@ async function start() {
|
|
456
495
|
});
|
457
496
|
|
458
497
|
res.send({
|
498
|
+
authorization: createAuthorizationToken(login_user_token),
|
459
499
|
username,
|
460
500
|
token: user.token,
|
461
501
|
expiration: maxAge,
|
@@ -463,6 +503,7 @@ async function start() {
|
|
463
503
|
});
|
464
504
|
});
|
465
505
|
|
506
|
+
|
466
507
|
app.get('/login', async (req, res) => {
|
467
508
|
scrypted.addAccessControlHeaders(req, res);
|
468
509
|
|
@@ -510,6 +551,7 @@ async function start() {
|
|
510
551
|
}
|
511
552
|
|
512
553
|
res.send({
|
554
|
+
authorization: createAuthorizationToken(login_user_token),
|
513
555
|
expiration: 86400000 - (Date.now() - timestamp),
|
514
556
|
username,
|
515
557
|
addresses,
|