@scrypted/server 0.4.6 → 0.4.9
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/db-types.js +1 -2
- package/dist/db-types.js.map +1 -1
- package/dist/mixin/mixin-cycle.js +2 -1
- package/dist/mixin/mixin-cycle.js.map +1 -1
- package/dist/plugin/acl.js +83 -0
- package/dist/plugin/acl.js.map +1 -0
- package/dist/plugin/plugin-api.js +28 -11
- package/dist/plugin/plugin-api.js.map +1 -1
- package/dist/plugin/plugin-device.js +5 -1
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +0 -16
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +13 -7
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +1 -0
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-websocket.js +25 -14
- package/dist/plugin/plugin-remote-websocket.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +3 -0
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +41 -8
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/rpc.js +37 -8
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +58 -14
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +11 -17
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/services/plugin.js +1 -12
- package/dist/services/plugin.js.map +1 -1
- package/dist/services/users.js +46 -0
- package/dist/services/users.js.map +1 -0
- package/dist/usertoken.js +9 -5
- package/dist/usertoken.js.map +1 -1
- package/package.json +2 -2
- package/src/db-types.ts +2 -2
- package/src/mixin/mixin-cycle.ts +1 -1
- package/src/plugin/acl.ts +104 -0
- package/src/plugin/plugin-api.ts +41 -25
- package/src/plugin/plugin-device.ts +7 -1
- package/src/plugin/plugin-host-api.ts +1 -20
- package/src/plugin/plugin-host.ts +21 -12
- package/src/plugin/plugin-http.ts +1 -0
- package/src/plugin/plugin-remote-websocket.ts +26 -17
- package/src/plugin/plugin-remote-worker.ts +3 -0
- package/src/plugin/plugin-remote.ts +49 -11
- package/src/rpc.ts +43 -9
- package/src/runtime.ts +64 -21
- package/src/scrypted-server-main.ts +11 -18
- package/src/services/plugin.ts +2 -12
- package/src/services/users.ts +43 -0
- package/src/usertoken.ts +13 -6
@@ -1,5 +1,6 @@
|
|
1
|
-
import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager,
|
1
|
+
import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
|
2
2
|
import { RpcPeer, RPCResultError } from '../rpc';
|
3
|
+
import { AccessControls } from './acl';
|
3
4
|
import { BufferSerializer } from './buffer-serializer';
|
4
5
|
import { PluginAPI, PluginLogger, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
|
5
6
|
import { createWebSocketClass, WebSocketConnectCallbacks, WebSocketConnection, WebSocketMethods, WebSocketSerializer } from './plugin-remote-websocket';
|
@@ -121,10 +122,6 @@ class EndpointManagerImpl implements EndpointManager {
|
|
121
122
|
return this.mediaManager.convertMediaObjectToUrl(mo, ScryptedMimeTypes.PushEndpoint);
|
122
123
|
}
|
123
124
|
|
124
|
-
async deliverPush(id: string, request: HttpRequest) {
|
125
|
-
return this.api.deliverPush(id, request);
|
126
|
-
}
|
127
|
-
|
128
125
|
async getPath(nativeId?: string, options?: { public?: boolean; }): Promise<string> {
|
129
126
|
return `/endpoint/${this.getEndpoint(nativeId)}/${options?.public ? 'public/' : ''}`
|
130
127
|
}
|
@@ -133,7 +130,7 @@ class EndpointManagerImpl implements EndpointManager {
|
|
133
130
|
const protocol = options?.insecure ? 'http' : 'https';
|
134
131
|
const port = await this.api.getComponent(options?.insecure ? 'SCRYPTED_INSECURE_PORT' : 'SCRYPTED_SECURE_PORT');
|
135
132
|
const path = await this.getPath(nativeId, options);
|
136
|
-
const url =
|
133
|
+
const url = `${protocol}://${await this.getUrlSafeIp()}:${port}${path}`;
|
137
134
|
return url;
|
138
135
|
}
|
139
136
|
|
@@ -368,8 +365,48 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
|
|
368
365
|
const getRemote = await peer.getParam('getRemote');
|
369
366
|
const remote = await getRemote(api, pluginId) as PluginRemote;
|
370
367
|
|
371
|
-
|
368
|
+
const accessControls: AccessControls = peer.tags.acl;
|
369
|
+
|
370
|
+
const getAccessControlDeviceState = (id: string, state?: { [property: string]: SystemDeviceState } ) => {
|
371
|
+
state = state || getSystemState()[id];
|
372
|
+
if (accessControls && state) {
|
373
|
+
state = Object.assign({}, state);
|
374
|
+
for (const property of Object.keys(state)) {
|
375
|
+
if (accessControls.shouldRejectProperty(id, property))
|
376
|
+
delete state[property];
|
377
|
+
}
|
378
|
+
let interfaces: ScryptedInterface[] = state.interfaces?.value;
|
379
|
+
if (interfaces) {
|
380
|
+
interfaces = interfaces.filter(scryptedInterface => !accessControls.shouldRejectInterface(id, scryptedInterface));
|
381
|
+
state.interfaces = {
|
382
|
+
value: interfaces,
|
383
|
+
}
|
384
|
+
}
|
385
|
+
}
|
386
|
+
return state;
|
387
|
+
}
|
388
|
+
|
389
|
+
const getAccessControlSystemState = () => {
|
390
|
+
let state = getSystemState();
|
391
|
+
if (accessControls) {
|
392
|
+
state = Object.assign({}, state);
|
393
|
+
for (const id of Object.keys(state)) {
|
394
|
+
if (accessControls.shouldRejectDevice(id)) {
|
395
|
+
delete state[id];
|
396
|
+
continue;
|
397
|
+
}
|
398
|
+
state[id] = getAccessControlDeviceState(id, state[id]);
|
399
|
+
}
|
400
|
+
}
|
401
|
+
|
402
|
+
return state;
|
403
|
+
}
|
404
|
+
|
405
|
+
await remote.setSystemState(getAccessControlSystemState());
|
372
406
|
api.listen((id, eventDetails, eventData) => {
|
407
|
+
if (accessControls?.shouldRejectEvent(eventDetails.property === ScryptedInterfaceProperty.id ? eventData : id, eventDetails))
|
408
|
+
return;
|
409
|
+
|
373
410
|
// ScryptedDevice events will be handled specially and repropagated by the remote.
|
374
411
|
if (eventDetails.eventInterface === ScryptedInterface.ScryptedDevice) {
|
375
412
|
if (eventDetails.property === ScryptedInterfaceProperty.id) {
|
@@ -378,7 +415,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
|
|
378
415
|
}
|
379
416
|
else {
|
380
417
|
// a change on anything else is a descriptor update
|
381
|
-
remote.updateDeviceState(id,
|
418
|
+
remote.updateDeviceState(id, getAccessControlDeviceState(id));
|
382
419
|
}
|
383
420
|
return;
|
384
421
|
}
|
@@ -429,15 +466,16 @@ export function attachPluginRemote(peer: RpcPeer, options?: PluginRemoteAttachOp
|
|
429
466
|
const retPromise = new Promise<ScryptedStatic>(resolve => done = resolve);
|
430
467
|
|
431
468
|
peer.params.getRemote = async (api: PluginAPI, pluginId: string) => {
|
432
|
-
websocketSerializer.WebSocket = createWebSocketClass((
|
469
|
+
websocketSerializer.WebSocket = createWebSocketClass((connection, callbacks) => {
|
470
|
+
const { url } = connection;
|
433
471
|
if (url.startsWith('io://') || url.startsWith('ws://')) {
|
434
472
|
const id = url.substring('xx://'.length);
|
435
473
|
|
436
474
|
ioSockets[id] = callbacks;
|
437
475
|
|
438
476
|
callbacks.connect(undefined, {
|
439
|
-
close: () =>
|
440
|
-
send: (message
|
477
|
+
close: (message) => connection.close(message),
|
478
|
+
send: (message) => connection.send(message),
|
441
479
|
});
|
442
480
|
}
|
443
481
|
else {
|
package/src/rpc.ts
CHANGED
@@ -4,19 +4,29 @@ type CompileFunction = (code: string, params?: ReadonlyArray<string>, options?:
|
|
4
4
|
export function startPeriodicGarbageCollection() {
|
5
5
|
if (!global.gc) {
|
6
6
|
console.warn('rpc peer garbage collection not available: global.gc is not exposed.');
|
7
|
-
return;
|
8
7
|
}
|
8
|
+
let g: typeof global;
|
9
9
|
try {
|
10
|
-
|
11
|
-
if (g.gc) {
|
12
|
-
return setInterval(() => {
|
13
|
-
g.gc!();
|
14
|
-
}, 10000);
|
15
|
-
}
|
10
|
+
g = global;
|
16
11
|
}
|
17
12
|
catch (e) {
|
18
|
-
|
19
13
|
}
|
14
|
+
|
15
|
+
// periodically see if new objects were created or finalized,
|
16
|
+
// and collect gc if so.
|
17
|
+
let lastCollection = 0;
|
18
|
+
return setInterval(() => {
|
19
|
+
const now = Date.now();
|
20
|
+
const sinceLastCollection = now - lastCollection;
|
21
|
+
const remotesCreated = RpcPeer.remotesCreated;
|
22
|
+
RpcPeer.remotesCreated = 0;
|
23
|
+
const remotesCollected = RpcPeer.remotesCollected;
|
24
|
+
RpcPeer.remotesCollected = 0;
|
25
|
+
if (remotesCreated || remotesCollected || sinceLastCollection > 5 * 60 * 1000) {
|
26
|
+
lastCollection = now;
|
27
|
+
g?.gc?.();
|
28
|
+
}
|
29
|
+
}, 10000);
|
20
30
|
}
|
21
31
|
|
22
32
|
export interface RpcMessage {
|
@@ -219,8 +229,12 @@ export class RpcPeer {
|
|
219
229
|
transportSafeArgumentTypes = RpcPeer.getDefaultTransportSafeArgumentTypes();
|
220
230
|
killed: Promise<void>;
|
221
231
|
killedDeferred: Deferred;
|
232
|
+
tags: any = {};
|
222
233
|
|
223
234
|
static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
|
235
|
+
static remotesCollected = 0;
|
236
|
+
static remotesCreated = 0;
|
237
|
+
static activeRpcPeer: RpcPeer;
|
224
238
|
|
225
239
|
static isRpcProxy(value: any) {
|
226
240
|
return !!value?.[RpcPeer.PROPERTY_PROXY_ID];
|
@@ -315,6 +329,8 @@ export class RpcPeer {
|
|
315
329
|
}
|
316
330
|
|
317
331
|
finalize(entry: LocalProxiedEntry) {
|
332
|
+
RpcPeer.remotesCollected++;
|
333
|
+
|
318
334
|
delete this.remoteWeakProxies[entry.id];
|
319
335
|
const rpcFinalize: RpcFinalize = {
|
320
336
|
__local_proxy_id: entry.id,
|
@@ -378,6 +394,12 @@ export class RpcPeer {
|
|
378
394
|
if (!proxy)
|
379
395
|
proxy = this.newProxy(__remote_proxy_id, __remote_constructor_name, __remote_proxy_props, __remote_proxy_oneway_methods);
|
380
396
|
proxy[RpcPeer.finalizerIdSymbol] = __remote_proxy_finalizer_id;
|
397
|
+
|
398
|
+
const deserializer = this.nameDeserializerMap.get(__remote_constructor_name);
|
399
|
+
if (deserializer) {
|
400
|
+
return deserializer.deserialize(proxy, deserializationContext);
|
401
|
+
}
|
402
|
+
|
381
403
|
return proxy;
|
382
404
|
}
|
383
405
|
|
@@ -471,6 +493,8 @@ export class RpcPeer {
|
|
471
493
|
}
|
472
494
|
|
473
495
|
newProxy(proxyId: string, proxyConstructorName: string, proxyProps: any, proxyOneWayMethods: string[]) {
|
496
|
+
RpcPeer.remotesCreated++;
|
497
|
+
|
474
498
|
const localProxiedEntry: LocalProxiedEntry = {
|
475
499
|
id: proxyId,
|
476
500
|
finalizerId: undefined,
|
@@ -484,7 +508,17 @@ export class RpcPeer {
|
|
484
508
|
return proxy;
|
485
509
|
}
|
486
510
|
|
487
|
-
|
511
|
+
handleMessage(message: RpcMessage, deserializationContext?: any) {
|
512
|
+
try {
|
513
|
+
RpcPeer.activeRpcPeer = this;
|
514
|
+
this.handleMessageInternal(message, deserializationContext);
|
515
|
+
}
|
516
|
+
finally {
|
517
|
+
RpcPeer.activeRpcPeer = undefined;
|
518
|
+
}
|
519
|
+
}
|
520
|
+
|
521
|
+
private async handleMessageInternal(message: RpcMessage, deserializationContext?: any) {
|
488
522
|
if (Object.isFrozen(this.pendingResults))
|
489
523
|
return;
|
490
524
|
|
package/src/runtime.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Device, DeviceInformation, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient,
|
1
|
+
import { Device, DeviceInformation, DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, ScryptedDevice, ScryptedInterface, ScryptedInterfaceMethod, ScryptedInterfaceProperty, ScryptedNativeId, ScryptedUser as SU } from '@scrypted/types';
|
2
2
|
import AdmZip from 'adm-zip';
|
3
3
|
import axios from 'axios';
|
4
4
|
import * as io from 'engine.io';
|
@@ -14,13 +14,14 @@ import { PassThrough } from 'stream';
|
|
14
14
|
import tar from 'tar';
|
15
15
|
import { URL } from "url";
|
16
16
|
import WebSocket, { Server as WebSocketServer } from "ws";
|
17
|
-
import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
|
17
|
+
import { Plugin, PluginDevice, ScryptedAlert, ScryptedUser } from './db-types';
|
18
18
|
import { createResponseInterface } from './http-interfaces';
|
19
19
|
import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefault, getProvidedRoomOrDefault, getProvidedTypeOrDefault } from './infer-defaults';
|
20
20
|
import { IOServer } from './io';
|
21
21
|
import { Level } from './level';
|
22
22
|
import { LogEntry, Logger, makeAlertId } from './logger';
|
23
|
-
import { hasMixinCycle } from './mixin/mixin-cycle';
|
23
|
+
import { getMixins, hasMixinCycle } from './mixin/mixin-cycle';
|
24
|
+
import { AccessControls } from './plugin/acl';
|
24
25
|
import { PluginDebug } from './plugin/plugin-debug';
|
25
26
|
import { PluginDeviceProxyHandler } from './plugin/plugin-device';
|
26
27
|
import { PluginHost } from './plugin/plugin-host';
|
@@ -34,6 +35,7 @@ import { CORSControl, CORSServer } from './services/cors';
|
|
34
35
|
import { Info } from './services/info';
|
35
36
|
import { PluginComponent } from './services/plugin';
|
36
37
|
import { ServiceControl } from './services/service-control';
|
38
|
+
import { UsersService } from './services/users';
|
37
39
|
import { getState, ScryptedStateManager, setState } from './state';
|
38
40
|
|
39
41
|
interface DeviceProxyPair {
|
@@ -76,6 +78,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
76
78
|
alerts = new Alerts(this);
|
77
79
|
corsControl = new CORSControl(this);
|
78
80
|
addressSettings = new AddressSettings(this);
|
81
|
+
usersService = new UsersService(this);
|
79
82
|
|
80
83
|
constructor(datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
|
81
84
|
super(app);
|
@@ -91,6 +94,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
91
94
|
});
|
92
95
|
|
93
96
|
app.all('/engine.io/shell', (req, res) => {
|
97
|
+
if (res.locals.aclId) {
|
98
|
+
res.writeHead(401);
|
99
|
+
res.end();
|
100
|
+
return;
|
101
|
+
}
|
94
102
|
this.shellHandler(req, res);
|
95
103
|
});
|
96
104
|
|
@@ -251,21 +259,6 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
251
259
|
};
|
252
260
|
}
|
253
261
|
|
254
|
-
async deliverPush(endpoint: string, request: HttpRequest) {
|
255
|
-
const { pluginHost, pluginDevice } = await this.getPluginForEndpoint(endpoint);
|
256
|
-
if (!pluginDevice) {
|
257
|
-
console.error('plugin device missing for', endpoint);
|
258
|
-
return;
|
259
|
-
}
|
260
|
-
|
261
|
-
if (!pluginDevice?.state.interfaces.value.includes(ScryptedInterface.PushHandler)) {
|
262
|
-
return;
|
263
|
-
}
|
264
|
-
|
265
|
-
const handler = this.getDevice<PushHandler>(pluginDevice._id);
|
266
|
-
return handler.onPush(request);
|
267
|
-
}
|
268
|
-
|
269
262
|
async shellHandler(req: Request, res: Response) {
|
270
263
|
const isUpgrade = isConnectionUpgrade(req.headers);
|
271
264
|
|
@@ -345,7 +338,14 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
345
338
|
});
|
346
339
|
|
347
340
|
// @ts-expect-error
|
348
|
-
await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}
|
341
|
+
await handler.onConnection(httpRequest, new WebSocketConnection(`ws://${id}`, {
|
342
|
+
send(message) {
|
343
|
+
ws.send(message);
|
344
|
+
},
|
345
|
+
close(message) {
|
346
|
+
ws.close();
|
347
|
+
},
|
348
|
+
}));
|
349
349
|
}
|
350
350
|
|
351
351
|
async getComponent(componentId: string): Promise<any> {
|
@@ -370,6 +370,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
370
370
|
return this.corsControl;
|
371
371
|
case 'addresses':
|
372
372
|
return this.addressSettings;
|
373
|
+
case "users":
|
374
|
+
return this.usersService;
|
373
375
|
}
|
374
376
|
}
|
375
377
|
|
@@ -385,9 +387,31 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
385
387
|
return packageJson;
|
386
388
|
}
|
387
389
|
|
388
|
-
handleEngineIOEndpoint(req: Request, res: ServerResponse, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
|
390
|
+
async handleEngineIOEndpoint(req: Request, res: ServerResponse & { locals: any }, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
|
389
391
|
const { pluginHost, pluginDevice } = pluginData;
|
390
392
|
|
393
|
+
const { username } = res.locals;
|
394
|
+
let accessControls: AccessControls;
|
395
|
+
if (username) {
|
396
|
+
const user = await this.datastore.tryGet(ScryptedUser, username);
|
397
|
+
if (user.aclId) {
|
398
|
+
const accessControl = this.getDevice<SU>(user.aclId);
|
399
|
+
try {
|
400
|
+
const acls = await accessControl.getScryptedUserAccessControl();
|
401
|
+
if (acls) {
|
402
|
+
accessControls = new AccessControls(acls);
|
403
|
+
if (accessControls.shouldRejectMethod(pluginDevice._id, ScryptedInterfaceMethod.onConnection))
|
404
|
+
accessControls.deny();
|
405
|
+
}
|
406
|
+
}
|
407
|
+
catch (e) {
|
408
|
+
res.writeHead(401);
|
409
|
+
res.end();
|
410
|
+
return;
|
411
|
+
}
|
412
|
+
}
|
413
|
+
}
|
414
|
+
|
391
415
|
if (!pluginHost || !pluginDevice) {
|
392
416
|
console.error('plugin does not exist or is still starting up.');
|
393
417
|
res.writeHead(500);
|
@@ -398,6 +422,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
398
422
|
(req as any).scrypted = {
|
399
423
|
endpointRequest,
|
400
424
|
pluginDevice,
|
425
|
+
accessControls,
|
401
426
|
};
|
402
427
|
if ((req as any).upgradeHead)
|
403
428
|
pluginHost.io.handleUpgrade(req, res.socket, (req as any).upgradeHead)
|
@@ -627,8 +652,23 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
627
652
|
this.setupPluginHostAutoRestart(pluginHost);
|
628
653
|
this.plugins[pluginId] = pluginHost;
|
629
654
|
|
655
|
+
const pluginDeviceSet = new Set<string>();
|
630
656
|
for (const pluginDevice of pluginDevices) {
|
631
|
-
|
657
|
+
if (pluginDeviceSet.has(pluginDevice._id))
|
658
|
+
continue;
|
659
|
+
pluginDeviceSet.add(pluginDevice._id);
|
660
|
+
this.getDevice(pluginDevice._id)?.probe().catch(() => { });
|
661
|
+
}
|
662
|
+
|
663
|
+
for (const pluginDevice of Object.values(this.pluginDevices)) {
|
664
|
+
const { _id } = pluginDevice;
|
665
|
+
if (pluginDeviceSet.has(_id))
|
666
|
+
continue;
|
667
|
+
for (const mixinId of getMixins(this, _id)) {
|
668
|
+
if (pluginDeviceSet.has(mixinId)) {
|
669
|
+
this.getDevice(_id)?.probe().catch(() => { });
|
670
|
+
}
|
671
|
+
}
|
632
672
|
}
|
633
673
|
|
634
674
|
return pluginHost;
|
@@ -684,6 +724,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
684
724
|
continue;
|
685
725
|
await this.removeDevice(provided);
|
686
726
|
}
|
727
|
+
const providerId = device.state?.providerId?.value;
|
687
728
|
device.state = undefined;
|
688
729
|
|
689
730
|
this.invalidatePluginDevice(device._id);
|
@@ -706,6 +747,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
706
747
|
// notify the plugin that a device was removed.
|
707
748
|
const plugin = this.plugins[device.pluginId];
|
708
749
|
await plugin.remote.setNativeId(device.nativeId, undefined, undefined);
|
750
|
+
const provider = this.getDevice<DeviceProvider>(providerId);
|
751
|
+
await provider?.releaseDevice(device._id, device.nativeId);
|
709
752
|
}
|
710
753
|
catch (e) {
|
711
754
|
// may throw if the plugin is killed, etc.
|
@@ -25,6 +25,7 @@ import { PluginError } from './plugin/plugin-error';
|
|
25
25
|
import { getScryptedVolume } from './plugin/plugin-volume';
|
26
26
|
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
|
27
27
|
import os from 'os';
|
28
|
+
import { setScryptedUserPassword } from './services/users';
|
28
29
|
|
29
30
|
if (!semver.gte(process.version, '16.0.0')) {
|
30
31
|
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
@@ -123,7 +124,8 @@ async function start() {
|
|
123
124
|
realm: 'Scrypted',
|
124
125
|
}, async (username, password, callback) => {
|
125
126
|
const user = await db.tryGet(ScryptedUser, username);
|
126
|
-
|
127
|
+
// disallow basic auth for non-admin as it can deploy plugins, etc.
|
128
|
+
if (!user || user.aclId) {
|
127
129
|
callback(false);
|
128
130
|
return;
|
129
131
|
}
|
@@ -159,13 +161,6 @@ async function start() {
|
|
159
161
|
next();
|
160
162
|
})
|
161
163
|
|
162
|
-
app.options('*', (req, res) => {
|
163
|
-
// add more?
|
164
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
165
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
|
166
|
-
res.send(200);
|
167
|
-
});
|
168
|
-
|
169
164
|
const authSalt = crypto.randomBytes(16);
|
170
165
|
const createAuthorizationToken = (login_user_token: string) => {
|
171
166
|
const salted = login_user_token + authSalt;
|
@@ -188,7 +183,7 @@ async function start() {
|
|
188
183
|
|
189
184
|
const userToken = getSignedLoginUserToken(req);
|
190
185
|
if (userToken) {
|
191
|
-
const { username } = userToken;
|
186
|
+
const { username, aclId } = userToken;
|
192
187
|
|
193
188
|
// this database lookup on every web request is not necessary, the cookie
|
194
189
|
// itself is the auth, and is signed. furthermore, this is currently
|
@@ -202,6 +197,7 @@ async function start() {
|
|
202
197
|
// }
|
203
198
|
|
204
199
|
res.locals.username = username;
|
200
|
+
res.locals.aclId = aclId;
|
205
201
|
}
|
206
202
|
else if (req.headers.authorization?.startsWith('Bearer ')) {
|
207
203
|
const [checkHash, ...tokenParts] = req.headers.authorization.substring('Bearer '.length).split('#');
|
@@ -216,6 +212,7 @@ async function start() {
|
|
216
212
|
const userToken = validateToken(tokenPart);
|
217
213
|
if (userToken)
|
218
214
|
res.locals.username = userToken.username;
|
215
|
+
res.locals.aclId = userToken.aclId;
|
219
216
|
}
|
220
217
|
}
|
221
218
|
}
|
@@ -240,7 +237,7 @@ async function start() {
|
|
240
237
|
|
241
238
|
// verify all plugin related requests have some sort of auth
|
242
239
|
app.all('/web/component/*', (req, res, next) => {
|
243
|
-
if (!res.locals.username) {
|
240
|
+
if (!res.locals.username || res.locals.aclId) {
|
244
241
|
res.status(401);
|
245
242
|
res.send('Not Authorized');
|
246
243
|
return;
|
@@ -466,7 +463,7 @@ async function start() {
|
|
466
463
|
return;
|
467
464
|
}
|
468
465
|
|
469
|
-
const userToken = new UserToken(username, timestamp, maxAge);
|
466
|
+
const userToken = new UserToken(username, user.aclId, timestamp, maxAge);
|
470
467
|
const login_user_token = userToken.toString();
|
471
468
|
res.cookie(getLoginUserToken(req.secure), login_user_token, {
|
472
469
|
maxAge,
|
@@ -476,9 +473,7 @@ async function start() {
|
|
476
473
|
});
|
477
474
|
|
478
475
|
if (change_password) {
|
479
|
-
user
|
480
|
-
user.passwordHash = crypto.createHash('sha256').update(user.salt + change_password).digest().toString('hex');
|
481
|
-
user.passwordDate = timestamp;
|
476
|
+
setScryptedUserPassword(user, change_password, timestamp);
|
482
477
|
await db.upsert(user);
|
483
478
|
}
|
484
479
|
|
@@ -502,14 +497,12 @@ async function start() {
|
|
502
497
|
|
503
498
|
const user = new ScryptedUser();
|
504
499
|
user._id = username;
|
505
|
-
user
|
506
|
-
user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
|
507
|
-
user.passwordDate = timestamp;
|
500
|
+
setScryptedUserPassword(user, password, timestamp);
|
508
501
|
user.token = crypto.randomBytes(16).toString('hex');
|
509
502
|
await db.upsert(user);
|
510
503
|
hasLogin = true;
|
511
504
|
|
512
|
-
const userToken = new UserToken(username, timestamp);
|
505
|
+
const userToken = new UserToken(username, user.aclId, timestamp);
|
513
506
|
const login_user_token = userToken.toString();
|
514
507
|
res.cookie(getLoginUserToken(req.secure), login_user_token, {
|
515
508
|
maxAge,
|
package/src/services/plugin.ts
CHANGED
@@ -29,12 +29,10 @@ export class PluginComponent {
|
|
29
29
|
await this.reload(pluginDevice.pluginId);
|
30
30
|
}
|
31
31
|
|
32
|
-
getNativeId(id: string) {
|
33
|
-
return this.scrypted.findPluginDeviceById(id)?.nativeId;
|
34
|
-
}
|
35
32
|
getStorage(id: string) {
|
36
33
|
return this.scrypted.findPluginDeviceById(id)?.storage || {};
|
37
34
|
}
|
35
|
+
|
38
36
|
async setStorage(id: string, storage: { [key: string]: string }) {
|
39
37
|
const pluginDevice = this.scrypted.findPluginDeviceById(id);
|
40
38
|
pluginDevice.storage = storage;
|
@@ -65,17 +63,9 @@ export class PluginComponent {
|
|
65
63
|
async getIdForNativeId(pluginId: string, nativeId: ScryptedNativeId) {
|
66
64
|
return this.scrypted.findPluginDevice(pluginId, nativeId)?._id;
|
67
65
|
}
|
68
|
-
/**
|
69
|
-
* @deprecated available as device.pluginId now.
|
70
|
-
* Remove at some point after core/ui rolls out 6/20/2022.
|
71
|
-
*/
|
72
|
-
async getPluginId(id: string) {
|
73
|
-
const pluginDevice = this.scrypted.findPluginDeviceById(id);
|
74
|
-
return pluginDevice.pluginId;
|
75
|
-
}
|
76
66
|
async reload(pluginId: string) {
|
77
67
|
const plugin = await this.scrypted.datastore.tryGet(Plugin, pluginId);
|
78
|
-
|
68
|
+
this.scrypted.runPlugin(plugin);
|
79
69
|
}
|
80
70
|
async kill(pluginId: string) {
|
81
71
|
return this.scrypted.plugins[pluginId]?.kill();
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { ScryptedUser } from "../db-types";
|
2
|
+
import { ScryptedRuntime } from "../runtime";
|
3
|
+
import crypto from 'crypto';
|
4
|
+
|
5
|
+
export class UsersService {
|
6
|
+
constructor(public scrypted: ScryptedRuntime) {
|
7
|
+
}
|
8
|
+
|
9
|
+
async getAllUsers() {
|
10
|
+
const users: ScryptedUser[] = [];
|
11
|
+
for await (const user of this.scrypted.datastore.getAll(ScryptedUser)) {
|
12
|
+
users.push(user);
|
13
|
+
}
|
14
|
+
|
15
|
+
return users.map(user => ({
|
16
|
+
username: user._id,
|
17
|
+
admin: !user.aclId,
|
18
|
+
}));
|
19
|
+
}
|
20
|
+
|
21
|
+
async removeUser(username: string) {
|
22
|
+
await this.scrypted.datastore.removeId(ScryptedUser, username);
|
23
|
+
}
|
24
|
+
|
25
|
+
async removeAllUsers() {
|
26
|
+
await this.scrypted.datastore.removeAll(ScryptedUser);
|
27
|
+
}
|
28
|
+
|
29
|
+
async addUser(username: string, password: string, aclId: string) {
|
30
|
+
const user = new ScryptedUser();
|
31
|
+
user._id = username;
|
32
|
+
user.aclId = aclId;
|
33
|
+
setScryptedUserPassword(user, password, Date.now());
|
34
|
+
await this.scrypted.datastore.upsert(user);
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
export function setScryptedUserPassword(user: ScryptedUser, password: string, timestamp: number) {
|
39
|
+
user.salt = crypto.randomBytes(64).toString('base64');
|
40
|
+
user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
|
41
|
+
user.passwordDate = timestamp;
|
42
|
+
user.token = crypto.randomBytes(16).toString('hex');
|
43
|
+
}
|
package/src/usertoken.ts
CHANGED
@@ -2,21 +2,27 @@ export const ONE_DAY_MILLISECONDS = 86400000;
|
|
2
2
|
export const ONE_YEAR_MILLISECONDS = ONE_DAY_MILLISECONDS * 365;
|
3
3
|
|
4
4
|
export class UserToken {
|
5
|
-
constructor(public username: string, public timestamp = Date.now(), public duration = ONE_DAY_MILLISECONDS) {
|
5
|
+
constructor(public username: string, public aclId: string, public timestamp = Date.now(), public duration = ONE_DAY_MILLISECONDS) {
|
6
6
|
}
|
7
7
|
|
8
8
|
static validateToken(token: string): UserToken {
|
9
|
-
let json:
|
9
|
+
let json: {
|
10
|
+
u: string,
|
11
|
+
a: string,
|
12
|
+
t: number,
|
13
|
+
d: number,
|
14
|
+
};
|
10
15
|
try {
|
11
16
|
json = JSON.parse(token);
|
12
17
|
}
|
13
18
|
catch (e) {
|
14
19
|
throw new Error('Token malformed, unparseable.');
|
15
20
|
}
|
16
|
-
let { u, t, d } = json;
|
21
|
+
let { u, a, t, d } = json;
|
17
22
|
u = u?.toString();
|
18
|
-
t = parseInt(t);
|
19
|
-
d = parseInt(d);
|
23
|
+
t = parseInt(t?.toString());
|
24
|
+
d = parseInt(d?.toString());
|
25
|
+
a = a?.toString();
|
20
26
|
if (!u || !t || !d)
|
21
27
|
throw new Error('Token malformed, missing properties.');
|
22
28
|
if (d > ONE_YEAR_MILLISECONDS)
|
@@ -25,12 +31,13 @@ export class UserToken {
|
|
25
31
|
throw new Error('Token from the future.');
|
26
32
|
if (t + d < Date.now())
|
27
33
|
throw new Error('Token expired.');
|
28
|
-
return new UserToken(u, t, d);
|
34
|
+
return new UserToken(u, a, t, d);
|
29
35
|
}
|
30
36
|
|
31
37
|
toString(): string {
|
32
38
|
return JSON.stringify({
|
33
39
|
u: this.username,
|
40
|
+
a: this.aclId,
|
34
41
|
t: this.timestamp,
|
35
42
|
d: this.duration,
|
36
43
|
})
|