feat: stream private office screens
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHmac } from "node:crypto";
|
||||
import { after, describe, it } from "node:test";
|
||||
import { buildApp } from "../app.ts";
|
||||
import { loadConfig, type IceConfig } from "../config.ts";
|
||||
import { createIceCredentialProvider } from "../media/index.ts";
|
||||
import { parseIceConfigResponse } from "../../../src/media/iceValidation.ts";
|
||||
import type { IceConfigGrant } from "../../../src/media/iceTypes.ts";
|
||||
|
||||
const SHARED_SECRET = "turn-shared-secret-with-at-least-thirty-two-bytes";
|
||||
const JWT_SECRET = "ice-route-jwt-secret-that-is-long-enough";
|
||||
|
||||
const configured = (overrides: Partial<IceConfig> = {}): IceConfig => ({
|
||||
configured: true,
|
||||
urls: ["stun:relay.example.test:3478", "turn:relay.example.test:3478", "turns:relay.example.test:5349"],
|
||||
sharedSecret: SHARED_SECRET,
|
||||
credentialTtlSeconds: 600,
|
||||
rateAttempts: 2,
|
||||
rateWindowSeconds: 60,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const request = { type: "ice-config-request", protocolVersion: 1, requestId: "ice-request-1" } as const;
|
||||
|
||||
function bearer(subject: string) {
|
||||
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
||||
const signed = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ sub: subject, exp: Math.floor(Date.now() / 1000) + 600 })}`;
|
||||
return { authorization: `Bearer ${signed}.${createHmac("sha256", JWT_SECRET).update(signed).digest("base64url")}` };
|
||||
}
|
||||
|
||||
describe("coturn REST ICE credential provider", () => {
|
||||
it("uses an opaque nonce and the standard expiration:HMAC-SHA1 credential", () => {
|
||||
const provider = createIceCredentialProvider(configured(), {
|
||||
now: () => 1_700_000_000_500,
|
||||
nonce: () => "opaque_random_nonce_1234",
|
||||
});
|
||||
const result = provider.issue(request, "rate-key");
|
||||
assert.equal(result.ok, true);
|
||||
if (!result.ok) return;
|
||||
const turn = result.value.iceServers.find((server) => "username" in server);
|
||||
assert.ok(turn && "username" in turn);
|
||||
if (!turn || !("username" in turn)) return;
|
||||
assert.equal(turn.username, "1700000600:opaque_random_nonce_1234");
|
||||
assert.equal(turn.credential, createHmac("sha1", SHARED_SECRET).update(turn.username).digest("base64"));
|
||||
assert.equal(turn.username.includes("auth-subject"), false);
|
||||
assert.equal(JSON.stringify(result.value).includes(SHARED_SECRET), false);
|
||||
assert.equal(parseIceConfigResponse(result.value).ok, true);
|
||||
});
|
||||
|
||||
it("fails closed when unconfigured and rate bounds issuance", () => {
|
||||
const unavailable = createIceCredentialProvider(configured({ configured: false, sharedSecret: "", urls: [] }));
|
||||
const closed = unavailable.issue(request, "caller");
|
||||
assert.equal(closed.ok, false);
|
||||
if (!closed.ok) assert.equal(closed.code, "unavailable");
|
||||
const limited = createIceCredentialProvider(configured({ rateAttempts: 1 }), {
|
||||
now: () => 10_000, nonce: () => "opaque_random_nonce_1234",
|
||||
});
|
||||
assert.equal(limited.issue(request, "same-ip").ok, true);
|
||||
const second = limited.issue(request, "same-ip");
|
||||
assert.equal(second.ok, false);
|
||||
if (!second.ok) assert.equal(second.code, "rate-limited");
|
||||
assert.equal(limited.issue(request, "other-ip").ok, true);
|
||||
});
|
||||
|
||||
it("keeps the client-rate table bounded under address churn", () => {
|
||||
let time = 10_000;
|
||||
const provider = createIceCredentialProvider(configured({ rateAttempts: 2 }), {
|
||||
now: () => time, nonce: () => "opaque_random_nonce_1234",
|
||||
});
|
||||
for (let index = 0; index < 4_096; index += 1) {
|
||||
assert.equal(provider.issue(request, `client-${index}`).ok, true);
|
||||
}
|
||||
const full = provider.issue(request, "client-over-cap");
|
||||
assert.equal(full.ok, false);
|
||||
if (!full.ok) assert.equal(full.code, "rate-limited");
|
||||
time += 60_000;
|
||||
assert.equal(provider.issue(request, "client-after-window").ok, true);
|
||||
});
|
||||
|
||||
it("strictly disables partial or malformed environment configuration", () => {
|
||||
assert.equal(loadConfig({ TERA_ICE_URLS: "turn:relay.example.test:3478" }).ice.configured, false);
|
||||
assert.equal(loadConfig({ TERA_TURN_SHARED_SECRET: SHARED_SECRET }).ice.configured, false);
|
||||
assert.equal(loadConfig({
|
||||
TERA_ICE_URLS: "turn:user@relay.example.test:3478", TERA_TURN_SHARED_SECRET: SHARED_SECRET,
|
||||
}).ice.configured, false);
|
||||
assert.equal(loadConfig({
|
||||
TERA_ICE_URLS: "turn:relay.example.test:3478", TERA_TURN_SHARED_SECRET: SHARED_SECRET,
|
||||
TERA_TURN_CREDENTIAL_TTL: "60.5",
|
||||
}).ice.configured, false);
|
||||
const valid = loadConfig({
|
||||
TERA_ICE_URLS: "stun:relay.example.test:3478, turns:relay.example.test:5349",
|
||||
TERA_TURN_SHARED_SECRET: SHARED_SECRET,
|
||||
});
|
||||
assert.equal(valid.ice.configured, true);
|
||||
assert.equal(valid.degraded.some((line) => line.includes(SHARED_SECRET)), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticated ICE route", () => {
|
||||
it("returns private ephemeral credentials only to authenticated POST callers", async () => {
|
||||
const config = loadConfig({
|
||||
TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: JWT_SECRET,
|
||||
TERA_ICE_URLS: "turn:relay.example.test:3478", TERA_TURN_SHARED_SECRET: SHARED_SECRET,
|
||||
});
|
||||
config.logLevel = "silent";
|
||||
const app = buildApp(config); after(() => app.close());
|
||||
assert.equal((await app.inject({ method: "GET", url: "/api/v1/media/ice" })).statusCode, 404);
|
||||
assert.equal((await app.inject({ method: "POST", url: "/api/v1/media/ice", payload: request })).statusCode, 401);
|
||||
const malformed = await app.inject({
|
||||
method: "POST", url: "/api/v1/media/ice", headers: bearer("auth-subject"),
|
||||
payload: { ...request, profile: "do-not-send" },
|
||||
});
|
||||
assert.equal(malformed.statusCode, 400);
|
||||
const response = await app.inject({
|
||||
method: "POST", url: "/api/v1/media/ice", headers: bearer("auth-subject"), payload: request,
|
||||
});
|
||||
assert.equal(response.statusCode, 200);
|
||||
assert.equal(response.headers["cache-control"], "private, no-store");
|
||||
assert.equal(response.headers.pragma, "no-cache");
|
||||
assert.equal(response.headers.location, undefined);
|
||||
const body = response.json<IceConfigGrant>();
|
||||
assert.equal(parseIceConfigResponse(body).ok, true);
|
||||
assert.equal(JSON.stringify(body).includes("auth-subject"), false);
|
||||
});
|
||||
|
||||
it("returns typed unavailable without emitting credentials when TURN is absent", async () => {
|
||||
const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: JWT_SECRET });
|
||||
config.logLevel = "silent";
|
||||
const app = buildApp(config); after(() => app.close());
|
||||
const response = await app.inject({
|
||||
method: "POST", url: "/api/v1/media/ice", headers: bearer("auth-subject"), payload: request,
|
||||
});
|
||||
assert.equal(response.statusCode, 503);
|
||||
assert.equal(response.json().type, "ice-config-unavailable");
|
||||
assert.equal(JSON.stringify(response.json()).includes("credential"), false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user