@scrypted/server 0.0.71 → 0.0.78

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 (42) hide show
  1. package/.vscode/settings.json +2 -1
  2. package/dist/cert.js +75 -0
  3. package/dist/cert.js.map +1 -0
  4. package/dist/plugin/media.js +50 -14
  5. package/dist/plugin/media.js.map +1 -1
  6. package/dist/plugin/plugin-device.js +11 -1
  7. package/dist/plugin/plugin-device.js.map +1 -1
  8. package/dist/plugin/plugin-host-api.js +7 -3
  9. package/dist/plugin/plugin-host-api.js.map +1 -1
  10. package/dist/plugin/plugin-host.js +66 -45
  11. package/dist/plugin/plugin-host.js.map +1 -1
  12. package/dist/plugin/plugin-npm-dependencies.js +53 -0
  13. package/dist/plugin/plugin-npm-dependencies.js.map +1 -0
  14. package/dist/plugin/plugin-remote.js +5 -5
  15. package/dist/plugin/plugin-remote.js.map +1 -1
  16. package/dist/plugin/plugin-volume.js +20 -0
  17. package/dist/plugin/plugin-volume.js.map +1 -0
  18. package/dist/plugin/system.js +9 -3
  19. package/dist/plugin/system.js.map +1 -1
  20. package/dist/rpc.js +32 -12
  21. package/dist/rpc.js.map +1 -1
  22. package/dist/runtime.js +20 -27
  23. package/dist/runtime.js.map +1 -1
  24. package/dist/scrypted-main.js +4 -19
  25. package/dist/scrypted-main.js.map +1 -1
  26. package/package.json +4 -3
  27. package/python/media.py +40 -0
  28. package/python/plugin-remote.py +67 -30
  29. package/python/rpc.py +24 -81
  30. package/src/cert.ts +74 -0
  31. package/src/plugin/media.ts +53 -14
  32. package/src/plugin/plugin-api.ts +0 -1
  33. package/src/plugin/plugin-device.ts +13 -2
  34. package/src/plugin/plugin-host-api.ts +15 -3
  35. package/src/plugin/plugin-host.ts +69 -49
  36. package/src/plugin/plugin-npm-dependencies.ts +55 -0
  37. package/src/plugin/plugin-remote.ts +8 -7
  38. package/src/plugin/plugin-volume.ts +13 -0
  39. package/src/plugin/system.ts +11 -3
  40. package/src/rpc.ts +35 -12
  41. package/src/runtime.ts +24 -30
  42. package/src/scrypted-main.ts +5 -24
package/python/rpc.py CHANGED
@@ -1,11 +1,7 @@
1
- from asyncio.events import AbstractEventLoop
2
1
  from asyncio.futures import Future
3
2
  from typing import Callable
4
- import asyncio
5
- import json
6
3
  import traceback
7
4
  import inspect
8
- import json
9
5
  from collections.abc import Mapping, Sequence
10
6
  import weakref
11
7
 
@@ -50,17 +46,19 @@ class RpcProxyMethod:
50
46
  return self.__proxy.__apply__(self.__proxy_method_name, args)
51
47
 
52
48
 
53
- class RpcProxy:
49
+ class RpcProxy(object):
54
50
  def __init__(self, peer, proxyId: str, proxyConstructorName: str, proxyProps: any, proxyOneWayMethods: list[str]):
55
- self.__proxy_id = proxyId
56
- self.__proxy_constructor = proxyConstructorName
57
- self.__proxy_peer = peer
58
- self.__proxy_props = proxyProps
59
- self.__proxy_oneway_methods = proxyOneWayMethods
51
+ self.__dict__['__proxy_id'] = proxyId
52
+ self.__dict__['__proxy_constructor'] = proxyConstructorName
53
+ self.__dict__['__proxy_peer'] = peer
54
+ self.__dict__['__proxy_props'] = proxyProps
55
+ self.__dict__['__proxy_oneway_methods'] = proxyOneWayMethods
60
56
 
61
57
  def __getattr__(self, name):
62
- if self.__proxy_props and hasattr(self.__proxy_props, name):
63
- return self.__proxy_props[name]
58
+ if name in self.__dict__:
59
+ return self.__dict__[name]
60
+ if self.__dict__['__proxy_props'] and name in self.__dict__['__proxy_props']:
61
+ return self.__dict__['__proxy_props'][name]
64
62
  return RpcProxyMethod(self, name)
65
63
 
66
64
  def __call__(self, *args, **kwargs):
@@ -68,7 +66,7 @@ class RpcProxy:
68
66
  pass
69
67
 
70
68
  def __apply__(self, method: str, args: list):
71
- return self.__proxy_peer.__apply__(self.__proxy_id, self.__proxy_oneway_methods, method, args)
69
+ return self.__dict__['__proxy_peer'].__apply__(self.__dict__['__proxy_id'], self.__dict__['__proxy_oneway_methods'], method, args)
72
70
 
73
71
 
74
72
  class RpcPeer:
@@ -86,20 +84,20 @@ class RpcPeer:
86
84
  def __init__(self, send: Callable[[object, Callable[[Exception], None]], None]) -> None:
87
85
  self.send = send
88
86
 
89
- def __apply__(self, proxyId: str, oneWayMethods: list[str], method: str, argArray: list):
90
- args = []
91
- for arg in argArray:
92
- args.append(self.serialize(arg, False))
87
+ def __apply__(self, proxyId: str, oneWayMethods: list[str], method: str, args: list):
88
+ serializedArgs = []
89
+ for arg in args:
90
+ serializedArgs.append(self.serialize(arg, False))
93
91
 
94
92
  rpcApply = {
95
93
  'type': 'apply',
96
94
  'id': None,
97
95
  'proxyId': proxyId,
98
- 'argArray': args,
96
+ 'args': serializedArgs,
99
97
  'method': method,
100
98
  }
101
99
 
102
- if not oneWayMethods or method not in oneWayMethods:
100
+ if oneWayMethods and method in oneWayMethods:
103
101
  rpcApply['oneway'] = True
104
102
  self.send(rpcApply)
105
103
  future = Future()
@@ -142,8 +140,7 @@ class RpcPeer:
142
140
  }
143
141
  return ret
144
142
 
145
- serializerMapName = self.constructorSerializerMap.get(
146
- type(value).__name__)
143
+ serializerMapName = self.constructorSerializerMap.get(type(value), None)
147
144
  if serializerMapName:
148
145
  __remote_constructor_name = serializerMapName
149
146
  serializer = self.nameDeserializerMap.get(serializerMapName, None)
@@ -227,8 +224,8 @@ class RpcPeer:
227
224
 
228
225
  async def handleMessage(self, message: any):
229
226
  try:
230
- type = message['type']
231
- if type == 'param':
227
+ messageType = message['type']
228
+ if messageType == 'param':
232
229
  result = {
233
230
  'type': 'result',
234
231
  'id': message['id'],
@@ -246,7 +243,7 @@ class RpcPeer:
246
243
 
247
244
  self.send(result)
248
245
 
249
- elif type == 'apply':
246
+ elif messageType == 'apply':
250
247
  result = {
251
248
  'type': 'result',
252
249
  'id': message['id'],
@@ -261,7 +258,7 @@ class RpcPeer:
261
258
  message['proxyId'])
262
259
 
263
260
  args = []
264
- for arg in (message['argArray'] or []):
261
+ for arg in (message['args'] or []):
265
262
  args.append(self.deserialize(arg))
266
263
 
267
264
  value = None
@@ -284,7 +281,7 @@ class RpcPeer:
284
281
  if not message.get('oneway', False):
285
282
  self.send(result)
286
283
 
287
- elif type == 'result':
284
+ elif messageType == 'result':
288
285
  future = self.pendingResults.get(message['id'], None)
289
286
  if not future:
290
287
  raise RpcResultException(
@@ -299,7 +296,7 @@ class RpcPeer:
299
296
  return
300
297
  future.set_result(self.deserialize(
301
298
  message.get('result', None)))
302
- elif type == 'finalize':
299
+ elif messageType == 'finalize':
303
300
  local = self.localProxyMap.pop(
304
301
  message['__local_proxy_id'], None)
305
302
  self.localProxied.pop(local, None)
@@ -330,57 +327,3 @@ class RpcPeer:
330
327
  }
331
328
  self.send(paramMessage, reject)
332
329
  return await self.createPendingResult(send)
333
-
334
- # c = RpcPeer()
335
-
336
-
337
- async def readLoop(loop, peer, reader):
338
- async for line in reader:
339
- try:
340
- message = json.loads(line)
341
- asyncio.run_coroutine_threadsafe(peer.handleMessage(message), loop)
342
- except Exception as e:
343
- print('read loop error', e)
344
- pass
345
-
346
-
347
- async def async_main(loop: AbstractEventLoop):
348
- reader, writer = await asyncio.open_connection(
349
- '127.0.0.1', 3033)
350
-
351
- async def send(message, reject):
352
- jsonString = json.dumps(message)
353
- writer.write(bytes(jsonString + '\n', 'utf8'))
354
- try:
355
- await writer.drain()
356
- except Exception as e:
357
- if reject:
358
- reject(e)
359
-
360
- peer = RpcPeer(send)
361
- peer.params['print'] = print
362
-
363
- async def consoleTest():
364
- console = await peer.getParam('console')
365
- await console.log('test', 'poops', 'peddeps')
366
-
367
- await asyncio.gather(readLoop(loop, peer, reader), consoleTest())
368
- print('done')
369
-
370
- # print("line %s" % line)
371
-
372
- # async with aiofiles.open(0, mode='r') as f:
373
- # async for line in f:
374
- # print("line %s" % line)
375
- # # pokemon = json.loads(contents)
376
- # # print(pokemon['name'])
377
-
378
-
379
- def main():
380
- loop = asyncio.get_event_loop()
381
- loop.run_until_complete(async_main(loop))
382
- loop.close()
383
-
384
-
385
- if __name__ == "__main__":
386
- main()
package/src/cert.ts ADDED
@@ -0,0 +1,74 @@
1
+ // import libraries
2
+ import forge from 'node-forge';
3
+ import crypto from 'crypto';
4
+
5
+ const { pki } = forge;
6
+
7
+
8
+ export const CURRENT_SELF_SIGNED_CERTIFICATE_VERSION = 'v2';
9
+
10
+ export function createSelfSignedCertificate() {
11
+
12
+ // generate a keypair and create an X.509v3 certificate
13
+ const keys = pki.rsa.generateKeyPair(2048);
14
+ const cert = pki.createCertificate();
15
+ cert.publicKey = keys.publicKey;
16
+
17
+ // NOTE: serialNumber is the hex encoded value of an ASN.1 INTEGER.
18
+ // Conforming CAs should ensure serialNumber is:
19
+ // - no more than 20 octets
20
+ // - non-negative (prefix a '00' if your value starts with a '1' bit)
21
+ cert.serialNumber = '01' + crypto.randomBytes(19).toString("hex"); // 1 octet = 8 bits = 1 byte = 2 hex chars
22
+ cert.validity.notBefore = new Date();
23
+ cert.validity.notAfter = new Date();
24
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); // adding 1 year of validity from now
25
+ const attrs = [{
26
+ name: 'commonName',
27
+ value: 'localhost'
28
+ }];
29
+ cert.setSubject(attrs);
30
+ cert.setIssuer(attrs);
31
+ cert.setExtensions([{
32
+ name: 'basicConstraints',
33
+ cA: true
34
+ }, {
35
+ name: 'keyUsage',
36
+ keyCertSign: true,
37
+ digitalSignature: true,
38
+ nonRepudiation: true,
39
+ keyEncipherment: true,
40
+ dataEncipherment: true
41
+ }, {
42
+ name: 'extKeyUsage',
43
+ serverAuth: true,
44
+ clientAuth: true,
45
+ codeSigning: true,
46
+ emailProtection: true,
47
+ timeStamping: true
48
+ }, {
49
+ name: 'nsCertType',
50
+ client: true,
51
+ server: true,
52
+ email: true,
53
+ objsign: true,
54
+ sslCA: true,
55
+ emailCA: true,
56
+ objCA: true
57
+ }, {
58
+ name: 'subjectAltName',
59
+ altNames: [{
60
+ type: 7, // IP
61
+ ip: '127.0.0.1'
62
+ }]
63
+ }, {
64
+ name: 'subjectKeyIdentifier'
65
+ }]);
66
+
67
+ // self-sign certificate
68
+ cert.sign(keys.privateKey);
69
+ return {
70
+ serviceKey: pki.privateKeyToPem(keys.privateKey),
71
+ certificate: pki.certificateToPem(cert),
72
+ version: CURRENT_SELF_SIGNED_CERTIFICATE_VERSION,
73
+ };
74
+ }
@@ -1,4 +1,4 @@
1
- import { MediaStreamUrl, VideoCamera, Camera, BufferConverter, FFMpegInput, MediaManager, MediaObject, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, SystemManager, SCRYPTED_MEDIA_SCHEME } from "@scrypted/sdk/types";
1
+ import { ScryptedInterfaceProperty, SystemDeviceState, MediaStreamUrl, VideoCamera, Camera, BufferConverter, FFMpegInput, MediaManager, MediaObject, ScryptedDevice, ScryptedInterface, ScryptedMimeTypes, SystemManager, SCRYPTED_MEDIA_SCHEME } from "@scrypted/sdk/types";
2
2
  import { convert, ensureBuffer } from "../convert";
3
3
  import { MediaObjectRemote } from "./plugin-api";
4
4
  import mimeType from 'mime'
@@ -15,14 +15,27 @@ function addBuiltins(console: Console, mediaManager: MediaManager) {
15
15
  fromMimeType: ScryptedMimeTypes.Url + ';' + ScryptedMimeTypes.AcceptUrlParameter,
16
16
  toMimeType: ScryptedMimeTypes.FFmpegInput,
17
17
  async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
18
+ const url = data.toString();
18
19
  const args: FFMpegInput = {
19
- inputArguments: ['-i', data.toString()]
20
+ url,
21
+ inputArguments: [
22
+ '-i',
23
+ url,
24
+ ],
20
25
  }
21
26
 
22
27
  return Buffer.from(JSON.stringify(args));
23
28
  }
24
29
  });
25
30
 
31
+ mediaManager.builtinConverters.push({
32
+ fromMimeType: ScryptedMimeTypes.FFmpegInput,
33
+ toMimeType: ScryptedMimeTypes.MediaStreamUrl,
34
+ async convert(data: string | Buffer, fromMimeType: string): Promise<Buffer | string> {
35
+ return data;
36
+ }
37
+ });
38
+
26
39
  mediaManager.builtinConverters.push({
27
40
  fromMimeType: ScryptedMimeTypes.MediaStreamUrl,
28
41
  toMimeType: ScryptedMimeTypes.FFmpegInput,
@@ -52,10 +65,9 @@ function addBuiltins(console: Console, mediaManager: MediaManager) {
52
65
  )
53
66
  }
54
67
 
55
- const ret: FFMpegInput = {
68
+ const ret: FFMpegInput = Object.assign({
56
69
  inputArguments,
57
- mediaStreamOptions: mediaUrl.mediaStreamOptions,
58
- }
70
+ }, mediaUrl);
59
71
 
60
72
  return Buffer.from(JSON.stringify(ret));
61
73
  }
@@ -87,15 +99,16 @@ function addBuiltins(console: Console, mediaManager: MediaManager) {
87
99
  });
88
100
  }
89
101
 
90
- export class MediaManagerImpl implements MediaManager {
91
- systemManager: SystemManager;
102
+ export abstract class MediaManagerBase implements MediaManager {
92
103
  builtinConverters: BufferConverter[] = [];
93
104
 
94
- constructor(systemManager: SystemManager, public console: Console) {
95
- this.systemManager = systemManager;
105
+ constructor(public console: Console) {
96
106
  addBuiltins(this.console, this);
97
107
  }
98
108
 
109
+ abstract getSystemState(): { [id: string]: { [property: string]: SystemDeviceState } };
110
+ abstract getDeviceById<T>(id: string): T;
111
+
99
112
  async getFFmpegPath(): Promise<string> {
100
113
  // try to get the ffmpeg path as a value of another variable
101
114
  // ie, in docker builds:
@@ -119,9 +132,9 @@ export class MediaManagerImpl implements MediaManager {
119
132
  }
120
133
 
121
134
  getConverters(): BufferConverter[] {
122
- const devices = Object.keys(this.systemManager.getSystemState()).map(id => this.systemManager.getDeviceById(id));
123
- const converters: BufferConverter[] = Object.values(devices).filter(device => device.interfaces?.includes(ScryptedInterface.BufferConverter))
124
- .map(device => device as ScryptedDevice & BufferConverter);
135
+ const converters = Object.entries(this.getSystemState())
136
+ .filter(([id, state]) => state[ScryptedInterfaceProperty.interfaces]?.value?.includes(ScryptedInterface.BufferConverter))
137
+ .map(([id]) => this.getDeviceById<BufferConverter>(id));
125
138
  converters.push(...this.builtinConverters);
126
139
  return converters;
127
140
  }
@@ -185,10 +198,10 @@ export class MediaManagerImpl implements MediaManager {
185
198
  const path = url.pathname.split('/')[1];
186
199
  let mo: MediaObject;
187
200
  if (path === ScryptedInterface.VideoCamera) {
188
- mo = await this.systemManager.getDeviceById<VideoCamera>(id).getVideoStream();
201
+ mo = await this.getDeviceById<VideoCamera>(id).getVideoStream();
189
202
  }
190
203
  else if (path === ScryptedInterface.Camera) {
191
- mo = await this.systemManager.getDeviceById<Camera>(id).takePicture() as any;
204
+ mo = await this.getDeviceById<Camera>(id).takePicture() as any;
192
205
  }
193
206
  else {
194
207
  throw new Error('Unrecognized Scrypted Media interface.')
@@ -197,3 +210,29 @@ export class MediaManagerImpl implements MediaManager {
197
210
  return mo;
198
211
  }
199
212
  }
213
+
214
+ export class MediaManagerImpl extends MediaManagerBase {
215
+ constructor(public systemManager: SystemManager, console: Console) {
216
+ super(console);
217
+ }
218
+
219
+ getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
220
+ return this.systemManager.getSystemState();
221
+ }
222
+
223
+ getDeviceById<T>(id: string): T {
224
+ return this.systemManager.getDeviceById<T>(id);
225
+ }
226
+ }
227
+
228
+ export class MediaManagerHostImpl extends MediaManagerBase {
229
+ constructor(public systemState: { [id: string]: { [property: string]: SystemDeviceState } },
230
+ public getDeviceById: (id: string) => any,
231
+ console: Console) {
232
+ super(console);
233
+ }
234
+
235
+ getSystemState(): { [id: string]: { [property: string]: SystemDeviceState; }; } {
236
+ return this.systemState;
237
+ }
238
+ }
@@ -136,7 +136,6 @@ export interface PluginRemoteLoadZipOptions {
136
136
  }
137
137
 
138
138
  export interface PluginRemote {
139
- __proxy_oneway_methods?: string[],
140
139
  loadZip(packageJson: any, zipData: Buffer, options?: PluginRemoteLoadZipOptions): Promise<any>;
141
140
  setSystemState(state: {[id: string]: {[property: string]: SystemDeviceState}}): Promise<void>;
142
141
  setNativeId(nativeId: ScryptedNativeId, id: string, storage: {[key: string]: any}): Promise<void>;
@@ -2,7 +2,7 @@ import { DeviceProvider, EventDetails, EventListenerOptions, EventListenerRegist
2
2
  import { ScryptedRuntime } from "../runtime";
3
3
  import { PluginDevice } from "../db-types";
4
4
  import { MixinProvider } from "@scrypted/sdk/types";
5
- import { handleFunctionInvocations, PROPERTY_PROXY_ONEWAY_METHODS } from "../rpc";
5
+ import { handleFunctionInvocations } from "../rpc";
6
6
  import { getState } from "../state";
7
7
  import { getDisplayType } from "../infer-defaults";
8
8
  import { allInterfaceProperties, isValidInterfaceMethod, methodInterfaces } from "./descriptor";
@@ -39,6 +39,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
39
39
  })().catch(() => { });;
40
40
  }
41
41
 
42
+ // this must not be async, because it potentially changes execution order.
42
43
  ensureProxy(): Promise<PluginDevice> {
43
44
  const pluginDevice = this.scrypted.findPluginDeviceById(this.id);
44
45
  if (!pluginDevice)
@@ -115,7 +116,7 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
115
116
  return mixinTable;
116
117
  })();
117
118
 
118
- return this.mixinTable.then(mixinTable => pluginDevice);
119
+ return this.mixinTable.then(_ => pluginDevice);
119
120
  }
120
121
 
121
122
  get(target: any, p: PropertyKey, receiver: any): any {
@@ -164,6 +165,16 @@ export class PluginDeviceProxyHandler implements ProxyHandler<any>, ScryptedDevi
164
165
  this.scrypted.stateManager.updateDescriptor(device);
165
166
  }
166
167
 
168
+ async probe(): Promise<boolean> {
169
+ try {
170
+ await this.ensureProxy();
171
+ return true;
172
+ }
173
+ catch (e) {
174
+ return false;
175
+ }
176
+ }
177
+
167
178
  async applyMixin(method: string, argArray?: any): Promise<any> {
168
179
  const iface = methodInterfaces[method];
169
180
  if (!iface)
@@ -6,11 +6,23 @@ import { Logger } from '../logger';
6
6
  import { getState } from '../state';
7
7
  import { PluginHost } from './plugin-host';
8
8
  import debounce from 'lodash/debounce';
9
+ import { PROPERTY_PROXY_ONEWAY_METHODS } from '../rpc';
9
10
 
10
11
 
11
12
  export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAPI {
12
13
  pluginId: string;
13
14
 
15
+ [PROPERTY_PROXY_ONEWAY_METHODS]: [
16
+ 'onMixinEvent',
17
+ 'onDeviceEvent',
18
+ 'setStorage',
19
+ 'ioSend',
20
+ 'ioClose',
21
+ 'setDeviceProperty',
22
+ 'deliverPush',
23
+ 'requestRestart',
24
+ ];
25
+
14
26
  restartDebounced = debounce(async () => {
15
27
  const host = this.scrypted.plugins[this.pluginId];
16
28
  const logger = await this.getLogger(undefined);
@@ -23,7 +35,7 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
23
35
  this.scrypted.runPlugin(plugin);
24
36
  }, 15000);
25
37
 
26
- constructor(public scrypted: ScryptedRuntime, plugin: Plugin, public pluginHost: PluginHost) {
38
+ constructor(public scrypted: ScryptedRuntime, plugin: Plugin, public pluginHost: PluginHost, public mediaManager: MediaManager) {
27
39
  super();
28
40
  this.pluginId = plugin._id;
29
41
  }
@@ -40,8 +52,8 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
40
52
  this.scrypted.stateManager.notifyInterfaceEvent(device, eventInterface, eventData);
41
53
  }
42
54
 
43
- getMediaManager(): Promise<MediaManager> {
44
- return null;
55
+ async getMediaManager(): Promise<MediaManager> {
56
+ return this.mediaManager;
45
57
  }
46
58
 
47
59
  async deliverPush(endpoint: string, httpRequest: HttpRequest) {