p2p-swift / Sources / IrdP2PSignaling / TurnCredentials.swift
451 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
import Foundation

/// One ICE server entry, ready for `RTCIceServer`.
public struct IceServer: Sendable, Hashable, Codable {
    public var urls: [String]
    public var username: String?
    public var credential: String?

    public init(urls: [String], username: String? = nil, credential: String? = nil) {
        self.urls = urls
        self.username = username
        self.credential = credential
    }
}

/// The ICE configuration for a peer connection.
public struct IceConfiguration: Sendable, Hashable {
    public var servers: [IceServer]
    /// Only relayed candidates (TURN) — for testing the fallback path.
    public var relayOnly: Bool

    public init(servers: [IceServer], relayOnly: Bool = false) {
        self.servers = servers
        self.relayOnly = relayOnly
    }

    public static let none = IceConfiguration(servers: [])
}

/// `GET /turn-credentials` response.
struct TurnCredentialsResponse: Decodable {
    var username: String
    var credential: String
    var ttl: Int
    var urls: [String]
}

/// Fetches and caches TURN credentials: renewed 5 minutes before expiry, one
/// in-flight fetch shared by all callers, failures cached for 30 seconds and
/// degraded to an empty server list (direct connections still work).
public actor IceServerCache {
    public static let renewBefore: Duration = .seconds(300)
    public static let failureRetryAfter: Duration = .seconds(30)

    private let url: URL?
    private let origin: String?
    private let http: any HTTPTransport
    private let clock: any P2PClock
    private let relayOnly: Bool
    private let timeout: TimeInterval

    private var cached: (config: IceConfiguration, validUntil: Duration)?
    private var inflight: Task<IceConfiguration, Never>?

    public init(url: URL?, origin: String?, http: any HTTPTransport, clock: any P2PClock = SystemClock(), relayOnly: Bool = false, timeout: TimeInterval = 10) {
        self.url = url
        self.origin = origin
        self.http = http
        self.clock = clock
        self.relayOnly = relayOnly
        self.timeout = timeout
    }

    public func configuration() async -> IceConfiguration {
        if let cached, clock.now < cached.validUntil { return cached.config }
        if let inflight { return await inflight.value }
        let task = Task { await self.fetch() }
        inflight = task
        let result = await task.value
        inflight = nil
        return result
    }

    /// Drops the cache so the next call fetches again (e.g. after an ICE failure).
    public func invalidate() {
        cached = nil
    }

    private func fetch() async -> IceConfiguration {
        guard let url else {
            let config = IceConfiguration(servers: [], relayOnly: relayOnly)
            cached = (config, clock.now + .seconds(3600))
            return config
        }
        do {
            let response = try await http.send(.p2p(url, origin: origin, timeout: timeout))
            guard response.isSuccess else { throw P2PError.http(status: response.status, code: nil, body: response.body) }
            let creds = try JSONDecoder().decode(TurnCredentialsResponse.self, from: response.body)
            let config = IceConfiguration(servers: [IceServer(urls: creds.urls, username: creds.username, credential: creds.credential)], relayOnly: relayOnly)
            let ttl = Duration.seconds(max(0, creds.ttl))
            let valid = ttl > IceServerCache.renewBefore ? ttl - IceServerCache.renewBefore : ttl / 2
            cached = (config, clock.now + valid)
            return config
        } catch {
            let config = IceConfiguration(servers: [], relayOnly: relayOnly)
            cached = (config, clock.now + IceServerCache.failureRetryAfter)
            return config
        }
    }
}

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/p2p-swift.git