mirror of
https://github.com/koush/scrypted.git
synced 2026-02-09 00:39:56 +00:00
server: add support for query tokens
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@scrypted/server",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.4",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user