From 7826631971b00efb8bfd657d168567f587021dfa Mon Sep 17 00:00:00 2001 From: Fatih Nayebi Date: Wed, 7 Mar 2018 17:04:38 -0500 Subject: [PATCH 1/3] CompressResponse is public CompressResponse public to be used for typed filter definition --- .../HTTPContentCompression.swift | 270 +++++++++--------- 1 file changed, 139 insertions(+), 131 deletions(-) diff --git a/Sources/PerfectHTTPServer/HTTPContentCompression.swift b/Sources/PerfectHTTPServer/HTTPContentCompression.swift index 5e9a0a8..e780702 100644 --- a/Sources/PerfectHTTPServer/HTTPContentCompression.swift +++ b/Sources/PerfectHTTPServer/HTTPContentCompression.swift @@ -19,138 +19,146 @@ import PerfectHTTP import PerfectCZlib class ZlibStream { - var stream = z_stream() - var closed = false - - init?() { - stream.zalloc = nil - stream.zfree = nil - stream.opaque = nil - - let err = deflateInit_(&stream, Z_DEFAULT_COMPRESSION, ZLIB_VERSION, Int32(MemoryLayout.size)) - guard Z_OK == err else { - return nil - } - } - - deinit { - if !closed { - close() - } - } - - func compress(_ bytes: [UInt8], flush: Bool) -> [UInt8] { - if bytes.isEmpty && !flush { - return [] - } - let needed = Int(compressBound(UInt(bytes.count))) - let dest = UnsafeMutablePointer.allocate(capacity: needed) - defer { - dest.deallocate(capacity: needed) - } - if !bytes.isEmpty { - stream.next_in = UnsafeMutablePointer(mutating: bytes) - stream.avail_in = uInt(bytes.count) - } else { - stream.next_in = nil - stream.avail_in = 0 - } - var out = [UInt8]() - repeat { - stream.next_out = dest - stream.avail_out = uInt(needed) - let err = deflate(&stream, flush ? Z_FINISH : Z_NO_FLUSH) - guard err != Z_STREAM_ERROR else { - break - } - let have = uInt(needed) - stream.avail_out - let b2 = UnsafeRawBufferPointer(start: dest, count: Int(have)) - out.append(contentsOf: b2.map { $0 }) - } while stream.avail_out == 0 - return out - } - - func close() { - if !closed { - closed = true - deflateEnd(&stream) - } - } + var stream = z_stream() + var closed = false + + init?() { + stream.zalloc = nil + stream.zfree = nil + stream.opaque = nil + + let err = deflateInit_(&stream, Z_DEFAULT_COMPRESSION, ZLIB_VERSION, Int32(MemoryLayout.size)) + guard Z_OK == err else { + return nil + } + } + + deinit { + if !closed { + close() + } + } + + func compress(_ bytes: [UInt8], flush: Bool) -> [UInt8] { + if bytes.isEmpty && !flush { + return [] + } + let needed = Int(compressBound(UInt(bytes.count))) + let dest = UnsafeMutablePointer.allocate(capacity: needed) + defer { + dest.deallocate(capacity: needed) + } + if !bytes.isEmpty { + stream.next_in = UnsafeMutablePointer(mutating: bytes) + stream.avail_in = uInt(bytes.count) + } else { + stream.next_in = nil + stream.avail_in = 0 + } + var out = [UInt8]() + repeat { + stream.next_out = dest + stream.avail_out = uInt(needed) + let err = deflate(&stream, flush ? Z_FINISH : Z_NO_FLUSH) + guard err != Z_STREAM_ERROR else { + break + } + let have = uInt(needed) - stream.avail_out + let b2 = UnsafeRawBufferPointer(start: dest, count: Int(have)) + out.append(contentsOf: b2.map { $0 }) + } while stream.avail_out == 0 + return out + } + + func close() { + if !closed { + closed = true + deflateEnd(&stream) + } + } } public extension HTTPFilter { - /// Response filter which provides content compression. - /// Mime types which will be encoded or ignored can be specified with the "compressTypes" and - /// "ignoreTypes" keys, respectively. The values for these keys should be an array of String - /// containing either the full mime type or the the main type with a * wildcard. e.g. text/* - /// The default values for the compressTypes key are: "*/*" - /// The default values for the ignoreTypes key are: "image/*", "video/*", "audio/*" - public static func contentCompression(data: [String:Any]) throws -> HTTPResponseFilter { - let inCompressTypes = data["compressTypes"] as? [String] ?? ["*/*"] - let inIgnoreTypes = data["ignoreTypes"] as? [String] ?? ["image/*", "video/*", "audio/*"] - - struct CompressResponse: HTTPResponseFilter { - let compressTypes: [MimeType] - let ignoreTypes: [MimeType] - - func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) { - let req = response.request - if case .head = req.method { - return callback(.continue) - } - if case .notModified = response.status { - return callback(.continue) - } - if let acceptEncoding = req.header(.acceptEncoding), - let contentType = contentType(response: response), - clientWantsCompression(acceptEncoding: acceptEncoding), - shouldCompress(mimeType: contentType) { - - let skipCheck = response.request.scratchPad["no-compression"] as? Bool ?? false - if !skipCheck, let stream = ZlibStream() { - response.setHeader(.contentEncoding, value: "deflate") - if response.isStreaming { - response.request.scratchPad["zlib-stream"] = stream - } else { - let old = response.bodyBytes - let new = stream.compress(old, flush: true) - response.bodyBytes = new - stream.close() - response.setHeader(.contentLength, value: "\(new.count)") - } - } - } - return callback(.continue) - } - - func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) { - guard response.isStreaming, let stream = response.request.scratchPad["zlib-stream"] as? ZlibStream else { - return callback(.continue) - } - - let flush = response.request.scratchPad["_flushing_"] as? Bool ?? false - response.bodyBytes = stream.compress(response.bodyBytes, flush: flush) - return callback(.continue) - } - - private func contentType(response: HTTPResponse) -> String? { - if let contentType = response.header(.contentType) { - return contentType - } - let path = response.request.path - return MimeType.forExtension(path.lastFilePathComponent.filePathExtension) - } - - private func clientWantsCompression(acceptEncoding: String) -> Bool { - return acceptEncoding.contains("deflate") - } - - private func shouldCompress(mimeType: String) -> Bool { - let mime = MimeType(mimeType) - return compressTypes.contains(mime) && !ignoreTypes.contains(mime) - } - } - return CompressResponse(compressTypes: inCompressTypes.map { MimeType($0) }, - ignoreTypes: inIgnoreTypes.map { MimeType($0) }) - } + + /// Response filter which provides content compression. + /// Mime types which will be encoded or ignored can be specified with the "compressTypes" and + /// "ignoreTypes" keys, respectively. The values for these keys should be an array of String + /// containing either the full mime type or the the main type with a * wildcard. e.g. text/* + /// The default values for the compressTypes key are: "*/*" + /// The default values for the ignoreTypes key are: "image/*", "video/*", "audio/*" + public struct CompressResponse: HTTPResponseFilter { + + var compressTypes: [MimeType] + var ignoreTypes: [MimeType] + + public init(compressTypes: [MimeType]? = nil, ignoreTypes: [MimeType]? = nil) { + self.compressTypes = compressTypes ?? ["*/*"] + self.ignoreTypes = ignoreTypes ?? ["image/*", "video/*", "audio/*"] + } + + public func filterHeaders(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) { + let req = response.request + if case .head = req.method { + return callback(.continue) + } + if case .notModified = response.status { + return callback(.continue) + } + if let acceptEncoding = req.header(.acceptEncoding), + let contentType = contentType(response: response), + clientWantsCompression(acceptEncoding: acceptEncoding), + shouldCompress(mimeType: contentType) { + + let skipCheck = response.request.scratchPad["no-compression"] as? Bool ?? false + if !skipCheck, let stream = ZlibStream() { + response.setHeader(.contentEncoding, value: "deflate") + if response.isStreaming { + response.request.scratchPad["zlib-stream"] = stream + } else { + let old = response.bodyBytes + let new = stream.compress(old, flush: true) + response.bodyBytes = new + stream.close() + response.setHeader(.contentLength, value: "\(new.count)") + } + } + } + return callback(.continue) + } + + public func filterBody(response: HTTPResponse, callback: (HTTPResponseFilterResult) -> ()) { + guard response.isStreaming, let stream = response.request.scratchPad["zlib-stream"] as? ZlibStream else { + return callback(.continue) + } + + let flush = response.request.scratchPad["_flushing_"] as? Bool ?? false + response.bodyBytes = stream.compress(response.bodyBytes, flush: flush) + return callback(.continue) + } + + private func contentType(response: HTTPResponse) -> String? { + if let contentType = response.header(.contentType) { + return contentType + } + let path = response.request.path + return MimeType.forExtension(path.lastFilePathComponent.filePathExtension) + } + + private func clientWantsCompression(acceptEncoding: String) -> Bool { + return acceptEncoding.contains("deflate") + } + + private func shouldCompress(mimeType: String) -> Bool { + let mime = MimeType(mimeType) + return compressTypes.contains(mime) && !ignoreTypes.contains(mime) + } + } + + public static func contentCompression(data: [String:Any]) throws -> HTTPResponseFilter { + let inCompressTypes = data["compressTypes"] as? [String] ?? ["*/*"] + let inIgnoreTypes = data["ignoreTypes"] as? [String] ?? ["image/*", "video/*", "audio/*"] + + return CompressResponse(compressTypes: inCompressTypes.map { MimeType($0) }, + ignoreTypes: inIgnoreTypes.map { MimeType($0) }) + } } From b55ca80e098a691ec9d30dfd59bf802672918d98 Mon Sep 17 00:00:00 2001 From: Fatih Date: Sat, 5 May 2018 09:39:51 -0400 Subject: [PATCH 2/3] Date header is added to the HTTPResponse --- .../PerfectHTTPServer/HTTP11/HTTP11Response.swift | 7 +++++++ Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift b/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift index 3b4eaea..f20bdf5 100644 --- a/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift +++ b/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift @@ -26,6 +26,7 @@ import PerfectNet import PerfectThread import PerfectHTTP +import PerfectLib class HTTP11Response: HTTPResponse { var status = HTTPResponseStatus.ok @@ -158,6 +159,12 @@ class HTTP11Response: HTTPResponse { addHeader(.transferEncoding, value: "chunked") } else if !contentLengthSet { addHeader(.contentLength, value: "\(bodyBytes.count)") + } + var posixTime = timeval() + gettimeofday(&posixTime, nil) + let timeOfDay = Double((posixTime.tv_sec * 1000) + (Int(posixTime.tv_usec)/1000)) + if let formattedDate = try? formatDate(timeOfDay, format: "%a, %d-%b-%Y %T GMT") { + setHeader(.date, value: formattedDate) } if let filters = self.filters { return filterHeaders(allFilters: filters, callback: callback) diff --git a/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift b/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift index f7613bc..fd3e6d5 100644 --- a/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift +++ b/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift @@ -17,6 +17,12 @@ //===----------------------------------------------------------------------===// // +#if os(Linux) + import SwiftGlibc +#else + import Darwin +#endif + import PerfectLib import PerfectHTTP import PerfectThread @@ -97,6 +103,12 @@ final class HTTP2Response: HTTPResponse { guard h2Request.streamState != .closed else { return callback(false) } + var posixTime = timeval() + gettimeofday(&posixTime, nil) + let timeOfDay = Double((posixTime.tv_sec * 1000) + (Int(posixTime.tv_usec)/1000)) + if let formattedDate = try? formatDate(timeOfDay, format: "%a, %d-%b-%Y %T GMT") { + setHeader(.date, value: formattedDate) + } if let filters = self.filters { filterHeaders(allFilters: filters, callback: callback) } else { From 579b107ae2fe79cafa44edeba2637c1797b262ca Mon Sep 17 00:00:00 2001 From: Fatih Date: Sat, 5 May 2018 09:43:35 -0400 Subject: [PATCH 3/3] used tabs instead of spaces for indentation --- .../HTTP11/HTTP11Response.swift | 32 +++++++++---------- .../HTTP2/HTTP2Response.swift | 16 +++++----- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift b/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift index f20bdf5..48e168b 100644 --- a/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift +++ b/Sources/PerfectHTTPServer/HTTP11/HTTP11Response.swift @@ -150,27 +150,27 @@ class HTTP11Response: HTTPResponse { } } - func pushHeaders(callback: @escaping (Bool) -> ()) { - wroteHeaders = true - if isKeepAlive { - addHeader(.connection, value: "keep-alive") + func pushHeaders(callback: @escaping (Bool) -> ()) { + wroteHeaders = true + if isKeepAlive { + addHeader(.connection, value: "keep-alive") + } + if isStreaming { + addHeader(.transferEncoding, value: "chunked") + } else if !contentLengthSet { + addHeader(.contentLength, value: "\(bodyBytes.count)") + } + var posixTime = timeval() + gettimeofday(&posixTime, nil) + let timeOfDay = Double((posixTime.tv_sec * 1000) + (Int(posixTime.tv_usec)/1000)) + if let formattedDate = try? formatDate(timeOfDay, format: "%a, %d-%b-%Y %T GMT") { + setHeader(.date, value: formattedDate) } - if isStreaming { - addHeader(.transferEncoding, value: "chunked") - } else if !contentLengthSet { - addHeader(.contentLength, value: "\(bodyBytes.count)") - } - var posixTime = timeval() - gettimeofday(&posixTime, nil) - let timeOfDay = Double((posixTime.tv_sec * 1000) + (Int(posixTime.tv_usec)/1000)) - if let formattedDate = try? formatDate(timeOfDay, format: "%a, %d-%b-%Y %T GMT") { - setHeader(.date, value: formattedDate) - } if let filters = self.filters { return filterHeaders(allFilters: filters, callback: callback) } finishPushHeaders(callback: callback) - } + } func filterHeaders(allFilters: IndexingIterator<[[HTTPResponseFilter]]>, callback: @escaping (Bool) -> ()) { var allFilters = allFilters diff --git a/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift b/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift index fd3e6d5..1fd7f6d 100644 --- a/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift +++ b/Sources/PerfectHTTPServer/HTTP2/HTTP2Response.swift @@ -18,9 +18,9 @@ // #if os(Linux) - import SwiftGlibc + import SwiftGlibc #else - import Darwin + import Darwin #endif import PerfectLib @@ -103,12 +103,12 @@ final class HTTP2Response: HTTPResponse { guard h2Request.streamState != .closed else { return callback(false) } - var posixTime = timeval() - gettimeofday(&posixTime, nil) - let timeOfDay = Double((posixTime.tv_sec * 1000) + (Int(posixTime.tv_usec)/1000)) - if let formattedDate = try? formatDate(timeOfDay, format: "%a, %d-%b-%Y %T GMT") { - setHeader(.date, value: formattedDate) - } + var posixTime = timeval() + gettimeofday(&posixTime, nil) + let timeOfDay = Double((posixTime.tv_sec * 1000) + (Int(posixTime.tv_usec)/1000)) + if let formattedDate = try? formatDate(timeOfDay, format: "%a, %d-%b-%Y %T GMT") { + setHeader(.date, value: formattedDate) + } if let filters = self.filters { filterHeaders(allFilters: filters, callback: callback) } else {