RequestCompression.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 but could actually make requests
  33. /// larger. Additionally, not all servers support request compression, so test with all of your server
  34. /// configurations before deploying. Finally, body compression is a synchronous operation, so measuring the
  35. /// performance impact may be important to determine whether you want to use a dedicated `requestQueue` in your
  36. /// `Session` instance.
  37. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  38. public struct DeflateRequestCompressor: RequestInterceptor {
  39. /// Type that determines the action taken when the `URLRequest` already has a `Content-Encoding` header.
  40. public enum DuplicateHeaderBehavior {
  41. /// Throws a `DuplicateHeaderError`. The default.
  42. case error
  43. /// Replaces the existing header value with `deflate`.
  44. case replace
  45. /// Silently skips compression when the header exists.
  46. case skip
  47. }
  48. /// `Error` produced when the outgoing `URLRequest` already has a `Content-Encoding` header, when the instance has
  49. /// been configured to produce an error.
  50. public struct DuplicateHeaderError: Error {}
  51. /// Behavior to use when the outgoing `URLRequest` already has a `Content-Encoding` header.
  52. public let duplicateHeaderBehavior: DuplicateHeaderBehavior
  53. /// Closure which determines whether the outgoing body data should be compressed.
  54. public let shouldCompressBodyData: (_ request: URLRequest) -> Bool
  55. /// Creates an instance with the provided parameters.
  56. ///
  57. /// - Parameters:
  58. /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use. `.error` by default.
  59. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
  60. public init(duplicateHeaderBehavior: DuplicateHeaderBehavior = .error,
  61. shouldCompressBodyData: @escaping (_ request: URLRequest) -> Bool = { _ in true }) {
  62. self.duplicateHeaderBehavior = duplicateHeaderBehavior
  63. self.shouldCompressBodyData = shouldCompressBodyData
  64. }
  65. public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
  66. // No need to compress unless we have body data. No support for compressing streams.
  67. guard let bodyData = urlRequest.httpBody else {
  68. completion(.success(urlRequest))
  69. return
  70. }
  71. guard shouldCompressBodyData(urlRequest) else {
  72. completion(.success(urlRequest))
  73. return
  74. }
  75. if urlRequest.headers.value(for: "Content-Encoding") != nil {
  76. switch duplicateHeaderBehavior {
  77. case .error:
  78. completion(.failure(DuplicateHeaderError()))
  79. return
  80. case .replace:
  81. // Header will be replaced once the body data is compressed.
  82. break
  83. case .skip:
  84. completion(.success(urlRequest))
  85. return
  86. }
  87. }
  88. var compressedRequest = urlRequest
  89. do {
  90. compressedRequest.httpBody = try deflate(bodyData)
  91. compressedRequest.headers.update(.contentEncoding("deflate"))
  92. completion(.success(compressedRequest))
  93. } catch {
  94. completion(.failure(error))
  95. }
  96. }
  97. func deflate(_ data: Data) throws -> Data {
  98. var output = Data([0x78, 0x5E]) // Header
  99. try output.append((data as NSData).compressed(using: .zlib) as Data)
  100. var checksum = adler32Checksum(of: data).bigEndian
  101. output.append(Data(bytes: &checksum, count: MemoryLayout<UInt32>.size))
  102. return output
  103. }
  104. func adler32Checksum(of data: Data) -> UInt32 {
  105. #if swift(>=5.6)
  106. data.withUnsafeBytes { buffer in
  107. UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
  108. }
  109. #else
  110. data.withUnsafeBytes { buffer in
  111. let buffer = buffer.bindMemory(to: UInt8.self)
  112. return UInt32(adler32(1, buffer.baseAddress, UInt32(buffer.count)))
  113. }
  114. #endif
  115. }
  116. }
  117. @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
  118. extension RequestInterceptor where Self == DeflateRequestCompressor {
  119. /// Create a `DeflateRequestCompressor` with default `duplicateHeaderBehavior` and `shouldCompressBodyData` values.
  120. public static var deflateCompressor: DeflateRequestCompressor {
  121. DeflateRequestCompressor()
  122. }
  123. /// Creates a `DeflateRequestCompressor` with the provided `DuplicateHeaderBehavior` and `shouldCompressBodyData`
  124. /// closure.
  125. ///
  126. /// - Parameters:
  127. /// - duplicateHeaderBehavior: `DuplicateHeaderBehavior` to use.
  128. /// - shouldCompressBodyData: Closure which determines whether the outgoing body data should be compressed. `true` by default.
  129. ///
  130. /// - Returns: The `DeflateRequestCompressor`.
  131. public static func deflateCompressor(
  132. duplicateHeaderBehavior: DeflateRequestCompressor.DuplicateHeaderBehavior = .error,
  133. shouldCompressBodyData: @escaping (_ request: URLRequest) -> Bool = { _ in true }
  134. ) -> DeflateRequestCompressor {
  135. DeflateRequestCompressor(duplicateHeaderBehavior: duplicateHeaderBehavior,
  136. shouldCompressBodyData: shouldCompressBodyData)
  137. }
  138. }
  139. #endif