import assert from "node:assert/strict"; import { createHash, 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, deriveIceCredentialRateKeys } from "../media/index.ts"; import { parseIceConfigResponse } from "../../../src/media/iceValidation.ts"; import type { IceConfigGrant } from "../../../src/media/iceTypes.ts"; import type { ScreenShareCredential } from "../../../src/media/signalingTypes.ts"; import type { ScreenShareBinding } from "../../../src/media/signalingTypes.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 => ({ 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 BINDING = { officeId: "lumbridge-hq", levelId: "level-1", roomId: "lobby", screenId: "lobby-monitor" } as const; const SECOND_BINDING = { officeId: "lumbridge-hq", levelId: "level-1", roomId: "open-floor", screenId: "open-display" } as const; const PROVIDER_CREDENTIAL: ScreenShareCredential = { sessionId: "opaque-session", participantId: "opaque-participant", role: "presenter", grantToken: "opaque-grant-token-value", }; const iceRequest = (credential: ScreenShareCredential = PROVIDER_CREDENTIAL, binding: ScreenShareBinding = BINDING) => ({ type: "ice-config-request", protocolVersion: 1, requestId: "ice-request-1", binding: { ...binding }, credential: { ...credential }, } as const); const request = iceRequest(); const rateKeys = (subject = "auth-subject", trustedIp = "203.0.113.8") => ({ subject, trustedIp }); 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 quota identity and the standard expiration:HMAC-SHA1 credential", () => { const provider = createIceCredentialProvider(configured(), { now: () => 1_700_000_000_500, }); const result = provider.issue(request, rateKeys(), 1_700_000_090_000); 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; const suffix = createHmac("sha256", SHARED_SECRET) .update("tera-turn-user-v1\0").update(request.credential.sessionId) .update("\0").update(request.credential.participantId).digest("base64url"); assert.equal(turn.username, `1700000090:${suffix}`); 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, rateKeys(), Date.now() + 90_000); assert.equal(closed.ok, false); if (!closed.ok) assert.equal(closed.code, "unavailable"); const limited = createIceCredentialProvider(configured({ rateAttempts: 1 }), { now: () => 10_000, }); assert.equal(limited.issue(request, rateKeys(), 100_000).ok, true); const second = limited.issue(request, rateKeys(), 100_000); assert.equal(second.ok, false); if (!second.ok) assert.equal(second.code, "rate-limited"); assert.equal(limited.issue(request, rateKeys("other-subject", "other-ip"), 100_000).ok, true); }); it("enforces independent subject and trusted-IP buckets", () => { const bySubject = createIceCredentialProvider(configured({ rateAttempts: 1 }), { now: () => 10_000 }); assert.equal(bySubject.issue(request, rateKeys("subject-a", "ip-a"), 100_000).ok, true); const rotatedIp = bySubject.issue(request, rateKeys("subject-a", "ip-b"), 100_000); assert.equal(rotatedIp.ok, false, "changing address cannot evade a subject limit"); const byIp = createIceCredentialProvider(configured({ rateAttempts: 1 }), { now: () => 10_000 }); assert.equal(byIp.issue(request, rateKeys("subject-a", "shared-ip"), 100_000).ok, true); const sharedNat = byIp.issue(request, rateKeys("subject-b", "shared-ip"), 100_000); assert.equal(sharedNat.ok, false, "changing account cannot evade an address limit"); }); it("retains keyed domain-separated rate pseudonyms, not raw or plain hashes", () => { const identity = rateKeys("stable-auth-subject", "203.0.113.42"); const first = deriveIceCredentialRateKeys(SHARED_SECRET, identity); const second = deriveIceCredentialRateKeys(`${SHARED_SECRET}-rotated`, identity); assert.notEqual(first.subject, identity.subject); assert.notEqual(first.ip, identity.trustedIp); assert.notEqual(first.subject, first.ip); assert.notDeepEqual(first, second, "server secret rotation changes both pseudonyms"); assert.notEqual(first.subject, createHash("sha256").update(`tera-ice-subject-v1\0${identity.subject}`).digest("hex")); assert.notEqual(first.ip, createHash("sha256").update(`tera-ice-ip-v1\0${identity.trustedIp}`).digest("hex")); }); it("keeps the client-rate table bounded under address churn", () => { let time = 10_000; const provider = createIceCredentialProvider(configured({ rateAttempts: 2 }), { now: () => time, }); for (let index = 0; index < 4_096; index += 1) { assert.equal(provider.issue(request, rateKeys(`subject-${index}`, `ip-${index}`), 100_000).ok, true); } const full = provider.issue(request, rateKeys("subject-over-cap", "ip-over-cap"), 100_000); assert.equal(full.ok, false); if (!full.ok) assert.equal(full.code, "rate-limited"); time += 60_000; assert.equal(provider.issue(request, rateKeys("subject-after-window", "ip-after-window"), 200_000).ok, true); }); it("keeps the quota suffix stable per opaque participant and distinct across participants", () => { let time = 1_700_000_000_500; const provider = createIceCredentialProvider(configured({ rateAttempts: 10 }), { now: () => time }); const first = provider.issue(request, rateKeys("subject-a", "ip-a"), 1_700_000_090_000); time += 1_000; const second = provider.issue(request, rateKeys("subject-a", "ip-a"), 1_700_000_090_000); const another = provider.issue( iceRequest({ ...request.credential, participantId: "another-opaque-participant" }), rateKeys("subject-b", "ip-b"), 1_700_000_090_000, ); assert.ok(first.ok && second.ok && another.ok); if (!first.ok || !second.ok || !another.ok) return; const username = (value: IceConfigGrant) => { const turn = value.iceServers.find((server) => "username" in server); assert.ok(turn && "username" in turn); return turn && "username" in turn ? turn.username : ""; }; const suffix = (value: IceConfigGrant) => username(value).slice(username(value).indexOf(":") + 1); assert.equal(suffix(first.value), suffix(second.value)); assert.notEqual(suffix(first.value), suffix(another.value)); }); 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("issues private ephemeral credentials to active presenter and viewer grants", 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 presenterJoin = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("presenter-subject"), payload: { type: "screen-share-create-request", protocolVersion: 1, requestId: "create-1", sequence: 1, timestampMs: 1_000, binding: BINDING, role: "presenter", }, }); const presenter = presenterJoin.json<{ grant: { credential: ScreenShareCredential } }>(); const malformed = await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("presenter-subject"), payload: { ...iceRequest(presenter.grant.credential), profile: "do-not-send" }, }); assert.equal(malformed.statusCode, 400); const response = await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("presenter-subject"), payload: iceRequest(presenter.grant.credential), }); 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(); assert.equal(parseIceConfigResponse(body).ok, true); assert.equal(JSON.stringify(body).includes("presenter-subject"), false); assert.ok(body.expiresAtMs <= Date.now() + 90_000, "TURN credential cannot outlive the signaling grant"); const viewerJoin = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("viewer-subject"), payload: { type: "screen-share-join-request", protocolVersion: 1, requestId: "join-1", sequence: 1, timestampMs: 1_001, binding: BINDING, role: "viewer", viewerOptIn: true, }, }); const viewer = viewerJoin.json<{ grant: { credential: ScreenShareCredential } }>(); const viewerIce = await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("viewer-subject"), payload: iceRequest(viewer.grant.credential), }); assert.equal(viewerIce.statusCode, 200); }); it("rejects missing, stolen, wrong-binding, stopped, and revoked signaling grants", 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()); const createPayload = { type: "screen-share-create-request", protocolVersion: 1, requestId: "create-negative", sequence: 1, timestampMs: 1_000, binding: BINDING, role: "presenter", }; const created = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("presenter"), payload: createPayload }); const presenter = created.json<{ grant: { credential: ScreenShareCredential } }>().grant.credential; const missing = { ...iceRequest(presenter) } as Record; delete missing.credential; assert.equal((await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("presenter"), payload: missing })).statusCode, 400); const stolen = await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("thief"), payload: iceRequest(presenter) }); assert.equal(stolen.statusCode, 401, stolen.body); assert.equal((await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("presenter"), payload: iceRequest(presenter, SECOND_BINDING) })).statusCode, 401); const joined = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("viewer"), payload: { type: "screen-share-join-request", protocolVersion: 1, requestId: "join-negative", sequence: 1, timestampMs: 1_001, binding: BINDING, role: "viewer", viewerOptIn: true, } }); const viewer = joined.json<{ grant: { credential: ScreenShareCredential } }>().grant.credential; const revoked = await app.inject({ method: "POST", url: "/api/v1/media/leave", headers: bearer("viewer"), payload: { type: "screen-share-revoke-request", protocolVersion: 1, requestId: "viewer-left", sequence: 2, timestampMs: 1_002, binding: BINDING, credential: viewer, scope: "participant", targetParticipantId: viewer.participantId, reason: "viewer-left", } }); assert.equal(revoked.statusCode, 204); const revokedIce = await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("viewer"), payload: iceRequest(viewer) }); assert.equal(revokedIce.statusCode, 401, revokedIce.body); const stopped = await app.inject({ method: "POST", url: "/api/v1/media/leave", headers: bearer("presenter"), payload: { type: "screen-share-stop-request", protocolVersion: 1, requestId: "presenter-stop", sequence: 2, timestampMs: 1_003, binding: BINDING, credential: presenter, reason: "presenter-stopped", } }); assert.equal(stopped.statusCode, 204); assert.equal((await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("presenter"), payload: iceRequest(presenter) })).statusCode, 401); }); 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 created = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("auth-subject"), payload: { type: "screen-share-create-request", protocolVersion: 1, requestId: "create-unavailable", sequence: 1, timestampMs: 1_000, binding: BINDING, role: "presenter", } }); const credential = created.json<{ grant: { credential: ScreenShareCredential } }>().grant.credential; const response = await app.inject({ method: "POST", url: "/api/v1/media/ice", headers: bearer("auth-subject"), payload: iceRequest(credential), }); assert.equal(response.statusCode, 503, response.body); assert.equal(response.json().type, "ice-config-unavailable"); assert.equal(JSON.stringify(response.json()).includes("credential"), false); }); });