// // StreamingParser.swift // SwiftServerHttp // // Created by Carl Brown on 5/4/17. // // import Foundation import Dispatch import CHttpParser /// Class that wraps the CHTTPParser and calls the `WebApp` to get the response public class StreamingParser: HTTPResponseWriter { let webapp : WebApp /// Time to leave socket open waiting for next request to start public static let keepAliveTimeout: TimeInterval = 5 /// Flag to track if the client wants to send multiple requests on the same TCP connection var clientRequestedKeepAlive = false /// Tracks when socket should be closed. Needs to have a lock, since it's updated often private let _keepAliveUntilLock = DispatchSemaphore(value: 1) private var _keepAliveUntil: TimeInterval? var keepAliveUntil: TimeInterval? { get { _keepAliveUntilLock.wait() defer { _keepAliveUntilLock.signal() } return _keepAliveUntil } set { _keepAliveUntilLock.wait() defer { _keepAliveUntilLock.signal() } _keepAliveUntil = newValue } } /// Theoretical limit of how many open requests we can have. Used in Keep-Alive Header let maxRequests = 100 /// Optional delegate that can tell us how many connections are in-flight so we can set the Keep-Alive header /// to the correct number of available connections. If not present, the client will not be limited in number of /// connections that can be made simultaneously public weak var connectionCounter: CurrentConnectionCounting? /// Holds the bytes that come from the CHTTPParser until we have enough of them to do something with it var parserBuffer: Data? ///HTTP Parser var httpParser = http_parser() var httpParserSettings = http_parser_settings() /// Block that takes a chunk from the HTTPParser as input and writes to a Response as a result var httpBodyProcessingCallback: HTTPBodyProcessing? //Note: we want this to be strong so it holds onto the connector until it's explicitly cleared /// Protocol that we use to send data (and status info) back to the Network layer public var parserConnector: ParserConnecting? var lastCallBack = CallbackRecord.idle var lastHeaderName: String? var parsedHeaders = HTTPHeaders() var parsedHTTPMethod: HTTPMethod? var parsedHTTPVersion: HTTPVersion? var parsedURL: String? /// Is the currently parsed request an upgrade request? public private(set) var upgradeRequested = false /// Class that wraps the CHTTPParser and calls the `WebApp` to get the response /// /// - Parameter webapp: function that is used to create the response public init(webapp: @escaping WebApp, connectionCounter: CurrentConnectionCounting? = nil) { self.webapp = webapp self.connectionCounter = connectionCounter //Set up all the callbacks for the CHTTPParser library httpParserSettings.on_message_begin = { parser -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.messageBegan() } httpParserSettings.on_message_complete = { parser -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.messageCompleted() } httpParserSettings.on_headers_complete = { parser -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.headersCompleted() } httpParserSettings.on_header_field = { (parser, chunk, length) -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.headerFieldReceived(data: chunk, length: length) } httpParserSettings.on_header_value = { (parser, chunk, length) -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.headerValueReceived(data: chunk, length: length) } httpParserSettings.on_body = { (parser, chunk, length) -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.bodyReceived(data: chunk, length: length) } httpParserSettings.on_url = { (parser, chunk, length) -> Int32 in guard let listener = StreamingParser.getSelf(parser: parser) else { return Int32(0) } return listener.urlReceived(data: chunk, length: length) } http_parser_init(&httpParser, HTTP_REQUEST) self.httpParser.data = Unmanaged.passUnretained(self).toOpaque() } /// Read a stream from the network, pass it to the parser and return number of bytes consumed /// /// - Parameter data: data coming from network /// - Returns: number of bytes that we sent to the parser public func readStream(data:Data) -> Int { return data.withUnsafeBytes { (ptr) -> Int in return http_parser_execute(&self.httpParser, &self.httpParserSettings, ptr, data.count) } } /// States to track where we are in parsing the HTTP Stream from the client enum CallbackRecord { case idle, messageBegan, messageCompleted, headersCompleted, headerFieldReceived, headerValueReceived, bodyReceived, urlReceived } /// Process change of state as we get more and more parser callbacks /// /// - Parameter currentCallBack: state we are entering, as specified by the CHTTPParser /// - Returns: Whether or not the state actually changed @discardableResult func processCurrentCallback(_ currentCallBack:CallbackRecord) -> Bool { if lastCallBack == currentCallBack { return false } switch lastCallBack { case .headerFieldReceived: if let parserBuffer = self.parserBuffer { self.lastHeaderName = String(data: parserBuffer, encoding: .utf8) self.parserBuffer=nil } else { print("Missing parserBuffer after \(lastCallBack)") } case .headerValueReceived: if let parserBuffer = self.parserBuffer, let lastHeaderName = self.lastHeaderName, let headerValue = String(data:parserBuffer, encoding: .utf8) { self.parsedHeaders.append(newHeader: (lastHeaderName, headerValue)) self.lastHeaderName = nil self.parserBuffer=nil } else { print("Missing parserBuffer after \(lastCallBack)") } case .headersCompleted: let methodId = self.httpParser.method if let methodName = http_method_str(http_method(rawValue: methodId)) { self.parsedHTTPMethod = HTTPMethod(rawValue: String(validatingUTF8: methodName) ?? "GET") } self.parsedHTTPVersion = (Int(self.httpParser.http_major), Int(self.httpParser.http_minor)) self.parserBuffer=nil if !upgradeRequested { self.httpBodyProcessingCallback = self.webapp(self.createRequest(), self) } case .urlReceived: if let parserBuffer = self.parserBuffer { //Under heaptrack, this may appear to leak via _CFGetTSDCreateIfNeeded, // apparently, that's because it triggers thread metadata to be created self.parsedURL = String(data:parserBuffer, encoding: .utf8) self.parserBuffer=nil } else { print("Missing parserBuffer after \(lastCallBack)") } case .idle: break case .messageBegan: break case .messageCompleted: break case .bodyReceived: break } lastCallBack = currentCallBack return true } func messageBegan() -> Int32 { processCurrentCallback(.messageBegan) self.parserConnector?.responseBeginning() return 0 } func messageCompleted() -> Int32 { let didChangeState = processCurrentCallback(.messageCompleted) if let chunkHandler = self.httpBodyProcessingCallback, didChangeState { var stop=false switch chunkHandler { case .processBody(let handler): handler(.end, &stop) case .discardBody: break } } return 0 } func headersCompleted() -> Int32 { processCurrentCallback(.headersCompleted) //This needs to be set here and not messageCompleted if it's going to work here self.clientRequestedKeepAlive = (http_should_keep_alive(&httpParser) == 1) self.keepAliveUntil = Date(timeIntervalSinceNow: StreamingParser.keepAliveTimeout).timeIntervalSinceReferenceDate upgradeRequested = get_upgrade_value(&self.httpParser) == 1 return 0 } func headerFieldReceived(data: UnsafePointer?, length: Int) -> Int32 { processCurrentCallback(.headerFieldReceived) guard let data = data else { return 0 } data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length) } return 0 } func headerValueReceived(data: UnsafePointer?, length: Int) -> Int32 { processCurrentCallback(.headerValueReceived) guard let data = data else { return 0 } data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length) } return 0 } func bodyReceived(data: UnsafePointer?, length: Int) -> Int32 { processCurrentCallback(.bodyReceived) guard let data = data else { return 0 } data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in let buff = UnsafeBufferPointer(start: ptr, count: length) let chunk = DispatchData(bytes:buff) if let chunkHandler = self.httpBodyProcessingCallback { var stop=false var finished=false while !stop && !finished { switch chunkHandler { case .processBody(let handler): handler(.chunk(data: chunk, finishedProcessing: { finished=true }), &stop) case .discardBody: finished=true } } } } return 0 } func urlReceived(data: UnsafePointer?, length: Int) -> Int32 { processCurrentCallback(.urlReceived) guard let data = data else { return 0 } data.withMemoryRebound(to: UInt8.self, capacity: length) { (ptr) -> Void in self.parserBuffer == nil ? self.parserBuffer = Data(bytes:data, count:length) : self.parserBuffer?.append(ptr, count:length) } return 0 } static func getSelf(parser: UnsafeMutablePointer?) -> StreamingParser? { guard let pointee = parser?.pointee.data else { return nil } return Unmanaged.fromOpaque(pointee).takeUnretainedValue() } var headersWritten = false var isChunked = false /// Create a `HTTPRequest` struct from the parsed information public func createRequest() -> HTTPRequest { return HTTPRequest(method: parsedHTTPMethod!, target: parsedURL!, httpVersion: parsedHTTPVersion!, headers: parsedHeaders) } public func writeContinue(headers: HTTPHeaders?) /* to send an HTTP `100 Continue` */ { var status = "HTTP/1.1 \(HTTPResponseStatus.continue.code) \(HTTPResponseStatus.continue.reasonPhrase)\r\n" if let headers = headers { for (key, value) in headers.makeIterator() { status += "\(key): \(value)\r\n" } } status += "\r\n" // TODO use requested encoding if specified if let data = status.data(using: .utf8) { self.parserConnector?.queueSocketWrite(data) } else { //TODO handle encoding error } } public func writeResponse(_ response: HTTPResponse) { guard !headersWritten else { return } var headers = "HTTP/1.1 \(response.status.code) \(response.status.reasonPhrase)\r\n" switch(response.transferEncoding) { case .chunked: headers += "Transfer-Encoding: chunked\r\n" isChunked = true case .identity(let contentLength): headers += "Content-Length: \(contentLength)\r\n" } for (key, value) in response.headers.makeIterator() { headers += "\(key): \(value)\r\n" } let availableConnections = maxRequests - (self.connectionCounter?.connectionCount ?? 0) if clientRequestedKeepAlive && (availableConnections > 0) { headers.append("Connection: Keep-Alive\r\n") headers.append("Keep-Alive: timeout=\(Int(StreamingParser.keepAliveTimeout)), max=\(availableConnections)\r\n") } else { headers.append("Connection: Close\r\n") } headers.append("\r\n") // TODO use requested encoding if specified if let data = headers.data(using: .utf8) { self.parserConnector?.queueSocketWrite(data) headersWritten = true } else { //TODO handle encoding error } } public func writeTrailer(key: String, value: String) { fatalError("Not implemented") } public func writeBody(data: DispatchData, completion: @escaping (Result) -> Void) { writeBody(data: Data(data), completion: completion) } public func writeBody(data: DispatchData) /* convenience */ { writeBody(data: data) { _ in } } public func writeBody(data: Data, completion: @escaping (Result) -> Void) { guard headersWritten else { //TODO error or default headers? return } guard data.count > 0 else { // TODO fix Result completion(Result(completion: ())) return } var dataToWrite: Data! if isChunked { let chunkStart = (String(data.count, radix: 16) + "\r\n").data(using: .utf8)! dataToWrite = Data(chunkStart) dataToWrite.append(data) let chunkEnd = "\r\n".data(using: .utf8)! dataToWrite.append(chunkEnd) } else { dataToWrite = data } self.parserConnector?.queueSocketWrite(dataToWrite) completion(Result(completion: ())) } public func writeBody(data: Data) /* convenience */ { writeBody(data: data) { _ in } } public func done(completion: @escaping (Result) -> Void) { if isChunked { let chunkTerminate = "0\r\n\r\n".data(using: .utf8)! self.parserConnector?.queueSocketWrite(chunkTerminate) } self.parsedHTTPMethod = nil self.parsedURL=nil self.parsedHeaders = HTTPHeaders() self.lastHeaderName = nil self.parserBuffer = nil self.parsedHTTPMethod = nil self.parsedHTTPVersion = nil self.lastCallBack = .idle self.headersWritten = false self.httpBodyProcessingCallback = nil self.upgradeRequested = false let closeAfter = { if self.clientRequestedKeepAlive { self.keepAliveUntil = Date(timeIntervalSinceNow:StreamingParser.keepAliveTimeout).timeIntervalSinceReferenceDate self.parserConnector?.responseComplete() } else { self.parserConnector?.closeWriter() } } completion(Result(completion: closeAfter())) } public func done() /* convenience */ { done() { _ in } } public func abort() { fatalError("abort called, not sure what to do with it") } deinit { httpParser.data = nil } } /// Protocol implemented by the thing that sits in between us and the network layer public protocol ParserConnecting: class { /// Send data to the network do be written to the client func queueSocketWrite(_ from: Data) -> Void /// Let the network know that a response has started to avoid closing a connection during a slow write func responseBeginning() -> Void /// Let the network know that a response is complete, so it can be closed after timeout func responseComplete() -> Void /// Used to let the network know we're ready to close the connection func closeWriter() -> Void } /// Delegate that can tell us how many connections are in-flight so we can set the Keep-Alive header /// to the correct number of available connections public protocol CurrentConnectionCounting: class { /// Current number of active connections var connectionCount: Int { get } }