Middleware for Vapor 3 to allow serving gzip encoded content. Inspired by https://github.com/vapor-community/gzip-provider
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

87 lines
3.9 KiB

  1. import HTTP
  2. import Vapor
  3. import Foundation
  4. import ZIPFoundation
  5. /// Server Gzip middleware:
  6. /// 1. checks if the "Accept-Encoding" header contains "gzip"
  7. /// 2. if so, compresses the body and sets the response header "Content-Encoding" to "gzip",
  8. public struct GzipServerMiddleware: Middleware, ServiceType {
  9. public static func makeService(for worker: Container) throws -> GzipServerMiddleware {
  10. return .init()
  11. }
  12. private let shouldGzipRequest: (_ request: Request) -> Bool
  13. private let shouldGzipResponse: (_ response: Response) -> Bool
  14. /// The `shouldGzip` closure is asked for every request whether that request
  15. /// should allow response gzipping. Returns `true` always by default.
  16. public init(shouldGzip: @escaping (_ request: Request) -> Bool = { _ in true }) {
  17. self.shouldGzipRequest = shouldGzip
  18. self.shouldGzipResponse = { _ in true }
  19. }
  20. /// The `shouldGzipRequest` closure is asked for every request whether that request
  21. /// should allow response gzipping. `shouldGzipResponse` asks the same for the response.
  22. /// Both return`true` always by default.
  23. public init(shouldGzipRequest: @escaping (_ request: Request) -> Bool = { _ in true },
  24. shouldGzipResponse: @escaping (_ response: Response) -> Bool = { _ in true }
  25. ) {
  26. self.shouldGzipRequest = shouldGzipRequest
  27. self.shouldGzipResponse = shouldGzipResponse
  28. }
  29. public func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
  30. let acceptsGzip = request.http.headers[.acceptEncoding].first?.contains("gzip") ?? false
  31. let response = try next.respond(to: request)
  32. guard acceptsGzip && shouldGzipRequest(request) else {
  33. return response
  34. }
  35. return response.flatMap { response in
  36. guard self.shouldGzipResponse(response) else {
  37. return request.future(response)
  38. }
  39. var headers = response.http.headers
  40. headers.replaceOrAdd(name: .contentEncoding, value: "gzip")
  41. return response.http.body.consumeData(on: request).map { data in
  42. let stream = HTTPChunkedStream(on: request)
  43. let bufSize = 4096
  44. var buffer = ByteBufferAllocator().buffer(capacity: 16)
  45. let header : [UInt8] = [0x1f, 0x8b, 0x08, 0x00]
  46. let header2 : [UInt8] = [0x00, 0x03]
  47. buffer.write(bytes: header)
  48. buffer.write(integer: UInt32(Date().timeIntervalSince1970), endianness: .little)
  49. buffer.write(bytes: header2)
  50. var write = stream.write(.chunk(buffer))
  51. let crc32 = try Data.compress(size: data.count, bufferSize: bufSize,
  52. provider: {(offset, readSize) -> Data in
  53. return data.subdata(in: offset..<offset+readSize)
  54. },
  55. consumer: { data -> Void in
  56. guard data.count > 0 else { return } // Skip empty buffers
  57. var buffer = ByteBufferAllocator().buffer(capacity: bufSize)
  58. buffer.write(bytes: data)
  59. write = write.flatMap {
  60. return stream.write(.chunk(buffer))
  61. }
  62. })
  63. write = write.flatMap {
  64. buffer.clear()
  65. buffer.write(integer: crc32, endianness: .little)
  66. buffer.write(integer: UInt32(data.count), endianness: .little)
  67. return stream.write(.chunk(buffer))
  68. }.flatMap {
  69. return stream.write(.end)
  70. }
  71. DispatchQueue.global().async {
  72. _ = try? write.wait()
  73. }
  74. let httpResponse = HTTPResponse(status: response.http.status, headers: headers, body: stream)
  75. return request.response(http: httpResponse)
  76. }
  77. }
  78. }
  79. }