@scrypted/server 0.0.183 → 0.1.3
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/io.js +3 -0
- package/dist/io.js.map +1 -0
- package/dist/plugin/buffer-serializer.js +18 -1
- package/dist/plugin/buffer-serializer.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +2 -0
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +83 -14
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-http.js +7 -5
- package/dist/plugin/plugin-http.js.map +1 -1
- package/dist/plugin/plugin-remote-websocket.js +0 -2
- package/dist/plugin/plugin-remote-websocket.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +12 -10
- package/dist/plugin/plugin-remote-worker.js.map +1 -1
- package/dist/plugin/plugin-remote.js +4 -2
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/rpc.js +18 -27
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +96 -3
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-server-main.js +25 -24
- package/dist/scrypted-server-main.js.map +1 -1
- package/dist/server-settings.js +74 -15
- package/dist/server-settings.js.map +1 -1
- package/dist/services/cors.js +17 -0
- package/dist/services/cors.js.map +1 -0
- package/package.json +6 -4
- package/python/plugin-remote.py +22 -17
- package/python/rpc.py +0 -6
- package/src/io.ts +25 -0
- package/src/plugin/buffer-serializer.ts +19 -0
- package/src/plugin/plugin-host-api.ts +2 -0
- package/src/plugin/plugin-host.ts +70 -17
- package/src/plugin/plugin-http.ts +11 -8
- package/src/plugin/plugin-remote-websocket.ts +0 -2
- package/src/plugin/plugin-remote-worker.ts +12 -10
- package/src/plugin/plugin-remote.ts +4 -2
- package/src/rpc.ts +22 -36
- package/src/runtime.ts +87 -4
- package/src/scrypted-server-main.ts +29 -30
- package/src/server-settings.ts +62 -16
- package/src/services/cors.ts +19 -0
- package/dist/addresses.js +0 -12
- package/dist/addresses.js.map +0 -1
- package/src/addresses.ts +0 -5
package/src/rpc.ts
CHANGED
@@ -43,10 +43,6 @@ interface RpcResult extends RpcMessage {
|
|
43
43
|
result?: any;
|
44
44
|
}
|
45
45
|
|
46
|
-
interface RpcOob extends RpcMessage {
|
47
|
-
oob: any;
|
48
|
-
}
|
49
|
-
|
50
46
|
interface RpcRemoteProxyValue {
|
51
47
|
__remote_proxy_id: string | undefined;
|
52
48
|
__remote_proxy_finalizer_id: string | undefined;
|
@@ -127,8 +123,9 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
|
|
127
123
|
// undefined is not JSON serializable.
|
128
124
|
const method = target() || null;
|
129
125
|
const args: any[] = [];
|
126
|
+
const serializationContext: any = {};
|
130
127
|
for (const arg of (argArray || [])) {
|
131
|
-
args.push(this.peer.serialize(arg));
|
128
|
+
args.push(this.peer.serialize(arg, serializationContext));
|
132
129
|
}
|
133
130
|
|
134
131
|
const rpcApply: RpcApply = {
|
@@ -147,7 +144,7 @@ class RpcProxy implements PrimitiveProxyHandler<any> {
|
|
147
144
|
|
148
145
|
return this.peer.createPendingResult((id, reject) => {
|
149
146
|
rpcApply.id = id;
|
150
|
-
this.peer.send(rpcApply, reject);
|
147
|
+
this.peer.send(rpcApply, reject, serializationContext);
|
151
148
|
})
|
152
149
|
}
|
153
150
|
}
|
@@ -193,8 +190,8 @@ catch (e) {
|
|
193
190
|
}
|
194
191
|
|
195
192
|
export interface RpcSerializer {
|
196
|
-
serialize(value: any): any;
|
197
|
-
deserialize(serialized: any): any;
|
193
|
+
serialize(value: any, serializationContext?: any): any;
|
194
|
+
deserialize(serialized: any, serializationContext?: any): any;
|
198
195
|
}
|
199
196
|
|
200
197
|
interface LocalProxiedEntry {
|
@@ -204,7 +201,6 @@ interface LocalProxiedEntry {
|
|
204
201
|
|
205
202
|
export class RpcPeer {
|
206
203
|
idCounter = 1;
|
207
|
-
onOob?: (oob: any) => void;
|
208
204
|
params: { [name: string]: any } = {};
|
209
205
|
pendingResults: { [id: string]: Deferred } = {};
|
210
206
|
proxyCounter = 1;
|
@@ -213,7 +209,7 @@ export class RpcPeer {
|
|
213
209
|
remoteWeakProxies: { [id: string]: WeakRef<any> } = {};
|
214
210
|
finalizers = new FinalizationRegistry(entry => this.finalize(entry as LocalProxiedEntry));
|
215
211
|
nameDeserializerMap = new Map<string, RpcSerializer>();
|
216
|
-
constructorSerializerMap = new Map<
|
212
|
+
constructorSerializerMap = new Map<any, string>();
|
217
213
|
transportSafeArgumentTypes = RpcPeer.getDefaultTransportSafeArgumentTypes();
|
218
214
|
|
219
215
|
static readonly finalizerIdSymbol = Symbol('rpcFinalizerId');
|
@@ -251,7 +247,7 @@ export class RpcPeer {
|
|
251
247
|
static readonly PROPERTY_PROXY_PROPERTIES = '__proxy_props';
|
252
248
|
static readonly PROPERTY_JSON_COPY_SERIALIZE_CHILDREN = '__json_copy_serialize_children';
|
253
249
|
|
254
|
-
constructor(public selfName: string, public peerName: string, public send: (message: RpcMessage, reject?: (e: Error) => void) => void) {
|
250
|
+
constructor(public selfName: string, public peerName: string, public send: (message: RpcMessage, reject?: (e: Error) => void, serializationContext?: any) => void) {
|
255
251
|
}
|
256
252
|
|
257
253
|
createPendingResult(cb: (id: string, reject: (e: Error) => void) => void): Promise<any> {
|
@@ -310,13 +306,6 @@ export class RpcPeer {
|
|
310
306
|
});
|
311
307
|
}
|
312
308
|
|
313
|
-
sendOob(oob: any) {
|
314
|
-
this.send({
|
315
|
-
type: 'oob',
|
316
|
-
oob,
|
317
|
-
} as RpcOob)
|
318
|
-
}
|
319
|
-
|
320
309
|
evalLocal<T>(script: string, filename?: string, coercedParams?: { [name: string]: any }): T {
|
321
310
|
const params = Object.assign({}, this.params, coercedParams);
|
322
311
|
let compile: CompileFunction;
|
@@ -339,7 +328,7 @@ export class RpcPeer {
|
|
339
328
|
result.message = (e as Error).message || 'no message';
|
340
329
|
}
|
341
330
|
|
342
|
-
deserialize(value: any): any {
|
331
|
+
deserialize(value: any, deserializationContext: any): any {
|
343
332
|
if (!value)
|
344
333
|
return value;
|
345
334
|
|
@@ -347,7 +336,7 @@ export class RpcPeer {
|
|
347
336
|
if (copySerializeChildren) {
|
348
337
|
const ret: any = {};
|
349
338
|
for (const [key, val] of Object.entries(value)) {
|
350
|
-
ret[key] = this.deserialize(val);
|
339
|
+
ret[key] = this.deserialize(val, deserializationContext);
|
351
340
|
}
|
352
341
|
return ret;
|
353
342
|
}
|
@@ -370,17 +359,17 @@ export class RpcPeer {
|
|
370
359
|
|
371
360
|
const deserializer = this.nameDeserializerMap.get(__remote_constructor_name);
|
372
361
|
if (deserializer) {
|
373
|
-
return deserializer.deserialize(__serialized_value);
|
362
|
+
return deserializer.deserialize(__serialized_value, deserializationContext);
|
374
363
|
}
|
375
364
|
|
376
365
|
return value;
|
377
366
|
}
|
378
367
|
|
379
|
-
serialize(value: any): any {
|
368
|
+
serialize(value: any, serializationContext: any): any {
|
380
369
|
if (value?.[RpcPeer.PROPERTY_JSON_COPY_SERIALIZE_CHILDREN] === true) {
|
381
370
|
const ret: any = {};
|
382
371
|
for (const [key, val] of Object.entries(value)) {
|
383
|
-
ret[key] = this.serialize(val);
|
372
|
+
ret[key] = this.serialize(val, serializationContext);
|
384
373
|
}
|
385
374
|
return ret;
|
386
375
|
}
|
@@ -419,7 +408,7 @@ export class RpcPeer {
|
|
419
408
|
const serializer = this.nameDeserializerMap.get(serializerMapName);
|
420
409
|
if (!serializer)
|
421
410
|
throw new Error('serializer not found for ' + serializerMapName);
|
422
|
-
const serialized = serializer.serialize(value);
|
411
|
+
const serialized = serializer.serialize(value, serializationContext);
|
423
412
|
const ret: RpcRemoteProxyValue = {
|
424
413
|
__remote_proxy_id: undefined,
|
425
414
|
__remote_proxy_finalizer_id: undefined,
|
@@ -464,17 +453,18 @@ export class RpcPeer {
|
|
464
453
|
return proxy;
|
465
454
|
}
|
466
455
|
|
467
|
-
async handleMessage(message: RpcMessage) {
|
456
|
+
async handleMessage(message: RpcMessage, deserializationContext?: any) {
|
468
457
|
try {
|
469
458
|
switch (message.type) {
|
470
459
|
case 'param': {
|
471
460
|
const rpcParam = message as RpcParam;
|
461
|
+
const serializationContext: any = {};
|
472
462
|
const result: RpcResult = {
|
473
463
|
type: 'result',
|
474
464
|
id: rpcParam.id,
|
475
|
-
result: this.serialize(this.params[rpcParam.param])
|
465
|
+
result: this.serialize(this.params[rpcParam.param], serializationContext)
|
476
466
|
};
|
477
|
-
this.send(result);
|
467
|
+
this.send(result, undefined, serializationContext);
|
478
468
|
break;
|
479
469
|
}
|
480
470
|
case 'apply': {
|
@@ -483,6 +473,7 @@ export class RpcPeer {
|
|
483
473
|
type: 'result',
|
484
474
|
id: rpcApply.id || '',
|
485
475
|
};
|
476
|
+
const serializationContext: any = {};
|
486
477
|
|
487
478
|
try {
|
488
479
|
const target = this.localProxyMap[rpcApply.proxyId];
|
@@ -491,7 +482,7 @@ export class RpcPeer {
|
|
491
482
|
|
492
483
|
const args = [];
|
493
484
|
for (const arg of (rpcApply.args || [])) {
|
494
|
-
args.push(this.deserialize(arg));
|
485
|
+
args.push(this.deserialize(arg, deserializationContext));
|
495
486
|
}
|
496
487
|
|
497
488
|
let value: any;
|
@@ -505,7 +496,7 @@ export class RpcPeer {
|
|
505
496
|
value = await target(...args);
|
506
497
|
}
|
507
498
|
|
508
|
-
result.result = this.serialize(value);
|
499
|
+
result.result = this.serialize(value, serializationContext);
|
509
500
|
}
|
510
501
|
catch (e) {
|
511
502
|
// console.error('failure', rpcApply.method, e);
|
@@ -513,7 +504,7 @@ export class RpcPeer {
|
|
513
504
|
}
|
514
505
|
|
515
506
|
if (!rpcApply.oneway)
|
516
|
-
this.send(result);
|
507
|
+
this.send(result, undefined, serializationContext);
|
517
508
|
break;
|
518
509
|
}
|
519
510
|
case 'result': {
|
@@ -530,7 +521,7 @@ export class RpcPeer {
|
|
530
521
|
deferred.reject(e);
|
531
522
|
return;
|
532
523
|
}
|
533
|
-
deferred.resolve(this.deserialize(rpcResult.result));
|
524
|
+
deferred.resolve(this.deserialize(rpcResult.result, deserializationContext));
|
534
525
|
break;
|
535
526
|
}
|
536
527
|
case 'finalize': {
|
@@ -547,11 +538,6 @@ export class RpcPeer {
|
|
547
538
|
}
|
548
539
|
break;
|
549
540
|
}
|
550
|
-
case 'oob': {
|
551
|
-
const rpcOob = message as RpcOob;
|
552
|
-
this.onOob?.(rpcOob.oob);
|
553
|
-
break;
|
554
|
-
}
|
555
541
|
default:
|
556
542
|
throw new Error(`unknown rpc message type ${message.type}`);
|
557
543
|
}
|
package/src/runtime.ts
CHANGED
@@ -6,7 +6,7 @@ import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
|
|
6
6
|
import { getState, ScryptedStateManager, setState } from './state';
|
7
7
|
import { Request, Response } from 'express';
|
8
8
|
import { createResponseInterface } from './http-interfaces';
|
9
|
-
import http, { ServerResponse } from 'http';
|
9
|
+
import http, { ServerResponse, IncomingHttpHeaders } from 'http';
|
10
10
|
import https from 'https';
|
11
11
|
import express from 'express';
|
12
12
|
import { LogEntry, Logger, makeAlertId } from './logger';
|
@@ -25,20 +25,22 @@ import semver from 'semver';
|
|
25
25
|
import { ServiceControl } from './services/service-control';
|
26
26
|
import { Alerts } from './services/alerts';
|
27
27
|
import { Info } from './services/info';
|
28
|
-
import io from 'engine.io';
|
28
|
+
import * as io from 'engine.io';
|
29
29
|
import { spawn as ptySpawn } from 'node-pty';
|
30
30
|
import rimraf from 'rimraf';
|
31
31
|
import { getPluginVolume } from './plugin/plugin-volume';
|
32
32
|
import { PluginHttp } from './plugin/plugin-http';
|
33
33
|
import AdmZip from 'adm-zip';
|
34
34
|
import path from 'path';
|
35
|
+
import { CORSControl, CORSServer } from './services/cors';
|
36
|
+
import { IOServer, IOServerSocket } from './io';
|
35
37
|
|
36
38
|
interface DeviceProxyPair {
|
37
39
|
handler: PluginDeviceProxyHandler;
|
38
40
|
proxy: ScryptedDevice;
|
39
41
|
}
|
40
42
|
|
41
|
-
const MIN_SCRYPTED_CORE_VERSION = 'v0.
|
43
|
+
const MIN_SCRYPTED_CORE_VERSION = 'v0.1.5';
|
42
44
|
const PLUGIN_DEVICE_STATE_VERSION = 2;
|
43
45
|
|
44
46
|
interface HttpPluginData {
|
@@ -56,9 +58,18 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
56
58
|
devicesLogger = this.logger.getLogger('device', 'Devices');
|
57
59
|
wss = new WebSocketServer({ noServer: true });
|
58
60
|
wsAtomic = 0;
|
59
|
-
shellio = io(
|
61
|
+
shellio: IOServer = new io.Server({
|
60
62
|
pingTimeout: 120000,
|
63
|
+
perMessageDeflate: true,
|
64
|
+
cors: (req, callback) => {
|
65
|
+
const header = this.getAccessControlAllowOrigin(req.headers);
|
66
|
+
callback(undefined, {
|
67
|
+
origin: header,
|
68
|
+
credentials: true,
|
69
|
+
})
|
70
|
+
},
|
61
71
|
});
|
72
|
+
cors: CORSServer[] = [];
|
62
73
|
|
63
74
|
constructor(datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
|
64
75
|
super(app);
|
@@ -67,6 +78,48 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
67
78
|
|
68
79
|
app.disable('x-powered-by');
|
69
80
|
|
81
|
+
this.app.options(['/endpoint/@:owner/:pkg/engine.io/api/activate', '/endpoint/@:owner/:pkg/engine.io/api/activate'], (req, res) => {
|
82
|
+
this.addAccessControlHeaders(req, res);
|
83
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
84
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
|
85
|
+
res.send(200);
|
86
|
+
});
|
87
|
+
|
88
|
+
this.app.post(['/endpoint/@:owner/:pkg/engine.io/api/activate', '/endpoint/@:owner/:pkg/engine.io/api/activate'], (req, res) => {
|
89
|
+
const { username } = (req as any);
|
90
|
+
if (!username) {
|
91
|
+
res.status(401);
|
92
|
+
res.send('Not Authorized');
|
93
|
+
return;
|
94
|
+
}
|
95
|
+
|
96
|
+
const { owner, pkg } = req.params;
|
97
|
+
let endpoint = pkg;
|
98
|
+
if (owner)
|
99
|
+
endpoint = `@${owner}/${endpoint}`;
|
100
|
+
|
101
|
+
const { id } = req.body;
|
102
|
+
try {
|
103
|
+
const host = this.plugins?.[endpoint];
|
104
|
+
if (!host)
|
105
|
+
throw new Error('invalid plugin');
|
106
|
+
// @ts-expect-error
|
107
|
+
const socket: IOServerSocket = host.io.clients[id];
|
108
|
+
if (!socket)
|
109
|
+
throw new Error('invalid socket');
|
110
|
+
socket.emit('/api/activate');
|
111
|
+
res.send({
|
112
|
+
id,
|
113
|
+
})
|
114
|
+
}
|
115
|
+
catch (e) {
|
116
|
+
res.status(500);
|
117
|
+
res.end();
|
118
|
+
}
|
119
|
+
});
|
120
|
+
|
121
|
+
this.addMiddleware();
|
122
|
+
|
70
123
|
app.get('/web/oauth/callback', (req, res) => {
|
71
124
|
this.oauthCallback(req, res);
|
72
125
|
});
|
@@ -122,6 +175,34 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
122
175
|
}, 60 * 60 * 1000);
|
123
176
|
}
|
124
177
|
|
178
|
+
addAccessControlHeaders(req: http.IncomingMessage, res: http.ServerResponse) {
|
179
|
+
res.setHeader('Vary', 'Origin,Referer');
|
180
|
+
const header = this.getAccessControlAllowOrigin(req.headers);
|
181
|
+
if (header)
|
182
|
+
res.setHeader('Access-Control-Allow-Origin', header);
|
183
|
+
}
|
184
|
+
|
185
|
+
getAccessControlAllowOrigin(headers: http.IncomingHttpHeaders) {
|
186
|
+
let { origin, referer } = headers;
|
187
|
+
if (!origin && referer) {
|
188
|
+
try {
|
189
|
+
const u = new URL(headers.referer)
|
190
|
+
origin = u.origin;
|
191
|
+
}
|
192
|
+
catch (e) {
|
193
|
+
return;
|
194
|
+
}
|
195
|
+
}
|
196
|
+
if (!origin)
|
197
|
+
return;
|
198
|
+
const servers: string[] = process.env.SCRYPTED_ACCESS_CONTROL_ALLOW_ORIGINS?.split(',') || [];
|
199
|
+
servers.push(...Object.values(this.cors).map(entry => entry.server));
|
200
|
+
if (!servers.includes(origin))
|
201
|
+
return;
|
202
|
+
|
203
|
+
return origin;
|
204
|
+
}
|
205
|
+
|
125
206
|
getDeviceLogger(device: PluginDevice): Logger {
|
126
207
|
return this.devicesLogger.getLogger(device._id, getState(device, ScryptedInterfaceProperty.name));
|
127
208
|
}
|
@@ -311,6 +392,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
|
|
311
392
|
return this.logger;
|
312
393
|
case 'alerts':
|
313
394
|
return new Alerts(this);
|
395
|
+
case 'cors':
|
396
|
+
return new CORSControl(this);
|
314
397
|
}
|
315
398
|
}
|
316
399
|
|
@@ -8,7 +8,7 @@ import net from 'net';
|
|
8
8
|
import { ScryptedRuntime } from './runtime';
|
9
9
|
import level from './level';
|
10
10
|
import { Plugin, ScryptedUser, Settings } from './db-types';
|
11
|
-
import { SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
|
11
|
+
import { getHostAddresses, SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
|
12
12
|
import crypto from 'crypto';
|
13
13
|
import cookieParser from 'cookie-parser';
|
14
14
|
import axios from 'axios';
|
@@ -20,7 +20,6 @@ import { install as installSourceMapSupport } from 'source-map-support';
|
|
20
20
|
import httpAuth from 'http-auth';
|
21
21
|
import semver from 'semver';
|
22
22
|
import { Info } from './services/info';
|
23
|
-
import { getAddresses } from './addresses';
|
24
23
|
import { sleep } from './sleep';
|
25
24
|
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
|
26
25
|
import { PluginError } from './plugin/plugin-error';
|
@@ -55,7 +54,7 @@ let workerInspectPort: number = undefined;
|
|
55
54
|
|
56
55
|
async function doconnect(): Promise<net.Socket> {
|
57
56
|
return new Promise((resolve, reject) => {
|
58
|
-
const target = net.connect(workerInspectPort);
|
57
|
+
const target = net.connect(workerInspectPort, '127.0.0.1');
|
59
58
|
target.once('error', reject)
|
60
59
|
target.once('connect', () => resolve(target))
|
61
60
|
})
|
@@ -123,7 +122,6 @@ async function start() {
|
|
123
122
|
certSetting = await db.upsert(certSetting);
|
124
123
|
}
|
125
124
|
|
126
|
-
|
127
125
|
const basicAuth = httpAuth.basic({
|
128
126
|
realm: 'Scrypted',
|
129
127
|
}, async (username, password, callback) => {
|
@@ -184,6 +182,7 @@ async function start() {
|
|
184
182
|
// }
|
185
183
|
|
186
184
|
res.locals.username = username;
|
185
|
+
(req as any).username = username;
|
187
186
|
}
|
188
187
|
next();
|
189
188
|
});
|
@@ -193,6 +192,7 @@ async function start() {
|
|
193
192
|
if (req.protocol === 'https' && req.headers.authorization && req.headers.authorization.toLowerCase()?.indexOf('basic') !== -1) {
|
194
193
|
const basicChecker = basicAuth.check((req) => {
|
195
194
|
res.locals.username = req.user;
|
195
|
+
(req as any).username = req.user;
|
196
196
|
next();
|
197
197
|
});
|
198
198
|
|
@@ -226,7 +226,7 @@ async function start() {
|
|
226
226
|
|
227
227
|
console.log('#######################################################');
|
228
228
|
console.log(`Scrypted Server (Local) : https://localhost:${SCRYPTED_SECURE_PORT}/`);
|
229
|
-
for (const address of
|
229
|
+
for (const address of getHostAddresses(true, true)) {
|
230
230
|
console.log(`Scrypted Server (Remote) : https://${address}:${SCRYPTED_SECURE_PORT}/`);
|
231
231
|
}
|
232
232
|
console.log(`Version: : ${await new Info().getVersion()}`);
|
@@ -360,10 +360,20 @@ async function start() {
|
|
360
360
|
|
361
361
|
let hasLogin = await db.getCount(ScryptedUser) > 0;
|
362
362
|
|
363
|
+
app.options('/login', (req, res) => {
|
364
|
+
scrypted.addAccessControlHeaders(req, res);
|
365
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
366
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
|
367
|
+
res.send(200);
|
368
|
+
});
|
369
|
+
|
363
370
|
app.post('/login', async (req, res) => {
|
371
|
+
scrypted.addAccessControlHeaders(req, res);
|
372
|
+
|
364
373
|
const { username, password, change_password } = req.body;
|
365
374
|
const timestamp = Date.now();
|
366
375
|
const maxAge = 86400000;
|
376
|
+
const addresses = getHostAddresses(true, true).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`);
|
367
377
|
|
368
378
|
if (hasLogin) {
|
369
379
|
const user = await db.tryGet(ScryptedUser, username);
|
@@ -390,9 +400,11 @@ async function start() {
|
|
390
400
|
const login_user_token = `${username}#${timestamp}`;
|
391
401
|
res.cookie('login_user_token', login_user_token, {
|
392
402
|
maxAge,
|
393
|
-
|
403
|
+
// enabling this will disable insecure http login...
|
404
|
+
// secure: true,
|
394
405
|
signed: true,
|
395
406
|
httpOnly: true,
|
407
|
+
sameSite: 'none',
|
396
408
|
});
|
397
409
|
|
398
410
|
if (change_password) {
|
@@ -405,6 +417,7 @@ async function start() {
|
|
405
417
|
res.send({
|
406
418
|
username,
|
407
419
|
expiration: maxAge,
|
420
|
+
addresses,
|
408
421
|
});
|
409
422
|
|
410
423
|
return;
|
@@ -423,25 +436,32 @@ async function start() {
|
|
423
436
|
user.salt = crypto.randomBytes(64).toString('base64');
|
424
437
|
user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
|
425
438
|
user.passwordDate = timestamp;
|
439
|
+
user.token = crypto.randomBytes(16).toString('hex');
|
426
440
|
await db.upsert(user);
|
427
441
|
hasLogin = true;
|
428
442
|
|
429
443
|
const login_user_token = `${username}#${timestamp}`
|
430
444
|
res.cookie('login_user_token', login_user_token, {
|
431
445
|
maxAge,
|
432
|
-
|
446
|
+
// enabling this will disable insecure http login...
|
447
|
+
// secure: true,
|
433
448
|
signed: true,
|
434
449
|
httpOnly: true,
|
450
|
+
sameSite: 'none',
|
435
451
|
});
|
436
452
|
|
437
453
|
res.send({
|
438
454
|
username,
|
455
|
+
token: user.token,
|
439
456
|
expiration: maxAge,
|
457
|
+
addresses,
|
440
458
|
});
|
441
459
|
});
|
442
460
|
|
443
|
-
|
444
461
|
app.get('/login', async (req, res) => {
|
462
|
+
scrypted.addAccessControlHeaders(req, res);
|
463
|
+
|
464
|
+
const addresses = getHostAddresses(true, true).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`);
|
445
465
|
if (req.protocol === 'https' && req.headers.authorization) {
|
446
466
|
const username = await new Promise(resolve => {
|
447
467
|
const basicChecker = basicAuth.check((req) => {
|
@@ -484,31 +504,10 @@ async function start() {
|
|
484
504
|
return;
|
485
505
|
}
|
486
506
|
|
487
|
-
// this database lookup on every web request is not necessary, the cookie
|
488
|
-
// itself is the auth, and is signed. furthermore, this is currently
|
489
|
-
// a single user setup anywyas. revisit this at some point when
|
490
|
-
// multiple users are implemented.
|
491
|
-
|
492
|
-
// const user = await db.tryGet(ScryptedUser, username);
|
493
|
-
// if (!user) {
|
494
|
-
// res.send({
|
495
|
-
// error: 'User not found.',
|
496
|
-
// hasLogin,
|
497
|
-
// })
|
498
|
-
// return;
|
499
|
-
// }
|
500
|
-
|
501
|
-
// if (timestamp < user.passwordDate) {
|
502
|
-
// res.send({
|
503
|
-
// error: 'Login invalid. Password has changed.',
|
504
|
-
// hasLogin,
|
505
|
-
// })
|
506
|
-
// return;
|
507
|
-
// }
|
508
|
-
|
509
507
|
res.send({
|
510
508
|
expiration: 86400000 - (Date.now() - timestamp),
|
511
509
|
username,
|
510
|
+
addresses,
|
512
511
|
})
|
513
512
|
});
|
514
513
|
|
package/src/server-settings.ts
CHANGED
@@ -1,24 +1,70 @@
|
|
1
1
|
import os from 'os';
|
2
|
+
import * as nodeIp from 'ip';
|
2
3
|
|
3
4
|
export const SCRYPTED_INSECURE_PORT = parseInt(process.env.SCRYPTED_INSECURE_PORT) || 11080;
|
4
5
|
export const SCRYPTED_SECURE_PORT = parseInt(process.env.SCRYPTED_SECURE_PORT) || 10443;
|
5
6
|
export const SCRYPTED_DEBUG_PORT = parseInt(process.env.SCRYPTED_DEBUG_PORT) || 10081;
|
6
7
|
|
7
8
|
export function getIpAddress(): string {
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
9
|
+
return nodeIp.address();
|
10
|
+
}
|
11
|
+
|
12
|
+
function normalizeFamilyNodeV18(family: string|number) {
|
13
|
+
if (family === 'IPv4')
|
14
|
+
return 4;
|
15
|
+
if (family === 'IPv6')
|
16
|
+
return 6;
|
17
|
+
return family;
|
18
|
+
}
|
19
|
+
|
20
|
+
function nodeIpAddress(family: number): string[] {
|
21
|
+
// https://chromium.googlesource.com/external/webrtc/+/master/rtc_base/network.cc#236
|
22
|
+
const costlyNetworks = ["ipsec", "tun", "utun", "tap"];
|
23
|
+
|
24
|
+
const ignoreNetworks = [
|
25
|
+
// seen these on macos
|
26
|
+
'llw',
|
27
|
+
'awdl',
|
28
|
+
|
29
|
+
...costlyNetworks,
|
30
|
+
];
|
31
|
+
|
32
|
+
const interfaces = os.networkInterfaces();
|
33
|
+
|
34
|
+
const all = Object.keys(interfaces)
|
35
|
+
.map((nic) => {
|
36
|
+
for (const costly of ignoreNetworks) {
|
37
|
+
if (nic.startsWith(costly)) {
|
38
|
+
return {
|
39
|
+
nic,
|
40
|
+
addresses: [],
|
41
|
+
};
|
42
|
+
}
|
43
|
+
}
|
44
|
+
const addresses = interfaces[nic]!.filter(
|
45
|
+
(details) =>
|
46
|
+
normalizeFamilyNodeV18(details.family) === family
|
47
|
+
&& !nodeIp.isLoopback(details.address)
|
48
|
+
);
|
49
|
+
return {
|
50
|
+
nic,
|
51
|
+
addresses: addresses.map((address) => address.address),
|
52
|
+
};
|
53
|
+
})
|
54
|
+
.filter((address) => !!address);
|
55
|
+
|
56
|
+
// os.networkInterfaces doesn't actually return addresses in a good order.
|
57
|
+
// have seen instances where en0 (ethernet) is after en1 (wlan), etc.
|
58
|
+
// eth0 > eth1
|
59
|
+
all.sort((a, b) => a.nic.localeCompare(b.nic));
|
60
|
+
return Object.values(all)
|
61
|
+
.map((entry) => entry.addresses)
|
62
|
+
.flat();
|
63
|
+
}
|
64
|
+
|
65
|
+
export function getHostAddresses(useIpv4: boolean, useIpv6: boolean) {
|
66
|
+
const address: string[] = [];
|
67
|
+
if (useIpv4) address.push(...nodeIpAddress(4));
|
68
|
+
if (useIpv6) address.push(...nodeIpAddress(6));
|
69
|
+
return address;
|
24
70
|
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { ScryptedRuntime } from "../runtime";
|
2
|
+
|
3
|
+
export interface CORSServer {
|
4
|
+
tag: string;
|
5
|
+
server: string;
|
6
|
+
}
|
7
|
+
|
8
|
+
export class CORSControl {
|
9
|
+
constructor(public runtime: ScryptedRuntime) {
|
10
|
+
}
|
11
|
+
|
12
|
+
async getCORS(): Promise<CORSServer[]> {
|
13
|
+
return this.runtime.cors;
|
14
|
+
}
|
15
|
+
|
16
|
+
async setCORS(servers: CORSServer[]) {
|
17
|
+
this.runtime.cors = servers;
|
18
|
+
}
|
19
|
+
}
|
package/dist/addresses.js
DELETED
@@ -1,12 +0,0 @@
|
|
1
|
-
"use strict";
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
4
|
-
};
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
6
|
-
exports.getAddresses = void 0;
|
7
|
-
const os_1 = __importDefault(require("os"));
|
8
|
-
function getAddresses() {
|
9
|
-
return Object.entries(os_1.default.networkInterfaces()).filter(([iface]) => iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')).map(([_, addr]) => addr).flat().map(info => info.address).filter(address => address);
|
10
|
-
}
|
11
|
-
exports.getAddresses = getAddresses;
|
12
|
-
//# sourceMappingURL=addresses.js.map
|
package/dist/addresses.js.map
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
{"version":3,"file":"addresses.js","sourceRoot":"","sources":["../src/addresses.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AAEpB,SAAgB,YAAY;IACxB,OAAO,MAAM,CAAC,OAAO,CAAC,YAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AAC1O,CAAC;AAFD,oCAEC"}
|
package/src/addresses.ts
DELETED
@@ -1,5 +0,0 @@
|
|
1
|
-
import os from 'os';
|
2
|
-
|
3
|
-
export function getAddresses() {
|
4
|
-
return Object.entries(os.networkInterfaces()).filter(([iface]) => iface.startsWith('en') || iface.startsWith('eth') || iface.startsWith('wlan')).map(([_, addr]) => addr).flat().map(info => info.address).filter(address => address);
|
5
|
-
}
|