diff --git a/server/package-lock.json b/server/package-lock.json index bc53913f8..bb89607cf 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/server", - "version": "0.5.13", + "version": "0.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/server", - "version": "0.5.13", + "version": "0.6.1", "license": "ISC", "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", diff --git a/server/package.json b/server/package.json index 92d9ee7c7..21d63fef4 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/server", - "version": "0.5.13", + "version": "0.6.1", "description": "", "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", diff --git a/server/src/plugin/plugin-http.ts b/server/src/plugin/plugin-http.ts index 320975621..e055770d6 100644 --- a/server/src/plugin/plugin-http.ts +++ b/server/src/plugin/plugin-http.ts @@ -1,7 +1,7 @@ import { HttpRequest } from '@scrypted/types'; import bodyParser from 'body-parser'; import { Request, Response, Router } from 'express'; -import { ServerResponse, IncomingHttpHeaders } from 'http'; +import { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http'; import WebSocket, { Server as WebSocketServer } from "ws"; export function isConnectionUpgrade(headers: IncomingHttpHeaders) { @@ -40,6 +40,8 @@ export abstract class PluginHttp { abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void; abstract getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise; abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise; + abstract addAccessControlHeaders(req: IncomingMessage, res: ServerResponse): void; + abstract checkUpgrade(req: Request, res: Response, pluginData: T): void; async endpointHandler(req: Request, res: Response, isPublicEndpoint: boolean, isEngineIOEndpoint: boolean, handler: (req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T) => void) { @@ -59,28 +61,45 @@ export abstract class PluginHttp { } }; + const { owner, pkg } = req.params; + let endpoint = pkg; + if (owner) + endpoint = `@${owner}/${endpoint}`; + const pluginData = await this.getEndpointPluginData(req, endpoint, isUpgrade, isEngineIOEndpoint); + + if (!pluginData) { + end(404, `Not Found (plugin or device "${endpoint}" not found)`); + return; + } + + if (isEngineIOEndpoint && isUpgrade) { + this.checkUpgrade(req, res, pluginData); + } + + if (isEngineIOEndpoint && !isUpgrade) { + console.log('req', req.method, req.url, req.headers); + this.addAccessControlHeaders(req, res); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With, Access-Control-Request-Method'); + } + + if (isEngineIOEndpoint && req.method === 'OPTIONS') { + res.send(204); + return; + } + + if (!isPublicEndpoint && !res.locals.username) { end(401, 'Not Authorized'); console.log('rejected request', isPublicEndpoint, res.locals.username, req.originalUrl) return; } - const { owner, pkg } = req.params; - let endpoint = pkg; - if (owner) - endpoint = `@${owner}/${endpoint}`; - if (isUpgrade && req.headers.upgrade?.toLowerCase() !== 'websocket') { end(404, 'Not Found (unknown upgrade protocol)'); return; } - const pluginData = await this.getEndpointPluginData(req, endpoint, isUpgrade, isEngineIOEndpoint); - if (!pluginData) { - end(404, `Not Found (plugin or device "${endpoint}" not found)`); - return; - } - let rootPath = `/endpoint/${endpoint}`; if (isPublicEndpoint) rootPath += '/public' @@ -98,10 +117,6 @@ export abstract class PluginHttp { aclId: res.locals.aclId, }; - if (isEngineIOEndpoint && !isUpgrade && isPublicEndpoint) { - res.header("Access-Control-Allow-Origin", '*'); - } - if (!isEngineIOEndpoint && isUpgrade) { try { this.wss.handleUpgrade(req, req.socket, (req as any).upgradeHead, async (ws) => { diff --git a/server/src/runtime.ts b/server/src/runtime.ts index 5563806c1..982267468 100644 --- a/server/src/runtime.ts +++ b/server/src/runtime.ts @@ -4,10 +4,12 @@ import axios from 'axios'; import * as io from 'engine.io'; import { once } from 'events'; import express, { Request, Response } from 'express'; +import { ParamsDictionary } from 'express-serve-static-core'; import http, { ServerResponse } from 'http'; import https from 'https'; import type { spawn as ptySpawn } from 'node-pty-prebuilt-multiarch'; import path from 'path'; +import { ParsedQs } from 'qs'; import rimraf from 'rimraf'; import semver from 'semver'; import { PassThrough } from 'stream'; @@ -155,11 +157,23 @@ export class ScryptedRuntime extends PluginHttp { }, 60 * 60 * 1000); } + checkUpgrade(req: express.Request>, res: express.Response>, pluginData: HttpPluginData): void { + // pluginData.pluginHost.io. + const { sid } = req.query; + const client = (pluginData.pluginHost.io as any).clients[sid as string]; + if (client) { + res.locals.username = 'existing-io-session'; + } + } + addAccessControlHeaders(req: http.IncomingMessage, res: http.ServerResponse) { res.setHeader('Vary', 'Origin,Referer'); const header = this.getAccessControlAllowOrigin(req.headers); - if (header) + if (header) { res.setHeader('Access-Control-Allow-Origin', header); + res.setHeader("Access-Control-Allow-Credentials", "true"); + res.setHeader('Access-Control-Allow-Private-Network', 'true'); + } } getAccessControlAllowOrigin(headers: http.IncomingHttpHeaders) { diff --git a/server/src/scrypted-server-main.ts b/server/src/scrypted-server-main.ts index d1092a607..217e2d122 100644 --- a/server/src/scrypted-server-main.ts +++ b/server/src/scrypted-server-main.ts @@ -439,7 +439,7 @@ async function start() { const { username, password, change_password, maxAge: maxAgeRequested } = req.body; const timestamp = Date.now(); const maxAge = parseInt(maxAgeRequested) || ONE_DAY_MILLISECONDS; - const addresses = getHostAddresses(true, true).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`); + const addresses = ((await scrypted.addressSettings.getLocalAddresses()) || getHostAddresses(true, true)).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`); if (hasLogin) { const user = await db.tryGet(ScryptedUser, username); @@ -520,14 +520,13 @@ async function start() { }); }); - app.get('/login', async (req, res) => { await checkResetLogin(); scrypted.addAccessControlHeaders(req, res); const hostname = os.hostname()?.split('.')?.[0]; - const addresses = getHostAddresses(true, true).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`); + const addresses = ((await scrypted.addressSettings.getLocalAddresses()) || getHostAddresses(true, true)).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`); if (req.protocol === 'https' && req.headers.authorization) { const username = await new Promise(resolve => { const basicChecker = basicAuth.check((req) => {