diff --git a/README.md b/README.md index bbedd73..85b10e2 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,9 @@ dependencies: [ ### Binary HTTP Encoding To serialise binary HTTP messages use `BHTTPSerializer.serialize(message, buffer)`. +As defined in [RFC9292](https://www.rfc-editor.org/rfc/rfc9292), you can choose to use either the known-length format or an indeterminate-length format. This choice can be configured during the initialization of the serializer by passing the desired type: `BHTTPSerializer(type: .knownLength)`. -To deserialise binary HTTP messages use `BHTTPParser`, adding received data with `append()`, then calling `completeBodyRecieved()`. The read the message parts received call `nextMessage()`. +To deserialise binary HTTP messages use `BHTTPParser`, adding received data with `append()`, then calling `completeBodyReceived()`. The read the message parts received call `nextMessage()`. ### Oblivious Encapsulation diff --git a/Sources/ObliviousHTTP/BHTTPSerializer.swift b/Sources/ObliviousHTTP/BHTTPSerializer.swift index c4f8821..06941a9 100644 --- a/Sources/ObliviousHTTP/BHTTPSerializer.swift +++ b/Sources/ObliviousHTTP/BHTTPSerializer.swift @@ -14,92 +14,310 @@ import NIOCore import NIOHTTP1 -// For now this type is entirely stateless, which is achieved by using the indefinite-length encoding. -// It also means it does not enforce correctness, and so can produce invalid encodings if a user holds -// it wrong. -// -// Later optimizations can be made by adding more state into this type. /// Binary HTTP serialiser as described in [RFC9292](https://www.rfc-editor.org/rfc/rfc9292). -/// Currently only indeterminate-length encoding is supported. public struct BHTTPSerializer { + + private var fsm: BHTTPSerializerFSM + private var context: SerializerContext + /// Initialise a Binary HTTP Serialiser. - public init() {} + /// - Parameters: + /// - type: The type of BHTTPSerializer you want: either known or indeterminate length. + /// - allocator: Byte buffer allocator used. + public init( + type: SerializerType = .indeterminateLength, + allocator: ByteBufferAllocator = ByteBufferAllocator() + ) { + self.fsm = BHTTPSerializerFSM(initialState: BHTTPSerializerState.start) + self.context = SerializerContext(type: type, allocator: allocator) + } /// Serialise a message into a buffer using binary HTTP encoding. /// - Parameters: - /// - message: The message to serialise. File regions are currently not supported. + /// - message: The message to serialise. File regions are currently not supported. /// - buffer: Destination buffer to serialise into. - public func serialize(_ message: Message, into buffer: inout ByteBuffer) { + public mutating func serialize(_ message: Message, into buffer: inout ByteBuffer) throws { switch message { case .request(.head(let requestHead)): - Self.serializeRequestHead(requestHead, into: &buffer) + try self.fsm.writeRequestHead(requestHead, into: &buffer, using: &self.context) + case .response(.head(let responseHead)): - Self.serializeResponseHead(responseHead, into: &buffer) + try self.fsm.writeResponseHead(responseHead, into: &buffer, using: &self.context) + case .request(.body(.byteBuffer(let body))), .response(.body(.byteBuffer(let body))): - Self.serializeContentChunk(body, into: &buffer) + try self.fsm.writeBodyChunk(body, into: &buffer, using: &self.context) + case .request(.body(.fileRegion)), .response(.body(.fileRegion)): - fatalError("fileregion unsupported") + throw ObliviousHTTPError.unsupportedOption(reason: "fileregion unsupported") + case .request(.end(.some(let trailers))), .response(.end(.some(let trailers))): - // Send a 0 to terminate the body, then a field section. - buffer.writeInteger(UInt8(0)) - Self.serializeIndeterminateLengthFieldSection(trailers, into: &buffer) + try self.fsm.writeTrailers(trailers, into: &buffer, using: &self.context) + case .request(.end(.none)), .response(.end(.none)): - // We can omit the trailers in this context, but we will always send a zero - // byte, either to communicate no trailers or no body. - buffer.writeInteger(UInt8(0)) + try self.fsm.writeRequestEnd(into: &buffer, using: &self.context) } } - private static func serializeRequestHead(_ head: HTTPRequestHead, into buffer: inout ByteBuffer) { - // First, the framing indicator. 2 for indeterminate length request. - buffer.writeVarint(2) - let method = head.method - let scheme = "https" // Hardcoded for now, but not really the right option. - let path = head.uri - let authority = head.headers["Host"].first ?? "" +} - buffer.writeVarintPrefixedString(method.rawValue) - buffer.writeVarintPrefixedString(scheme) - buffer.writeVarintPrefixedString(authority) - buffer.writeVarintPrefixedString(path) +// Enum definitions for message, states, and types. +extension BHTTPSerializer { + + private struct SerializerContext { + private var chunkBuffer: ByteBuffer + private var fieldSectionBuffer: ByteBuffer + private var type: SerializerType + + + public init( + type: SerializerType, + allocator: ByteBufferAllocator = ByteBufferAllocator() + ) { + self.chunkBuffer = allocator.buffer(capacity: 0) + self.fieldSectionBuffer = allocator.buffer(capacity: 0) + self.type = type + } + private var requestFramingIndicator: Int { + switch self.type { + case .knownLength: + return FramingIndicator.requestKnownLength + default: + return FramingIndicator.requestIndeterminateLength + } + } - Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer) - } + private var responseFramingIndicator: Int { + switch self.type { + case .knownLength: + return FramingIndicator.responseKnownLength + default: + return FramingIndicator.responseIndeterminateLength + } + } + + + mutating func serializeRequestHead(_ head: HTTPRequestHead, into buffer: inout ByteBuffer) { + // First, the framing indicator + buffer.writeVarint(requestFramingIndicator) - private static func serializeResponseHead(_ head: HTTPResponseHead, into buffer: inout ByteBuffer) { - // First, the framing indicator. 3 for indeterminate length response. - buffer.writeVarint(3) - buffer.writeVarint(Int(head.status.code)) - Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer) - } + let method = head.method + let scheme = "https" // Hardcoded for now, but not really the right option. + let path = head.uri + let authority = head.headers["Host"].first ?? "" - private static func serializeContentChunk(_ chunk: ByteBuffer, into buffer: inout ByteBuffer) { - // Omit zero-length chunks. - if chunk.readableBytes == 0 { return } - buffer.writeVarintPrefixedImmutableBuffer(chunk) - } + buffer.writeVarintPrefixedString(method.rawValue) + buffer.writeVarintPrefixedString(scheme) + buffer.writeVarintPrefixedString(authority) + buffer.writeVarintPrefixedString(path) - private static func serializeIndeterminateLengthFieldSection( - _ fields: HTTPHeaders, - into buffer: inout ByteBuffer - ) { - for (name, value) in fields { - buffer.writeVarintPrefixedString(name) - buffer.writeVarintPrefixedString(value) + switch self.type { + case .knownLength: + self.stackKnownLengthFieldSection(head.headers) + self.serializeKnownLengthFieldSection(into: &buffer) + case .indeterminateLength: + Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer) + default: break + } + } + + mutating func serializeResponseHead(_ head: HTTPResponseHead, into buffer: inout ByteBuffer) { + // First, the framing indicator + buffer.writeVarint(responseFramingIndicator) + + buffer.writeVarint(Int(head.status.code)) + + switch self.type { + case .knownLength: + self.stackKnownLengthFieldSection(head.headers) + self.serializeKnownLengthFieldSection(into: &buffer) + case .indeterminateLength: + Self.serializeIndeterminateLengthFieldSection(head.headers, into: &buffer) + default: break + } + } + + mutating func serializeChunk(_ chunk: ByteBuffer, into buffer: inout ByteBuffer) { + switch self.type { + case .knownLength: + self.stackContentChunk(chunk) + case .indeterminateLength: + Self.serializeContentChunk(chunk, into: &buffer) + default: break + } + } + + static func serializeContentChunk(_ chunk: ByteBuffer, into buffer: inout ByteBuffer) { + if chunk.readableBytes == 0 { return } + buffer.writeVarintPrefixedImmutableBuffer(chunk) + } + + mutating func serializeStackedContent(into buffer: inout ByteBuffer) { + if self.chunkBuffer.readableBytes == 0 { return } + buffer.writeVarintPrefixedImmutableBuffer(self.chunkBuffer) + self.chunkBuffer.clear() + } + + mutating func stackContentChunk(_ chunk: ByteBuffer) { + self.chunkBuffer.writeImmutableBuffer(chunk) + } + + static func serializeIndeterminateLengthFieldSection( + _ fields: HTTPHeaders, + into buffer: inout ByteBuffer + ) { + for (name, value) in fields { + buffer.writeVarintPrefixedString(name) + buffer.writeVarintPrefixedString(value) + } + buffer.writeInteger(UInt8(0)) // End of field section + } + + mutating func serializeTrailers(_ trailers: HTTPHeaders, into buffer: inout ByteBuffer) { + switch self.type { + case .knownLength: + self.serializeStackedContent(into: &buffer) + self.stackKnownLengthFieldSection(trailers) + case .indeterminateLength: + // Send a 0 to terminate the body, then a field section. + buffer.writeInteger(UInt8(0)) + Self.serializeIndeterminateLengthFieldSection(trailers, into: &buffer) + default: break + } + } + + + mutating func endRequest(into buffer: inout ByteBuffer) { + switch self.type { + case .knownLength: + self.serializeStackedContent(into: &buffer) + self.serializeKnownLengthFieldSection(into: &buffer) + case .indeterminateLength: + buffer.writeInteger(UInt8(0)) + default: break + } + } + + mutating func stackKnownLengthFieldSection(_ fields: HTTPHeaders) { + for (name, value) in fields { + self.fieldSectionBuffer.writeVarintPrefixedString(name) + self.fieldSectionBuffer.writeVarintPrefixedString(value) + } + } + + + mutating func serializeKnownLengthFieldSection(into buffer: inout ByteBuffer) { + buffer.writeVarintPrefixedImmutableBuffer(self.fieldSectionBuffer) + self.fieldSectionBuffer.clear() } - // This is technically a varint but we can skip the check there because we know it can always encode in one byte. - buffer.writeInteger(UInt8(0)) } + + // Finite State Machine for managing transitions in BHTTPSerializer. + private struct BHTTPSerializerFSM { + private var currentState: BHTTPSerializerState -} + init(initialState: BHTTPSerializerState) { + self.currentState = initialState + } + + mutating func writeRequestHead( + _ requestHead: HTTPRequestHead, + into buffer: inout ByteBuffer, + using context: inout SerializerContext + ) throws { + try self.transition(to: .header) + context.serializeRequestHead(requestHead, into: &buffer) + } + + mutating func writeResponseHead( + _ responseHead: HTTPResponseHead, + into buffer: inout ByteBuffer, + using context: inout SerializerContext + ) throws { + try self.transition(to: .header) + context.serializeResponseHead(responseHead, into: &buffer) + } + + mutating func writeRequestEnd( + into buffer: inout ByteBuffer, + using context: inout SerializerContext + ) throws { + context.endRequest(into: &buffer) + try self.transition(to: .end) + } + mutating func writeBodyChunk( + _ body: ByteBuffer, + into buffer: inout ByteBuffer, + using context: inout SerializerContext + ) throws { + context.serializeChunk(body, into: &buffer) + try self.transition(to: .chunk) + } + + mutating func writeTrailers( + _ trailers: HTTPHeaders, + into buffer: inout ByteBuffer, + using context: inout SerializerContext + ) throws { + context.serializeTrailers(trailers, into: &buffer) + try self.transition(to: .trailers) + } + + private mutating func transition(to state: BHTTPSerializerState) throws { + let allowedNextStates: Set + switch currentState { + case .start: + allowedNextStates = [.header] + case .header: + allowedNextStates = [.chunk, .trailers, .end] + case .chunk: + allowedNextStates = [.trailers, .end, .chunk] + case .trailers: + allowedNextStates = [.trailers, .end] + case .end: + allowedNextStates = [] + } + guard allowedNextStates.contains(state) else { + throw ObliviousHTTPError.unexpectedHTTPMessageSection() + } + currentState = state + } + } -extension BHTTPSerializer { - /// Types of message for binary http serilaisation public enum Message { - /// Part of an HTTP request. case request(HTTPClientRequestPart) - /// Part of an HTTP response. case response(HTTPServerResponsePart) } + + public struct SerializerType: Hashable, Sendable { + private enum InternalType: Hashable { + case knownLength + case indeterminateLength + } + + private let type: InternalType + + public static let knownLength = SerializerType(type: .knownLength) + public static let indeterminateLength = SerializerType(type: .indeterminateLength) + + private init(type: InternalType) { + self.type = type + } + + } + + internal struct FramingIndicator { + static var requestKnownLength: Int { 0 } + static var responseKnownLength: Int { 1 } + static var requestIndeterminateLength: Int { 2 } + static var responseIndeterminateLength: Int { 3 } + } + + internal enum BHTTPSerializerState { + case start + case header + case chunk + case trailers + case end + } } diff --git a/Sources/ObliviousHTTP/Errors.swift b/Sources/ObliviousHTTP/Errors.swift index 62910d5..4794990 100644 --- a/Sources/ObliviousHTTP/Errors.swift +++ b/Sources/ObliviousHTTP/Errors.swift @@ -44,13 +44,29 @@ public struct ObliviousHTTPError: Error, Hashable { Self.init(backing: .truncatedEncoding(reason: reason)) } - /// Create an error indicating that parsing faileud due to an unexpected HTTP status code. + /// Create an error indicating that parsing failed due to an unexpected HTTP status code. /// - Parameter status: The status code encountered. /// - Returns: An Error representing this failure. @inline(never) public static func invalidStatus(status: Int) -> ObliviousHTTPError { Self.init(backing: .invalidStatus(status: status)) } + + /// Create an error indicating that serializing failed due to an unexpected HTTP section. + /// - Returns: An Error representing this failure. + @inline(never) + public static func unexpectedHTTPMessageSection() -> ObliviousHTTPError { + Self.init(backing: .unexpectedHTTPMessageSection(state: "An unexpected HTTP message section was encountered.")) + } + + /// Create an error indicating that serializing failed due to an unsupported option. + /// - Parameter reason: The unsupported option details. + /// - Returns: An Error representing this failure. + @inline(never) + public static func unsupportedOption(reason: String) -> ObliviousHTTPError { + Self.init(backing: .unsupportedOption(reason: reason)) + } + } extension ObliviousHTTPError { @@ -59,5 +75,7 @@ extension ObliviousHTTPError { case invalidFieldSection(reason: String) case truncatedEncoding(reason: String) case invalidStatus(status: Int) + case unexpectedHTTPMessageSection(state: String) + case unsupportedOption(reason: String) } } diff --git a/Tests/ObliviousHTTPTests/BHTTPSerializerTests.swift b/Tests/ObliviousHTTPTests/BHTTPSerializerTests.swift index b090c90..fae3b6f 100644 --- a/Tests/ObliviousHTTPTests/BHTTPSerializerTests.swift +++ b/Tests/ObliviousHTTPTests/BHTTPSerializerTests.swift @@ -29,13 +29,46 @@ final class BHTTPSerializerTests: XCTestCase { .head(.init(version: .http1_1, method: .GET, uri: "/example", headers: expectedHeaders)), .end(nil), ] - - let serializer = BHTTPSerializer() + var buffer = ByteBuffer() + var serializer = BHTTPSerializer() var parser = BHTTPParser(role: .server) + + for message in request { + try serializer.serialize(.request(message), into: &buffer) + } + + parser.append(buffer) + parser.completeBodyReceived() + var received: [HTTPServerRequestPart] = [] + + while let next = try parser.nextMessage(), case .request(let request) = next { + received.append(request) + } + + let expectedRequest: [HTTPServerRequestPart] = [ + .head(.init(version: .http1_1, method: .GET, uri: "/example", headers: expectedHeaders)), + .end(nil), + ] + XCTAssertEqual(expectedRequest, received) + } + + func testSimpleGetRequestRoundTripsWithKnownLengthSerializer() throws { + let expectedHeaders = HTTPHeaders([ + ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), + ("host", "www.example.com"), + ("accept-language", "en, mi"), + ]) + let request: [HTTPClientRequestPart] = [ + .head(.init(version: .http1_1, method: .GET, uri: "/example", headers: expectedHeaders)), + .end(nil), + ] + var buffer = ByteBuffer() + var serializer = BHTTPSerializer(type: .knownLength) + var parser = BHTTPParser(role: .server) for message in request { - serializer.serialize(.request(message), into: &buffer) + try serializer.serialize(.request(message), into: &buffer) } parser.append(buffer) @@ -53,6 +86,71 @@ final class BHTTPSerializerTests: XCTestCase { XCTAssertEqual(expectedRequest, received) } + func testSerializerThrowsForMultipleHeadPartsInRequest() throws { + let expectedHeaders = HTTPHeaders([ + ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), + ("host", "www.example.com"), + ("accept-language", "en, mi"), + ]) + let request: [HTTPClientRequestPart] = [ + .head(.init(version: .http1_1, method: .GET, uri: "/example", headers: expectedHeaders)), + .head(.init(version: .http1_1, method: .GET, uri: "/example", headers: expectedHeaders)), + .end(nil), + ] + + var buffer = ByteBuffer() + var serializer = BHTTPSerializer() + + var didThrowError = false + + for message in request { + do { + try serializer.serialize(.request(message), into: &buffer) + } catch { + didThrowError = true + XCTAssertNotNil(error, "Chunk section was not expected.") + break + } + } + + XCTAssertTrue(didThrowError, "Expected serializer to throw an error because the request has 2 heads part.") + } + + func testSerializerThrowsForOutOfOrderRequestParts() throws { + let expectedHeaders = HTTPHeaders([ + ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), + ("host", "www.example.com"), + ("accept-language", "en, mi"), + ("content-length", "5"), + ]) + let request: [HTTPClientRequestPart] = [ + .body(.byteBuffer(.init(string: "he"))), + .head(.init(version: .http1_1, method: .POST, uri: "/example", headers: expectedHeaders)), + .body(.byteBuffer(.init(string: "llo"))), + .end(nil), + ] + + var buffer = ByteBuffer() + var serializer = BHTTPSerializer() + + var didThrowError = false + + for message in request { + do { + try serializer.serialize(.request(message), into: &buffer) + } catch { + didThrowError = true + XCTAssertNotNil(error, "Header section was not expected.") + break + } + } + + XCTAssertTrue( + didThrowError, + "Expected serializer to throw an error because the request has out of order parts." + ) + } + func testSimplePOSTRequestRoundTrips() throws { let expectedHeaders = HTTPHeaders([ ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), @@ -67,12 +165,12 @@ final class BHTTPSerializerTests: XCTestCase { .end(nil), ] - let serializer = BHTTPSerializer() - var parser = BHTTPParser(role: .server) var buffer = ByteBuffer() + var serializer = BHTTPSerializer() + var parser = BHTTPParser(role: .server) for message in request { - serializer.serialize(.request(message), into: &buffer) + try serializer.serialize(.request(message), into: &buffer) } parser.append(buffer) @@ -92,6 +190,44 @@ final class BHTTPSerializerTests: XCTestCase { XCTAssertEqual(expectedRequest, received) } + func testSimplePOSTRequestRoundTripsWithKnownLengthSerialiser() throws { + let expectedHeaders = HTTPHeaders([ + ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), + ("host", "www.example.com"), + ("accept-language", "en, mi"), + ("content-length", "5"), + ]) + let request: [HTTPClientRequestPart] = [ + .head(.init(version: .http1_1, method: .POST, uri: "/example", headers: expectedHeaders)), + .body(.byteBuffer(.init(string: "he"))), + .body(.byteBuffer(.init(string: "llo"))), + .end(nil), + ] + + var buffer = ByteBuffer() + var serializer = BHTTPSerializer(type: .knownLength) + var parser = BHTTPParser(role: .server) + + for message in request { + try serializer.serialize(.request(message), into: &buffer) + } + + parser.append(buffer) + parser.completeBodyReceived() + var received: [HTTPServerRequestPart] = [] + + while let next = try parser.nextMessage(), case .request(let request) = next { + received.append(request) + } + + let expectedRequest: [HTTPServerRequestPart] = [ + .head(.init(version: .http1_1, method: .POST, uri: "/example", headers: expectedHeaders)), + .body(.init(string: "hello")), + .end(nil), + ] + XCTAssertEqual(expectedRequest, received) + } + func testSimplePOSTRequestWithTrailersRoundTrips() throws { let expectedHeaders = HTTPHeaders([ ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), @@ -110,12 +246,12 @@ final class BHTTPSerializerTests: XCTestCase { .end(expectedTrailers), ] - let serializer = BHTTPSerializer() - var parser = BHTTPParser(role: .server) var buffer = ByteBuffer() + var serializer = BHTTPSerializer() + var parser = BHTTPParser(role: .server) for message in request { - serializer.serialize(.request(message), into: &buffer) + try serializer.serialize(.request(message), into: &buffer) } parser.append(buffer) @@ -135,6 +271,49 @@ final class BHTTPSerializerTests: XCTestCase { XCTAssertEqual(expectedRequest, received) } + func testSimplePOSTRequestWithTrailersRoundTripsAndKnownLengthSerializer() throws { + let expectedHeaders = HTTPHeaders([ + ("user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"), + ("host", "www.example.com"), + ("accept-language", "en, mi"), + ("content-length", "5"), + ]) + let expectedTrailers = HTTPHeaders([ + ("foo", "bar"), + ("froo", "brar"), + ]) + let request: [HTTPClientRequestPart] = [ + .head(.init(version: .http1_1, method: .POST, uri: "/example", headers: expectedHeaders)), + .body(.byteBuffer(.init(string: "he"))), + .body(.byteBuffer(.init(string: "llo"))), + .end(expectedTrailers), + .end(nil), + ] + + var buffer = ByteBuffer() + var serializer = BHTTPSerializer(type: .knownLength) + var parser = BHTTPParser(role: .server) + + for message in request { + try serializer.serialize(.request(message), into: &buffer) + } + + parser.append(buffer) + parser.completeBodyReceived() + var received: [HTTPServerRequestPart] = [] + + while let next = try parser.nextMessage(), case .request(let request) = next { + received.append(request) + } + + let expectedRequest: [HTTPServerRequestPart] = [ + .head(.init(version: .http1_1, method: .POST, uri: "/example", headers: expectedHeaders)), + .body(.init(string: "hello")), + .end(expectedTrailers), + ] + XCTAssertEqual(expectedRequest, received) + } + func testSimple201ResponseRoundTrips() throws { let expectedHeaders = HTTPHeaders([ ("server", "apache"), @@ -145,12 +324,12 @@ final class BHTTPSerializerTests: XCTestCase { .end(nil), ] - let serializer = BHTTPSerializer() - var parser = BHTTPParser(role: .client) var buffer = ByteBuffer() + var serializer = BHTTPSerializer() + var parser = BHTTPParser(role: .client) for message in response { - serializer.serialize(.response(message), into: &buffer) + try serializer.serialize(.response(message), into: &buffer) } parser.append(buffer) @@ -179,13 +358,12 @@ final class BHTTPSerializerTests: XCTestCase { .body(.byteBuffer(ByteBuffer(string: "hello"))), .end(nil), ] - - let serializer = BHTTPSerializer() - var parser = BHTTPParser(role: .client) var buffer = ByteBuffer() + var serializer = BHTTPSerializer() + var parser = BHTTPParser(role: .client) for message in response { - serializer.serialize(.response(message), into: &buffer) + try serializer.serialize(.response(message), into: &buffer) } parser.append(buffer) @@ -220,12 +398,53 @@ final class BHTTPSerializerTests: XCTestCase { .end(expectedTrailers), ] - let serializer = BHTTPSerializer() + var buffer = ByteBuffer() + var serializer = BHTTPSerializer() var parser = BHTTPParser(role: .client) + + for message in response { + try serializer.serialize(.response(message), into: &buffer) + } + + parser.append(buffer) + parser.completeBodyReceived() + var received: [HTTPClientResponsePart] = [] + + while let next = try parser.nextMessage(), case .response(let response) = next { + received.append(response) + } + + let expectedResponse: [HTTPClientResponsePart] = [ + .head(.init(version: .http1_1, status: .noContent, headers: expectedHeaders)), + .body(ByteBuffer(string: "hello")), + .end(expectedTrailers), + ] + XCTAssertEqual(expectedResponse, received) + } + + func testSimple200ResponseWithBodyAndTrailersRoundTripsAndKnownLengthSerializer() throws { + let expectedHeaders = HTTPHeaders([ + ("server", "apache"), + ("other-header", "its value"), + ("content-length", "5"), + ]) + let expectedTrailers = HTTPHeaders([ + ("foo", "bar"), + ("froo", "brar"), + ]) + let response: [HTTPServerResponsePart] = [ + .head(.init(version: .http1_1, status: .noContent, headers: expectedHeaders)), + .body(.byteBuffer(ByteBuffer(string: "hello"))), + .end(expectedTrailers), + .end(nil), + ] + var buffer = ByteBuffer() + var serializer = BHTTPSerializer(type: .knownLength) + var parser = BHTTPParser(role: .client) for message in response { - serializer.serialize(.response(message), into: &buffer) + try serializer.serialize(.response(message), into: &buffer) } parser.append(buffer)