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 }