Merge branch 'main' of github.com:koush/scrypted

This commit is contained in:
Koushik Dutta
2022-06-02 16:12:51 -07:00
8 changed files with 157 additions and 83 deletions

120
server/package-lock.json generated
View File

@@ -17,7 +17,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"debug": "^4.3.1",
"engine.io": "^5.2.0",
"engine.io": "^6.2.0",
"express": "^4.17.2",
"http-auth": "^4.1.9",
"level": "^6.0.1",
@@ -50,7 +50,6 @@
"@types/adm-zip": "^0.4.33",
"@types/cookie-parser": "^1.4.2",
"@types/debug": "^4.1.5",
"@types/engine.io": "^3.1.5",
"@types/express": "^4.17.11",
"@types/http-auth": "^4.1.1",
"@types/lodash": "^4.14.168",
@@ -177,6 +176,11 @@
"@types/node": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"node_modules/@types/cookie-parser": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz",
@@ -186,6 +190,11 @@
"@types/express": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
},
"node_modules/@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@@ -195,15 +204,6 @@
"@types/ms": "*"
}
},
"node_modules/@types/engine.io": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz",
"integrity": "sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@@ -291,8 +291,7 @@
"node_modules/@types/node": {
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.15.tgz",
"integrity": "sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA==",
"dev": true
"integrity": "sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA=="
},
"node_modules/@types/node-dijkstra": {
"version": "2.5.2",
@@ -534,14 +533,6 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -895,39 +886,39 @@
}
},
"node_modules/engine.io": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.2.1.tgz",
"integrity": "sha512-hyNxjVgWp619QMfqi/+/6/LQF+ueOIWeVOza3TeyvxUGjeT9U/xPkkHW/NJNuhbStrxMujEoMadoc2EY7DDEnw==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.0",
"ws": "~7.4.2"
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
"dependencies": {
"base64-arraybuffer": "0.1.4"
},
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
"engines": {
"node": ">=8.0.0"
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"engines": {
"node": ">=8.3.0"
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
@@ -2630,6 +2621,11 @@
"@types/node": "*"
}
},
"@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
},
"@types/cookie-parser": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz",
@@ -2639,6 +2635,11 @@
"@types/express": "*"
}
},
"@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
},
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
@@ -2648,15 +2649,6 @@
"@types/ms": "*"
}
},
"@types/engine.io": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.7.tgz",
"integrity": "sha512-qNjVXcrp+1sS8YpRUa714r0pgzOwESdW5UjHL7D/2ZFdBX0BXUXtg1LUrp+ylvqbvMcMWUy73YpRoxPN2VoKAQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@@ -2744,8 +2736,7 @@
"@types/node": {
"version": "17.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.15.tgz",
"integrity": "sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA==",
"dev": true
"integrity": "sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA=="
},
"@types/node-dijkstra": {
"version": "2.5.2",
@@ -2957,11 +2948,6 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-arraybuffer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
"integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI="
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3223,34 +3209,34 @@
}
},
"engine.io": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.2.1.tgz",
"integrity": "sha512-hyNxjVgWp619QMfqi/+/6/LQF+ueOIWeVOza3TeyvxUGjeT9U/xPkkHW/NJNuhbStrxMujEoMadoc2EY7DDEnw==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~4.0.0",
"ws": "~7.4.2"
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
},
"dependencies": {
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
}
}
},
"engine.io-parser": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz",
"integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==",
"requires": {
"base64-arraybuffer": "0.1.4"
}
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
},
"env-paths": {
"version": "2.2.1",

View File

@@ -11,7 +11,7 @@
"body-parser": "^1.19.0",
"cookie-parser": "^1.4.5",
"debug": "^4.3.1",
"engine.io": "^5.2.0",
"engine.io": "^6.2.0",
"express": "^4.17.2",
"http-auth": "^4.1.9",
"level": "^6.0.1",
@@ -44,7 +44,6 @@
"@types/adm-zip": "^0.4.33",
"@types/cookie-parser": "^1.4.2",
"@types/debug": "^4.1.5",
"@types/engine.io": "^3.1.5",
"@types/express": "^4.17.11",
"@types/http-auth": "^4.1.1",
"@types/lodash": "^4.14.168",

View File

@@ -103,11 +103,13 @@ export class PluginHostAPI extends PluginAPIManagedListeners implements PluginAP
}
async ioClose(id: string) {
// @ts-ignore
this.pluginHost.io.clients[id]?.close();
this.pluginHost.ws[id]?.close();
}
async ioSend(id: string, message: string) {
// @ts-ignore
this.pluginHost.io.clients[id]?.send(message);
this.pluginHost.ws[id]?.send(message);
}

View File

@@ -1,13 +1,14 @@
import { Device, EngineIOHandler } from '@scrypted/types';
import AdmZip from 'adm-zip';
import crypto from 'crypto';
import io, { Socket } from 'engine.io';
import * as io from 'engine.io';
import fs from 'fs';
import mkdirp from 'mkdirp';
import path from 'path';
import rimraf from 'rimraf';
import WebSocket from 'ws';
import { Plugin } from '../db-types';
import { IOServer, IOSocket } from '../io';
import { Logger } from '../logger';
import { RpcPeer } from '../rpc';
import { ScryptedRuntime } from '../runtime';
@@ -34,8 +35,15 @@ export class PluginHost {
module: Promise<any>;
scrypted: ScryptedRuntime;
remote: PluginRemote;
io = io(undefined, {
io: IOServer<io.Socket> = new io.Server({
pingTimeout: 120000,
cors: (req, callback) => {
const header = this.scrypted.getAccessControlAllowOrigin(req.headers);
callback(undefined, {
origin: header,
credentials: true,
})
},
});
ws: { [id: string]: WebSocket } = {};
api: PluginHostAPI;
@@ -140,13 +148,13 @@ export class PluginHost {
const handler = this.scrypted.getDevice<EngineIOHandler>(pluginDevice._id);
socket.on('message', message => {
this.remote.ioEvent(socket.id, 'message', message)
this.remote.ioEvent(socket.transport.sid, 'message', message)
});
socket.on('close', reason => {
this.remote.ioEvent(socket.id, 'close');
this.remote.ioEvent(socket.transport.sid, 'close');
});
await handler.onConnection(endpointRequest, `io://${socket.id}`);
await handler.onConnection(endpointRequest, `io://${socket.transport.sid}`);
}
catch (e) {
console.error('engine.io plugin error', e);
@@ -318,7 +326,7 @@ export class PluginHost {
};
}
async createRpcIoPeer(socket: Socket) {
async createRpcIoPeer(socket: IOSocket) {
let connected = true;
const rpcPeer = new RpcPeer(`api/${this.pluginId}`, 'web', (message, reject) => {
if (!connected)

View File

@@ -8,22 +8,25 @@ export abstract class PluginHttp<T> {
wss = new WebSocketServer({ noServer: true });
constructor(public app: Router) {
app.all(['/endpoint/@:owner/:pkg/public/engine.io/*', '/endpoint/:pkg/public/engine.io/*'], (req, res) => {
}
addMiddleware() {
this.app.all(['/endpoint/@:owner/:pkg/public/engine.io/*', '/endpoint/:pkg/public/engine.io/*'], (req, res) => {
this.endpointHandler(req, res, true, true, this.handleEngineIOEndpoint.bind(this))
});
app.all(['/endpoint/@:owner/:pkg/engine.io/*', '/endpoint/@:owner/:pkg/engine.io/*'], (req, res) => {
this.app.all(['/endpoint/@:owner/:pkg/engine.io/*', '/endpoint/@:owner/:pkg/engine.io/*'], (req, res) => {
this.endpointHandler(req, res, false, true, this.handleEngineIOEndpoint.bind(this))
});
// stringify all http endpoints
app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], bodyParser.text() as any);
this.app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], bodyParser.text() as any);
app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg/public', '/endpoint/:pkg/public/*'], (req, res) => {
this.app.all(['/endpoint/@:owner/:pkg/public', '/endpoint/@:owner/:pkg/public/*', '/endpoint/:pkg/public', '/endpoint/:pkg/public/*'], (req, res) => {
this.endpointHandler(req, res, true, false, this.handleRequestEndpoint.bind(this))
});
app.all(['/endpoint/@:owner/:pkg', '/endpoint/@:owner/:pkg/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], (req, res) => {
this.app.all(['/endpoint/@:owner/:pkg', '/endpoint/@:owner/:pkg/*', '/endpoint/:pkg', '/endpoint/:pkg/*'], (req, res) => {
this.endpointHandler(req, res, false, false, this.handleRequestEndpoint.bind(this))
});
}

View File

@@ -6,7 +6,7 @@ import { Plugin, PluginDevice, ScryptedAlert } from './db-types';
import { getState, ScryptedStateManager, setState } from './state';
import { Request, Response } from 'express';
import { createResponseInterface } from './http-interfaces';
import http, { ServerResponse } from 'http';
import http, { ServerResponse, IncomingHttpHeaders } from 'http';
import https from 'https';
import express from 'express';
import { LogEntry, Logger, makeAlertId } from './logger';
@@ -25,13 +25,15 @@ import semver from 'semver';
import { ServiceControl } from './services/service-control';
import { Alerts } from './services/alerts';
import { Info } from './services/info';
import io from 'engine.io';
import * as io from 'engine.io';
import { spawn as ptySpawn } from 'node-pty';
import rimraf from 'rimraf';
import { getPluginVolume } from './plugin/plugin-volume';
import { PluginHttp } from './plugin/plugin-http';
import AdmZip from 'adm-zip';
import path from 'path';
import { CORSControl, CORSServer } from './services/cors';
import { IOServer } from './io';
interface DeviceProxyPair {
handler: PluginDeviceProxyHandler;
@@ -56,9 +58,17 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
devicesLogger = this.logger.getLogger('device', 'Devices');
wss = new WebSocketServer({ noServer: true });
wsAtomic = 0;
shellio = io(undefined, {
shellio: IOServer<io.Socket> = new io.Server({
pingTimeout: 120000,
cors: (req, callback) => {
const header = this.getAccessControlAllowOrigin(req.headers);
callback(undefined, {
origin: header,
credentials: true,
})
},
});
cors: CORSServer[] = [];
constructor(datastore: Level, insecure: http.Server, secure: https.Server, app: express.Application) {
super(app);
@@ -67,6 +77,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
app.disable('x-powered-by');
this.addMiddleware();
app.get('/web/oauth/callback', (req, res) => {
this.oauthCallback(req, res);
});
@@ -122,6 +134,27 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
}, 60 * 60 * 1000);
}
getAccessControlAllowOrigin(headers: IncomingHttpHeaders) {
let { origin, referer } = headers;
if (!origin && referer) {
try {
const u = new URL(headers.referer)
origin = u.origin;
}
catch (e) {
return;
}
}
if (!origin)
return;
const servers: string[] = process.env.SCRYPTED_ACCESS_CONTROL_ALLOW_ORIGINS?.split(',') || [];
servers.push(...Object.values(this.cors).map(entry => entry.server));
if (!servers.includes(origin))
return;
return origin;
}
getDeviceLogger(device: PluginDevice): Logger {
return this.devicesLogger.getLogger(device._id, getState(device, ScryptedInterfaceProperty.name));
}
@@ -311,6 +344,8 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
return this.logger;
case 'alerts':
return new Alerts(this);
case 'cors':
return new CORSControl(this);
}
}

View File

@@ -360,6 +360,19 @@ async function start() {
let hasLogin = await db.getCount(ScryptedUser) > 0;
app.options('/login', (req, res) => {
res.setHeader('Vary', 'Origin,Referer');
res.set('Access-Control-Allow-Credentials', 'true');
const header = scrypted.getAccessControlAllowOrigin(req.headers);
if (header)
res.setHeader('Access-Control-Allow-Origin', header);
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
res.set('Access-Control-Allow-Credentials', 'true');
res.send(200);
});
app.post('/login', async (req, res) => {
const { username, password, change_password } = req.body;
const timestamp = Date.now();
@@ -393,6 +406,7 @@ async function start() {
secure: true,
signed: true,
httpOnly: true,
sameSite: 'none',
});
if (change_password) {
@@ -432,6 +446,7 @@ async function start() {
secure: true,
signed: true,
httpOnly: true,
sameSite: 'none',
});
res.send({
@@ -442,6 +457,13 @@ async function start() {
app.get('/login', async (req, res) => {
res.setHeader('Vary', 'Origin,Referer');
res.set('Access-Control-Allow-Credentials', 'true');
const header = scrypted.getAccessControlAllowOrigin(req.headers);
if (header)
res.setHeader('Access-Control-Allow-Origin', header);
if (req.protocol === 'https' && req.headers.authorization) {
const username = await new Promise(resolve => {
const basicChecker = basicAuth.check((req) => {

View File

@@ -0,0 +1,19 @@
import { ScryptedRuntime } from "../runtime";
export interface CORSServer {
tag: string;
server: string;
}
export class CORSControl {
constructor(public runtime: ScryptedRuntime) {
}
async getCORS(): Promise<CORSServer[]> {
return this.runtime.cors;
}
async setCORS(servers: CORSServer[]) {
this.runtime.cors = servers;
}
}