@scrypted/server 0.1.14 → 0.2.1

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 (57) 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 +78 -67
  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-serializer.js +23 -10
  25. package/dist/rpc-serializer.js.map +1 -1
  26. package/dist/rpc.js +3 -3
  27. package/dist/rpc.js.map +1 -1
  28. package/dist/runtime.js +36 -33
  29. package/dist/runtime.js.map +1 -1
  30. package/dist/scrypted-plugin-main.js +4 -1
  31. package/dist/scrypted-plugin-main.js.map +1 -1
  32. package/dist/scrypted-server-main.js +53 -12
  33. package/dist/scrypted-server-main.js.map +1 -1
  34. package/dist/server-settings.js +5 -1
  35. package/dist/server-settings.js.map +1 -1
  36. package/dist/state.js +2 -1
  37. package/dist/state.js.map +1 -1
  38. package/package.json +5 -12
  39. package/src/event-registry.ts +3 -4
  40. package/src/http-interfaces.ts +13 -0
  41. package/src/plugin/media.ts +94 -75
  42. package/src/plugin/plugin-api.ts +5 -4
  43. package/src/plugin/plugin-device.ts +25 -11
  44. package/src/plugin/plugin-host-api.ts +1 -1
  45. package/src/plugin/plugin-host.ts +6 -5
  46. package/src/plugin/plugin-http.ts +2 -2
  47. package/src/plugin/plugin-remote-worker.ts +211 -23
  48. package/src/plugin/plugin-remote.ts +31 -95
  49. package/src/plugin/runtime/node-fork-worker.ts +11 -3
  50. package/src/plugin/runtime/runtime-worker.ts +1 -1
  51. package/src/plugin/socket-serializer.ts +15 -0
  52. package/src/rpc-serializer.ts +30 -13
  53. package/src/rpc.ts +3 -2
  54. package/src/runtime.ts +37 -38
  55. package/src/scrypted-plugin-main.ts +4 -1
  56. package/src/scrypted-server-main.ts +59 -13
  57. package/src/state.ts +2 -1
@@ -3,7 +3,7 @@ import { SidebandBufferSerializer } from "./plugin/buffer-serializer";
3
3
  import { RpcPeer } from "./rpc";
4
4
 
5
5
  export function createDuplexRpcPeer(selfName: string, peerName: string, readable: Readable, writable: Writable) {
6
- const serializer = createRpcDuplexSerializer(readable, writable);
6
+ const serializer = createRpcDuplexSerializer(writable);
7
7
 
8
8
  const rpcPeer = new RpcPeer(selfName, peerName, (message, reject, serializationContext) => {
9
9
  try {
@@ -16,6 +16,7 @@ export function createDuplexRpcPeer(selfName: string, peerName: string, readable
16
16
  });
17
17
 
18
18
  serializer.setupRpcPeer(rpcPeer);
19
+ readable.on('data', data => serializer.onData(data));
19
20
  readable.on('close', serializer.onDisconnected);
20
21
  readable.on('error', serializer.onDisconnected);
21
22
  return rpcPeer;
@@ -34,7 +35,7 @@ export function createRpcSerializer(options: {
34
35
  rpcPeer.kill('connection closed.');
35
36
  }
36
37
 
37
- const sendMessage = (message: any, reject: (e: Error) => void, serializationContext: any, ) => {
38
+ const sendMessage = (message: any, reject: (e: Error) => void, serializationContext: any,) => {
38
39
  if (!connected) {
39
40
  reject?.(new Error('peer disconnected'));
40
41
  return;
@@ -78,7 +79,9 @@ export function createRpcSerializer(options: {
78
79
  };
79
80
  }
80
81
 
81
- export function createRpcDuplexSerializer(readable: Readable, writable: Writable) {
82
+ export function createRpcDuplexSerializer(writable: {
83
+ write: (data: Buffer) => void;
84
+ }) {
82
85
  const socketSend = (type: number, data: Buffer) => {
83
86
  const header = Buffer.alloc(5);
84
87
  header.writeUInt32BE(data.length + 1, 0);
@@ -102,38 +105,52 @@ export function createRpcDuplexSerializer(readable: Readable, writable: Writable
102
105
  });
103
106
 
104
107
  let header: Buffer;
105
- const readMessages = () => {
108
+ let pending: Buffer;
109
+
110
+ const readPending = (length: number) => {
111
+ if (!pending || pending.length < length)
112
+ return;
113
+
114
+ const ret = pending.slice(0, length);
115
+ pending = pending.slice(length);
116
+ if (!pending.length)
117
+ pending = undefined;
118
+ return ret;
119
+ }
120
+
121
+ const onData = (data: Buffer) => {
122
+ if (!pending)
123
+ pending = data;
124
+ else
125
+ pending = Buffer.concat([pending, data]);
126
+
106
127
  while (true) {
107
128
  if (!header) {
108
- header = readable.read(5);
129
+ header = readPending(5);
109
130
  if (!header)
110
131
  return;
111
132
  }
112
133
 
113
134
  const length = header.readUInt32BE(0);
114
135
  const type = header.readUInt8(4);
115
- const payload: Buffer = readable.read(length - 1);
136
+ const payload: Buffer = readPending(length - 1);
116
137
  if (!payload)
117
138
  return;
118
139
 
119
140
  header = undefined;
120
141
 
121
- const data = payload;
122
-
123
142
  if (type === 0) {
124
- const message = JSON.parse(data.toString());
143
+ const message = JSON.parse(payload.toString());
125
144
  serializer.onMessageFinish(message);
126
145
  }
127
146
  else {
128
- serializer.onMessageBuffer(data);
147
+ serializer.onMessageBuffer(payload);
129
148
  }
130
149
  }
131
150
  }
132
151
 
133
- readable.on('readable', readMessages);
134
- readMessages();
135
-
136
152
  return {
153
+ onData,
137
154
  setupRpcPeer: serializer.setupRpcPeer,
138
155
  sendMessage: serializer.sendMessage,
139
156
  onDisconnected: serializer.onDisconnected,
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
@@ -1,39 +1,37 @@
1
- import { Level } from './level';
2
- import { PluginHost } from './plugin/plugin-host';
3
- import { ScryptedNativeId, Device, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, DeviceInformation } from '@scrypted/types';
4
- import { PluginDeviceProxyHandler } from './plugin/plugin-device';
5
- import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
6
- import { getState, ScryptedStateManager, setState } from './state';
7
- import { Request, Response } from 'express';
8
- import { createResponseInterface } from './http-interfaces';
9
- import http, { ServerResponse, IncomingHttpHeaders } from 'http';
10
- import https from 'https';
11
- import express from 'express';
12
- import { LogEntry, Logger, makeAlertId } from './logger';
13
- import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefault, getProvidedRoomOrDefault, getProvidedTypeOrDefault } from './infer-defaults';
14
- import { URL } from "url";
15
- import qs from "query-string";
16
- import { PluginComponent } from './services/plugin';
17
- import WebSocket, { Server as WebSocketServer } from "ws";
1
+ import { Device, DeviceInformation, EngineIOHandler, HttpRequest, HttpRequestHandler, OauthClient, PushHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceProperty, ScryptedNativeId } from '@scrypted/types';
2
+ import AdmZip from 'adm-zip';
18
3
  import axios from 'axios';
19
- import tar from 'tar';
4
+ import * as io from 'engine.io';
20
5
  import { once } from 'events';
6
+ import express, { Request, Response } from 'express';
7
+ import http, { ServerResponse } from 'http';
8
+ import https from 'https';
9
+ import { spawn as ptySpawn } from 'node-pty-prebuilt-multiarch';
10
+ import path from 'path';
11
+ import rimraf from 'rimraf';
12
+ import semver from 'semver';
21
13
  import { PassThrough } from 'stream';
14
+ import tar from 'tar';
15
+ import { URL } from "url";
16
+ import WebSocket, { Server as WebSocketServer } from "ws";
17
+ import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
18
+ import { createResponseInterface } from './http-interfaces';
19
+ import { getDisplayName, getDisplayRoom, getDisplayType, getProvidedNameOrDefault, getProvidedRoomOrDefault, getProvidedTypeOrDefault } from './infer-defaults';
20
+ import { IOServer } from './io';
21
+ import { Level } from './level';
22
+ import { LogEntry, Logger, makeAlertId } from './logger';
22
23
  import { PluginDebug } from './plugin/plugin-debug';
24
+ import { PluginDeviceProxyHandler } from './plugin/plugin-device';
25
+ import { PluginHost } from './plugin/plugin-host';
26
+ import { isConnectionUpgrade, PluginHttp } from './plugin/plugin-http';
27
+ import { getPluginVolume } from './plugin/plugin-volume';
23
28
  import { getIpAddress, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
24
- import semver from 'semver';
25
- import { ServiceControl } from './services/service-control';
26
29
  import { Alerts } from './services/alerts';
27
- import { Info } from './services/info';
28
- import * as io from 'engine.io';
29
- import { spawn as ptySpawn } from 'node-pty';
30
- import rimraf from 'rimraf';
31
- import { getPluginVolume } from './plugin/plugin-volume';
32
- import { isConnectionUpgrade, PluginHttp } from './plugin/plugin-http';
33
- import AdmZip from 'adm-zip';
34
- import path from 'path';
35
30
  import { CORSControl, CORSServer } from './services/cors';
36
- import { IOServer, IOServerSocket } from './io';
31
+ import { Info } from './services/info';
32
+ import { PluginComponent } from './services/plugin';
33
+ import { ServiceControl } from './services/service-control';
34
+ import { getState, ScryptedStateManager, setState } from './state';
37
35
 
38
36
  interface DeviceProxyPair {
39
37
  handler: PluginDeviceProxyHandler;
@@ -185,12 +183,10 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
185
183
 
186
184
  const url = new URL(callback_url as string);
187
185
  if (url.search) {
188
- const search = qs.parse(url.search);
189
- const state = search.state as string;
186
+ const state = url.searchParams.get('state');
190
187
  if (state) {
191
188
  const { s, d, r } = JSON.parse(state);
192
- search.state = s;
193
- url.search = '?' + qs.stringify(search);
189
+ url.searchParams.set('state', s);
194
190
  const oauthClient: ScryptedDevice & OauthClient = this.getDevice(d);
195
191
  await oauthClient.onOauthCallback(url.toString()).catch();
196
192
  res.redirect(r);
@@ -198,12 +194,12 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
198
194
  }
199
195
  }
200
196
  if (url.hash) {
201
- const hash = qs.parse(url.hash);
202
- const state = hash.state as string;
197
+ const hash = new URLSearchParams(url.hash.substring(1));
198
+ const state = hash.get('state');
203
199
  if (state) {
204
200
  const { s, d, r } = JSON.parse(state);
205
- hash.state = s;
206
- url.hash = '#' + qs.stringify(hash);
201
+ hash.set('state', s);
202
+ url.hash = '#' + hash.toString();
207
203
  const oauthClient: ScryptedDevice & OauthClient = this.getDevice(d);
208
204
  await oauthClient.onOauthCallback(url.toString());
209
205
  res.redirect(r);
@@ -283,8 +279,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
283
279
  this.shellio.handleRequest(req, res);
284
280
  }
285
281
 
286
- async getEndpointPluginData(endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
282
+ async getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
287
283
  const ret = await this.getPluginForEndpoint(endpoint);
284
+ if (req.url.indexOf('/engine.io/api') !== -1)
285
+ return ret;
286
+
288
287
  const { pluginDevice } = ret;
289
288
 
290
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
  }