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

RadrootsNativeDestinationPolicy.swift (3745B)


      1 import Foundation
      2 
      3 /// URL authority checks are not DNS/IP-at-connect enforcement. New native
      4 /// transfers are limited to the explicitly isolated simulator foreground path;
      5 /// hosts requiring public endpoint guarantees use their shared foreground I/O.
      6 enum RadrootsNativeDestinationPolicy {
      7     static func validate(_ url: URL, policy: RadrootsBackgroundTransferNetworkPolicy) throws {
      8         guard url.absoluteString.utf8.count <= 4096,
      9               let value = URLComponents(url: url, resolvingAgainstBaseURL: false),
     10               let host = value.host, !host.isEmpty, value.user == nil, value.password == nil,
     11               value.query == nil, value.fragment == nil,
     12               value.port.map({ (1 ... 65535).contains($0) }) ?? true,
     13               !url.absoluteString.contains("\\")
     14         else { throw RadrootsBackgroundTransferError.invalidRequest }
     15         let path = value.percentEncodedPath
     16         guard path.isEmpty || path.hasPrefix("/"),
     17               path.split(separator: "/", omittingEmptySubsequences: false).allSatisfy(safeComponent)
     18         else { throw RadrootsBackgroundTransferError.invalidRequest }
     19         switch policy {
     20         case .publicHTTPS:
     21             guard value.scheme?.lowercased() == "https" else { throw RadrootsBackgroundTransferError.invalidRequest }
     22         case .simulatorLoopbackHTTP:
     23             #if os(iOS) && !targetEnvironment(simulator)
     24                 throw RadrootsBackgroundTransferError.invalidRequest
     25             #else
     26                 guard value.scheme?.lowercased() == "http",
     27                       ["127.0.0.1", "localhost", "::1"].contains(normalizedHost(host))
     28                 else { throw RadrootsBackgroundTransferError.invalidRequest }
     29             #endif
     30         }
     31     }
     32 
     33     static func responseMatches(_ response: URL?, original: URLRequest?, current: URLRequest?) -> Bool {
     34         guard let original, let current, let requested = original.url, let response,
     35               let requestedParts = URLComponents(url: requested, resolvingAgainstBaseURL: false),
     36               let responseParts = URLComponents(url: response, resolvingAgainstBaseURL: false),
     37               current.httpMethod == original.httpMethod, current.url == requested
     38         else { return false }
     39         let policy: RadrootsBackgroundTransferNetworkPolicy = requestedParts.scheme?.lowercased() == "https"
     40             ? .publicHTTPS : .simulatorLoopbackHTTP
     41         guard (try? validate(requested, policy: policy)) != nil,
     42               (try? validate(response, policy: policy)) != nil
     43         else { return false }
     44         return requestedParts.scheme?.lowercased() == responseParts.scheme?.lowercased()
     45             && normalizedHost(requestedParts.host ?? "") == normalizedHost(responseParts.host ?? "")
     46             && effectivePort(requestedParts) == effectivePort(responseParts)
     47             && normalizedPath(requestedParts) == normalizedPath(responseParts)
     48     }
     49 
     50     private static func safeComponent(_ component: Substring) -> Bool {
     51         guard let decoded = String(component).removingPercentEncoding else { return false }
     52         return decoded != "." && decoded != ".." && !decoded.contains("/") && !decoded.contains("\\")
     53             && decoded.unicodeScalars.allSatisfy { !CharacterSet.controlCharacters.contains($0) }
     54     }
     55 
     56     private static func normalizedHost(_ host: String) -> String {
     57         host.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: "[]"))
     58     }
     59 
     60     private static func effectivePort(_ parts: URLComponents) -> Int {
     61         parts.port ?? (parts.scheme?.lowercased() == "https" ? 443 : 80)
     62     }
     63 
     64     private static func normalizedPath(_ parts: URLComponents) -> String {
     65         parts.percentEncodedPath.isEmpty ? "/" : parts.percentEncodedPath
     66     }
     67 }