@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.

Files changed (52) hide show
  1. package/dist/db-types.js +1 -2
  2. package/dist/db-types.js.map +1 -1
  3. package/dist/mixin/mixin-cycle.js +2 -1
  4. package/dist/mixin/mixin-cycle.js.map +1 -1
  5. package/dist/plugin/acl.js +83 -0
  6. package/dist/plugin/acl.js.map +1 -0
  7. package/dist/plugin/plugin-api.js +28 -11
  8. package/dist/plugin/plugin-api.js.map +1 -1
  9. package/dist/plugin/plugin-device.js +5 -1
  10. package/dist/plugin/plugin-device.js.map +1 -1
  11. package/dist/plugin/plugin-host-api.js +0 -16
  12. package/dist/plugin/plugin-host-api.js.map +1 -1
  13. package/dist/plugin/plugin-host.js +13 -7
  14. package/dist/plugin/plugin-host.js.map +1 -1
  15. package/dist/plugin/plugin-http.js +1 -0
  16. package/dist/plugin/plugin-http.js.map +1 -1
  17. package/dist/plugin/plugin-remote-websocket.js +25 -14
  18. package/dist/plugin/plugin-remote-websocket.js.map +1 -1
  19. package/dist/plugin/plugin-remote-worker.js +3 -0
  20. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  21. package/dist/plugin/plugin-remote.js +41 -8
  22. package/dist/plugin/plugin-remote.js.map +1 -1
  23. package/dist/rpc.js +37 -8
  24. package/dist/rpc.js.map +1 -1
  25. package/dist/runtime.js +58 -14
  26. package/dist/runtime.js.map +1 -1
  27. package/dist/scrypted-server-main.js +11 -17
  28. package/dist/scrypted-server-main.js.map +1 -1
  29. package/dist/services/plugin.js +1 -12
  30. package/dist/services/plugin.js.map +1 -1
  31. package/dist/services/users.js +46 -0
  32. package/dist/services/users.js.map +1 -0
  33. package/dist/usertoken.js +9 -5
  34. package/dist/usertoken.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/db-types.ts +2 -2
  37. package/src/mixin/mixin-cycle.ts +1 -1
  38. package/src/plugin/acl.ts +104 -0
  39. package/src/plugin/plugin-api.ts +41 -25
  40. package/src/plugin/plugin-device.ts +7 -1
  41. package/src/plugin/plugin-host-api.ts +1 -20
  42. package/src/plugin/plugin-host.ts +21 -12
  43. package/src/plugin/plugin-http.ts +1 -0
  44. package/src/plugin/plugin-remote-websocket.ts +26 -17
  45. package/src/plugin/plugin-remote-worker.ts +3 -0
  46. package/src/plugin/plugin-remote.ts +49 -11
  47. package/src/rpc.ts +43 -9
  48. package/src/runtime.ts +64 -21
  49. package/src/scrypted-server-main.ts +11 -18
  50. package/src/services/plugin.ts +2 -12
  51. package/src/services/users.ts +43 -0
  52. package/src/usertoken.ts +13 -6
@@ -1,5 +1,6 @@
1
- import { Device, DeviceManager, DeviceManifest, DeviceState, EndpointManager, HttpRequest, Logger, MediaManager, ScryptedInterface, ScryptedInterfaceProperty, ScryptedMimeTypes, ScryptedNativeId, ScryptedStatic, SystemDeviceState, SystemManager } from '@scrypted/types';
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 = `${protocol}://${await this.getUrlSafeIp()}:${port}${path}`;
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
- await remote.setSystemState(getSystemState());
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, getSystemState()[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((url: string, callbacks: WebSocketConnectCallbacks) => {
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: () => api.ioClose(id),
440
- send: (message: string) => api.ioSend(id, 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
- const g = global;
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
- async handleMessage(message: RpcMessage, deserializationContext?: any) {
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, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, ScryptedNativeId } from '@scrypted/types';
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
- this.getDevice(pluginDevice._id)?.probe().catch(() => {});
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
- if (!user) {
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.salt = crypto.randomBytes(64).toString('base64');
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.salt = crypto.randomBytes(64).toString('base64');
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,
@@ -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
- await this.scrypted.runPlugin(plugin);
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: any;
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
  })