@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.
- package/dist/event-registry.js +3 -4
- package/dist/event-registry.js.map +1 -1
- package/dist/http-interfaces.js +11 -0
- package/dist/http-interfaces.js.map +1 -1
- package/dist/plugin/media.js +77 -66
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-api.js +1 -1
- package/dist/plugin/plugin-api.js.map +1 -1
- package/dist/plugin/plugin-device.js +25 -11
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +11 -6
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +1 -1
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +170 -17
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +25 -85
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/runtime/node-fork-worker.js +11 -3
- package/dist/plugin/runtime/node-fork-worker.js.map +1 -1
- package/dist/plugin/socket-serializer.js +17 -0
- package/dist/plugin/socket-serializer.js.map +1 -0
- package/dist/rpc.js +3 -3
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +14 -11
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-plugin-main.js +4 -1
- package/dist/scrypted-plugin-main.js.map +1 -1
- package/dist/scrypted-server-main.js +53 -12
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/server-settings.js +5 -1
- package/dist/server-settings.js.map +1 -1
- package/dist/state.js +2 -1
- package/dist/state.js.map +1 -1
- package/package.json +5 -11
- package/src/event-registry.ts +3 -4
- package/src/http-interfaces.ts +13 -0
- package/src/plugin/media.ts +93 -74
- package/src/plugin/plugin-api.ts +5 -4
- package/src/plugin/plugin-device.ts +25 -11
- package/src/plugin/plugin-host-api.ts +1 -1
- package/src/plugin/plugin-host.ts +6 -5
- package/src/plugin/plugin-http.ts +2 -2
- package/src/plugin/plugin-remote-worker.ts +211 -23
- package/src/plugin/plugin-remote.ts +31 -94
- package/src/plugin/runtime/node-fork-worker.ts +11 -3
- package/src/plugin/runtime/runtime-worker.ts +1 -1
- package/src/plugin/socket-serializer.ts +15 -0
- package/src/rpc.ts +3 -2
- package/src/runtime.ts +10 -10
- package/src/scrypted-plugin-main.ts +4 -1
- package/src/scrypted-server-main.ts +59 -13
- 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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
201
|
-
const state = hash.state
|
|
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
|
|
205
|
-
url.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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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' : '
|
|
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
|
-
|
|
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
|
}
|