@scrypted/server 0.4.6 → 0.4.8
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/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 +4 -0
- 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 +44 -14
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +11 -11
- 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/plugin/acl.ts +104 -0
- package/src/plugin/plugin-api.ts +41 -25
- package/src/plugin/plugin-device.ts +6 -0
- 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 +48 -20
- package/src/scrypted-server-main.ts +11 -11
- 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
23
|
import { 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)
|
@@ -628,7 +653,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
628
653
|
this.plugins[pluginId] = pluginHost;
|
629
654
|
|
630
655
|
for (const pluginDevice of pluginDevices) {
|
631
|
-
this.getDevice(pluginDevice._id)?.probe().catch(() => {});
|
656
|
+
this.getDevice(pluginDevice._id)?.probe().catch(() => { });
|
632
657
|
}
|
633
658
|
|
634
659
|
return pluginHost;
|
@@ -684,6 +709,7 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
684
709
|
continue;
|
685
710
|
await this.removeDevice(provided);
|
686
711
|
}
|
712
|
+
const providerId = device.state?.providerId?.value;
|
687
713
|
device.state = undefined;
|
688
714
|
|
689
715
|
this.invalidatePluginDevice(device._id);
|
@@ -706,6 +732,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
706
732
|
// notify the plugin that a device was removed.
|
707
733
|
const plugin = this.plugins[device.pluginId];
|
708
734
|
await plugin.remote.setNativeId(device.nativeId, undefined, undefined);
|
735
|
+
const provider = this.getDevice<DeviceProvider>(providerId);
|
736
|
+
await provider?.releaseDevice(device._id, device.nativeId);
|
709
737
|
}
|
710
738
|
catch (e) {
|
711
739
|
// 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
|
}
|
@@ -188,7 +190,7 @@ async function start() {
|
|
188
190
|
|
189
191
|
const userToken = getSignedLoginUserToken(req);
|
190
192
|
if (userToken) {
|
191
|
-
const { username } = userToken;
|
193
|
+
const { username, aclId } = userToken;
|
192
194
|
|
193
195
|
// this database lookup on every web request is not necessary, the cookie
|
194
196
|
// itself is the auth, and is signed. furthermore, this is currently
|
@@ -202,6 +204,7 @@ async function start() {
|
|
202
204
|
// }
|
203
205
|
|
204
206
|
res.locals.username = username;
|
207
|
+
res.locals.aclId = aclId;
|
205
208
|
}
|
206
209
|
else if (req.headers.authorization?.startsWith('Bearer ')) {
|
207
210
|
const [checkHash, ...tokenParts] = req.headers.authorization.substring('Bearer '.length).split('#');
|
@@ -216,6 +219,7 @@ async function start() {
|
|
216
219
|
const userToken = validateToken(tokenPart);
|
217
220
|
if (userToken)
|
218
221
|
res.locals.username = userToken.username;
|
222
|
+
res.locals.aclId = userToken.aclId;
|
219
223
|
}
|
220
224
|
}
|
221
225
|
}
|
@@ -240,7 +244,7 @@ async function start() {
|
|
240
244
|
|
241
245
|
// verify all plugin related requests have some sort of auth
|
242
246
|
app.all('/web/component/*', (req, res, next) => {
|
243
|
-
if (!res.locals.username) {
|
247
|
+
if (!res.locals.username || res.locals.aclId) {
|
244
248
|
res.status(401);
|
245
249
|
res.send('Not Authorized');
|
246
250
|
return;
|
@@ -466,7 +470,7 @@ async function start() {
|
|
466
470
|
return;
|
467
471
|
}
|
468
472
|
|
469
|
-
const userToken = new UserToken(username, timestamp, maxAge);
|
473
|
+
const userToken = new UserToken(username, user.aclId, timestamp, maxAge);
|
470
474
|
const login_user_token = userToken.toString();
|
471
475
|
res.cookie(getLoginUserToken(req.secure), login_user_token, {
|
472
476
|
maxAge,
|
@@ -476,9 +480,7 @@ async function start() {
|
|
476
480
|
});
|
477
481
|
|
478
482
|
if (change_password) {
|
479
|
-
user
|
480
|
-
user.passwordHash = crypto.createHash('sha256').update(user.salt + change_password).digest().toString('hex');
|
481
|
-
user.passwordDate = timestamp;
|
483
|
+
setScryptedUserPassword(user, change_password, timestamp);
|
482
484
|
await db.upsert(user);
|
483
485
|
}
|
484
486
|
|
@@ -502,14 +504,12 @@ async function start() {
|
|
502
504
|
|
503
505
|
const user = new ScryptedUser();
|
504
506
|
user._id = username;
|
505
|
-
user
|
506
|
-
user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
|
507
|
-
user.passwordDate = timestamp;
|
507
|
+
setScryptedUserPassword(user, password, timestamp);
|
508
508
|
user.token = crypto.randomBytes(16).toString('hex');
|
509
509
|
await db.upsert(user);
|
510
510
|
hasLogin = true;
|
511
511
|
|
512
|
-
const userToken = new UserToken(username, timestamp);
|
512
|
+
const userToken = new UserToken(username, user.aclId, timestamp);
|
513
513
|
const login_user_token = userToken.toString();
|
514
514
|
res.cookie(getLoginUserToken(req.secure), login_user_token, {
|
515
515
|
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
|
})
|