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
//
// RequesterOAuthBridge.swift
// AT-IRC
//
// The embedded socialweb requester signs in with its own OAuth client (the
// MarketIdentity). It redirects to a loopback URI; this bridge keeps one
// loopback listener for the app's lifetime and forwards each callback to the
// in-process requester (CompleteOAuth), like the page's
// /oauth/callback/requester route.
//
import Foundation
import OSLog
@MainActor
final class RequesterOAuthBridge {
private var server: LoopbackServer?
private var task: Task<Void, Never>?
private let kcp = KCPClient()
private let log = Logger(subsystem: "com.fedfork.irc.AT-IRC", category: "market-oauth")
/// Start the redirect listener and return the redirect URI for the
/// requester config. Idempotent.
func start() async throws -> String {
if let server, server.port != 0 {
return "http://127.0.0.1:\(server.port)/"
}
let server = LoopbackServer()
let port = try await server.start()
self.server = server
task?.cancel()
task = Task { @MainActor [weak self] in
while !Task.isCancelled {
guard let self, let server = self.server else { return }
guard let query = try? await server.waitForCallback() else { return }
server.resetCallback()
log.notice("callback received keys=\(query.keys.sorted().joined(separator: ","), privacy: .public)")
var components = URLComponents()
components.queryItems = query.map { URLQueryItem(name: $0.key, value: $0.value) }
let path = "/oauth/callback/requester?" + (components.percentEncodedQuery ?? "")
do {
// This route belongs to the requester, not the
// apiserver: do not send X-Atirc-Direct.
let response = try await self.kcp.request("GET", path, direct: false)
log.notice("callback forwarded status=\(response.status, privacy: .public)")
} catch {
log.error("callback forward failed: \(error.localizedDescription, privacy: .public)")
}
}
}
return "http://127.0.0.1:\(port)/"
}
func stop() {
task?.cancel()
task = nil
server?.stop()
server = nil
}
}