feat: stream private office screens
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { createHmac, randomBytes } from "node:crypto";
|
||||
import {
|
||||
ICE_CONFIG_PROTOCOL_VERSION,
|
||||
type IceConfigGrant,
|
||||
type IceConfigRequest,
|
||||
type IceConfigUnavailable,
|
||||
type StunIceServer,
|
||||
type TurnIceServer,
|
||||
} from "../../../src/media/iceTypes.ts";
|
||||
import type { IceConfig } from "../config.ts";
|
||||
|
||||
export type IceCredentialResult =
|
||||
| { ok: true; value: IceConfigGrant }
|
||||
| { ok: false; code: "unavailable" | "rate-limited"; value: IceConfigUnavailable };
|
||||
|
||||
export interface IceCredentialProvider {
|
||||
issue(request: IceConfigRequest, rateKey: string): IceCredentialResult;
|
||||
}
|
||||
|
||||
export interface IceCredentialProviderOptions {
|
||||
now?: () => number;
|
||||
nonce?: () => string;
|
||||
}
|
||||
|
||||
interface RateEntry { startedAtMs: number; count: number }
|
||||
const MAX_RATE_KEYS = 4_096;
|
||||
|
||||
export function createIceCredentialProvider(
|
||||
config: IceConfig,
|
||||
options: IceCredentialProviderOptions = {},
|
||||
): IceCredentialProvider {
|
||||
const now = options.now ?? Date.now;
|
||||
const nonce = options.nonce ?? (() => randomBytes(18).toString("base64url"));
|
||||
const rates = new Map<string, RateEntry>();
|
||||
const windowMs = config.rateWindowSeconds * 1_000;
|
||||
|
||||
return {
|
||||
issue(request, rateKey) {
|
||||
const at = now();
|
||||
if (!config.configured) return unavailable(request.requestId, windowMs, "unavailable");
|
||||
let entry = rates.get(rateKey);
|
||||
if (!entry && rates.size >= MAX_RATE_KEYS) {
|
||||
for (const [key, value] of rates) {
|
||||
if (at - value.startedAtMs >= windowMs) rates.delete(key);
|
||||
}
|
||||
if (rates.size >= MAX_RATE_KEYS) {
|
||||
return unavailable(request.requestId, windowMs, "rate-limited");
|
||||
}
|
||||
entry = rates.get(rateKey);
|
||||
}
|
||||
if (!entry || at - entry.startedAtMs >= windowMs) {
|
||||
rates.set(rateKey, { startedAtMs: at, count: 1 });
|
||||
} else {
|
||||
entry.count += 1;
|
||||
if (entry.count > config.rateAttempts) {
|
||||
return unavailable(request.requestId, Math.max(1_000, windowMs - (at - entry.startedAtMs)), "rate-limited");
|
||||
}
|
||||
}
|
||||
|
||||
const expiresAtSeconds = Math.floor(at / 1_000) + config.credentialTtlSeconds;
|
||||
// Coturn REST convention: expiration timestamp, colon, opaque username.
|
||||
// No auth subject, email, profile id, IP address, or stable browser id.
|
||||
const username = `${expiresAtSeconds}:${nonce()}`;
|
||||
const credential = createHmac("sha1", config.sharedSecret).update(username).digest("base64");
|
||||
const stunUrls = config.urls.filter((url) => url.startsWith("stun:") || url.startsWith("stuns:"));
|
||||
const turnUrls = config.urls.filter((url) => url.startsWith("turn:") || url.startsWith("turns:"));
|
||||
const iceServers: Array<StunIceServer | TurnIceServer> = [];
|
||||
if (stunUrls.length > 0) iceServers.push({ urls: stunUrls });
|
||||
iceServers.push({ urls: turnUrls, username, credential, credentialType: "password" });
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
type: "ice-config-grant",
|
||||
protocolVersion: ICE_CONFIG_PROTOCOL_VERSION,
|
||||
requestId: request.requestId,
|
||||
issuedAtMs: at,
|
||||
expiresAtMs: expiresAtSeconds * 1_000,
|
||||
iceServers,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function unavailable(
|
||||
requestId: string,
|
||||
retryAfterMs: number,
|
||||
code: "unavailable" | "rate-limited",
|
||||
): IceCredentialResult {
|
||||
return {
|
||||
ok: false,
|
||||
code,
|
||||
value: {
|
||||
type: "ice-config-unavailable",
|
||||
protocolVersion: ICE_CONFIG_PROTOCOL_VERSION,
|
||||
requestId,
|
||||
retryAfterMs: Math.min(3_600_000, Math.max(1_000, Math.ceil(retryAfterMs))),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user