diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 0b566f1c0..e3d4ee1d3 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -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', () => { diff --git a/server/package-lock.json b/server/package-lock.json index 6dd2b0468..7431c6047 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index f8f35ce19..c3380b959 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/server", - "version": "0.6.3", + "version": "0.6.4", "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 085c55752..64ca3108c 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 { 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 { 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, @@ -76,12 +75,6 @@ export abstract class PluginHttp { 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; diff --git a/server/src/runtime.ts b/server/src/runtime.ts index 07fb53ef3..014105173 100644 --- a/server/src/runtime.ts +++ b/server/src/runtime.ts @@ -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 { 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) { diff --git a/server/src/scrypted-server-main.ts b/server/src/scrypted-server-main.ts index f2c52844e..73969630e 100644 --- a/server/src/scrypted-server-main.ts +++ b/server/src/scrypted-server-main.ts @@ -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,