RadrootsBackgroundTransferValidation.swift (8131B)
1 import Foundation 2 3 public enum RadrootsBackgroundTransferValidation { 4 public static func normalizedIdentifier(_ value: String) throws -> String { 5 let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() 6 guard !trimmed.isEmpty else { 7 throw RadrootsBackgroundTransferError.invalidRequest 8 } 9 guard trimmed.count <= 128 else { 10 throw RadrootsBackgroundTransferError.invalidRequest 11 } 12 guard 13 trimmed.range(of: "^[a-z0-9][a-z0-9._-]*[a-z0-9]$|^[a-z0-9]$", options: .regularExpression) 14 != nil 15 else { 16 throw RadrootsBackgroundTransferError.invalidRequest 17 } 18 guard !trimmed.contains("..") else { 19 throw RadrootsBackgroundTransferError.invalidRequest 20 } 21 return trimmed 22 } 23 24 public static func validate( 25 remoteURL: URL, method: RadrootsBackgroundTransferMethod, 26 operation: RadrootsBackgroundTransferOperation, headers: [String: String], 27 metadata: [String: String], networkPolicy: RadrootsBackgroundTransferNetworkPolicy, 28 responsePolicy: RadrootsBackgroundTransferResponsePolicy, expectedSourceSHA256: String?, 29 maximumTransferBytes: UInt64 30 ) throws { 31 try validate(remoteURL: remoteURL, networkPolicy: networkPolicy) 32 try validate(method: method, operation: operation) 33 try validate(headers: headers) 34 try validate(metadata: metadata) 35 guard maximumTransferBytes > 0, 36 maximumTransferBytes <= RadrootsBackgroundTransferRequest.absoluteMaximumTransferBytes 37 else { 38 throw RadrootsBackgroundTransferError.invalidRequest 39 } 40 if case .download = operation, responsePolicy != .discard { 41 throw RadrootsBackgroundTransferError.invalidRequest 42 } 43 if let expectedSourceSHA256 { 44 guard case .upload = operation, 45 expectedSourceSHA256.range(of: "^[0-9a-f]{64}$", options: .regularExpression) != nil 46 else { 47 throw RadrootsBackgroundTransferError.invalidRequest 48 } 49 } 50 if case let .upload(.stagedBlob(blob)) = operation, 51 UInt64(blob.sizeBytes) > maximumTransferBytes { 52 throw RadrootsBackgroundTransferError.invalidRequest 53 } 54 } 55 56 private static func validate( 57 remoteURL: URL, networkPolicy: RadrootsBackgroundTransferNetworkPolicy 58 ) throws { 59 try RadrootsNativeDestinationPolicy.validate(remoteURL, policy: networkPolicy) 60 } 61 62 private static func validate( 63 method: RadrootsBackgroundTransferMethod, operation: RadrootsBackgroundTransferOperation 64 ) throws { 65 switch operation { 66 case let .download(destination): 67 guard method == .get else { 68 throw RadrootsBackgroundTransferError.invalidRequest 69 } 70 guard case .file = destination else { 71 throw RadrootsBackgroundTransferError.invalidRequest 72 } 73 try validateLocalFile(destination) 74 case let .upload(source): 75 guard method == .post || method == .put else { 76 throw RadrootsBackgroundTransferError.invalidRequest 77 } 78 try validateLocalFile(source) 79 } 80 } 81 82 static func validateLocalFile(_ localFile: RadrootsBackgroundTransferLocalFile) throws { 83 switch localFile { 84 case let .file(reference): 85 let path = reference.relativePath.trimmingCharacters(in: .whitespacesAndNewlines) 86 guard !path.isEmpty, !NSString(string: path).isAbsolutePath, 87 !path.split(separator: "/", omittingEmptySubsequences: false).contains(where: { $0 == ".." }) 88 else { 89 throw RadrootsBackgroundTransferError.invalidRequest 90 } 91 case let .stagedBlob(blob): 92 guard 93 (try? RadrootsStagedBlobReference( 94 blobID: blob.blobID, sizeBytes: blob.sizeBytes, mediaType: blob.mediaType, 95 filenameHint: blob.filenameHint 96 )) != nil 97 else { 98 throw RadrootsBackgroundTransferError.invalidRequest 99 } 100 } 101 } 102 103 private static func validate(headers: [String: String]) throws { 104 guard headers.count <= 32 else { 105 throw RadrootsBackgroundTransferError.invalidRequest 106 } 107 for (key, value) in headers { 108 try validateSafeText(key, field: "background transfer header name", maximumLength: 80) 109 guard key.range(of: "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", options: .regularExpression) != nil, 110 !["connection", "content-length", "host", "transfer-encoding"].contains(key.lowercased()) 111 else { 112 throw RadrootsBackgroundTransferError.invalidRequest 113 } 114 try validateSafeText(value, field: "background transfer header value", maximumLength: 8192) 115 } 116 } 117 118 private static func validate(metadata: [String: String]) throws { 119 guard metadata.count <= 32 else { 120 throw RadrootsBackgroundTransferError.invalidRequest 121 } 122 for (key, value) in metadata { 123 try validateSafeText(key, field: "background transfer metadata key", maximumLength: 80) 124 try validateSafeText(value, field: "background transfer metadata value", maximumLength: 500) 125 let unsafeKey = key.lowercased() 126 guard 127 !["auth", "token", "secret", "credential", "cookie", "header", "path", "url"].contains( 128 where: { unsafeKey.contains($0) } 129 ) 130 else { 131 throw RadrootsBackgroundTransferError.invalidRequest 132 } 133 } 134 } 135 136 static func normalizedMediaType(_ value: String) throws -> String { 137 let normalized = 138 value.split(separator: ";", maxSplits: 1).first?.trimmingCharacters( 139 in: .whitespacesAndNewlines 140 ).lowercased() ?? "" 141 guard 142 normalized.range(of: "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$", options: .regularExpression) 143 != nil, 144 normalized.utf8.count <= 127 145 else { 146 throw RadrootsBackgroundTransferError.invalidRequest 147 } 148 return normalized 149 } 150 151 static func validate( 152 response: RadrootsBackgroundTransferResponse, policy: RadrootsBackgroundTransferResponsePolicy 153 ) throws { 154 guard (200 ... 299).contains(response.statusCode) else { 155 throw RadrootsBackgroundTransferError.invalidRequest 156 } 157 if policy.maximumBodyBytes == 0 { 158 guard response.body == nil, response.mediaType == nil else { 159 throw RadrootsBackgroundTransferError.invalidRequest 160 } 161 return 162 } 163 guard let body = response.body, body.count <= policy.maximumBodyBytes, 164 let mediaType = response.mediaType, 165 policy.acceptedMediaTypes.contains(mediaType) 166 else { 167 throw RadrootsBackgroundTransferError.invalidRequest 168 } 169 if mediaType == "application/json" { 170 guard (try? JSONSerialization.jsonObject(with: body)) is [String: Any] else { 171 throw RadrootsBackgroundTransferError.invalidRequest 172 } 173 } 174 } 175 176 private static func validateSafeText(_ value: String, field _: String, maximumLength: Int) throws { 177 let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines) 178 guard !trimmed.isEmpty else { 179 throw RadrootsBackgroundTransferError.invalidRequest 180 } 181 guard trimmed.count <= maximumLength else { 182 throw RadrootsBackgroundTransferError.invalidRequest 183 } 184 guard doesNotContainControlCharacters(trimmed) else { 185 throw RadrootsBackgroundTransferError.invalidRequest 186 } 187 } 188 189 private static func doesNotContainControlCharacters(_ value: String) -> Bool { 190 value.unicodeScalars.allSatisfy { !CharacterSet.controlCharacters.contains($0) } 191 } 192 }