RequestCompression.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  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. #if swift(>=5.6)
  105. data.withUnsafeBytes { buffer in
  106. UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
  107. }
  108. #else
  109. data.withUnsafeBytes { buffer in
  110. let buffer = buffer.bindMemory(to: UInt8.self)
  111. return UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
  112. }
  113. #endif
  114. }
  115. }
  116. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  117. extension RequestInterceptor where Self == DeflateRequestCompressor {
  118. /// Create a `DeflateRequestCompressor` with default `duplicateHeaderBehavior` and `shouldCompressBodyData` values.
  119. public static var deflateCompressor: DeflateRequestCompressor {
  120. DeflateRequestCompressor()
  121. }
  122. /// Creates a `DeflateRequestCompressor` with the provided `DuplicateHeaderBehavior` and `shouldCompressBodyData`
  123. /// closure.
  124. ///
  125. /// - Parameters:
  126. /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use.
  127. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
  128. ///
  129. /// - Returns: The `DeflateRequestCompressor`.
  130. public static func deflateCompressor(
  131. duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error,
  132. shouldCompressBodyData: @escaping (_ bodyData: Data) -> Bool = { _ in true }
  133. ) -> DeflateRequestCompressor {
  134. DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior,
  135. shouldCompressBodyData: shouldCompressBodyData)
  136. }
  137. }
  138. #endif