apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

RadrootsNativeRedirectServer.swift (3775B)


      1 #if os(iOS) && targetEnvironment(simulator)
      2     import Foundation
      3     import Network
      4     @testable import RadrootsKit
      5 
      6     /// Isolated loopback fixture. No external DNS, TLS trust changes, or credentials.
      7     final class NativeRedirectServer: @unchecked Sendable {
      8         private let listener: NWListener
      9         private let queue = DispatchQueue(label: "org.radroots.tests.redirect-http")
     10         private let lock = NSLock()
     11         private var connections: [NWConnection] = []
     12         private var received = 0
     13         private var location: String?
     14         private var wireResponse: Data?
     15 
     16         init() throws {
     17             let parameters = NWParameters.tcp
     18             parameters.requiredLocalEndpoint = .hostPort(host: "127.0.0.1", port: .any)
     19             listener = try NWListener(using: parameters)
     20         }
     21 
     22         var requestCount: Int {
     23             lock.withLock { received }
     24         }
     25 
     26         func redirect(to location: String) {
     27             lock.withLock { self.location = location }
     28         }
     29 
     30         func respond(with bytes: Data) {
     31             lock.withLock { wireResponse = bytes }
     32         }
     33 
     34         func start() async throws -> UInt16 {
     35             listener.newConnectionHandler = { [weak self] connection in
     36                 guard let self else { connection.cancel(); return }
     37                 connections.append(connection)
     38                 connection.start(queue: queue)
     39                 read(connection, accumulated: Data())
     40             }
     41             listener.start(queue: queue)
     42             for _ in 0 ..< 500 {
     43                 if let port = listener.port, port.rawValue > 0 {
     44                     return port.rawValue
     45                 }
     46                 try await Task.sleep(for: .milliseconds(10))
     47             }
     48             throw RadrootsBackgroundTransferError.unavailable
     49         }
     50 
     51         func stop() {
     52             listener.cancel()
     53             queue.async { for connection in self.connections {
     54                 connection.cancel()
     55             } }
     56         }
     57 
     58         private func read(_ connection: NWConnection, accumulated: Data) {
     59             connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, complete, error in
     60                 var bytes = accumulated
     61                 if let data {
     62                     bytes.append(data)
     63                 }
     64                 guard error == nil, bytes.count <= 131_072 else { connection.cancel(); return }
     65                 if self.hasBody(bytes) {
     66                     let destination = self.lock.withLock { self.received += 1; return self.location }
     67                     let status = destination.map { "307 Temporary Redirect\r\nLocation: \($0)" } ?? "200 OK"
     68                     let response = self.lock.withLock { self.wireResponse }
     69                         ?? Data("HTTP/1.1 \(status)\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".utf8)
     70                     connection.send(content: response, completion: .contentProcessed { _ in connection.cancel() })
     71                 } else if complete {
     72                     connection.cancel()
     73                 } else {
     74                     self.read(connection, accumulated: bytes)
     75                 }
     76             }
     77         }
     78 
     79         private func hasBody(_ bytes: Data) -> Bool {
     80             guard let separator = bytes.range(of: Data("\r\n\r\n".utf8)),
     81                   let header = String(data: bytes[..<separator.lowerBound], encoding: .utf8)
     82             else { return false }
     83             let length = header.components(separatedBy: "\r\n").first {
     84                 $0.lowercased().hasPrefix("content-length:")
     85             }.flatMap { Int($0.dropFirst("content-length:".count).trimmingCharacters(in: .whitespaces)) } ?? 0
     86             return (0 ... 65536).contains(length) && bytes.count - separator.upperBound == length
     87         }
     88     }
     89 #endif