@scrypted/server 0.114.0 → 0.115.0

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 (44) hide show
  1. package/dist/fetch/http-fetch.js +6 -27
  2. package/dist/fetch/http-fetch.js.map +1 -1
  3. package/dist/fetch/index.d.ts +5 -2
  4. package/dist/fetch/index.js +6 -26
  5. package/dist/fetch/index.js.map +1 -1
  6. package/dist/listen-zero.d.ts +2 -2
  7. package/dist/listen-zero.js +1 -1
  8. package/dist/listen-zero.js.map +1 -1
  9. package/dist/plugin/plugin-console.js +2 -2
  10. package/dist/plugin/plugin-console.js.map +1 -1
  11. package/dist/plugin/plugin-npm-dependencies.js +5 -1
  12. package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
  13. package/dist/plugin/plugin-remote-worker.js +4 -3
  14. package/dist/plugin/plugin-remote-worker.js.map +1 -1
  15. package/dist/plugin/plugin-remote.js +0 -1
  16. package/dist/plugin/plugin-remote.js.map +1 -1
  17. package/dist/plugin/plugin-repl.js +1 -1
  18. package/dist/plugin/plugin-repl.js.map +1 -1
  19. package/dist/rpc-peer-eval.d.ts +4 -0
  20. package/dist/rpc-peer-eval.js +25 -0
  21. package/dist/rpc-peer-eval.js.map +1 -0
  22. package/dist/rpc-serializer.js.map +1 -1
  23. package/dist/rpc.d.ts +0 -3
  24. package/dist/rpc.js +0 -21
  25. package/dist/rpc.js.map +1 -1
  26. package/dist/runtime.js +7 -5
  27. package/dist/runtime.js.map +1 -1
  28. package/dist/scrypted-server-main.js +14 -0
  29. package/dist/scrypted-server-main.js.map +1 -1
  30. package/package.json +12 -12
  31. package/python/plugin_remote.py +15 -1
  32. package/src/fetch/http-fetch.ts +7 -32
  33. package/src/fetch/index.ts +10 -33
  34. package/src/listen-zero.ts +3 -3
  35. package/src/plugin/plugin-console.ts +2 -2
  36. package/src/plugin/plugin-npm-dependencies.ts +5 -1
  37. package/src/plugin/plugin-remote-worker.ts +4 -3
  38. package/src/plugin/plugin-remote.ts +1 -2
  39. package/src/plugin/plugin-repl.ts +1 -1
  40. package/src/rpc-peer-eval.ts +27 -0
  41. package/src/rpc-serializer.ts +2 -2
  42. package/src/rpc.ts +2 -28
  43. package/src/runtime.ts +9 -5
  44. package/src/scrypted-server-main.ts +16 -0
@@ -89,11 +89,11 @@ export async function httpFetch<T extends HttpFetchOptions<Readable>>(options: T
89
89
  controller = new AbortController();
90
90
  timeout = setTimeout(() => controller.abort(), options.timeout);
91
91
 
92
- options.signal?.addEventListener('abort', () => controller.abort('abort'));
92
+ options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
93
93
  }
94
94
 
95
95
  const signal = controller?.signal || options.signal;
96
- signal?.addEventListener('abort', () => request.destroy(new Error('abort')));
96
+ signal?.addEventListener('abort', () => request.destroy(new Error(options.signal?.reason || 'abort')));
97
97
 
98
98
  const nodeHeaders: Record<string, string[]> = {};
99
99
  for (const [k, v] of headers) {
@@ -122,9 +122,12 @@ export async function httpFetch<T extends HttpFetchOptions<Readable>>(options: T
122
122
  try {
123
123
  const [response] = await once(request, 'response') as [IncomingMessage];
124
124
 
125
- if (!options?.ignoreStatusCode) {
125
+
126
+ if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
126
127
  try {
127
- checkStatus(response.statusCode);
128
+ const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
129
+ if (!checker(response.statusCode))
130
+ throw new Error(`http response statusCode ${response.statusCode}`);
128
131
  }
129
132
  catch (e) {
130
133
  readMessageBuffer(response).catch(() => { });
@@ -150,31 +153,3 @@ export async function httpFetch<T extends HttpFetchOptions<Readable>>(options: T
150
153
  }
151
154
  }
152
155
 
153
- function ensureType<T>(v: T) {
154
- }
155
-
156
- async function test() {
157
- const a = await httpFetch({
158
- url: 'http://example.com',
159
- });
160
-
161
- ensureType<Buffer>(a.body);
162
-
163
- const b = await httpFetch({
164
- url: 'http://example.com',
165
- responseType: 'json',
166
- });
167
- ensureType<any>(b.body);
168
-
169
- const c = await httpFetch({
170
- url: 'http://example.com',
171
- responseType: 'readable',
172
- });
173
- ensureType<IncomingMessage>(c.body);
174
-
175
- const d = await httpFetch({
176
- url: 'http://example.com',
177
- responseType: 'buffer',
178
- });
179
- ensureType<Buffer>(d.body);
180
- }
@@ -7,7 +7,10 @@ export interface HttpFetchOptionsBase<B> {
7
7
  signal?: AbortSignal,
8
8
  timeout?: number;
9
9
  rejectUnauthorized?: boolean;
10
- ignoreStatusCode?: boolean;
10
+ /**
11
+ * Checks the status code. Defaults to true.
12
+ */
13
+ checkStatusCode?: boolean | ((statusCode: number) => boolean);
11
14
  body?: B | string | ArrayBufferView | any;
12
15
  withCredentials?: boolean;
13
16
  }
@@ -40,6 +43,7 @@ export function fetchStatusCodeOk(statusCode: number) {
40
43
  export function checkStatus(statusCode: number) {
41
44
  if (!fetchStatusCodeOk(statusCode))
42
45
  throw new Error(`http response statusCode ${statusCode}`);
46
+ return true;
43
47
  }
44
48
 
45
49
  export function getFetchMethod(options: HttpFetchOptions<any>) {
@@ -177,7 +181,7 @@ export async function domFetch<T extends HttpFetchOptions<BodyInit>>(options: T)
177
181
  controller = new AbortController();
178
182
  timeout = setTimeout(() => controller.abort(), options.timeout);
179
183
 
180
- options.signal?.addEventListener('abort', () => controller.abort('abort'));
184
+ options.signal?.addEventListener('abort', () => controller.abort(options.signal?.reason));
181
185
  }
182
186
 
183
187
  try {
@@ -190,9 +194,11 @@ export async function domFetch<T extends HttpFetchOptions<BodyInit>>(options: T)
190
194
  body,
191
195
  });
192
196
 
193
- if (!options?.ignoreStatusCode) {
197
+ if (options?.checkStatusCode === undefined || options?.checkStatusCode) {
194
198
  try {
195
- checkStatus(response.status);
199
+ const checker = typeof options?.checkStatusCode === 'function' ? options.checkStatusCode : checkStatus;
200
+ if (!checker(response.status))
201
+ throw new Error(`http response statusCode ${response.status}`);
196
202
  }
197
203
  catch (e) {
198
204
  response.arrayBuffer().catch(() => { });
@@ -210,32 +216,3 @@ export async function domFetch<T extends HttpFetchOptions<BodyInit>>(options: T)
210
216
  clearTimeout(timeout);
211
217
  }
212
218
  }
213
-
214
- function ensureType<T>(v: T) {
215
- }
216
-
217
- async function test() {
218
- const a = await domFetch({
219
- url: 'http://example.com',
220
- });
221
-
222
- ensureType<Buffer>(a.body);
223
-
224
- const b = await domFetch({
225
- url: 'http://example.com',
226
- responseType: 'json',
227
- });
228
- ensureType<any>(b.body);
229
-
230
- const c = await domFetch({
231
- url: 'http://example.com',
232
- responseType: 'readable',
233
- });
234
- ensureType<Response>(c.body);
235
-
236
- const d = await domFetch({
237
- url: 'http://example.com',
238
- responseType: 'buffer',
239
- });
240
- ensureType<Buffer>(d.body);
241
- }
@@ -7,13 +7,13 @@ export class ListenZeroSingleClientTimeoutError extends Error {
7
7
  }
8
8
  }
9
9
 
10
- export async function listenZero(server: net.Server, hostname?: string) {
11
- server.listen(0, hostname);
10
+ export async function listenZero(server: net.Server, hostname: string) {
11
+ server.listen(0, hostname || '127.0.0.1');
12
12
  await once(server, 'listening');
13
13
  return (server.address() as net.AddressInfo).port;
14
14
  }
15
15
 
16
- export async function listenZeroSingleClient(hostname?: string, options?: net.ServerOpts, listenTimeout = 30000) {
16
+ export async function listenZeroSingleClient(hostname: string, options?: net.ServerOpts, listenTimeout = 30000) {
17
17
  const server = new net.Server(options);
18
18
  const port = await listenZero(server, hostname);
19
19
 
@@ -295,8 +295,8 @@ export async function createConsoleServer(remoteStdout: Readable, remoteStderr:
295
295
  socket.once('error', cleanup);
296
296
  socket.once('end', cleanup);
297
297
  });
298
- const readPort = await listenZero(readServer);
299
- const writePort = await listenZero(writeServer);
298
+ const readPort = await listenZero(readServer, '127.0.0.1');
299
+ const writePort = await listenZero(writeServer, '127.0.0.1');
300
300
 
301
301
  return {
302
302
  clear(nativeId: ScryptedNativeId) {
@@ -9,8 +9,12 @@ import { ensurePluginVolume } from "./plugin-volume";
9
9
 
10
10
  export function defaultNpmExec(args: string[], options: child_process.SpawnOptions) {
11
11
  let npm = 'npm';
12
- if (os.platform() === 'win32')
12
+ if (os.platform() === 'win32') {
13
13
  npm += '.cmd';
14
+ // wrap each argument in a quote to handle spaces in paths
15
+ // https://github.com/nodejs/node/issues/38490#issuecomment-927330248
16
+ args = args.map(arg => '"' + arg + '"');
17
+ }
14
18
  const cp = child_process.spawn(npm, args, options);
15
19
  return cp;
16
20
  }
@@ -9,6 +9,7 @@ import { computeClusterObjectHash } from '../cluster/cluster-hash';
9
9
  import { ClusterObject, ConnectRPCObject } from '../cluster/connect-rpc-object';
10
10
  import { listenZero } from '../listen-zero';
11
11
  import { RpcMessage, RpcPeer } from '../rpc';
12
+ import { evalLocal } from '../rpc-peer-eval';
12
13
  import { createDuplexRpcPeer } from '../rpc-serializer';
13
14
  import { MediaManagerImpl } from './media';
14
15
  import { PluginAPI, PluginAPIProxy, PluginRemote, PluginRemoteLoadZipOptions } from './plugin-api';
@@ -269,11 +270,11 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
269
270
 
270
271
  process.on('uncaughtException', e => {
271
272
  getPluginConsole().error('uncaughtException', e);
272
- scrypted.log.e('uncaughtException ' + e?.toString());
273
+ scrypted.log.e('uncaughtException ' + (e.stack || e?.toString()));
273
274
  });
274
275
  process.on('unhandledRejection', e => {
275
276
  getPluginConsole().error('unhandledRejection', e);
276
- scrypted.log.e('unhandledRejection ' + e?.toString());
277
+ scrypted.log.e('unhandledRejection ' + ((e as Error).stack || e?.toString()));
277
278
  });
278
279
 
279
280
  installSourceMapSupport({
@@ -376,7 +377,7 @@ export function startPluginRemote(mainFilename: string, pluginId: string, peerSe
376
377
 
377
378
  try {
378
379
  const filename = zipOptions?.debug ? '/plugin/main.nodejs.js' : `/${pluginId}/main.nodejs.js`;
379
- peer.evalLocal(script, filename, params);
380
+ evalLocal(peer, script, filename, params);
380
381
 
381
382
  if (zipOptions?.fork) {
382
383
  // pluginConsole?.log('plugin forked');
@@ -179,7 +179,6 @@ class DeviceStateProxyHandler implements ProxyHandler<any> {
179
179
 
180
180
  set?(target: any, p: PropertyKey, value: any, receiver: any) {
181
181
  checkProperty(p.toString(), value);
182
- const now = Date.now();
183
182
  this.deviceManager.systemManager.state[this.id][p as string] = {
184
183
  value,
185
184
  };
@@ -446,7 +445,7 @@ export async function setupPluginRemote(peer: RpcPeer, api: PluginAPI, pluginId:
446
445
  return remote;
447
446
  }
448
447
  catch (e) {
449
- throw new RPCResultError(peer, 'error while retrieving PluginRemote', e);
448
+ throw new RPCResultError(peer, 'error while retrieving PluginRemote', e as Error);
450
449
  }
451
450
  }
452
451
 
@@ -75,5 +75,5 @@ export async function createREPLServer(scrypted: ScryptedStatic, params: any, pl
75
75
  socket.on('error', cleanup);
76
76
  socket.on('end', cleanup);
77
77
  });
78
- return listenZero(server);
78
+ return listenZero(server, '127.0.0.1');
79
79
  }
@@ -0,0 +1,27 @@
1
+ import type { CompileFunctionOptions } from 'vm';
2
+ import { RpcPeer } from "./rpc";
3
+
4
+ type CompileFunction = (code: string, params?: ReadonlyArray<string>, options?: CompileFunctionOptions) => Function;
5
+
6
+ function compileFunction(code: string, params?: ReadonlyArray<string>, options?: CompileFunctionOptions): any {
7
+ params = params || [];
8
+ const f = `(function(${params.join(',')}) {;${code};})`;
9
+ return eval(f);
10
+ }
11
+
12
+ export function evalLocal<T>(peer: RpcPeer, script: string, filename?: string, coercedParams?: { [name: string]: any }): T {
13
+ const params = Object.assign({}, peer.params, coercedParams);
14
+ let compile: CompileFunction;
15
+ try {
16
+ // prevent bundlers from trying to include non-existent vm module.
17
+ compile = module[`require`]('vm').compileFunction;
18
+ }
19
+ catch (e) {
20
+ compile = compileFunction;
21
+ }
22
+ const f = compile(script, Object.keys(params), {
23
+ filename,
24
+ });
25
+ const value = f(...Object.values(params));
26
+ return value;
27
+ }
@@ -10,7 +10,7 @@ export function createDuplexRpcPeer(selfName: string, peerName: string, readable
10
10
  serializer.sendMessage(message, reject, serializationContext);
11
11
  }
12
12
  catch (e) {
13
- reject?.(e);
13
+ reject?.(e as Error);
14
14
  readable.destroy();
15
15
  }
16
16
  });
@@ -165,7 +165,7 @@ export function createRpcDuplexSerializer(writable: {
165
165
  serializer.onMessageFinish(message);
166
166
  }
167
167
  catch (e) {
168
- serializer.kill('message parse failure ' + e.message);
168
+ serializer.kill('message parse failure ' + (e as Error).message);
169
169
  }
170
170
  }
171
171
  else {
package/src/rpc.ts CHANGED
@@ -1,6 +1,3 @@
1
- import type { CompileFunctionOptions } from 'vm';
2
- type CompileFunction = (code: string, params?: ReadonlyArray<string>, options?: CompileFunctionOptions) => Function;
3
-
4
1
  export function startPeriodicGarbageCollection() {
5
2
  if (!global.gc) {
6
3
  console.warn('rpc peer garbage collection not available: global.gc is not exposed.');
@@ -253,12 +250,6 @@ export class RPCResultError extends Error {
253
250
  }
254
251
  }
255
252
 
256
- function compileFunction(code: string, params?: ReadonlyArray<string>, options?: CompileFunctionOptions): any {
257
- params = params || [];
258
- const f = `(function(${params.join(',')}) {;${code};})`;
259
- return eval(f);
260
- }
261
-
262
253
  declare class WeakRef<T> {
263
254
  target: T;
264
255
  constructor(target: any);
@@ -499,23 +490,6 @@ export class RpcPeer {
499
490
  });
500
491
  }
501
492
 
502
- evalLocal<T>(script: string, filename?: string, coercedParams?: { [name: string]: any }): T {
503
- const params = Object.assign({}, this.params, coercedParams);
504
- let compile: CompileFunction;
505
- try {
506
- // prevent bundlers from trying to include non-existent vm module.
507
- compile = module[`require`]('vm').compileFunction;
508
- }
509
- catch (e) {
510
- compile = compileFunction;
511
- }
512
- const f = compile(script, Object.keys(params), {
513
- filename,
514
- });
515
- const value = f(...Object.values(params));
516
- return value;
517
- }
518
-
519
493
  /**
520
494
  * @deprecated
521
495
  * @param result
@@ -723,7 +697,7 @@ export class RpcPeer {
723
697
  }
724
698
  catch (e) {
725
699
  // console.error('failure', rpcApply.method, e);
726
- this.createErrorResult(result, e);
700
+ this.createErrorResult(result, e as Error);
727
701
  }
728
702
 
729
703
  this.send(result, undefined, serializationContext);
@@ -785,7 +759,7 @@ export class RpcPeer {
785
759
  }
786
760
  catch (e) {
787
761
  // console.error('failure', rpcApply.method, e);
788
- this.createErrorResult(result, e);
762
+ this.createErrorResult(result, e as Error);
789
763
  }
790
764
 
791
765
  if (!rpcApply.oneway)
package/src/runtime.ts CHANGED
@@ -275,10 +275,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
275
275
  return;
276
276
  }
277
277
 
278
+ const reqany = req as any;
278
279
  if ((req as any).upgradeHead)
279
- this.connectRPCObjectIO.handleUpgrade(req, res.socket, (req as any).upgradeHead)
280
+ this.connectRPCObjectIO.handleUpgrade(reqany, res.socket, reqany.upgradeHead)
280
281
  else
281
- this.connectRPCObjectIO.handleRequest(req, res);
282
+ this.connectRPCObjectIO.handleRequest(reqany, res);
282
283
  }
283
284
 
284
285
  async getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
@@ -422,15 +423,18 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
422
423
  return;
423
424
  }
424
425
 
425
- (req as any).scrypted = {
426
+ const reqany = req as any;
427
+
428
+ reqany.scrypted = {
426
429
  endpointRequest,
427
430
  pluginDevice,
428
431
  accessControls,
429
432
  };
433
+
430
434
  if ((req as any).upgradeHead)
431
- pluginHost.io.handleUpgrade(req, res.socket, (req as any).upgradeHead)
435
+ pluginHost.io.handleUpgrade(reqany, res.socket, reqany.upgradeHead)
432
436
  else
433
- pluginHost.io.handleRequest(req, res);
437
+ pluginHost.io.handleRequest(reqany, res);
434
438
  }
435
439
 
436
440
  handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: HttpPluginData) {
@@ -161,6 +161,16 @@ async function start(mainFilename: string, options?: {
161
161
  callback(sha === user.passwordHash || password === user.token);
162
162
  });
163
163
 
164
+ // the default http-auth will returns a WWW-Authenticate header if login fails.
165
+ // this causes the Safari to prompt for login.
166
+ // https://github.com/gevorg/http-auth/blob/4158fa75f58de70fd44aa68876a8674725e0556e/src/auth/base.js#L81
167
+ // override the ask function to return a bare 401 instead.
168
+ // @ts-expect-error
169
+ basicAuth.ask = (res) => {
170
+ res.statusCode = 401;
171
+ res.end();
172
+ };
173
+
164
174
  const httpsServerOptions = process.env.SCRYPTED_HTTPS_OPTIONS_FILE
165
175
  ? JSON.parse(fs.readFileSync(process.env.SCRYPTED_HTTPS_OPTIONS_FILE).toString())
166
176
  : {};
@@ -219,6 +229,12 @@ async function start(mainFilename: string, options?: {
219
229
  }
220
230
 
221
231
  app.use(async (req, res, next) => {
232
+ // /web/component requires basic auth admin access.
233
+ if (req.url.startsWith('/web/component/')) {
234
+ next();
235
+ return;
236
+ }
237
+
222
238
  // the remote address may be ipv6 prefixed so use a fuzzy match.
223
239
  // eg ::ffff:192.168.2.124
224
240
  if (process.env.SCRYPTED_ADMIN_USERNAME