server: add support for query tokens

This commit is contained in:
Koushik Dutta
2023-01-25 23:32:07 -08:00
parent f0a30ce8cd
commit 760e4d2062
6 changed files with 86 additions and 64 deletions

View File

@@ -34,7 +34,7 @@ function once(socket: IOClientSocket, event: 'open' | 'message') {
});
}
export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-local';
export type ScryptedClientConnectionType = 'http' | 'webrtc' | 'http-direct';
export interface ScryptedClientStatic extends ScryptedStatic {
userId?: string;
@@ -42,9 +42,12 @@ export interface ScryptedClientStatic extends ScryptedStatic {
disconnect(): void;
onClose?: Function;
version: string;
connectionType: ScryptedClientConnectionType;
rtcConnectionManagement?: RTCConnectionManagement;
browserSignalingSession?: BrowserSignalingSession;
address?: string;
connectionType: ScryptedClientConnectionType;
authorization?: string;
queryToken?: { [parameter: string]: string };
}
export interface ScryptedConnectionOptions {
@@ -109,8 +112,9 @@ export async function loginScryptedClient(options: ScryptedLoginOptions) {
const directAddress = response.headers['x-scrypted-direct-address'];
return {
authorization: response.data.authorization as string,
error: response.data.error as string,
authorization: response.data.authorization as string,
queryToken: response.data.queryToken as any,
token: response.data.token as string,
addresses,
scryptedCloud,
@@ -130,11 +134,12 @@ export async function checkScryptedClientLogin(options?: ScryptedConnectionOptio
return {
redirect: response.data.redirect as string,
error: response.data.error as string,
authorization: response.data.authorization as string,
username: response.data.username as string,
expiration: response.data.expiration as number,
hasLogin: !!response.data.hasLogin,
error: response.data.error as string,
authorization: response.data.authorization as string,
queryToken: response.data.queryToken as any,
addresses: response.data.addresses as string[],
scryptedCloud,
directAddress,
@@ -174,6 +179,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
const start = Date.now();
let { baseUrl, pluginId, clientName, username, password } = options;
let authorization: string;
let queryToken: any;
const extraHeaders: { [header: string]: string } = {};
let addresses: string[];
@@ -188,6 +194,8 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
extraHeaders['Authorization'] = loginResult.authorization;
addresses = loginResult.addresses;
scryptedCloud = loginResult.scryptedCloud;
authorization = loginResult.authorization;
queryToken = loginResult.queryToken;
console.log('login result', Date.now() - start, loginResult);
}
else {
@@ -201,6 +209,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
directAddress = loginCheck.directAddress;
username = loginCheck.username;
authorization = loginCheck.authorization;
queryToken = loginCheck.queryToken;
console.log('login checked', Date.now() - start, loginCheck);
}
@@ -257,7 +266,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
promises.push((async () => {
await once(check, 'open');
return {
connectionType: 'http-local',
connectionType: 'http-direct',
ready: check,
address,
};
@@ -307,7 +316,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
await once(check, 'open');
return {
ready: check,
connectionType: scryptedCloud ? 'http' : 'http-local',
connectionType: 'http',
};
})());
@@ -557,6 +566,7 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
serverVersion,
username,
pluginRemoteAPI: undefined,
address,
connectionType,
version,
systemManager,
@@ -569,6 +579,8 @@ export async function connectScryptedClient(options: ScryptedClientOptions): Pro
pluginHostAPI: undefined,
rtcConnectionManagement,
browserSignalingSession,
authorization,
queryToken,
}
socket.on('close', () => {

View File

@@ -1,12 +1,12 @@
{
"name": "@scrypted/server",
"version": "0.6.3",
"version": "0.6.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@scrypted/server",
"version": "0.6.3",
"version": "0.6.4",
"license": "ISC",
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@scrypted/server",
"version": "0.6.3",
"version": "0.6.4",
"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 { IncomingHttpHeaders, IncomingMessage, ServerResponse } from 'http';
import { IncomingHttpHeaders, ServerResponse } from 'http';
import WebSocket, { Server as WebSocketServer } from "ws";
export function isConnectionUpgrade(headers: IncomingHttpHeaders) {
@@ -40,7 +40,6 @@ 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,
@@ -76,12 +75,6 @@ export abstract class PluginHttp<T> {
this.checkUpgrade(req, res, pluginData);
}
if (isEngineIOEndpoint && !isUpgrade) {
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;

View File

@@ -31,7 +31,7 @@ import { isConnectionUpgrade, PluginHttp } from './plugin/plugin-http';
import { WebSocketConnection } from './plugin/plugin-remote-websocket';
import { getPluginVolume } from './plugin/plugin-volume';
import { getIpAddress, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
import { AddressSettings as AddressSettings } from './services/addresses';
import { AddressSettings } from './services/addresses';
import { Alerts } from './services/alerts';
import { CORSControl } from './services/cors';
import { Info } from './services/info';
@@ -170,9 +170,11 @@ export class ScryptedRuntime extends PluginHttp<HttpPluginData> {
const header = this.getAccessControlAllowOrigin(req.headers);
if (header) {
res.setHeader('Access-Control-Allow-Origin', header);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader('Access-Control-Allow-Private-Network', 'true');
}
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader('Access-Control-Allow-Private-Network', 'true');
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');
}
getAccessControlAllowOrigin(headers: http.IncomingHttpHeaders) {

View File

@@ -1,32 +1,32 @@
import axios from 'axios';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import crypto from 'crypto';
import express, { Request } from 'express';
import fs from 'fs';
import http from 'http';
import httpAuth from 'http-auth';
import https from 'https';
import ip from 'ip';
import mkdirp from 'mkdirp';
import net from 'net';
import os from 'os';
import path from 'path';
import process from 'process';
import http from 'http';
import https from 'https';
import express, { Request } from 'express';
import bodyParser from 'body-parser';
import net from 'net';
import { ScryptedRuntime } from './runtime';
import level from './level';
import { Plugin, ScryptedUser, Settings } from './db-types';
import { getHostAddresses, SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
import crypto from 'crypto';
import cookieParser from 'cookie-parser';
import axios from 'axios';
import { RPCResultError } from './rpc';
import fs from 'fs';
import mkdirp from 'mkdirp';
import { install as installSourceMapSupport } from 'source-map-support';
import httpAuth from 'http-auth';
import semver from 'semver';
import { Info } from './services/info';
import { sleep } from './sleep';
import { install as installSourceMapSupport } from 'source-map-support';
import { createSelfSignedCertificate, CURRENT_SELF_SIGNED_CERTIFICATE_VERSION } from './cert';
import { Plugin, ScryptedUser, Settings } from './db-types';
import level from './level';
import { PluginError } from './plugin/plugin-error';
import { getScryptedVolume } from './plugin/plugin-volume';
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
import os from 'os';
import { RPCResultError } from './rpc';
import { ScryptedRuntime } from './runtime';
import { getHostAddresses, SCRYPTED_DEBUG_PORT, SCRYPTED_INSECURE_PORT, SCRYPTED_SECURE_PORT } from './server-settings';
import { Info } from './services/info';
import { setScryptedUserPassword } from './services/users';
import ip from 'ip';
import { sleep } from './sleep';
import { ONE_DAY_MILLISECONDS, UserToken } from './usertoken';
if (!semver.gte(process.version, '16.0.0')) {
throw new Error('"node" version out of date. Please update node to v16 or higher.')
@@ -163,12 +163,21 @@ async function start() {
})
const authSalt = crypto.randomBytes(16);
const createAuthorizationToken = (login_user_token: string) => {
const createTokens = (userToken: UserToken) => {
const login_user_token = userToken.toString();
const salted = login_user_token + authSalt;
const hash = crypto.createHash('sha256');
hash.update(salted);
const sha = hash.digest().toString('hex');
return `Bearer ${sha}#${login_user_token}`;
const queryToken = `${sha}#${login_user_token}`;
return {
authorization: `Bearer ${queryToken}`,
// query token are the query parameters that must be added to an url for authorization.
// useful for cross origin img tags.
queryToken: {
scryptedToken: queryToken,
},
};
}
app.use(async (req, res, next) => {
@@ -182,6 +191,24 @@ async function start() {
// only basic auth will fail with 401. it is up to the endpoints to manage
// lack of login from cookie auth.
const checkToken = (token: string) => {
const [checkHash, ...tokenParts] = token.split('#');
const tokenPart = tokenParts?.join('#');
if (checkHash && tokenPart) {
const salted = tokenPart + authSalt;
const hash = crypto.createHash('sha256');
hash.update(salted);
const sha = hash.digest().toString('hex');
if (checkHash === sha) {
const userToken = validateToken(tokenPart);
if (userToken)
res.locals.username = userToken.username;
res.locals.aclId = userToken.aclId;
}
}
}
const userToken = getSignedLoginUserToken(req);
if (userToken) {
const { username, aclId } = userToken;
@@ -201,21 +228,10 @@ async function start() {
res.locals.aclId = aclId;
}
else if (req.headers.authorization?.startsWith('Bearer ')) {
const [checkHash, ...tokenParts] = req.headers.authorization.substring('Bearer '.length).split('#');
const tokenPart = tokenParts?.join('#');
if (checkHash && tokenPart) {
const salted = tokenPart + authSalt;
const hash = crypto.createHash('sha256');
hash.update(salted);
const sha = hash.digest().toString('hex');
if (checkHash === sha) {
const userToken = validateToken(tokenPart);
if (userToken)
res.locals.username = userToken.username;
res.locals.aclId = userToken.aclId;
}
}
checkToken(req.headers.authorization.substring('Bearer '.length));
}
else if (req.query['scryptedToken']) {
checkToken(req.query.scryptedToken.toString());
}
next();
});
@@ -479,7 +495,7 @@ async function start() {
}
res.send({
authorization: createAuthorizationToken(login_user_token),
...createTokens(userToken),
username,
expiration: maxAge,
addresses,
@@ -513,7 +529,7 @@ async function start() {
});
res.send({
authorization: createAuthorizationToken(login_user_token),
...createTokens(userToken),
username,
token: user.token,
expiration: maxAge,
@@ -524,7 +540,6 @@ async function start() {
app.get('/login', async (req, res) => {
await checkResetLogin();
scrypted.addAccessControlHeaders(req, res);
const hostname = os.hostname()?.split('.')?.[0];
const addresses = ((await scrypted.addressSettings.getLocalAddresses()) || getHostAddresses(true, true)).map(address => `https://${address}:${SCRYPTED_SECURE_PORT}`);
@@ -569,7 +584,7 @@ async function start() {
const userToken = UserToken.validateToken(login_user_token);
res.send({
authorization: createAuthorizationToken(login_user_token),
...createTokens(userToken),
expiration: (userToken.timestamp + userToken.duration) - Date.now(),
username: userToken.username,
addresses,