// // RequestCompression.swift // // Copyright (c) 2023 Alamofire Software Foundation (http://alamofire.org/) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // #if canImport(zlib) import Foundation import zlib /// `RequestAdapter` which compresses outgoing `URLRequest` bodies using the `deflate` `Content-Encoding` and adds the /// appropriate header. /// /// - Note: Most requests to most APIs are small and so would only be slowed down by applying this adapter. Measure the /// size of your request bodies and the performance impact of using this adapter before use. Using this adapter /// with already compressed data, such as images, will, at best, have no effect. Additionally, body compression /// is a synchronous operation, so measuring the performance impact may be important to determine whether you /// want to use a dedicated `requestQueue` in your `Session` instance. Finally, not all servers support request /// compression, so test with all of your server configurations before deploying. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct DeflateRequestCompressor: RequestInterceptor { /// Type that determines the action taken when the `URLRequest` already has a `Content-Encoding` header. public enum DuplicateHeaderBehavior { /// Throws a `DuplicateHeaderError`. The default. case error /// Replaces the existing header value with `deflate`. case replace /// Silently skips compression when the header exists. case skip } /// `Error` produced when the outgoing `URLRequest` already has a `Content-Encoding` header, when the instance has /// been configured to produce an error. public struct DuplicateHeaderError: Error {} /// Behavior to use when the outgoing `URLRequest` already has a `Content-Encoding` header. public let duplicateHeaderBehavior: DuplicateHeaderBehavior /// Closure which determines whether the outgoing body data should be compressed. public let shouldCompressBodyData: (_ bodyData: Data) -> Bool /// Creates an instance with the provided parameters. /// /// - Parameters: /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. `.error` by default. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default. public init(duplicateHeaderBehavior: DuplicateHeaderBehavior = .error, shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }) { self.duplicateHeaderBehavior = duplicateHeaderBehavior self.shouldCompressBodyData = shouldCompressBodyData } public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { // No need to compress unless we have body data. No support for compressing streams. guard let bodyData = urlRequest.httpBody else { completion(.success(urlRequest)) return } guard shouldCompressBodyData(bodyData) else { completion(.success(urlRequest)) return } if urlRequest.headers.value(for: "Content-Encoding") != nil { switch duplicateHeaderBehavior { case .error: completion(.failure(DuplicateHeaderError())) return case .replace: // Header will be replaced once the body data is compressed. break case .skip: completion(.success(urlRequest)) return } } var compressedRequest = urlRequest do { compressedRequest.httpBody = try deflate(bodyData) compressedRequest.headers.update(.contentEncoding("deflate")) completion(.success(compressedRequest)) } catch { completion(.failure(error)) } } func deflate(_ data: Data) throws -> Data { var output = Data([0x78, 0x5E]) // Header try output.append((data as NSData).compressed(using: .zlib) as Data) var checksum = adler32Checksum(of: data).bigEndian output.append(Data(bytes: &checksum, count: MemoryLayout.size)) return output } func adler32Checksum(of data: Data) -> UInt32 { #if swift(>=5.6) data.withUnsafeBytes { buffer in UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count))) } #else data.withUnsafeBytes { buffer in let buffer = buffer.bindMemory(to: UInt8.self) return UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count))) } #endif } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension RequestInterceptor where Self == DeflateRequestCompressor { /// Create a `DeflateRequestCompressor` with default `duplicateHeaderBehavior` and `shouldCompressBodyData` values. public static var deflateCompressor: DeflateRequestCompressor { DeflateRequestCompressor() } /// Creates a `DeflateRequestCompressor` with the provided `DuplicateHeaderBehavior` and `shouldCompressBodyData` /// closure. /// /// - Parameters: /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default. /// /// - Returns: The `DeflateRequestCompressor`. public static func deflateCompressor( duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error, shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true } ) -> DeflateRequestCompressor { DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior, shouldCompressBodyData: shouldCompressBodyData) } } #endif