RequestCompression.swift 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. //
  2. // RequestCompression.swift
  3. //
  4. // Copyright (c) 2023 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. #if canImport(zlib)
  25. import Foundation
  26. import zlib
  27. /// `RequestAdapter` which compresses outgoing `URLRequest` bodies using the `deflate` `Content-Encoding` and adds the
  28. /// appropriate header.
  29. ///
  30. /// - Note: Most requests to most APIs are small and so would only be slowed down by applying this adapter. Measure the
  31. /// size of your request bodies and the performance impact of using this adapter before use. Using this adapter
  32. /// with already compressed data, such as images, will, at best, have no effect. Additionally, body compression
  33. /// is a synchronous operation, so measuring the performance impact may be important to determine whether you
  34. /// want to use a dedicated `requestQueue` in your `Session` instance. Finally, not all servers support request
  35. /// compression, so test with all of your server configurations before deploying.
  36. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  37. public struct DeflateRequestCompressor: RequestInterceptor {
  38. /// Type that determines the action taken when the `URLRequest` already has a `Content-Encoding` header.
  39. public enum DuplicateHeaderBehavior {
  40. /// Throws a `DuplicateHeaderError`. The default.
  41. case error
  42. /// Replaces the existing header value with `deflate`.
  43. case replace
  44. /// Silently skips compression when the header exists.
  45. case skip
  46. }
  47. /// `Error` produced when the outgoing `URLRequest` already has a `Content-Encoding` header, when the instance has
  48. /// been configured to produce an error.
  49. public struct DuplicateHeaderError: Error {}
  50. /// Behavior to use when the outgoing `URLRequest` already has a `Content-Encoding` header.
  51. public let duplicateHeaderBehavior: DuplicateHeaderBehavior
  52. /// Closure which determines whether the outgoing body data should be compressed.
  53. public let shouldCompressBodyData: (_ bodyData: Data) -> Bool
  54. /// Creates an instance with the provided parameters.
  55. ///
  56. /// - Parameters:
  57. /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. `.error` by default.
  58. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
  59. public init(duplicateHeaderBehavior: DuplicateHeaderBehavior = .error,
  60. shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }) {
  61. self.duplicateHeaderBehavior = duplicateHeaderBehavior
  62. self.shouldCompressBodyData = shouldCompressBodyData
  63. }
  64. public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
  65. // No need to compress unless we have body data. No support for compressing streams.
  66. guard let bodyData = urlRequest.httpBody else {
  67. completion(.success(urlRequest))
  68. return
  69. }
  70. guard shouldCompressBodyData(bodyData) else {
  71. completion(.success(urlRequest))
  72. return
  73. }
  74. if urlRequest.headers.value(for: "Content-Encoding") != nil {
  75. switch duplicateHeaderBehavior {
  76. case .error:
  77. completion(.failure(DuplicateHeaderError()))
  78. return
  79. case .replace:
  80. // Header will be replaced once the body data is compressed.
  81. break
  82. case .skip:
  83. completion(.success(urlRequest))
  84. return
  85. }
  86. }
  87. var compressedRequest = urlRequest
  88. do {
  89. compressedRequest.httpBody = try deflate(bodyData)
  90. compressedRequest.headers.update(.contentEncoding("deflate"))
  91. completion(.success(compressedRequest))
  92. } catch {
  93. completion(.failure(error))
  94. }
  95. }
  96. func deflate(_ data: Data) throws -> Data {
  97. var output = Data([0x78, 0x5E]) // Header
  98. try output.append((data as NSData).compressed(using: .zlib) as Data)
  99. var checksum = adler32Checksum(of: data).bigEndian
  100. output.append(Data(bytes: &checksum, count: MemoryLayout<UInt32>.size))
  101. return output
  102. }
  103. func adler32Checksum(of data: Data) -> UInt32 {
  104. data.withUnsafeBytes { buffer in
  105. UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
  106. }
  107. }
  108. }
  109. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  110. extension RequestInterceptor where Self == DeflateRequestCompressor {
  111. /// Create a `DeflateRequestCompressor` with default `duplicateHeaderBehavior` and `shouldCompressBodyData` values.
  112. public static var deflateCompressor: DeflateRequestCompressor {
  113. DeflateRequestCompressor()
  114. }
  115. /// Creates a `DeflateRequestCompressor` with the provided `DuplicateHeaderBehavior` and `shouldCompressBodyData`
  116. /// closure.
  117. ///
  118. /// - Parameters:
  119. /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use.
  120. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
  121. ///
  122. /// - Returns: The `DeflateRequestCompressor`.
  123. public static func deflateCompressor(
  124. duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error,
  125. shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }
  126. ) -> DeflateRequestCompressor {
  127. DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior,
  128. shouldCompressBodyData: shouldCompressBodyData)
  129. }
  130. }
  131. #endif