@scrypted/server 0.0.113 → 0.0.118
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/.vscode/launch.json +1 -0
- package/dist/http-interfaces.js +11 -4
- package/dist/http-interfaces.js.map +1 -1
- package/dist/level.js.map +1 -1
- package/dist/plugin/media.js +3 -3
- package/dist/plugin/media.js.map +1 -1
- package/dist/plugin/plugin-console.js.map +1 -1
- package/dist/plugin/plugin-device.js +25 -0
- package/dist/plugin/plugin-device.js.map +1 -1
- package/dist/plugin/plugin-host-api.js +2 -2
- package/dist/plugin/plugin-host-api.js.map +1 -1
- package/dist/plugin/plugin-host.js +111 -227
- package/dist/plugin/plugin-host.js.map +1 -1
- package/dist/plugin/plugin-lazy-remote.js.map +1 -1
- package/dist/plugin/plugin-npm-dependencies.js +8 -3
- package/dist/plugin/plugin-npm-dependencies.js.map +1 -1
- package/dist/plugin/plugin-remote-worker.js +253 -0
- package/dist/plugin/plugin-remote-worker.js.map +1 -0
- package/dist/plugin/plugin-remote.js +39 -15
- package/dist/plugin/plugin-remote.js.map +1 -1
- package/dist/plugin/plugin-repl.js +3 -1
- package/dist/plugin/plugin-repl.js.map +1 -1
- package/dist/plugin/system.js +11 -6
- package/dist/plugin/system.js.map +1 -1
- package/dist/rpc.js +11 -1
- package/dist/rpc.js.map +1 -1
- package/dist/runtime.js +19 -4
- package/dist/runtime.js.map +1 -1
- package/dist/scrypted-main.js +3 -437
- package/dist/scrypted-main.js.map +1 -1
- package/dist/scrypted-plugin-main.js +8 -0
- package/dist/scrypted-plugin-main.js.map +1 -0
- package/dist/scrypted-server-main.js +448 -0
- package/dist/scrypted-server-main.js.map +1 -0
- package/package.json +5 -4
- package/python/media.py +1 -12
- package/python/plugin-remote.py +15 -7
- package/python/rpc.py +1 -1
- package/src/http-interfaces.ts +12 -5
- package/src/level.ts +0 -2
- package/src/plugin/media.ts +3 -3
- package/src/plugin/plugin-api.ts +9 -1
- package/src/plugin/plugin-console.ts +0 -1
- package/src/plugin/plugin-device.ts +26 -2
- package/src/plugin/plugin-host-api.ts +2 -2
- package/src/plugin/plugin-host.ts +122 -253
- package/src/plugin/plugin-http.ts +2 -2
- package/src/plugin/plugin-lazy-remote.ts +1 -1
- package/src/plugin/plugin-npm-dependencies.ts +7 -2
- package/src/plugin/plugin-remote-worker.ts +272 -0
- package/src/plugin/plugin-remote.ts +46 -17
- package/src/plugin/plugin-repl.ts +4 -2
- package/src/plugin/system.ts +15 -13
- package/src/rpc.ts +18 -3
- package/src/runtime.ts +19 -4
- package/src/scrypted-main.ts +3 -508
- package/src/scrypted-plugin-main.ts +6 -0
- package/src/scrypted-server-main.ts +516 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import process from 'process';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import bodyParser from 'body-parser';
|
|
7
|
+
import net from 'net';
|
|
8
|
+
import { ScryptedRuntime } from './runtime';
|
|
9
|
+
import level from './level';
|
|
10
|
+
import { Plugin, ScryptedUser, Settings } from './db-types';
|
|
11
|
+
import { SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
|
|
12
|
+
import crypto from 'crypto';
|
|
13
|
+
import cookieParser from 'cookie-parser';
|
|
14
|
+
import axios from 'axios';
|
|
15
|
+
import qs from 'query-string';
|
|
16
|
+
import { RPCResultError } from './rpc';
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import mkdirp from 'mkdirp';
|
|
19
|
+
import { install as installSourceMapSupport } from 'source-map-support';
|
|
20
|
+
import httpAuth from 'http-auth';
|
|
21
|
+
import semver from 'semver';
|
|
22
|
+
import { Info } from './services/info';
|
|
23
|
+
import { getAddresses } from './addresses';
|
|
24
|
+
import { sleep } from './sleep';
|
|
25
|
+
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
|
|
26
|
+
import { PluginError } from './plugin/plugin-error';
|
|
27
|
+
import { getScryptedVolume } from './plugin/plugin-volume';
|
|
28
|
+
|
|
29
|
+
if (!semver.gte(process.version, '16.0.0')) {
|
|
30
|
+
throw new Error('"node" version out of date. Please update node to v16 or higher.')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.on('unhandledRejection', error => {
|
|
34
|
+
if (error?.constructor !== RPCResultError && error?.constructor !== PluginError) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
console.warn('unhandled rejection of RPC Result', error);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function listenServerPort(env: string, port: number, server: any) {
|
|
41
|
+
server.listen(port,);
|
|
42
|
+
server.on('error', (e: Error) => {
|
|
43
|
+
console.error(`Failed to listen on port ${port}. It may be in use.`);
|
|
44
|
+
console.error(`Use the environment variable ${env} to change the port.`);
|
|
45
|
+
throw e;
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
installSourceMapSupport({
|
|
50
|
+
environment: 'node',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
let workerInspectPort: number = undefined;
|
|
54
|
+
|
|
55
|
+
async function doconnect(): Promise<net.Socket> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const target = net.connect(workerInspectPort);
|
|
58
|
+
target.once('error', reject)
|
|
59
|
+
target.once('connect', () => resolve(target))
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const debugServer = net.createServer(async (socket) => {
|
|
64
|
+
if (!workerInspectPort) {
|
|
65
|
+
socket.destroy();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < 10; i++) {
|
|
70
|
+
try {
|
|
71
|
+
const target = await doconnect();
|
|
72
|
+
socket.pipe(target).pipe(socket);
|
|
73
|
+
const destroy = () => {
|
|
74
|
+
socket.destroy();
|
|
75
|
+
target.destroy();
|
|
76
|
+
}
|
|
77
|
+
socket.on('error', destroy);
|
|
78
|
+
target.on('error', destroy);
|
|
79
|
+
socket.on('close', destroy);
|
|
80
|
+
target.on('close', destroy);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
await sleep(500);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
console.warn('debugger connect timed out');
|
|
88
|
+
socket.destroy();
|
|
89
|
+
})
|
|
90
|
+
listenServerPort('SCRYPTED_DEBUG_PORT', SCRYPTED_DEBUG_PORT, debugServer);
|
|
91
|
+
|
|
92
|
+
const app = express();
|
|
93
|
+
|
|
94
|
+
// parse application/x-www-form-urlencoded
|
|
95
|
+
app.use(bodyParser.urlencoded({ extended: false }) as any)
|
|
96
|
+
|
|
97
|
+
// parse application/json
|
|
98
|
+
app.use(bodyParser.json())
|
|
99
|
+
|
|
100
|
+
// parse some custom thing into a Buffer
|
|
101
|
+
app.use(bodyParser.raw({ type: 'application/zip', limit: 100000000 }) as any)
|
|
102
|
+
|
|
103
|
+
async function start() {
|
|
104
|
+
const volumeDir = getScryptedVolume();
|
|
105
|
+
mkdirp.sync(volumeDir);
|
|
106
|
+
const dbPath = path.join(volumeDir, 'scrypted.db');
|
|
107
|
+
const db = level(dbPath);
|
|
108
|
+
await db.open();
|
|
109
|
+
|
|
110
|
+
if (process.env.SCRYPTED_RESET_ALL_USERS === 'true') {
|
|
111
|
+
await db.removeAll(ScryptedUser);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let certSetting = await db.tryGet(Settings, 'certificate') as Settings;
|
|
115
|
+
|
|
116
|
+
if (certSetting?.value?.version !== CURRENT_SELF_SIGNED_CERTIFICATE_VERSION) {
|
|
117
|
+
const cert = createSelfSignedCertificate();
|
|
118
|
+
|
|
119
|
+
certSetting = new Settings();
|
|
120
|
+
certSetting._id = 'certificate';
|
|
121
|
+
certSetting.value = cert;
|
|
122
|
+
certSetting = await db.upsert(certSetting);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
const basicAuth = httpAuth.basic({
|
|
127
|
+
realm: 'Scrypted',
|
|
128
|
+
}, async (username, password, callback) => {
|
|
129
|
+
const user = await db.tryGet(ScryptedUser, username);
|
|
130
|
+
if (!user) {
|
|
131
|
+
callback(false);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const salted = user.salt + password;
|
|
136
|
+
const hash = crypto.createHash('sha256');
|
|
137
|
+
hash.update(salted);
|
|
138
|
+
const sha = hash.digest().toString('hex');
|
|
139
|
+
|
|
140
|
+
callback(sha === user.passwordHash || password === user.token);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const keys = certSetting.value;
|
|
144
|
+
|
|
145
|
+
const httpsServerOptions = process.env.SCRYPTED_HTTPS_OPTIONS_FILE
|
|
146
|
+
? JSON.parse(fs.readFileSync(process.env.SCRYPTED_HTTPS_OPTIONS_FILE).toString())
|
|
147
|
+
: {};
|
|
148
|
+
|
|
149
|
+
const mergedHttpsServerOptions = Object.assign({
|
|
150
|
+
key: keys.serviceKey,
|
|
151
|
+
cert: keys.certificate
|
|
152
|
+
}, httpsServerOptions);
|
|
153
|
+
const secure = https.createServer(mergedHttpsServerOptions, app);
|
|
154
|
+
listenServerPort('SCRYPTED_SECURE_PORT', SCRYPTED_SECURE_PORT, secure);
|
|
155
|
+
const insecure = http.createServer(app);
|
|
156
|
+
listenServerPort('SCRYPTED_INSECURE_PORT', SCRYPTED_INSECURE_PORT, insecure);
|
|
157
|
+
|
|
158
|
+
// legacy secure port 9443 is now in use by portainer.
|
|
159
|
+
let shownLegacyPortAlert = false
|
|
160
|
+
const legacySecure = https.createServer(mergedHttpsServerOptions, (req, res) => {
|
|
161
|
+
if (!shownLegacyPortAlert) {
|
|
162
|
+
const core = scrypted.findPluginDevice('@scrypted/core');
|
|
163
|
+
if (core) {
|
|
164
|
+
const logger = scrypted.getDeviceLogger(core);
|
|
165
|
+
shownLegacyPortAlert = true;
|
|
166
|
+
const host = (req.headers.host || 'localhost').split(':')[0];
|
|
167
|
+
const newUrl = `https://${host}:${SCRYPTED_SECURE_PORT}`;
|
|
168
|
+
logger.log('a', `Due to a port conflict with Portainer, the default Scrypted URL has changed to ${newUrl}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
app(req, res);
|
|
172
|
+
});
|
|
173
|
+
legacySecure.listen(9443);
|
|
174
|
+
legacySecure.on('error', () => {
|
|
175
|
+
// can ignore.
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// use a hash of the private key as the cookie secret.
|
|
179
|
+
app.use(cookieParser(crypto.createHash('sha256').update(certSetting.value.serviceKey).digest().toString('hex')));
|
|
180
|
+
|
|
181
|
+
app.all('*', async (req, res, next) => {
|
|
182
|
+
// this is a trap for all auth.
|
|
183
|
+
// only basic auth will fail with 401. it is up to the endpoints to manage
|
|
184
|
+
// lack of login from cookie auth.
|
|
185
|
+
|
|
186
|
+
const { login_user_token } = req.signedCookies;
|
|
187
|
+
if (login_user_token) {
|
|
188
|
+
const userTokenParts = login_user_token.split('#');
|
|
189
|
+
const username = userTokenParts[0];
|
|
190
|
+
const timestamp = parseInt(userTokenParts[1]);
|
|
191
|
+
if (timestamp + 86400000 < Date.now()) {
|
|
192
|
+
console.warn('login expired');
|
|
193
|
+
return next();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const user = await db.tryGet(ScryptedUser, username);
|
|
197
|
+
if (!user) {
|
|
198
|
+
console.warn('login not found');
|
|
199
|
+
return next();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
res.locals.username = username;
|
|
203
|
+
}
|
|
204
|
+
next();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// allow basic auth to deploy plugins
|
|
208
|
+
app.all('/web/component/*', (req, res, next) => {
|
|
209
|
+
if (req.protocol === 'https' && req.headers.authorization && req.headers.authorization.toLowerCase()?.indexOf('basic') !== -1) {
|
|
210
|
+
const basicChecker = basicAuth.check((req) => {
|
|
211
|
+
res.locals.username = req.user;
|
|
212
|
+
next();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// this automatically handles unauthorized.
|
|
216
|
+
basicChecker(req, res);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
next();
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// verify all plugin related requests have some sort of auth
|
|
223
|
+
app.all('/web/component/*', (req, res, next) => {
|
|
224
|
+
if (!res.locals.username) {
|
|
225
|
+
res.status(401);
|
|
226
|
+
res.send('Not Authorized');
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
next();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
console.log('#######################################################');
|
|
233
|
+
console.log(`Scrypted Server (Local) : https://localhost:${SCRYPTED_SECURE_PORT}/`);
|
|
234
|
+
for (const address of getAddresses()) {
|
|
235
|
+
console.log(`Scrypted Server (Remote) : https://${address}:${SCRYPTED_SECURE_PORT}/`);
|
|
236
|
+
}
|
|
237
|
+
console.log(`Version: : ${await new Info().getVersion()}`);
|
|
238
|
+
console.log('#######################################################');
|
|
239
|
+
console.log('Chrome Users: You may need to type "thisisunsafe" into')
|
|
240
|
+
console.log(' the window to bypass the warning. There')
|
|
241
|
+
console.log(' may be no button to continue, type the')
|
|
242
|
+
console.log(' letters "thisisunsafe" and it will proceed.')
|
|
243
|
+
console.log('#######################################################');
|
|
244
|
+
console.log('Scrypted insecure http service port:', SCRYPTED_INSECURE_PORT);
|
|
245
|
+
console.log('Ports can be changed with environment variables.')
|
|
246
|
+
console.log('https: $SCRYPTED_SECURE_PORT')
|
|
247
|
+
console.log('http : $SCRYPTED_INSECURE_PORT')
|
|
248
|
+
console.log('Certificate can be modified via tls.createSecureContext options in')
|
|
249
|
+
console.log('JSON file located at SCRYPTED_HTTPS_OPTIONS_FILE environment variable:');
|
|
250
|
+
console.log('export SCRYPTED_HTTPS_OPTIONS_FILE=/path/to/options.json');
|
|
251
|
+
console.log('https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions')
|
|
252
|
+
console.log('#######################################################');
|
|
253
|
+
const scrypted = new ScryptedRuntime(db, insecure, secure, app);
|
|
254
|
+
await scrypted.start();
|
|
255
|
+
|
|
256
|
+
app.get(['/web/component/script/npm/:pkg', '/web/component/script/npm/@:owner/:pkg'], async (req, res) => {
|
|
257
|
+
const { owner, pkg } = req.params;
|
|
258
|
+
let endpoint = pkg;
|
|
259
|
+
if (owner)
|
|
260
|
+
endpoint = `@${owner}/${endpoint}`;
|
|
261
|
+
try {
|
|
262
|
+
const response = await axios(`https://registry.npmjs.org/${endpoint}`);
|
|
263
|
+
res.send(response.data);
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
res.status(500);
|
|
267
|
+
res.end();
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
app.post(['/web/component/script/install/:pkg', '/web/component/script/install/@:owner/:pkg', '/web/component/script/install/@:owner/:pkg/:tag'], async (req, res) => {
|
|
272
|
+
const { owner, pkg, tag } = req.params;
|
|
273
|
+
let endpoint = pkg;
|
|
274
|
+
if (owner)
|
|
275
|
+
endpoint = `@${owner}/${endpoint}`;
|
|
276
|
+
try {
|
|
277
|
+
const plugin = await scrypted.installNpm(endpoint, tag);
|
|
278
|
+
res.send({
|
|
279
|
+
id: scrypted.findPluginDevice(plugin.pluginId)._id,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
res.status(500);
|
|
284
|
+
res.end();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
app.get('/web/component/script/search', async (req, res) => {
|
|
289
|
+
try {
|
|
290
|
+
const query = qs.stringify({
|
|
291
|
+
text: req.query.text,
|
|
292
|
+
})
|
|
293
|
+
const response = await axios(`https://registry.npmjs.org/-/v1/search?${query}`);
|
|
294
|
+
res.send(response.data);
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
res.status(500);
|
|
298
|
+
res.end();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
app.post('/web/component/script/setup', async (req, res) => {
|
|
303
|
+
const npmPackage = req.query.npmPackage as string;
|
|
304
|
+
const plugin = await db.tryGet(Plugin, npmPackage) || new Plugin();
|
|
305
|
+
|
|
306
|
+
plugin._id = npmPackage;
|
|
307
|
+
plugin.packageJson = req.body;
|
|
308
|
+
|
|
309
|
+
await db.upsert(plugin);
|
|
310
|
+
|
|
311
|
+
res.send('ok');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
app.post('/web/component/script/deploy', async (req, res) => {
|
|
315
|
+
const npmPackage = req.query.npmPackage as string;
|
|
316
|
+
const plugin = await db.tryGet(Plugin, npmPackage);
|
|
317
|
+
|
|
318
|
+
if (!plugin) {
|
|
319
|
+
res.status(500);
|
|
320
|
+
res.send(`npm package ${npmPackage} not found`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
plugin.zip = req.body.toString('base64');
|
|
325
|
+
await db.upsert(plugin);
|
|
326
|
+
|
|
327
|
+
const noRebind = req.query['no-rebind'] !== undefined;
|
|
328
|
+
if (!noRebind)
|
|
329
|
+
await scrypted.installPlugin(plugin);
|
|
330
|
+
|
|
331
|
+
res.send('ok');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
app.post('/web/component/script/debug', async (req, res) => {
|
|
335
|
+
const npmPackage = req.query.npmPackage as string;
|
|
336
|
+
const plugin = await db.tryGet(Plugin, npmPackage);
|
|
337
|
+
|
|
338
|
+
if (!plugin) {
|
|
339
|
+
res.status(500);
|
|
340
|
+
res.send(`npm package ${npmPackage} not found`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const waitDebug = new Promise<void>((resolve, reject) => {
|
|
345
|
+
setTimeout(() => reject(new Error('timed out waiting for debug session')), 30000);
|
|
346
|
+
debugServer.on('connection', resolve);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
workerInspectPort = Math.round(Math.random() * 10000) + 30000;
|
|
350
|
+
try {
|
|
351
|
+
await scrypted.installPlugin(plugin, {
|
|
352
|
+
waitDebug,
|
|
353
|
+
inspectPort: workerInspectPort,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
res.status(500);
|
|
358
|
+
res.send(e.toString());
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
res.send({
|
|
363
|
+
workerInspectPort,
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
app.get('/logout', (req, res) => {
|
|
368
|
+
res.clearCookie('login_user_token');
|
|
369
|
+
res.send({});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
app.post('/login', async (req, res) => {
|
|
373
|
+
const hasLogin = await db.getCount(ScryptedUser) > 0;
|
|
374
|
+
|
|
375
|
+
const { username, password, change_password } = req.body;
|
|
376
|
+
const timestamp = Date.now();
|
|
377
|
+
const maxAge = 86400000;
|
|
378
|
+
|
|
379
|
+
if (hasLogin) {
|
|
380
|
+
|
|
381
|
+
const user = await db.tryGet(ScryptedUser, username);
|
|
382
|
+
if (!user) {
|
|
383
|
+
res.send({
|
|
384
|
+
error: 'User does not exist.',
|
|
385
|
+
hasLogin,
|
|
386
|
+
})
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const salted = user.salt + password;
|
|
391
|
+
const hash = crypto.createHash('sha256');
|
|
392
|
+
hash.update(salted);
|
|
393
|
+
const sha = hash.digest().toString('hex');
|
|
394
|
+
if (user.passwordHash !== sha) {
|
|
395
|
+
res.send({
|
|
396
|
+
error: 'Incorrect password.',
|
|
397
|
+
hasLogin,
|
|
398
|
+
})
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const login_user_token = `${username}#${timestamp}`;
|
|
403
|
+
res.cookie('login_user_token', login_user_token, {
|
|
404
|
+
maxAge,
|
|
405
|
+
secure: true,
|
|
406
|
+
signed: true,
|
|
407
|
+
httpOnly: true,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (change_password) {
|
|
411
|
+
user.salt = crypto.randomBytes(64).toString('base64');
|
|
412
|
+
user.passwordHash = crypto.createHash('sha256').update(user.salt + change_password).digest().toString('hex');
|
|
413
|
+
user.passwordDate = timestamp;
|
|
414
|
+
await db.upsert(user);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
res.send({
|
|
418
|
+
username,
|
|
419
|
+
expiration: maxAge,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const user = new ScryptedUser();
|
|
426
|
+
user._id = username;
|
|
427
|
+
user.salt = crypto.randomBytes(64).toString('base64');
|
|
428
|
+
user.passwordHash = crypto.createHash('sha256').update(user.salt + password).digest().toString('hex');
|
|
429
|
+
user.passwordDate = timestamp;
|
|
430
|
+
await db.upsert(user);
|
|
431
|
+
|
|
432
|
+
const login_user_token = `${username}#${timestamp}`
|
|
433
|
+
res.cookie('login_user_token', login_user_token, {
|
|
434
|
+
maxAge,
|
|
435
|
+
secure: true,
|
|
436
|
+
signed: true,
|
|
437
|
+
httpOnly: true,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
res.send({
|
|
441
|
+
username,
|
|
442
|
+
expiration: maxAge,
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
app.get('/login', async (req, res) => {
|
|
447
|
+
if (req.protocol === 'https' && req.headers.authorization) {
|
|
448
|
+
const username = await new Promise(resolve => {
|
|
449
|
+
const basicChecker = basicAuth.check((req) => {
|
|
450
|
+
resolve(req.user);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// this automatically handles unauthorized.
|
|
454
|
+
basicChecker(req, res);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const user = await db.tryGet(ScryptedUser, username);
|
|
458
|
+
if (!user.token) {
|
|
459
|
+
user.token = crypto.randomBytes(16).toString('hex');
|
|
460
|
+
await db.upsert(user);
|
|
461
|
+
}
|
|
462
|
+
res.send({
|
|
463
|
+
username,
|
|
464
|
+
token: user.token,
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const hasLogin = await db.getCount(ScryptedUser) > 0;
|
|
470
|
+
const { login_user_token } = req.signedCookies;
|
|
471
|
+
if (!login_user_token) {
|
|
472
|
+
res.send({
|
|
473
|
+
error: 'Not logged in.',
|
|
474
|
+
hasLogin,
|
|
475
|
+
})
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const userTokenParts = login_user_token.split('#');
|
|
480
|
+
const username = userTokenParts[0];
|
|
481
|
+
const timestamp = parseInt(userTokenParts[1]);
|
|
482
|
+
if (timestamp + 86400000 < Date.now()) {
|
|
483
|
+
res.send({
|
|
484
|
+
error: 'Login expired.',
|
|
485
|
+
hasLogin,
|
|
486
|
+
})
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const user = await db.tryGet(ScryptedUser, username);
|
|
491
|
+
if (!user) {
|
|
492
|
+
res.send({
|
|
493
|
+
error: 'User not found.',
|
|
494
|
+
hasLogin,
|
|
495
|
+
})
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (timestamp < user.passwordDate) {
|
|
500
|
+
res.send({
|
|
501
|
+
error: 'Login invalid. Password has changed.',
|
|
502
|
+
hasLogin,
|
|
503
|
+
})
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
res.send({
|
|
508
|
+
expiration: 86400000 - (Date.now() - timestamp),
|
|
509
|
+
username,
|
|
510
|
+
})
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
app.get('/', (_req, res) => res.redirect('/endpoint/@scrypted/core/public/'));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
start();
|