|
1 | 1 | import { WebSocket, WebSocketServer } from "ws"; |
2 | 2 | import express from "express"; |
3 | 3 | import * as jose from "jose"; |
| 4 | +import * as crypto from "crypto"; |
4 | 5 | import { prisma } from "./db"; |
5 | | -import { NotFoundError, UnprocessableEntityError } from "./errors"; |
| 6 | +import { BadRequestError, InternalServerError, NotFoundError, UnprocessableEntityError } from "./errors"; |
6 | 7 | import { activeConnections, iceServers, inFlight } from "./webrtc-signaling"; |
7 | 8 |
|
| 9 | +const CLOUDFLARE_TURN_ID = process.env.CLOUDFLARE_TURN_ID; |
| 10 | +const CLOUDFLARE_TURN_TOKEN = process.env.CLOUDFLARE_TURN_TOKEN; |
| 11 | +const COTURN_TURN_URLS = process.env.COTURN_TURN_URLS?.split(",") |
| 12 | + .map(url => url.trim()) |
| 13 | + .filter(Boolean); |
| 14 | +const COTURN_TURN_SECRET = process.env.COTURN_TURN_SECRET; |
| 15 | +const TURN_TTL = Number.parseInt(process.env.TURN_TTL ?? "", 10) || 3600; |
| 16 | + |
8 | 17 | export const CreateSession = async (req: express.Request, res: express.Response) => { |
9 | 18 | const idToken = req.session?.id_token; |
10 | 19 | const { sub } = jose.decodeJwt(idToken); |
@@ -102,31 +111,61 @@ export const CreateIceCredentials = async ( |
102 | 111 | req: express.Request, |
103 | 112 | res: express.Response, |
104 | 113 | ) => { |
105 | | - const resp = await fetch( |
106 | | - `https://rtc.live.cloudflare.com/v1/turn/keys/${process.env.CLOUDFLARE_TURN_ID}/credentials/generate`, |
107 | | - { |
108 | | - method: "POST", |
109 | | - headers: { |
110 | | - Authorization: `Bearer ${process.env.CLOUDFLARE_TURN_TOKEN}`, |
111 | | - "Content-Type": "application/json", |
112 | | - }, |
113 | | - body: JSON.stringify({ ttl: 3600 }), |
114 | | - }, |
115 | | - ); |
| 114 | + const idToken = req.session?.id_token; |
| 115 | + if (!idToken) { |
| 116 | + throw new UnprocessableEntityError("Missing ID token"); |
| 117 | + } |
| 118 | + const { sub } = jose.decodeJwt(idToken); |
116 | 119 |
|
117 | | - const data = (await resp.json()) as { |
118 | | - iceServers: { credential?: string; urls: string | string[]; username?: string }; |
| 120 | + let iceConfig: { |
| 121 | + iceServers: { urls: string | string[]; username?: string, credential?: string } |
119 | 122 | }; |
120 | 123 |
|
121 | | - if (!data.iceServers.urls) { |
122 | | - throw new Error("No ice servers returned"); |
123 | | - } |
| 124 | + if (CLOUDFLARE_TURN_ID && CLOUDFLARE_TURN_TOKEN) { |
| 125 | + const resp = await fetch( |
| 126 | + `https://rtc.live.cloudflare.com/v1/turn/keys/${CLOUDFLARE_TURN_ID}/credentials/generate`, |
| 127 | + { |
| 128 | + method: "POST", |
| 129 | + headers: { |
| 130 | + Authorization: `Bearer ${CLOUDFLARE_TURN_TOKEN}`, |
| 131 | + "Content-Type": "application/json", |
| 132 | + }, |
| 133 | + body: JSON.stringify({ ttl: TURN_TTL }), |
| 134 | + }, |
| 135 | + ); |
| 136 | + |
| 137 | + const cloudflareIceConfig = await resp.json() as { |
| 138 | + iceServers: { urls: string | string[]; username?: string, credential?: string } |
| 139 | + }; |
| 140 | + |
| 141 | + if (!cloudflareIceConfig?.iceServers.urls) { |
| 142 | + throw new InternalServerError("No ice servers returned"); |
| 143 | + } |
| 144 | + |
| 145 | + if (cloudflareIceConfig.iceServers.urls instanceof Array) { |
| 146 | + cloudflareIceConfig.iceServers.urls = cloudflareIceConfig.iceServers.urls.filter(url => !url.startsWith("turns")); |
| 147 | + } |
124 | 148 |
|
125 | | - if (data.iceServers.urls instanceof Array) { |
126 | | - data.iceServers.urls = data.iceServers.urls.filter(url => !url.startsWith("turns")); |
| 149 | + iceConfig = cloudflareIceConfig; |
| 150 | + } else if (COTURN_TURN_URLS && COTURN_TURN_SECRET && COTURN_TURN_URLS.length > 0) { |
| 151 | + const username = `${Math.floor(Date.now() / 1000) + TURN_TTL}:${sub}`; |
| 152 | + const credential = crypto |
| 153 | + .createHmac("sha1", COTURN_TURN_SECRET) |
| 154 | + .update(username) |
| 155 | + .digest("base64"); |
| 156 | + |
| 157 | + iceConfig = { |
| 158 | + iceServers: { |
| 159 | + urls: COTURN_TURN_URLS, |
| 160 | + username: username, |
| 161 | + credential: credential, |
| 162 | + } |
| 163 | + }; |
| 164 | + } else { |
| 165 | + throw new BadRequestError("No TURN configuration available", "no_turn_configuration"); |
127 | 166 | } |
128 | 167 |
|
129 | | - return res.json(data); |
| 168 | + return res.json(iceConfig); |
130 | 169 | }; |
131 | 170 |
|
132 | 171 | export const CreateTurnActivity = async (req: express.Request, res: express.Response) => { |
|
0 commit comments