@scrypted/server 0.2.7 → 0.2.9
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.
- package/dist/plugin/media.js +11 -1
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-host.js +2 -2
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +1 -1
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-state-check.js +3 -0
- package/dist/plugin/plugin-state-check.js.map +1 -1
- package/dist/plugin/runtime/python-worker.js +14 -9
- package/dist/plugin/runtime/python-worker.js.map +1 -1
- package/dist/rpc.js +5 -1
- package/dist/rpc.js.map +1 -1
- package/dist/scrypted-server-main.js +39 -42
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/state.js +3 -0
- package/dist/state.js.map +1 -1
- package/dist/usertoken.js +46 -0
- package/dist/usertoken.js.map +1 -0
- package/package.json +2 -2
- package/python/plugin-remote.py +120 -19
- package/python/rpc.py +24 -21
- package/src/plugin/media.ts +11 -1
- package/src/plugin/plugin-host.ts +2 -2
- package/src/plugin/plugin-remote-worker.ts +1 -1
- package/src/plugin/plugin-state-check.ts +3 -0
- package/src/plugin/runtime/python-worker.ts +14 -9
- package/src/rpc.ts +6 -1
- package/src/scrypted-server-main.ts +41 -46
- package/src/state.ts +3 -0
- package/src/usertoken.ts +38 -0
@@ -5,10 +5,12 @@ import path from 'path';
|
|
5
5
|
import readline from 'readline';
|
6
6
|
import { Readable, Writable } from 'stream';
|
7
7
|
import { RpcMessage, RpcPeer } from "../../rpc";
|
8
|
+
import { createRpcDuplexSerializer } from '../../rpc-serializer';
|
8
9
|
import { ChildProcessWorker } from "./child-process-worker";
|
9
10
|
import { RuntimeWorkerOptions } from "./runtime-worker";
|
10
11
|
|
11
12
|
export class PythonRuntimeWorker extends ChildProcessWorker {
|
13
|
+
serializer: ReturnType<typeof createRpcDuplexSerializer>;
|
12
14
|
|
13
15
|
constructor(pluginId: string, options: RuntimeWorkerOptions) {
|
14
16
|
super(pluginId, options);
|
@@ -62,21 +64,24 @@ export class PythonRuntimeWorker extends ChildProcessWorker {
|
|
62
64
|
const peerin = this.worker.stdio[3] as Writable;
|
63
65
|
const peerout = this.worker.stdio[4] as Readable;
|
64
66
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
67
|
+
const serializer = this.serializer = createRpcDuplexSerializer(peerin);
|
68
|
+
serializer.setupRpcPeer(peer);
|
69
|
+
peerout.on('data', data => serializer.onData(data));
|
70
|
+
peerin.on('error', e => {
|
71
|
+
this.emit('error', e);
|
72
|
+
serializer.onDisconnected();
|
73
|
+
});
|
74
|
+
peerout.on('error', e => {
|
75
|
+
this.emit('error', e)
|
76
|
+
serializer.onDisconnected();
|
71
77
|
});
|
72
|
-
readInterface.on('line', line => peer.handleMessage(JSON.parse(line)));
|
73
78
|
}
|
74
79
|
|
75
|
-
send(message: RpcMessage, reject?: (e: Error) => void): void {
|
80
|
+
send(message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any): void {
|
76
81
|
try {
|
77
82
|
if (!this.worker)
|
78
83
|
throw new Error('worked has been killed');
|
79
|
-
|
84
|
+
this.serializer.sendMessage(message, reject, serializationContext);
|
80
85
|
}
|
81
86
|
catch (e) {
|
82
87
|
reject?.(e);
|
package/src/rpc.ts
CHANGED
@@ -84,7 +84,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
|
|
84
84
|
}
|
85
85
|
|
86
86
|
get(target: any, p: PropertyKey, receiver: any): any {
|
87
|
-
if (p ===
|
87
|
+
if (p === RpcPeer.PROPERTY_PROXY_ID)
|
88
88
|
return this.entry.id;
|
89
89
|
if (p === '__proxy_constructor')
|
90
90
|
return this.constructorName;
|
@@ -214,6 +214,10 @@ export class RpcPeer {
|
|
214
214
|
|
215
215
|
static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
|
216
216
|
|
217
|
+
static isRpcProxy(value: any) {
|
218
|
+
return !!value?.[RpcPeer.PROPERTY_PROXY_ID];
|
219
|
+
}
|
220
|
+
|
217
221
|
static getDefaultTransportSafeArgumentTypes() {
|
218
222
|
const jsonSerializable = new Set<string>();
|
219
223
|
jsonSerializable.add(Number.name);
|
@@ -242,6 +246,7 @@ export class RpcPeer {
|
|
242
246
|
}
|
243
247
|
}
|
244
248
|
|
249
|
+
static readonly PROPERTY_PROXY_ID = '__proxy_id';
|
245
250
|
static readonly PROPERTY_PROXY_ONEWAY_METHODS = '__proxy_oneway_methods';
|
246
251
|
static readonly PROPERTY_JSON_DISABLE_SERIALIZATION = '__json_disable_serialization';
|
247
252
|
static readonly PROPERTY_PROXY_PROPERTIES = '__proxy_props';
|
@@ -23,8 +23,7 @@ import { sleep } from './sleep';
|
|
23
23
|
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
|
24
24
|
import { PluginError } from './plugin/plugin-error';
|
25
25
|
import { getScryptedVolume } from './plugin/plugin-volume';
|
26
|
-
|
27
|
-
const ONE_DAY_MILLISECONDS = 86400000;
|
26
|
+
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
|
28
27
|
|
29
28
|
if (!semver.gte(process.version, '16.0.0')) {
|
30
29
|
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
@@ -184,13 +183,9 @@ async function start() {
|
|
184
183
|
// only basic auth will fail with 401. it is up to the endpoints to manage
|
185
184
|
// lack of login from cookie auth.
|
186
185
|
|
187
|
-
const
|
188
|
-
if (
|
189
|
-
const
|
190
|
-
const username = userTokenParts[0];
|
191
|
-
const timestamp = parseInt(userTokenParts[1]);
|
192
|
-
if (timestamp + ONE_DAY_MILLISECONDS < Date.now())
|
193
|
-
return next();
|
186
|
+
const userToken = getSignedLoginUserToken(req);
|
187
|
+
if (userToken) {
|
188
|
+
const { username } = userToken;
|
194
189
|
|
195
190
|
// this database lookup on every web request is not necessary, the cookie
|
196
191
|
// itself is the auth, and is signed. furthermore, this is currently
|
@@ -206,23 +201,18 @@ async function start() {
|
|
206
201
|
res.locals.username = username;
|
207
202
|
}
|
208
203
|
else if (req.headers.authorization?.startsWith('Bearer ')) {
|
209
|
-
const
|
210
|
-
const
|
211
|
-
if (
|
212
|
-
const
|
213
|
-
|
214
|
-
const salted = login_user_token + authSalt;
|
204
|
+
const [checkHash, ...tokenParts] = req.headers.authorization.substring('Bearer '.length).split('#');
|
205
|
+
const tokenPart = tokenParts?.join('#');
|
206
|
+
if (checkHash && tokenPart) {
|
207
|
+
const salted = tokenPart + authSalt;
|
215
208
|
const hash = crypto.createHash('sha256');
|
216
209
|
hash.update(salted);
|
217
210
|
const sha = hash.digest().toString('hex');
|
218
211
|
|
219
|
-
if (
|
220
|
-
const
|
221
|
-
|
222
|
-
|
223
|
-
if (timestamp + ONE_DAY_MILLISECONDS < Date.now())
|
224
|
-
return next();
|
225
|
-
res.locals.username = username;
|
212
|
+
if (checkHash === sha) {
|
213
|
+
const userToken = validateToken(tokenPart);
|
214
|
+
if (userToken)
|
215
|
+
res.locals.username = userToken.username;
|
226
216
|
}
|
227
217
|
}
|
228
218
|
}
|
@@ -400,9 +390,19 @@ async function start() {
|
|
400
390
|
return reqSecure ? 'login_user_token' : 'login_user_token_insecure';
|
401
391
|
};
|
402
392
|
|
403
|
-
const
|
404
|
-
|
405
|
-
|
393
|
+
const validateToken = (token: string) => {
|
394
|
+
if (!token)
|
395
|
+
return;
|
396
|
+
try {
|
397
|
+
return UserToken.validateToken(token);
|
398
|
+
}
|
399
|
+
catch (e) {
|
400
|
+
console.warn('invalid token', e.message);
|
401
|
+
}
|
402
|
+
}
|
403
|
+
|
404
|
+
const getSignedLoginUserTokenRawValue = (req: Request<any>) => req.signedCookies[getLoginUserToken(req.secure)] as string;
|
405
|
+
const getSignedLoginUserToken = (req: Request<any>) => validateToken(getSignedLoginUserTokenRawValue(req));
|
406
406
|
|
407
407
|
app.get('/logout', (req, res) => {
|
408
408
|
res.clearCookie(getLoginUserToken(req.secure));
|
@@ -450,7 +450,8 @@ async function start() {
|
|
450
450
|
return;
|
451
451
|
}
|
452
452
|
|
453
|
-
const
|
453
|
+
const userToken = new UserToken(username, timestamp, maxAge);
|
454
|
+
const login_user_token = userToken.toString();
|
454
455
|
res.cookie(getLoginUserToken(req.secure), login_user_token, {
|
455
456
|
maxAge,
|
456
457
|
secure: req.secure,
|
@@ -492,7 +493,8 @@ async function start() {
|
|
492
493
|
await db.upsert(user);
|
493
494
|
hasLogin = true;
|
494
495
|
|
495
|
-
const
|
496
|
+
const userToken = new UserToken(username, timestamp);
|
497
|
+
const login_user_token = userToken.toString();
|
496
498
|
res.cookie(getLoginUserToken(req.secure), login_user_token, {
|
497
499
|
maxAge,
|
498
500
|
secure: req.secure,
|
@@ -536,32 +538,25 @@ async function start() {
|
|
536
538
|
return;
|
537
539
|
}
|
538
540
|
|
539
|
-
|
540
|
-
|
541
|
+
try {
|
542
|
+
const login_user_token = getSignedLoginUserTokenRawValue(req);
|
543
|
+
if (!login_user_token)
|
544
|
+
throw new Error('Not logged in.');
|
545
|
+
const userToken = UserToken.validateToken(login_user_token);
|
546
|
+
|
541
547
|
res.send({
|
542
|
-
|
543
|
-
|
548
|
+
authorization: createAuthorizationToken(login_user_token),
|
549
|
+
expiration: (userToken.timestamp + userToken.duration) - Date.now(),
|
550
|
+
username: userToken.username,
|
551
|
+
addresses,
|
544
552
|
})
|
545
|
-
return;
|
546
553
|
}
|
547
|
-
|
548
|
-
const userTokenParts = login_user_token.split('#');
|
549
|
-
const username = userTokenParts[0];
|
550
|
-
const timestamp = parseInt(userTokenParts[1]);
|
551
|
-
if (timestamp + ONE_DAY_MILLISECONDS < Date.now()) {
|
554
|
+
catch (e) {
|
552
555
|
res.send({
|
553
|
-
error: '
|
556
|
+
error: e?.message || 'Unknown Error.',
|
554
557
|
hasLogin,
|
555
558
|
})
|
556
|
-
return;
|
557
559
|
}
|
558
|
-
|
559
|
-
res.send({
|
560
|
-
authorization: createAuthorizationToken(login_user_token),
|
561
|
-
expiration: ONE_DAY_MILLISECONDS - (Date.now() - timestamp),
|
562
|
-
username,
|
563
|
-
addresses,
|
564
|
-
})
|
565
560
|
});
|
566
561
|
|
567
562
|
app.get('/', (_req, res) => res.redirect('/endpoint/@scrypted/core/public/'));
|
package/src/state.ts
CHANGED
@@ -206,6 +206,9 @@ function isSameValue(value1: any, value2: any) {
|
|
206
206
|
}
|
207
207
|
|
208
208
|
export function setState(pluginDevice: PluginDevice, property: string, value: any): boolean {
|
209
|
+
// device may have been deleted.
|
210
|
+
if (!pluginDevice.state)
|
211
|
+
return;
|
209
212
|
if (!pluginDevice.state[property])
|
210
213
|
pluginDevice.state[property] = {};
|
211
214
|
const state = pluginDevice.state[property];
|
package/src/usertoken.ts
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
export const ONE_DAY_MILLISECONDS = 86400000;
|
2
|
+
export const ONE_YEAR_MILLISECONDS = ONE_DAY_MILLISECONDS * 365;
|
3
|
+
|
4
|
+
export class UserToken {
|
5
|
+
constructor(public username: string, public timestamp = Date.now(), public duration = ONE_DAY_MILLISECONDS) {
|
6
|
+
}
|
7
|
+
|
8
|
+
static validateToken(token: string): UserToken {
|
9
|
+
let json: any;
|
10
|
+
try {
|
11
|
+
json = JSON.parse(token);
|
12
|
+
}
|
13
|
+
catch (e) {
|
14
|
+
throw new Error('Token malformed, unparseable.');
|
15
|
+
}
|
16
|
+
let { u, t, d } = json;
|
17
|
+
u = u?.toString();
|
18
|
+
t = parseInt(t);
|
19
|
+
d = parseInt(d);
|
20
|
+
if (!u || !t || !d)
|
21
|
+
throw new Error('Token malformed, missing properties.');
|
22
|
+
if (d > ONE_YEAR_MILLISECONDS)
|
23
|
+
throw new Error('Token duration too long.')
|
24
|
+
if (t > Date.now())
|
25
|
+
throw new Error('Token from the future.');
|
26
|
+
if (t + d < Date.now())
|
27
|
+
throw new Error('Token expired.');
|
28
|
+
return new UserToken(u, t, d);
|
29
|
+
}
|
30
|
+
|
31
|
+
toString(): string {
|
32
|
+
return JSON.stringify({
|
33
|
+
u: this.username,
|
34
|
+
t: this.timestamp,
|
35
|
+
d: this.duration,
|
36
|
+
})
|
37
|
+
}
|
38
|
+
}
|