server: add support for CORS API access

This commit is contained in:
Koushik Dutta
2023-01-25 13:18:12 -08:00
parent 1d1af318a2
commit c314ec3be6
5 changed files with 51 additions and 23 deletions

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.5.13",
"version": "0.6.1",
"description": "",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",

View File

@@ -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<T> {
abstract handleRequestEndpoint(req: Request, res: Response, endpointRequest: HttpRequest, pluginData: T): void;
abstract getEndpointPluginData(req: Request, endpoint: string, isUpgrade: boolean, isEngineIOEndpoint: boolean): Promise<T>;
abstract handleWebSocket(endpoint: string, httpRequest: HttpRequest, ws: WebSocket, pluginData: T): Promise<void>;
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<T> {
}
};
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<T> {
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) => {

View File

@@ -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<HttpPluginData> {
}, 60 * 60 * 1000);
}
checkUpgrade(req: express.Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res: express.Response<any, Record<string, any>>, 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) {

View File

@@ -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) => {