fix(turn): generate HMAC-SHA1 credentials for coturn use-auth-secret
coturn's use-auth-secret mode requires the TURN password to be HMAC-SHA1(secret, username), not the raw secret. Pass ice servers as a constructor argument so the async credential generation happens before RTCPeerConnection is created. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,13 +16,23 @@ const EKEY_PREFIX = 'yaw/2.1 ekey'
|
||||
const FS_TIMEOUT = 2000
|
||||
const STUN = 'stun:stun.l.google.com:19302'
|
||||
|
||||
function iceServers(): RTCIceServer[] {
|
||||
async function turnCredential(secret: string, username: string): Promise<string> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', new TextEncoder().encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-1' },
|
||||
false, ['sign']
|
||||
)
|
||||
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(username))
|
||||
return btoa(String.fromCharCode(...new Uint8Array(sig)))
|
||||
}
|
||||
|
||||
async function iceServers(): Promise<RTCIceServer[]> {
|
||||
const cfg = (window as unknown as { WASTE_CONFIG?: { turnURL?: string; turnSecret?: string } }).WASTE_CONFIG
|
||||
const servers: RTCIceServer[] = [{ urls: STUN }]
|
||||
if (cfg?.turnURL && cfg?.turnSecret) {
|
||||
// TURN with time-limited credentials derived from static-auth-secret
|
||||
const user = Math.floor(Date.now() / 1000) + 3600 + ':waste'
|
||||
servers.push({ urls: cfg.turnURL, username: user, credential: cfg.turnSecret })
|
||||
const credential = await turnCredential(cfg.turnSecret, user)
|
||||
servers.push({ urls: cfg.turnURL, username: user, credential })
|
||||
}
|
||||
return servers
|
||||
}
|
||||
@@ -267,14 +277,14 @@ class PeerConn {
|
||||
// push-initiated files waiting for file-accept: xid → File
|
||||
private _pushQueue: Map<string, File> = new Map()
|
||||
|
||||
constructor(identity: Identity, sig: Signaling, peerId: string, on: PeerCallback, nick: string, share: Map<string, File>) {
|
||||
constructor(identity: Identity, sig: Signaling, peerId: string, on: PeerCallback, nick: string, share: Map<string, File>, ice: RTCIceServer[]) {
|
||||
this.identity = identity
|
||||
this.sig = sig
|
||||
this.peerId = peerId
|
||||
this.on = on
|
||||
this.nick = nick
|
||||
this.share = share
|
||||
this.pc = new RTCPeerConnection({ iceServers: iceServers() })
|
||||
this.pc = new RTCPeerConnection({ iceServers: ice })
|
||||
const kp = sodium.crypto_box_keypair()
|
||||
this._esk = kp.privateKey
|
||||
this._epk = kp.publicKey
|
||||
@@ -715,8 +725,9 @@ export class BrowserAdapter {
|
||||
if (existing.verified || open || (alive && fresh)) return
|
||||
}
|
||||
|
||||
const ice = await iceServers()
|
||||
const peer = new PeerConn(this.identity, this.sig!, pid,
|
||||
(ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles)
|
||||
(ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles, ice)
|
||||
this.peers.set(pid, peer)
|
||||
this._emitDiscovered(pid)
|
||||
await peer.startOffer()
|
||||
@@ -726,8 +737,9 @@ export class BrowserAdapter {
|
||||
if (!this.identity || from === this.identity.id) return
|
||||
let peer = this.peers.get(from)
|
||||
if (!peer || peer.pc.connectionState === 'failed' || peer.pc.connectionState === 'closed') {
|
||||
const ice = await iceServers()
|
||||
peer = new PeerConn(this.identity, this.sig!, from,
|
||||
(ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles)
|
||||
(ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles, ice)
|
||||
this._emitDiscovered(from)
|
||||
this.peers.set(from, peer)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user