feat: stream private office screens
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { parseScryptHash, type ScryptHash } from "./auth/password.ts";
|
||||
import { loadRegions, type RegionSet } from "./regions.ts";
|
||||
import { isSafeIceUrl } from "../../src/media/iceValidation.ts";
|
||||
import type {
|
||||
AuthMode,
|
||||
FlightsSourceId,
|
||||
@@ -136,6 +137,16 @@ export interface AuthConfig {
|
||||
admins: AdminGrant;
|
||||
}
|
||||
|
||||
export interface IceConfig {
|
||||
configured: boolean;
|
||||
urls: string[];
|
||||
/** Server-only coturn REST shared secret. Never serialize this config. */
|
||||
sharedSecret: string;
|
||||
credentialTtlSeconds: number;
|
||||
rateAttempts: number;
|
||||
rateWindowSeconds: number;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -168,6 +179,7 @@ export interface Config {
|
||||
*/
|
||||
presence: { dir: string };
|
||||
auth: AuthConfig;
|
||||
ice: IceConfig;
|
||||
/** One sentence per demotion. Empty on a fully-configured box. */
|
||||
degraded: string[];
|
||||
}
|
||||
@@ -181,6 +193,7 @@ export function loadConfig(env: Env = process.env): Config {
|
||||
const flights = loadFlights(env, degraded);
|
||||
const satellites = loadSatellites(env, degraded);
|
||||
const auth = loadAuth(env, degraded);
|
||||
const ice = loadIce(env, degraded);
|
||||
// After auth, because a marker feed with nobody able to sign in is worth a
|
||||
// sentence and the sentence is only true once `mode` has finished demoting.
|
||||
const markers = loadMarkers(env, auth.mode, degraded);
|
||||
@@ -212,12 +225,59 @@ export function loadConfig(env: Env = process.env): Config {
|
||||
offices: { dir: str(env, "TERA_OFFICES_DIR", "") },
|
||||
presence: { dir: str(env, "TERA_PRESENCE_DIR", "") },
|
||||
auth,
|
||||
ice,
|
||||
degraded,
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Sections -------------------------------------------------------------
|
||||
|
||||
function loadIce(env: Env, degraded: string[]): IceConfig {
|
||||
const rawUrls = env.TERA_ICE_URLS;
|
||||
const rawSecret = env.TERA_TURN_SHARED_SECRET;
|
||||
const absent = (rawUrls === undefined || rawUrls.trim() === "") &&
|
||||
(rawSecret === undefined || rawSecret === "");
|
||||
const disabled: IceConfig = {
|
||||
configured: false,
|
||||
urls: [],
|
||||
sharedSecret: "",
|
||||
credentialTtlSeconds: 300,
|
||||
rateAttempts: 30,
|
||||
rateWindowSeconds: 60,
|
||||
};
|
||||
if (absent) return disabled;
|
||||
|
||||
const urls = (rawUrls ?? "").split(",").map((value) => value.trim()).filter(Boolean);
|
||||
const secret = rawSecret ?? "";
|
||||
const ttl = strictInteger(env.TERA_TURN_CREDENTIAL_TTL, 300, 60, 3_600);
|
||||
const attempts = strictInteger(env.TERA_ICE_RATE_ATTEMPTS, 30, 1, 300);
|
||||
const windowSeconds = strictInteger(env.TERA_ICE_RATE_WINDOW, 60, 1, 3_600);
|
||||
const urlsValid = urls.length > 0 && urls.length <= 8 && new Set(urls).size === urls.length &&
|
||||
urls.every(isSafeIceUrl) && urls.some((url) => url.startsWith("turn:") || url.startsWith("turns:"));
|
||||
const secretValid = secret.length >= 32 && secret.length <= 4_096 && secret === secret.trim() &&
|
||||
!/[\u0000-\u001f\u007f]/.test(secret);
|
||||
|
||||
if (!urlsValid || !secretValid || ttl === null || attempts === null || windowSeconds === null) {
|
||||
degraded.push("ICE credential service is disabled because its TURN configuration is incomplete or invalid.");
|
||||
return disabled;
|
||||
}
|
||||
return {
|
||||
configured: true,
|
||||
urls,
|
||||
sharedSecret: secret,
|
||||
credentialTtlSeconds: ttl,
|
||||
rateAttempts: attempts,
|
||||
rateWindowSeconds: windowSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
function strictInteger(raw: string | undefined, fallback: number, minimum: number, maximum: number): number | null {
|
||||
if (raw === undefined || raw === "") return fallback;
|
||||
if (!/^[0-9]+$/.test(raw)) return null;
|
||||
const parsed = Number(raw);
|
||||
return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum ? parsed : null;
|
||||
}
|
||||
|
||||
const WEATHER_SOURCES: WeatherSourceId[] = ["none", "nws", "metno", "openmeteo"];
|
||||
|
||||
function loadWeather(env: Env, degraded: string[]): WeatherConfig {
|
||||
|
||||
Reference in New Issue
Block a user