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

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 }