RadrootsTransferResponseCollector.swift (4586B)
1 import Foundation 2 3 /// Bounds bytes as they arrive. Header strings are bounded before normalization; 4 /// no request headers or arbitrary response headers enter the receipt. 5 final class RadrootsTransferResponseCollector: @unchecked Sendable { 6 private static let maximumBodyBytes = 65536 7 private let lock = NSLock() 8 private var limits: [Int: Int] = [:] 9 private var bodies: [Int: Data] = [:] 10 private var counts: [Int: Int] = [:] 11 private var failures: [Int: RadrootsBackgroundTransferFailure] = [:] 12 13 func register(_ limit: Int, taskIdentifier: Int) { 14 lock.withLock { limits[taskIdentifier] = min(max(limit, 0), Self.maximumBodyBytes) } 15 } 16 17 func begin(_ response: URLResponse, taskIdentifier: Int, fallbackLimit: Int) -> Bool { 18 lock.withLock { 19 guard let http = response as? HTTPURLResponse else { 20 failures[taskIdentifier] = .responseInvalid 21 return false 22 } 23 let limit = effectiveLimit(taskIdentifier, fallback: fallbackLimit) 24 let failure = Self.headerFailure(http) ?? ( 25 response.expectedContentLength > Int64(limit) ? .responseTooLarge : nil 26 ) 27 if let failure { 28 failures[taskIdentifier] = failure 29 } 30 return failure == nil 31 } 32 } 33 34 /// Returns true when the task must be cancelled, including discard-policy 35 /// responses that exceed the absolute transport bound. 36 func append(_ data: Data, taskIdentifier: Int, fallbackLimit: Int) -> Bool { 37 lock.withLock { 38 guard failures[taskIdentifier] == nil else { return true } 39 let limit = effectiveLimit(taskIdentifier, fallback: fallbackLimit) 40 let count = counts[taskIdentifier] ?? 0 41 guard data.count <= limit - count else { 42 bodies.removeValue(forKey: taskIdentifier) 43 failures[taskIdentifier] = .responseTooLarge 44 return true 45 } 46 counts[taskIdentifier] = count + data.count 47 if (limits[taskIdentifier] ?? fallbackLimit) > 0 { 48 bodies[taskIdentifier, default: Data()].append(data) 49 } 50 return false 51 } 52 } 53 54 func take(taskIdentifier: Int, response: HTTPURLResponse?, 55 destinationMismatch: Bool) -> RadrootsBackgroundHTTPResult { 56 lock.withLock { 57 let body = bodies.removeValue(forKey: taskIdentifier) 58 let failure = failures.removeValue(forKey: taskIdentifier) ?? response.flatMap(Self.headerFailure) 59 limits.removeValue(forKey: taskIdentifier) 60 counts.removeValue(forKey: taskIdentifier) 61 let rawType = response?.value(forHTTPHeaderField: "Content-Type") 62 let mediaType = rawType.flatMap { value -> String? in 63 guard value.utf8.count <= 256 else { return nil } 64 return try? RadrootsBackgroundTransferValidation.normalizedMediaType(value) 65 } 66 let rawEncoding = response?.value(forHTTPHeaderField: "Content-Encoding") 67 let encoding = rawEncoding.flatMap { value -> String? in 68 guard value.utf8.count <= 32 else { return "invalid" } 69 return value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 70 } 71 return RadrootsBackgroundHTTPResult( 72 statusCode: response?.statusCode, mediaType: mediaType, body: body, contentEncoding: encoding, 73 bodyExceeded: failure == .responseTooLarge, mediaTypeWasMalformed: rawType != nil && mediaType == nil, 74 destinationMismatch: destinationMismatch, headerFailure: failure 75 ) 76 } 77 } 78 79 private func effectiveLimit(_ identifier: Int, fallback: Int) -> Int { 80 let limit = limits[identifier] ?? min(max(fallback, 0), Self.maximumBodyBytes) 81 return limit == 0 ? Self.maximumBodyBytes : limit 82 } 83 84 private static func headerFailure(_ response: HTTPURLResponse) -> RadrootsBackgroundTransferFailure? { 85 if let value = response.value(forHTTPHeaderField: "Content-Encoding"), 86 value.utf8.count > 32 || value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() != "identity" { 87 return .responseContentEncoding 88 } 89 if let value = response.value(forHTTPHeaderField: "Content-Type"), 90 value.utf8.count > 256 || (try? RadrootsBackgroundTransferValidation.normalizedMediaType(value)) == nil { 91 return .responseMediaType 92 } 93 return nil 94 } 95 }