p2p-swift / Sources / IrdP2PSignaling / Mailbox / MailboxToken.swift
507 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
import Foundation

/// Supplies bearer tokens for the mailbox. Implement it to hand the SDK tokens
/// minted by your own backend (the path for apps whose `mailbox_open_mint` is off).
public protocol MailboxTokenProvider: Sendable {
    /// A token valid for `room` (app-wide tokens qualify).
    func token(for room: String) async throws -> MailboxToken
    /// The node refused the token (401/403): forget it so the next call mints afresh.
    func invalidate(for room: String) async
}

/// Mints tokens at the node's open mint (`GET <mint>?room=&ttl=`), sending the
/// configured `Origin`. App-scoped by default so one mint covers every room;
/// renewed five minutes before expiry; one in-flight mint at a time.
public actor OpenMintTokenProvider: MailboxTokenProvider {
    public static let renewBefore: TimeInterval = 300

    private let mintURL: URL
    private let origin: String?
    private let http: any HTTPTransport
    private let ttlSeconds: Int
    private let scopeToRoom: Bool
    private let now: @Sendable () -> Date
    private let timeout: TimeInterval
    private var cache: [String: MailboxToken] = [:]
    private var inflight: [String: Task<MailboxToken, any Error>] = [:]

    public init(mintURL: URL, origin: String?, http: any HTTPTransport, ttlSeconds: Int = 86_400, scopeToRoom: Bool = false,
                now: @escaping @Sendable () -> Date = { Date() }, timeout: TimeInterval = 15) {
        self.mintURL = mintURL
        self.origin = origin
        self.http = http
        self.ttlSeconds = ttlSeconds
        self.scopeToRoom = scopeToRoom
        self.now = now
        self.timeout = timeout
    }

    private func key(_ room: String) -> String { scopeToRoom ? room : "" }

    public func token(for room: String) async throws -> MailboxToken {
        let k = key(room)
        if let t = cache[k], t.expiresAt.timeIntervalSince(now()) > OpenMintTokenProvider.renewBefore { return t }
        if let task = inflight[k] { return try await task.value }
        let task = Task { try await self.mint(room: self.scopeToRoom ? room : nil) }
        inflight[k] = task
        defer { inflight[k] = nil }
        let t = try await task.value
        cache[k] = t
        return t
    }

    public func invalidate(for room: String) {
        cache[key(room)] = nil
    }

    private func mint(room: String?) async throws -> MailboxToken {
        var comps = URLComponents(url: mintURL, resolvingAgainstBaseURL: false)!
        var items = comps.queryItems ?? []
        if let room { items.append(URLQueryItem(name: "room", value: room)) }
        items.append(URLQueryItem(name: "ttl", value: String(ttlSeconds)))
        comps.queryItems = items
        // the room may contain '+' or '/' which URLComponents leaves alone in queries; encode them explicitly
        comps.percentEncodedQuery = comps.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
        let response = try await http.send(.p2p(comps.url!, origin: origin, timeout: timeout))
        guard response.isSuccess else { throw MailboxWire.error(response) }
        let m: MintResponse
        do { m = try JSONDecoder().decode(MintResponse.self, from: response.body) } catch { throw P2PError.transport("mint response not decodable: \(error)") }
        return MailboxToken(token: m.token, expiresAt: Date(timeIntervalSince1970: m.expires_at), room: m.room, endpoint: m.endpoint)
    }
}

/// A fixed token, or a closure that fetches one (e.g. from your backend).
public struct StaticTokenProvider: MailboxTokenProvider {
    private let fetch: @Sendable (String) async throws -> MailboxToken

    public init(_ token: MailboxToken) { fetch = { _ in token } }
    public init(_ fetch: @escaping @Sendable (String) async throws -> MailboxToken) { self.fetch = fetch }

    public func token(for room: String) async throws -> MailboxToken { try await fetch(room) }
    public func invalidate(for room: String) async {}
}

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