Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Calls/RTCIceServerFetcher.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalRingRTC
import SignalServiceKit
import WebRTC

struct RTCIceServerFetcher {
    private let networkManager: NetworkManager

    /// A cache to limit requests for relay servers.
    private struct Cache {
        let servers: [RTCIceServer]
        let expirationTimestamp: MonotonicDate

        var isValid: Bool {
            return MonotonicDate() < expirationTimestamp
        }

        static func update(servers: [RTCIceServer], ttl: Int) -> Cache {
            return Cache(
                servers: servers,
                expirationTimestamp: MonotonicDate().adding(TimeInterval(ttl)),
            )
        }
    }

    private static let lock = NSRecursiveLock()

    /// Should only be accessed while holding `lock`.
    private static var _cache: Cache?
    private static var cache: Cache? {
        get {
            return lock.withLock {
                return _cache
            }
        }
        set {
            lock.withLock {
                _cache = newValue
            }
        }
    }

    init(networkManager: NetworkManager) {
        self.networkManager = networkManager
    }

    // MARK: -

    /// RTCIceServers are used when attempting to establish an optimal
    /// connection to the other party. SignalService supplies a list of servers.
    func getIceServers() async throws -> [RTCIceServer] {
        if let cache = Self.cache, cache.isValid {
            return cache.servers
        }

        let request = OWSRequestFactory.callingRelaysRequest()
        let response = try await networkManager.asyncRequest(request)

        guard let jsonData = response.responseBodyData else {
            throw OWSAssertionError("Missing or invalid JSON!")
        }

        let (servers, ttl) = try Self.parse(turnServerInfoJsonData: jsonData)

        Self.cache = Cache.update(servers: servers, ttl: ttl)

        return servers
    }

    // MARK: -

    static func parse(turnServerInfoJsonData: Data) throws -> ([RTCIceServer], Int) {
        let relays = try JSONDecoder().decode(
            CallingRelays.self,
            from: turnServerInfoJsonData,
        ).relays

        var minTTL: Int = Int.max

        /// We want to order our returned ICE servers firstly by the order in
        /// which the server info objects appeared in the parsed response. Then,
        /// within each server-info object we want to return ICE servers for the
        /// contained URLs with IPs first, then for the URLs without IPs.
        let servers = relays.flatMap { turnServer -> [RTCIceServer] in
            // Use *any* provided ttl values as long as they are valid.
            if let ttl = turnServer.ttl, ttl > 0 {
                minTTL = min(minTTL, ttl)
            }

            let serversWithIP = turnServer.urlsWithIps.map { urlWithIP in
                return RTCIceServer(
                    urlStrings: [urlWithIP],
                    username: turnServer.username,
                    credential: turnServer.password,
                    tlsCertPolicy: .secure,
                    hostname: turnServer.hostname ?? "",
                )
            }

            let serversWithoutIP = turnServer.urls.map { urlWithoutIP in
                return RTCIceServer(
                    urlStrings: [urlWithoutIP],
                    username: turnServer.username,
                    credential: turnServer.password,
                )
            }

            return serversWithIP + serversWithoutIP
        }

        // If no ttl value was found, 0 will disable the cache.
        return (servers, minTTL == Int.max ? 0 : minTTL)
    }
}

// MARK: -

/// Represents a calling relays response.
private struct CallingRelays: Decodable {
    struct TurnServer: Decodable {
        let username: String
        let password: String
        let urls: [String]
        let urlsWithIps: [String]
        let hostname: String?
        let ttl: Int?
    }

    let relays: [TurnServer]
}