Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions src/app/api/ice/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@ import { NextResponse } from 'next/server'
import crypto from 'crypto'
import { setTurnCredentials } from '../../../coturn'

const ICE_RATE_LIMIT_WINDOW_MS = 60_000
const ICE_RATE_LIMIT_MAX_REQUESTS = 10

type IceRateLimitEntry = {
count: number
expiresAt: number
}

const iceRateLimitStore =
(globalThis as typeof globalThis & { __iceRateLimitStore?: Map<string, IceRateLimitEntry> }).__iceRateLimitStore ||
new Map<string, IceRateLimitEntry>()
;(globalThis as typeof globalThis & { __iceRateLimitStore?: Map<string, IceRateLimitEntry> }).__iceRateLimitStore = iceRateLimitStore

const turnHost = process.env.TURN_HOST || '127.0.0.1'
const stunServer = process.env.STUN_SERVER || 'stun:stun.l.google.com:19302'
const peerjsHost = process.env.PEERJS_HOST || '0.peerjs.com'
const peerjsPath = process.env.PEERJS_PATH || '/'

export async function POST(): Promise<NextResponse> {
export async function POST(request: Request): Promise<NextResponse> {
if (!process.env.COTURN_ENABLED) {
return NextResponse.json({
host: peerjsHost,
Expand All @@ -16,12 +29,31 @@ export async function POST(): Promise<NextResponse> {
})
}

// Generate ephemeral credentials
const expectedToken = process.env.ICE_API_TOKEN
const providedToken = request.headers.get('authorization')?.replace(/^Bearer\s+/i, '').trim()
if (!expectedToken || providedToken !== expectedToken) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const forwardedFor = request.headers.get('x-forwarded-for')
const realIp = request.headers.get('x-real-ip')
const ip = forwardedFor?.split(',')[0]?.trim() || realIp || 'unknown'
const now = Date.now()
const rateLimitEntry = iceRateLimitStore.get(ip)

if (!rateLimitEntry || rateLimitEntry.expiresAt <= now) {
iceRateLimitStore.set(ip, { count: 1, expiresAt: now + ICE_RATE_LIMIT_WINDOW_MS })
} else if (rateLimitEntry.count >= ICE_RATE_LIMIT_MAX_REQUESTS) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 })
} else {
rateLimitEntry.count += 1
iceRateLimitStore.set(ip, rateLimitEntry)
}

const username = crypto.randomBytes(8).toString('hex')
const password = crypto.randomBytes(8).toString('hex')
const ttl = 86400 // 24 hours
const ttl = 600

// Store credentials in Redis
await setTurnCredentials(username, password, ttl)

return NextResponse.json({
Expand Down