@scrypted/server 0.56.0 → 0.58.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.
package/src/runtime.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import net from 'net';
1
2
  import { Device, DeviceInformation, DeviceProvider, EngineIOHandler, HttpRequest, HttpRequestHandler, ScryptedDevice, ScryptedInterface, ScryptedInterfaceMethod, ScryptedInterfaceProperty, ScryptedNativeId, ScryptedUser as SU } from '@scrypted/types';
2
3
  import AdmZip from 'adm-zip';
3
4
  import crypto from 'crypto';
@@ -34,6 +35,7 @@ import { getPluginVolume } from './plugin/plugin-volume';
34
35
  import { NodeForkWorker } from './plugin/runtime/node-fork-worker';
35
36
  import { PythonRuntimeWorker } from './plugin/runtime/python-worker';
36
37
  import { RuntimeWorker, RuntimeWorkerOptions } from './plugin/runtime/runtime-worker';
38
+ import { ClusterObject } from './cluster/connect-rpc-object';
37
39
  import { getIpAddress, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
38
40
  import { AddressSettings } from './services/addresses';
39
41
  import { Alerts } from './services/alerts';
@@ -43,6 +45,7 @@ import { PluginComponent } from './services/plugin';
43
45
  import { ServiceControl } from './services/service-control';
44
46
  import { UsersService } from './services/users';
45
47
  import { getState, ScryptedStateManager, setState } from './state';
48
+ import { computeClusterObjectHash } from './cluster/cluster-hash';
46
49
 
47
50
  interface DeviceProxyPair {
48
51
  handler: PluginDeviceProxyHandler;
@@ -82,6 +85,17 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
82
85
  })
83
86
  },
84
87
  });
88
+ connectRPCObjectIO: IOServer = new io.Server({
89
+ pingTimeout: 120000,
90
+ perMessageDeflate: true,
91
+ cors: (req, callback) => {
92
+ const header = this.getAccessControlAllowOrigin(req.headers);
93
+ callback(undefined, {
94
+ origin: header,
95
+ credentials: true,
96
+ })
97
+ },
98
+ });
85
99
  pluginComponent = new PluginComponent(this);
86
100
  serviceControl = new ServiceControl(this);
87
101
  alerts = new Alerts(this);
@@ -120,14 +134,71 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
120
134
  const cp = spawn(process.env.SHELL, [], {
121
135
  });
122
136
  cp.onData(data => connection.send(data));
123
- connection.on('message', message => cp.write(message.toString()));
137
+ connection.on('message', message => {
138
+ if (Buffer.isBuffer(message)) {
139
+ cp.write(message.toString());
140
+ return;
141
+ }
142
+
143
+ try {
144
+ const parsed = JSON.parse(message.toString());
145
+ if (parsed.dim) {
146
+ cp.resize(parsed.dim.cols, parsed.dim.rows);
147
+ }
148
+ } catch (e) {
149
+ // we should only get here if an outdated core plugin
150
+ // is sending us string data instead of buffer data
151
+ cp.write(message.toString());
152
+ }
153
+ });
124
154
  connection.on('close', () => cp.kill());
155
+ cp.onExit(() => connection.close());
125
156
  }
126
157
  catch (e) {
127
158
  connection.close();
128
159
  }
129
160
  });
130
161
 
162
+ app.all('/engine.io/connectRPCObject', (req, res) => this.connectRPCObjectHandler(req, res));
163
+
164
+ /*
165
+ * Handle incoming connections that will be
166
+ * proxied to a connectRPCObject socket.
167
+ *
168
+ * Note that the clusterObject hash must be
169
+ * verified before connecting to the target port.
170
+ */
171
+ this.connectRPCObjectIO.on('connection', connection => {
172
+ try {
173
+ const clusterObject: ClusterObject = JSON.parse((connection.request as Request).query.clusterObject as string);
174
+ const sha256 = computeClusterObjectHash(clusterObject, this.clusterSecret);
175
+ if (sha256 != clusterObject.sha256) {
176
+ connection.send({
177
+ error: 'invalid signature'
178
+ });
179
+ connection.close();
180
+ return;
181
+ }
182
+
183
+ const socket = net.connect(clusterObject.port, '127.0.0.1');
184
+ socket.on('error', () => connection.close());
185
+ socket.on('close', () => connection.close());
186
+ socket.on('data', data => connection.send(data));
187
+ connection.on('close', () => socket.destroy());
188
+ connection.on('message', message => {
189
+ if (typeof message !== 'string') {
190
+ socket.write(message);
191
+ }
192
+ else {
193
+ console.warn('unexpected string data on engine.io rpc connection. terminating.')
194
+ connection.close();
195
+ }
196
+ });
197
+ } catch {
198
+ connection.close();
199
+ }
200
+ });
201
+
131
202
  insecure.on('upgrade', (req, socket, upgradeHead) => {
132
203
  (req as any).upgradeHead = upgradeHead;
133
204
  (app as any).handle(req, {
@@ -262,6 +333,33 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
262
333
  this.shellio.handleRequest(req, res);
263
334
  }
264
335
 
336
+ async connectRPCObjectHandler(req: Request, res: Response) {
337
+ const isUpgrade = isConnectionUpgrade(req.headers);
338
+
339
+ const end = (code: number, message: string) => {
340
+ if (isUpgrade) {
341
+ const socket = res.socket;
342
+ socket.write(`HTTP/1.1 ${code} ${message}\r\n` +
343
+ '\r\n');
344
+ socket.destroy();
345
+ }
346
+ else {
347
+ res.status(code);
348
+ res.send(message);
349
+ }
350
+ };
351
+
352
+ if (!res.locals.username) {
353
+ end(401, 'Not Authorized');
354
+ return;
355
+ }
356
+
357
+ if ((req as any).upgradeHead)
358
+ this.connectRPCObjectIO.handleUpgrade(req, res.socket, (req as any).upgradeHead)
359
+ else
360
+ this.connectRPCObjectIO.handleRequest(req, res);
361
+ }
362
+
265
363
  async getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<HttpPluginData> {
266
364
  const ret = await this.getPluginForEndpoint(endpoint);
267
365
  if (req.url.indexOf('/engine.io/api') !== -1)