@scrypted/server 0.2.8 → 0.2.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.

@@ -5,10 +5,12 @@ import path from 'path';
5
5
  import readline from 'readline';
6
6
  import { Readable, Writable } from 'stream';
7
7
  import { RpcMessage, RpcPeer } from "../../rpc";
8
+ import { createRpcDuplexSerializer } from '../../rpc-serializer';
8
9
  import { ChildProcessWorker } from "./child-process-worker";
9
10
  import { RuntimeWorkerOptions } from "./runtime-worker";
10
11
 
11
12
  export class PythonRuntimeWorker extends ChildProcessWorker {
13
+ serializer: ReturnType<typeof createRpcDuplexSerializer>;
12
14
 
13
15
  constructor(pluginId: string, options: RuntimeWorkerOptions) {
14
16
  super(pluginId, options);
@@ -62,21 +64,24 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
62
64
  const peerin = this.worker.stdio[3] as Writable;
63
65
  const peerout = this.worker.stdio[4] as Readable;
64
66
 
65
- peerin.on('error', e => this.emit('error', e));
66
- peerout.on('error', e => this.emit('error', e));
67
-
68
- const readInterface = readline.createInterface({
69
- input: peerout,
70
- terminal: false,
67
+ const serializer = this.serializer = createRpcDuplexSerializer(peerin);
68
+ serializer.setupRpcPeer(peer);
69
+ peerout.on('data', data => serializer.onData(data));
70
+ peerin.on('error', e => {
71
+ this.emit('error', e);
72
+ serializer.onDisconnected();
73
+ });
74
+ peerout.on('error', e => {
75
+ this.emit('error', e)
76
+ serializer.onDisconnected();
71
77
  });
72
- readInterface.on('line', line => peer.handleMessage(JSON.parse(line)));
73
78
  }
74
79
 
75
- send(message: RpcMessage, reject?: (e: Error) => void): void {
80
+ send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void {
76
81
  try {
77
82
  if (!this.worker)
78
83
  throw new Error('worked has been killed');
79
- (this.worker.stdio[3] as Writable).write(JSON.stringify(message) + '\n', e => e && reject?.(e));
84
+ this.serializer.sendMessage(message, reject, serializationContext);
80
85
  }
81
86
  catch (e) {
82
87
  reject?.(e);
package/src/rpc.ts CHANGED
@@ -84,7 +84,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
84
84
  }
85
85
 
86
86
  get(target: any, p: PropertyKey, receiver: any): any {
87
- if (p === '__proxy_id')
87
+ if (p === RpcPeer.PROPERTY_PROXY_ID)
88
88
  return this.entry.id;
89
89
  if (p === '__proxy_constructor')
90
90
  return this.constructorName;
@@ -214,6 +214,10 @@ export class RpcPeer {
214
214
 
215
215
  static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
216
216
 
217
+ static isRpcProxy(value: any) {
218
+ return !!value?.[RpcPeer.PROPERTY_PROXY_ID];
219
+ }
220
+
217
221
  static getDefaultTransportSafeArgumentTypes() {
218
222
  const jsonSerializable = new Set<string>();
219
223
  jsonSerializable.add(Number.name);
@@ -242,6 +246,7 @@ export class RpcPeer {
242
246
  }
243
247
  }
244
248
 
249
+ static readonly PROPERTY_PROXY_ID = '__proxy_id';
245
250
  static readonly PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods';
246
251
  static readonly PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization';
247
252
  static readonly PROPERTY_PROXY_PROPERTIES = '__proxy_props';
@@ -23,8 +23,7 @@ import { sleep } from './sleep';
23
23
  import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
24
24
  import { PluginError } from './plugin/plugin-error';
25
25
  import { getScryptedVolume } from './plugin/plugin-volume';
26
-
27
- const ONE_DAY_MILLISECONDS = 86400000;
26
+ import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
28
27
 
29
28
  if (!semver.gte(process.version, '16.0.0')) {
30
29
  throw new Error('"node" version out of date. Please update node to v16 or higher.')
@@ -184,13 +183,9 @@ async function start() {
184
183
  // only basic auth will fail with 401. it is up to the endpoints to manage
185
184
  // lack of login from cookie auth.
186
185
 
187
- const login_user_token = getSignedLoginUserToken(req);
188
- if (login_user_token) {
189
- const userTokenParts = login_user_token.split('#');
190
- const username = userTokenParts[0];
191
- const timestamp = parseInt(userTokenParts[1]);
192
- if (timestamp + ONE_DAY_MILLISECONDS < Date.now())
193
- return next();
186
+ const userToken = getSignedLoginUserToken(req);
187
+ if (userToken) {
188
+ const { username } = userToken;
194
189
 
195
190
  // this database lookup on every web request is not necessary, the cookie
196
191
  // itself is the auth, and is signed. furthermore, this is currently
@@ -206,23 +201,18 @@ async function start() {
206
201
  res.locals.username = username;
207
202
  }
208
203
  else if (req.headers.authorization?.startsWith('Bearer ')) {
209
- const splits = req.headers.authorization.substring('Bearer '.length).split('#');
210
- const login_user_token = splits[1] + '#' + splits[2];
211
- if (login_user_token) {
212
- const check = splits[0];
213
-
214
- const salted = login_user_token + authSalt;
204
+ const [checkHash, ...tokenParts] = req.headers.authorization.substring('Bearer '.length).split('#');
205
+ const tokenPart = tokenParts?.join('#');
206
+ if (checkHash && tokenPart) {
207
+ const salted = tokenPart + authSalt;
215
208
  const hash = crypto.createHash('sha256');
216
209
  hash.update(salted);
217
210
  const sha = hash.digest().toString('hex');
218
211
 
219
- if (check === sha) {
220
- const splits2 = login_user_token.split('#');
221
- const username = splits2[0];
222
- const timestamp = parseInt(splits2[1]);
223
- if (timestamp + ONE_DAY_MILLISECONDS < Date.now())
224
- return next();
225
- res.locals.username = username;
212
+ if (checkHash === sha) {
213
+ const userToken = validateToken(tokenPart);
214
+ if (userToken)
215
+ res.locals.username = userToken.username;
226
216
  }
227
217
  }
228
218
  }
@@ -400,9 +390,19 @@ async function start() {
400
390
  return reqSecure ? 'login_user_token' : 'login_user_token_insecure';
401
391
  };
402
392
 
403
- const getSignedLoginUserToken = (req: Request<any>): string => {
404
- return req.signedCookies[getLoginUserToken(req.secure)];
405
- };
393
+ const validateToken = (token: string) => {
394
+ if (!token)
395
+ return;
396
+ try {
397
+ return UserToken.validateToken(token);
398
+ }
399
+ catch (e) {
400
+ console.warn('invalid token', e.message);
401
+ }
402
+ }
403
+
404
+ const getSignedLoginUserTokenRawValue = (req: Request<any>) => req.signedCookies[getLoginUserToken(req.secure)] as string;
405
+ const getSignedLoginUserToken = (req: Request<any>) => validateToken(getSignedLoginUserTokenRawValue(req));
406
406
 
407
407
  app.get('/logout', (req, res) => {
408
408
  res.clearCookie(getLoginUserToken(req.secure));
@@ -450,7 +450,8 @@ async function start() {
450
450
  return;
451
451
  }
452
452
 
453
- const login_user_token = `${username}#${timestamp}`;
453
+ const userToken = new UserToken(username, timestamp, maxAge);
454
+ const login_user_token = userToken.toString();
454
455
  res.cookie(getLoginUserToken(req.secure), login_user_token, {
455
456
  maxAge,
456
457
  secure: req.secure,
@@ -492,7 +493,8 @@ async function start() {
492
493
  await db.upsert(user);
493
494
  hasLogin = true;
494
495
 
495
- const login_user_token = `${username}#${timestamp}`
496
+ const userToken = new UserToken(username, timestamp);
497
+ const login_user_token = userToken.toString();
496
498
  res.cookie(getLoginUserToken(req.secure), login_user_token, {
497
499
  maxAge,
498
500
  secure: req.secure,
@@ -536,32 +538,25 @@ async function start() {
536
538
  return;
537
539
  }
538
540
 
539
- const login_user_token = getSignedLoginUserToken(req);
540
- if (!login_user_token) {
541
+ try {
542
+ const login_user_token = getSignedLoginUserTokenRawValue(req);
543
+ if (!login_user_token)
544
+ throw new Error('Not logged in.');
545
+ const userToken = UserToken.validateToken(login_user_token);
546
+
541
547
  res.send({
542
- error: 'Not logged in.',
543
- hasLogin,
548
+ authorization: createAuthorizationToken(login_user_token),
549
+ expiration: (userToken.timestamp + userToken.duration) - Date.now(),
550
+ username: userToken.username,
551
+ addresses,
544
552
  })
545
- return;
546
553
  }
547
-
548
- const userTokenParts = login_user_token.split('#');
549
- const username = userTokenParts[0];
550
- const timestamp = parseInt(userTokenParts[1]);
551
- if (timestamp + ONE_DAY_MILLISECONDS < Date.now()) {
554
+ catch (e) {
552
555
  res.send({
553
- error: 'Login expired.',
556
+ error: e?.message || 'Unknown Error.',
554
557
  hasLogin,
555
558
  })
556
- return;
557
559
  }
558
-
559
- res.send({
560
- authorization: createAuthorizationToken(login_user_token),
561
- expiration: ONE_DAY_MILLISECONDS - (Date.now() - timestamp),
562
- username,
563
- addresses,
564
- })
565
560
  });
566
561
 
567
562
  app.get('/', (_req, res) => res.redirect('/endpoint/@scrypted/core/public/'));
package/src/state.ts CHANGED
@@ -206,6 +206,9 @@ function isSameValue(value1: any, value2: any) {
206
206
  }
207
207
 
208
208
  export function setState(pluginDevice: PluginDevice, property: string, value: any): boolean {
209
+ // device may have been deleted.
210
+ if (!pluginDevice.state)
211
+ return;
209
212
  if (!pluginDevice.state[property])
210
213
  pluginDevice.state[property] = {};
211
214
  const state = pluginDevice.state[property];
@@ -0,0 +1,38 @@
1
+ export const ONE_DAY_MILLISECONDS = 86400000;
2
+ export const ONE_YEAR_MILLISECONDS = ONE_DAY_MILLISECONDS * 365;
3
+
4
+ export class UserToken {
5
+ constructor(public username: string, public timestamp = Date.now(), public duration = ONE_DAY_MILLISECONDS) {
6
+ }
7
+
8
+ static validateToken(token: string): UserToken {
9
+ let json: any;
10
+ try {
11
+ json = JSON.parse(token);
12
+ }
13
+ catch (e) {
14
+ throw new Error('Token malformed, unparseable.');
15
+ }
16
+ let { u, t, d } = json;
17
+ u = u?.toString();
18
+ t = parseInt(t);
19
+ d = parseInt(d);
20
+ if (!u || !t || !d)
21
+ throw new Error('Token malformed, missing properties.');
22
+ if (d > ONE_YEAR_MILLISECONDS)
23
+ throw new Error('Token duration too long.')
24
+ if (t > Date.now())
25
+ throw new Error('Token from the future.');
26
+ if (t + d < Date.now())
27
+ throw new Error('Token expired.');
28
+ return new UserToken(u, t, d);
29
+ }
30
+
31
+ toString(): string {
32
+ return JSON.stringify({
33
+ u: this.username,
34
+ t: this.timestamp,
35
+ d: this.duration,
36
+ })
37
+ }
38
+ }