@scrypted/server 0.1.15 → 0.2.2

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.
Files changed (54) hide show
  1. package/dist/event-registry.js +3 -4
  2. package/dist/event-registry.js.map +1 -1
  3. package/dist/http-interfaces.js +11 -0
  4. package/dist/http-interfaces.js.map +1 -1
  5. package/dist/plugin/media.js +77 -66
  6. package/dist/plugin/media.js.map +1 -1
  7. package/dist/plugin/plugin-api.js +1 -1
  8. package/dist/plugin/plugin-api.js.map +1 -1
  9. package/dist/plugin/plugin-device.js +25 -11
  10. package/dist/plugin/plugin-device.js.map +1 -1
  11. package/dist/plugin/plugin-host-api.js.map +1 -1
  12. package/dist/plugin/plugin-host.js +11 -6
  13. package/dist/plugin/plugin-host.js.map +1 -1
  14. package/dist/plugin/plugin-http.js +1 -1
  15. package/dist/plugin/plugin-http.js.map +1 -1
  16. package/dist/plugin/plugin-remote-worker.js +170 -17
  17. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  18. package/dist/plugin/plugin-remote.js +25 -85
  19. package/dist/plugin/plugin-remote.js.map +1 -1
  20. package/dist/plugin/runtime/node-fork-worker.js +11 -3
  21. package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
  22. package/dist/plugin/socket-serializer.js +17 -0
  23. package/dist/plugin/socket-serializer.js.map +1 -0
  24. package/dist/rpc.js +3 -3
  25. package/dist/rpc.js.map +1 -1
  26. package/dist/runtime.js +14 -11
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/scrypted-plugin-main.js +4 -1
  29. package/dist/scrypted-plugin-main.js.map +1 -1
  30. package/dist/scrypted-server-main.js +53 -12
  31. package/dist/scrypted-server-main.js.map +1 -1
  32. package/dist/server-settings.js +5 -1
  33. package/dist/server-settings.js.map +1 -1
  34. package/dist/state.js +2 -1
  35. package/dist/state.js.map +1 -1
  36. package/package.json +5 -11
  37. package/src/event-registry.ts +3 -4
  38. package/src/http-interfaces.ts +13 -0
  39. package/src/plugin/media.ts +93 -74
  40. package/src/plugin/plugin-api.ts +5 -4
  41. package/src/plugin/plugin-device.ts +25 -11
  42. package/src/plugin/plugin-host-api.ts +1 -1
  43. package/src/plugin/plugin-host.ts +6 -5
  44. package/src/plugin/plugin-http.ts +2 -2
  45. package/src/plugin/plugin-remote-worker.ts +211 -23
  46. package/src/plugin/plugin-remote.ts +31 -94
  47. package/src/plugin/runtime/node-fork-worker.ts +11 -3
  48. package/src/plugin/runtime/runtime-worker.ts +1 -1
  49. package/src/plugin/socket-serializer.ts +15 -0
  50. package/src/rpc.ts +3 -2
  51. package/src/runtime.ts +10 -10
  52. package/src/scrypted-plugin-main.ts +4 -1
  53. package/src/scrypted-server-main.ts +59 -13
  54. package/src/state.ts +2 -1
@@ -4,6 +4,8 @@ import path from 'path';
4
4
  import { RpcMessage, RpcPeer } from "../../rpc";
5
5
  import { ChildProcessWorker } from "./child-process-worker";
6
6
  import { getPluginNodePath } from "../plugin-npm-dependencies";
7
+ import { SidebandSocketSerializer } from "../socket-serializer";
8
+ import net from "net";
7
9
 
8
10
  export class NodeForkWorker extends ChildProcessWorker {
9
11
 
@@ -31,7 +33,12 @@ export class NodeForkWorker extends ChildProcessWorker {
31
33
 
32
34
  setupRpcPeer(peer: RpcPeer): void {
33
35
  this.worker.on('message', (message, sendHandle) => {
34
- if (sendHandle) {
36
+ if ((message as any).type && sendHandle) {
37
+ peer.handleMessage(message as any, {
38
+ sendHandle,
39
+ });
40
+ }
41
+ else if (sendHandle) {
35
42
  this.emit('rpc', message, sendHandle);
36
43
  }
37
44
  else {
@@ -39,13 +46,14 @@ export class NodeForkWorker extends ChildProcessWorker {
39
46
  }
40
47
  });
41
48
  peer.transportSafeArgumentTypes.add(Buffer.name);
49
+ peer.addSerializer(net.Socket, net.Socket.name, new SidebandSocketSerializer());
42
50
  }
43
51
 
44
- send(message: RpcMessage, reject?: (e: Error) => void): void {
52
+ send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void {
45
53
  try {
46
54
  if (!this.worker)
47
55
  throw new Error('worked has been killed');
48
- this.worker.send(message, undefined, e => {
56
+ this.worker.send(message, serializationContext?.sendHandle, e => {
49
57
  if (e && reject)
50
58
  reject(e);
51
59
  });
@@ -23,7 +23,7 @@ export interface RuntimeWorker {
23
23
  on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
24
24
  once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
25
25
 
26
- send(message: RpcMessage, reject?: (e: Error) => void): void;
26
+ send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void;
27
27
 
28
28
  setupRpcPeer(peer: RpcPeer): void;
29
29
  }
@@ -0,0 +1,15 @@
1
+ import { RpcSerializer } from "../rpc";
2
+
3
+ export class SidebandSocketSerializer implements RpcSerializer {
4
+ serialize(value: any, serializationContext?: any) {
5
+ if (!serializationContext)
6
+ throw new Error('socket serialization context unavailable');
7
+ serializationContext.sendHandle = value;
8
+ }
9
+
10
+ deserialize(serialized: any, serializationContext?: any) {
11
+ if (!serializationContext)
12
+ throw new Error('socket deserialization context unavailable');
13
+ return serializationContext.sendHandle;
14
+ }
15
+ }
package/src/rpc.ts CHANGED
@@ -138,7 +138,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
138
138
 
139
139
  if (this.proxyOneWayMethods?.includes?.(method)) {
140
140
  rpcApply.oneway = true;
141
- this.peer.send(rpcApply);
141
+ this.peer.send(rpcApply, undefined, serializationContext);
142
142
  return Promise.resolve();
143
143
  }
144
144
 
@@ -321,7 +321,8 @@ export class RpcPeer {
321
321
  const params = Object.assign({}, this.params, coercedParams);
322
322
  let compile: CompileFunction;
323
323
  try {
324
- compile = require('vm').compileFunction;;
324
+ // prevent bundlers from trying to include non-existent vm module.
325
+ compile = module[`require`]('vm').compileFunction;
325
326
  }
326
327
  catch (e) {
327
328
  compile = compileFunction;
package/src/runtime.ts CHANGED
@@ -8,7 +8,6 @@ import http, { ServerResponse } from 'http';
8
8
  import https from 'https';
9
9
  import { spawn as ptySpawn } from 'node-pty-prebuilt-multiarch';
10
10
  import path from 'path';
11
- import qs from "query-string";
12
11
  import rimraf from 'rimraf';
13
12
  import semver from 'semver';
14
13
  import { PassThrough } from 'stream';
@@ -184,12 +183,10 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
184
183
 
185
184
  const url = new URL(callback_url as string);
186
185
  if (url.search) {
187
- const search = qs.parse(url.search);
188
- const state = search.state as string;
186
+ const state = url.searchParams.get('state');
189
187
  if (state) {
190
188
  const { s, d, r } = JSON.parse(state);
191
- search.state = s;
192
- url.search = '?' + qs.stringify(search);
189
+ url.searchParams.set('state', s);
193
190
  const oauthClient: ScryptedDevice & OauthClient = this.getDevice(d);
194
191
  await oauthClient.onOauthCallback(url.toString()).catch();
195
192
  res.redirect(r);
@@ -197,12 +194,12 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
197
194
  }
198
195
  }
199
196
  if (url.hash) {
200
- const hash = qs.parse(url.hash);
201
- const state = hash.state as string;
197
+ const hash = new URLSearchParams(url.hash.substring(1));
198
+ const state = hash.get('state');
202
199
  if (state) {
203
200
  const { s, d, r } = JSON.parse(state);
204
- hash.state = s;
205
- url.hash = '#' + qs.stringify(hash);
201
+ hash.set('state', s);
202
+ url.hash = '#' + hash.toString();
206
203
  const oauthClient: ScryptedDevice & OauthClient = this.getDevice(d);
207
204
  await oauthClient.onOauthCallback(url.toString());
208
205
  res.redirect(r);
@@ -282,8 +279,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
282
279
  this.shellio.handleRequest(req, res);
283
280
  }
284
281
 
285
- async getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
282
+ async getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
286
283
  const ret = await this.getPluginForEndpoint(endpoint);
284
+ if (req.url.indexOf('/engine.io/api') !== -1)
285
+ return ret;
286
+
287
287
  const { pluginDevice } = ret;
288
288
 
289
289
  // check if upgrade requests can be handled. must be websocket.
@@ -2,6 +2,8 @@ import { startPluginRemote } from "./plugin/plugin-remote-worker";
2
2
  import { RpcMessage } from "./rpc";
3
3
  import worker_threads from "worker_threads";
4
4
  import v8 from 'v8';
5
+ import net from 'net';
6
+ import { SidebandSocketSerializer } from "./plugin/socket-serializer";
5
7
 
6
8
  if (process.argv[2] === 'child-thread') {
7
9
  const peer = startPluginRemote(process.argv[3], (message, reject) => {
@@ -16,7 +18,7 @@ if (process.argv[2] === 'child-thread') {
16
18
  worker_threads.parentPort.on('message', message => peer.handleMessage(v8.deserialize(message)));
17
19
  }
18
20
  else {
19
- const peer = startPluginRemote(process.argv[3], (message, reject) => process.send(message, undefined, {
21
+ const peer = startPluginRemote(process.argv[3], (message, reject, serializationContext) => process.send(message, serializationContext?.sendHandle, {
20
22
  swallowErrors: !reject,
21
23
  }, e => {
22
24
  if (e)
@@ -24,6 +26,7 @@ else {
24
26
  }));
25
27
 
26
28
  peer.transportSafeArgumentTypes.add(Buffer.name);
29
+ peer.addSerializer(net.Socket, net.Socket.name, new SidebandSocketSerializer());
27
30
  process.on('message', message => peer.handleMessage(message as RpcMessage));
28
31
  process.on('disconnect', () => {
29
32
  console.error('peer host disconnected, exiting.');
@@ -12,7 +12,6 @@ import { getHostAddresses, SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED
12
12
  import crypto from 'crypto';
13
13
  import cookieParser from 'cookie-parser';
14
14
  import axios from 'axios';
15
- import qs from 'query-string';
16
15
  import { RPCResultError } from './rpc';
17
16
  import fs from 'fs';
18
17
  import mkdirp from 'mkdirp';
@@ -155,7 +154,30 @@ async function start() {
155
154
  // use a hash of the private key as the cookie secret.
156
155
  app.use(cookieParser(crypto.createHash('sha256').update(certSetting.value.serviceKey).digest().toString('hex')));
157
156
 
158
- app.all('*', async (req, res, next) => {
157
+ // trap to add access control headers.
158
+ app.use((req, res, next) => {
159
+ if (!req.headers.upgrade)
160
+ scrypted.addAccessControlHeaders(req, res);
161
+ next();
162
+ })
163
+
164
+ app.options('*', (req, res) => {
165
+ // add more?
166
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
167
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
168
+ res.send(200);
169
+ });
170
+
171
+ const authSalt = crypto.randomBytes(16);
172
+ const createAuthorizationToken = (login_user_token: string) => {
173
+ const salted = login_user_token + authSalt;
174
+ const hash = crypto.createHash('sha256');
175
+ hash.update(salted);
176
+ const sha = hash.digest().toString('hex');
177
+ return `Bearer ${sha}#${login_user_token}`;
178
+ }
179
+
180
+ app.use(async (req, res, next) => {
159
181
  // this is a trap for all auth.
160
182
  // only basic auth will fail with 401. it is up to the endpoints to manage
161
183
  // lack of login from cookie auth.
@@ -165,10 +187,8 @@ async function start() {
165
187
  const userTokenParts = login_user_token.split('#');
166
188
  const username = userTokenParts[0];
167
189
  const timestamp = parseInt(userTokenParts[1]);
168
- if (timestamp + 86400000 < Date.now()) {
169
- console.warn('login expired');
190
+ if (timestamp + 86400000 < Date.now())
170
191
  return next();
171
- }
172
192
 
173
193
  // this database lookup on every web request is not necessary, the cookie
174
194
  // itself is the auth, and is signed. furthermore, this is currently
@@ -182,7 +202,27 @@ async function start() {
182
202
  // }
183
203
 
184
204
  res.locals.username = username;
185
- (req as any).username = username;
205
+ }
206
+ else if (req.headers.authorization?.startsWith('Bearer ')) {
207
+ const splits = req.headers.authorization.substring('Bearer '.length).split('#');
208
+ const login_user_token = splits[1] + '#' + splits[2];
209
+ if (login_user_token) {
210
+ const check = splits[0];
211
+
212
+ const salted = login_user_token + authSalt;
213
+ const hash = crypto.createHash('sha256');
214
+ hash.update(salted);
215
+ const sha = hash.digest().toString('hex');
216
+
217
+ if (check === sha) {
218
+ const splits2 = login_user_token.split('#');
219
+ const username = splits2[0];
220
+ const timestamp = parseInt(splits2[1]);
221
+ if (timestamp + 86400000 < Date.now())
222
+ return next();
223
+ res.locals.username = username;
224
+ }
225
+ }
186
226
  }
187
227
  next();
188
228
  });
@@ -277,8 +317,8 @@ async function start() {
277
317
 
278
318
  app.get('/web/component/script/search', async (req, res) => {
279
319
  try {
280
- const query = qs.stringify({
281
- text: req.query.text,
320
+ const query = new URLSearchParams({
321
+ text: req.query.text.toString(),
282
322
  })
283
323
  const response = await axios(`https://registry.npmjs.org/-/v1/search?${query}`);
284
324
  res.send(response.data);
@@ -355,7 +395,7 @@ async function start() {
355
395
  });
356
396
 
357
397
  const getLoginUserToken = (reqSecure: boolean) => {
358
- return reqSecure ? 'login_user_token' : 'login_user_token_inseucre';
398
+ return reqSecure ? 'login_user_token' : 'login_user_token_insecure';
359
399
  };
360
400
 
361
401
  const getSignedLoginUserToken = (req: Request<any>): string => {
@@ -364,21 +404,23 @@ async function start() {
364
404
 
365
405
  app.get('/logout', (req, res) => {
366
406
  res.clearCookie(getLoginUserToken(req.secure));
367
- res.send({});
407
+ if (req.headers['accept']?.startsWith('application/json')) {
408
+ res.send({});
409
+ }
410
+ else {
411
+ res.redirect('/endpoint/@scrypted/core/public/');
412
+ }
368
413
  });
369
414
 
370
415
  let hasLogin = await db.getCount(ScryptedUser) > 0;
371
416
 
372
417
  app.options('/login', (req, res) => {
373
- scrypted.addAccessControlHeaders(req, res);
374
418
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
375
419
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
376
420
  res.send(200);
377
421
  });
378
422
 
379
423
  app.post('/login', async (req, res) => {
380
- scrypted.addAccessControlHeaders(req, res);
381
-
382
424
  const { username, password, change_password } = req.body;
383
425
  const timestamp = Date.now();
384
426
  const maxAge = 86400000;
@@ -422,6 +464,7 @@ async function start() {
422
464
  }
423
465
 
424
466
  res.send({
467
+ authorization: createAuthorizationToken(login_user_token),
425
468
  username,
426
469
  expiration: maxAge,
427
470
  addresses,
@@ -456,6 +499,7 @@ async function start() {
456
499
  });
457
500
 
458
501
  res.send({
502
+ authorization: createAuthorizationToken(login_user_token),
459
503
  username,
460
504
  token: user.token,
461
505
  expiration: maxAge,
@@ -463,6 +507,7 @@ async function start() {
463
507
  });
464
508
  });
465
509
 
510
+
466
511
  app.get('/login', async (req, res) => {
467
512
  scrypted.addAccessControlHeaders(req, res);
468
513
 
@@ -510,6 +555,7 @@ async function start() {
510
555
  }
511
556
 
512
557
  res.send({
558
+ authorization: createAuthorizationToken(login_user_token),
513
559
  expiration: 86400000 - (Date.now() - timestamp),
514
560
  username,
515
561
  addresses,
package/src/state.ts CHANGED
@@ -116,7 +116,7 @@ export class ScryptedStateManager extends EventRegistry {
116
116
  let cb = (eventDetails: EventDetails, eventData: any) => {
117
117
  if (denoise && lastData === eventData)
118
118
  return;
119
- callback(eventDetails, eventData);
119
+ callback?.(eventDetails, eventData);
120
120
  };
121
121
 
122
122
  const wrappedRegister = super.listenDevice(id, options, cb);
@@ -124,6 +124,7 @@ export class ScryptedStateManager extends EventRegistry {
124
124
  return new EventListenerRegisterImpl(() => {
125
125
  wrappedRegister.removeListener();
126
126
  cb = undefined;
127
+ callback = undefined;
127
128
  polling = false;
128
129
  });
129
130
  }