From d269c1c5e27aea628191442aa638cc2b259fdf6d Mon Sep 17 00:00:00 2001 From: dkz2 Date: Mon, 25 Dec 2023 10:08:52 -0600 Subject: [PATCH 1/6] fixed sandbox bind error --- Sources/NIOCore/Channel.swift | 3 + Sources/NIOPosix/SocketChannel.swift | 18 +- .../NIOHTTPTests/HTTPDecoderLengthTest.swift | 556 ++++++++++++++++++ Tests/NIOPosixTests/SALChannelTests.swift | 42 ++ .../SyscallAbstractionLayer.swift | 13 +- 5 files changed, 624 insertions(+), 8 deletions(-) create mode 100644 Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift diff --git a/Sources/NIOCore/Channel.swift b/Sources/NIOCore/Channel.swift index d61e3b7dcf..2925eb0768 100644 --- a/Sources/NIOCore/Channel.swift +++ b/Sources/NIOCore/Channel.swift @@ -371,6 +371,9 @@ public enum ChannelError: Error { /// An attempt was made to remove a ChannelHandler that is not removable. case unremovableHandler + + /// An attempt to bind a `Channel` failed. + case bindFailed } extension ChannelError: Equatable { } diff --git a/Sources/NIOPosix/SocketChannel.swift b/Sources/NIOPosix/SocketChannel.swift index 51fdc3a87e..46a7b83ea6 100644 --- a/Sources/NIOPosix/SocketChannel.swift +++ b/Sources/NIOPosix/SocketChannel.swift @@ -266,14 +266,18 @@ final class ServerSocketChannel: BaseSocketChannel { promise?.fail(error) } executeAndComplete(p) { - switch target { - case .socketAddress(let address): - try socket.bind(to: address) - case .vsockAddress(let address): - try socket.bind(to: address) + do { + switch target { + case .socketAddress(let address): + try socket.bind(to: address) + case .vsockAddress(let address): + try socket.bind(to: address) + } + self.updateCachedAddressesFromSocket(updateRemote: false) + try self.socket.listen(backlog: backlog) + } catch { + promise?.fail(ChannelError.bindFailed) } - self.updateCachedAddressesFromSocket(updateRemote: false) - try self.socket.listen(backlog: backlog) } } diff --git a/Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift b/Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift new file mode 100644 index 0000000000..13ec391776 --- /dev/null +++ b/Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift @@ -0,0 +1,556 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import XCTest +import NIOCore +import NIOEmbedded +import NIOHTTP1 + +private class MessageEndHandler: ChannelInboundHandler { + typealias InboundIn = HTTPPart + + var seenEnd = false + var seenBody = false + var seenHead = false + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + switch self.unwrapInboundIn(data) { + case .head: + XCTAssertFalse(self.seenHead) + self.seenHead = true + case .body: + XCTAssertFalse(self.seenBody) + self.seenBody = true + case .end: + XCTAssertFalse(self.seenEnd) + self.seenEnd = true + } + } +} + +/// Tests for the HTTP decoder's handling of message body framing. +/// +/// Mostly tests assertions in [RFC 7230 § 3.3.3](https://tools.ietf.org/html/rfc7230#section-3.3.3). +class HTTPDecoderLengthTest: XCTestCase { + private var channel: EmbeddedChannel! + private var loop: EmbeddedEventLoop { + return self.channel.embeddedEventLoop + } + + override func setUp() { + self.channel = EmbeddedChannel() + } + + override func tearDown() { + XCTAssertNoThrow(try self.channel?.finish(acceptAlreadyClosed: true)) + self.channel = nil + } + + /// The mechanism by which EOF is being sent. + enum EOFMechanism { + case channelInactive + case halfClosure + } + + /// The various header fields that can be used to frame a response. + enum FramingField { + case contentLength + case transferEncoding + case neither + } + + private func assertSemanticEOFOnChannelInactiveResponse(version: HTTPVersion, how eofMechanism: EOFMechanism) throws { + print("yes") + class ChannelInactiveHandler: ChannelInboundHandler { + typealias InboundIn = HTTPClientResponsePart + var response: HTTPResponseHead? + var receivedEnd = false + var eof = false + var body: [UInt8]? + private let eofMechanism: EOFMechanism + + init(_ eofMechanism: EOFMechanism) { + self.eofMechanism = eofMechanism + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + switch self.unwrapInboundIn(data) { + case .head(let h): + self.response = h + case .end: + self.receivedEnd = true + case .body(var b): + XCTAssertNil(self.body) + self.body = b.readBytes(length: b.readableBytes)! + } + } + + func channelInactive(context: ChannelHandlerContext) { + if case .channelInactive = self.eofMechanism { + XCTAssert(self.receivedEnd, "Received channelInactive before response end!") + self.eof = true + } else { + XCTAssert(self.eof, "Did not receive .inputClosed") + } + } + + func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + guard case .halfClosure = self.eofMechanism else { + XCTFail("Got half closure when not expecting it") + return + } + + guard let evt = event as? ChannelEvent, case .inputClosed = evt else { + context.fireUserInboundEventTriggered(event) + return + } + + XCTAssert(self.receivedEnd, "Received inputClosed before response end!") + self.eof = true + } + } + + XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) + + let handler = ChannelInactiveHandler(eofMechanism) + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Prime the decoder with a GET and consume it. + XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: version, method: .GET, uri: "/"))).isFull) + XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) + + // We now want to send a HTTP/1.1 response. This response has no content-length, no transfer-encoding, + // is not a response to a HEAD request, is not a 2XX response to CONNECT, and is not 1XX, 204, or 304. + // That means, per RFC 7230 § 3.3.3, the body is framed by EOF. Because this is a response, that EOF + // may be transmitted by channelInactive. + let response = "HTTP/\(version.major).\(version.minor) 200 OK\r\nServer: example\r\n\r\n" + XCTAssertNoThrow(try channel.writeInbound(IOData.byteBuffer(channel.allocator.buffer(string: response)))) + + // We should have a response but no body. + XCTAssertNotNil(handler.response) + XCTAssertNil(handler.body) + XCTAssertFalse(handler.receivedEnd) + XCTAssertFalse(handler.eof) + + // Send a body chunk. This should be immediately passed on. Still no end or EOF. + XCTAssertNoThrow(try channel.writeInbound(IOData.byteBuffer(channel.allocator.buffer(string: "some body data")))) + XCTAssertNotNil(handler.response) + XCTAssertEqual(handler.body!, Array("some body data".utf8)) + XCTAssertFalse(handler.receivedEnd) + XCTAssertFalse(handler.eof) + + // Now we send EOF. This should cause a response end. The handler will enforce ordering. + if case .halfClosure = eofMechanism { + channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + } else { + channel.pipeline.fireChannelInactive() + } + XCTAssertNotNil(handler.response) + XCTAssertEqual(handler.body!, Array("some body data".utf8)) + XCTAssertTrue(handler.receivedEnd) + XCTAssertTrue(handler.eof) + + XCTAssertTrue(try channel.finish().isClean) + } + + func testHTTP11SemanticEOFOnChannelInactive() throws { + try assertSemanticEOFOnChannelInactiveResponse(version: .http1_1, how: .channelInactive) + } + + func testHTTP10SemanticEOFOnChannelInactive() throws { + try assertSemanticEOFOnChannelInactiveResponse(version: .http1_0, how: .channelInactive) + } + + func testHTTP11SemanticEOFOnHalfClosure() throws { + try assertSemanticEOFOnChannelInactiveResponse(version: .http1_1, how: .halfClosure) + } + + func testHTTP10SemanticEOFOnHalfClosure() throws { + try assertSemanticEOFOnChannelInactiveResponse(version: .http1_0, how: .halfClosure) + } + + private func assertIgnoresLengthFields(requestMethod: HTTPMethod, + responseStatus: HTTPResponseStatus, + responseFramingField: FramingField) throws { + print("asyn") + XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) + let decoder = HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes, informationalResponseStrategy: .forward) + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(decoder)).wait()) + + let handler = MessageEndHandler() + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Prime the decoder with a request and consume it. + XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, + method: requestMethod, + uri: "/"))).isFull) + XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) + + // We now want to send a HTTP/1.1 response. This response may contain some length framing fields that RFC 7230 says MUST + // be ignored. + var response = channel.allocator.buffer(capacity: 256) + response.writeString("HTTP/1.1 \(responseStatus.code) \(responseStatus.reasonPhrase)\r\nServer: example\r\n") + + switch responseFramingField { + case .contentLength: + response.writeStaticString("Content-Length: 16\r\n") + case .transferEncoding: + response.writeStaticString("Transfer-Encoding: chunked\r\n") + case .neither: + break + } + response.writeStaticString("\r\n") + + XCTAssertNoThrow(try channel.writeInbound(IOData.byteBuffer(response))) + + // We should have a response, no body, and immediately see EOF. + XCTAssert(handler.seenHead) + switch responseStatus.code { + case 100, 102..<200: + // If an informational response header is tested, we expect another "real" header to + // follow. For this reason, we don't expect an `.end` here. + XCTAssertFalse(handler.seenBody) + XCTAssertFalse(handler.seenEnd) + + default: + XCTAssertFalse(handler.seenBody) + XCTAssert(handler.seenEnd) + } + + XCTAssertTrue(try channel.finish().isClean) + } + + func testIgnoresTransferEncodingFieldOnCONNECTResponses() throws { + try assertIgnoresLengthFields(requestMethod: .CONNECT, responseStatus: .ok, responseFramingField: .transferEncoding) + } + + func testIgnoresContentLengthFieldOnCONNECTResponses() throws { + try assertIgnoresLengthFields(requestMethod: .CONNECT, responseStatus: .ok, responseFramingField: .contentLength) + } + + func testEarlyFinishWithoutLengthAtAllOnCONNECTResponses() throws { + try assertIgnoresLengthFields(requestMethod: .CONNECT, responseStatus: .ok, responseFramingField: .neither) + } + + func testIgnoresTransferEncodingFieldOnHEADResponses() throws { + try assertIgnoresLengthFields(requestMethod: .HEAD, responseStatus: .ok, responseFramingField: .transferEncoding) + } + + func testIgnoresContentLengthFieldOnHEADResponses() throws { + try assertIgnoresLengthFields(requestMethod: .HEAD, responseStatus: .ok, responseFramingField: .contentLength) + } + + func testEarlyFinishWithoutLengthAtAllOnHEADResponses() throws { + try assertIgnoresLengthFields(requestMethod: .HEAD, responseStatus: .ok, responseFramingField: .neither) + } + + func testIgnoresTransferEncodingFieldOn1XXResponses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, + responseStatus: .custom(code: 103, reasonPhrase: "Early Hints"), + responseFramingField: .transferEncoding) + } + + func testIgnoresContentLengthFieldOn1XXResponses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, + responseStatus: .custom(code: 103, reasonPhrase: "Early Hints"), + responseFramingField: .contentLength) + } + + func testEarlyFinishWithoutLengthAtAllOn1XXResponses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, + responseStatus: .custom(code: 103, reasonPhrase: "Early Hints"), + responseFramingField: .neither) + } + + func testIgnoresTransferEncodingFieldOn204Responses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .noContent, responseFramingField: .transferEncoding) + } + + func testIgnoresContentLengthFieldOn204Responses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .noContent, responseFramingField: .contentLength) + } + + func testEarlyFinishWithoutLengthAtAllOn204Responses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .noContent, responseFramingField: .neither) + } + + func testIgnoresTransferEncodingFieldOn304Responses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .transferEncoding) + } + + func testIgnoresContentLengthFieldOn304Responses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .contentLength) + } + + func testEarlyFinishWithoutLengthAtAllOn304Responses() throws { + try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .neither) + } + + private func assertRequestTransferEncodingInError(transferEncodingHeader: String) throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + let handler = MessageEndHandler() + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Send a GET with the appropriate Transfer Encoding header. + XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) { error in + XCTAssertEqual(error as? HTTPParserError, .unknown) + } + } + + func testMultipleTEWithChunkedLastWorksFine() throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + let handler = MessageEndHandler() + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Send a GET with the appropriate Transfer Encoding header. + XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: gzip, chunked\r\n\r\n0\r\n\r\n"))) + + // We should have a request, no body, and immediately see end of request. + XCTAssert(handler.seenHead) + XCTAssertFalse(handler.seenBody) + XCTAssert(handler.seenEnd) + + XCTAssertTrue(try channel.finish().isClean) + } + + func testMultipleTEWithChunkedFirstHasNoBodyOnRequest() throws { + try assertRequestTransferEncodingInError(transferEncodingHeader: "chunked, gzip") + } + + func testMultipleTEWithChunkedInTheMiddleHasNoBodyOnRequest() throws { + try assertRequestTransferEncodingInError(transferEncodingHeader: "gzip, chunked, deflate") + } + + private func assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: String, eofMechanism: EOFMechanism) throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) + + let handler = MessageEndHandler() + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Prime the decoder with a request and consume it. + XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, + method: .GET, + uri: "/"))).isFull) + XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) + + // Send a 200 with the appropriate Transfer Encoding header. We should see the request, + // but no body or end. + XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "HTTP/1.1 200 OK\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) + XCTAssert(handler.seenHead) + XCTAssertFalse(handler.seenBody) + XCTAssertFalse(handler.seenEnd) + + // Now send body. Note that this is *not* chunk encoded. We should also see a body. + XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "caribbean"))) + XCTAssert(handler.seenHead) + XCTAssert(handler.seenBody) + XCTAssertFalse(handler.seenEnd) + + // Now send EOF. This should send the end as well. + if case .halfClosure = eofMechanism { + channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + } else { + channel.pipeline.fireChannelInactive() + } + XCTAssert(handler.seenHead) + XCTAssert(handler.seenBody) + XCTAssert(handler.seenEnd) + + XCTAssertTrue(try channel.finish().isClean) + } + + private func assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: String, eofMechanism: EOFMechanism) throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) + + let handler = MessageEndHandler() + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Prime the decoder with a request and consume it. + XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, + method: .GET, + uri: "/"))).isFull) + XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) + + // Send a 200 with the appropriate Transfer Encoding header. We should see the request. + XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "HTTP/1.1 200 OK\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) + XCTAssert(handler.seenHead) + XCTAssertFalse(handler.seenBody) + XCTAssertFalse(handler.seenEnd) + + // Now send body. Note that this *is* chunk encoded. We should also see a body. + XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "9\r\ncaribbean\r\n"))) + XCTAssert(handler.seenHead) + XCTAssert(handler.seenBody) + XCTAssertFalse(handler.seenEnd) + + // Now send EOF. This should error, as we're expecting the end chunk. + if case .halfClosure = eofMechanism { + channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed) + } else { + channel.pipeline.fireChannelInactive() + } + + XCTAssert(handler.seenHead) + XCTAssert(handler.seenBody) + XCTAssertFalse(handler.seenEnd) + + XCTAssertThrowsError(try channel.throwIfErrorCaught()) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidEOFState) + } + + XCTAssertTrue(try channel.finish().isClean) + } + + func testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithChannelInactive() throws { + try assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: "gzip, chunked", eofMechanism: .channelInactive) + } + + func testMultipleTEWithChunkedFirstHasEOFBodyOnResponseWithChannelInactive() throws { + // Here http_parser is right, and this is EOF terminated. + try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "chunked, gzip", eofMechanism: .channelInactive) + } + + func testMultipleTEWithChunkedInTheMiddleHasEOFBodyOnResponseWithChannelInactive() throws { + // Here http_parser is right, and this is EOF terminated. + try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "gzip, chunked, deflate", eofMechanism: .channelInactive) + } + + func testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithHalfClosure() throws { + try assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: "gzip, chunked", eofMechanism: .halfClosure) + } + + func testMultipleTEWithChunkedFirstHasEOFBodyOnResponseWithHalfClosure() throws { + // Here http_parser is right, and this is EOF terminated. + try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "chunked, gzip", eofMechanism: .halfClosure) + } + + func testMultipleTEWithChunkedInTheMiddleHasEOFBodyOnResponseWithHalfClosure() throws { + // Here http_parser is right, and this is EOF terminated. + try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "gzip, chunked, deflate", eofMechanism: .halfClosure) + } + + func testRequestWithTEAndContentLengthErrors() throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + // Send a GET with the invalid headers. + let request = channel.allocator.buffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n") + XCTAssertThrowsError(try channel.writeInbound(request)) { error in + XCTAssertEqual(HTTPParserError.unexpectedContentLength, error as? HTTPParserError) + } + + // Must spin the loop. + XCTAssertFalse(channel.isActive) + channel.embeddedEventLoop.run() + } + + func testResponseWithTEAndContentLengthErrors() throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) + + // Prime the decoder with a request. + XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, + method: .GET, + uri: "/"))).isFull) + + // Send a 200 OK with the invalid headers. + let response = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n" + XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: response))) { error in + XCTAssertEqual(HTTPParserError.unexpectedContentLength, error as? HTTPParserError) + } + + // Must spin the loop. + XCTAssertFalse(channel.isActive) + channel.embeddedEventLoop.run() + } + + private func assertRequestWithInvalidCLErrors(contentLengthField: String) throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + // Send a GET with the invalid headers. + let request = "POST / HTTP/1.1\r\nContent-Length: \(contentLengthField)\r\n\r\n" + XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: request))) { error in + XCTAssert(HTTPParserError.unexpectedContentLength == error as? HTTPParserError || + HTTPParserError.invalidContentLength == error as? HTTPParserError) + } + + // Must spin the loop. + XCTAssertFalse(channel.isActive) + channel.embeddedEventLoop.run() + } + + func testRequestWithMultipleDifferentContentLengthsFails() throws { + try assertRequestWithInvalidCLErrors(contentLengthField: "4, 5") + } + + func testRequestWithMultipleDifferentContentLengthsOnDifferentLinesFails() throws { + try assertRequestWithInvalidCLErrors(contentLengthField: "4\r\nContent-Length: 5") + } + + func testRequestWithInvalidContentLengthFails() throws { + try assertRequestWithInvalidCLErrors(contentLengthField: "pie") + } + + func testRequestWithIdenticalContentLengthRepeatedErrors() throws { + // This is another case where http_parser is, if not wrong, then aggressively interpreting + // the spec. Regardless, we match it. + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + // Send two POSTs with repeated content length, one with one field and one with two. + // Both should error. + let request = "POST / HTTP/1.1\r\nContent-Length: 4, 4\r\n\r\n" + XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: request))) { error in + XCTAssertEqual(HTTPParserError.invalidContentLength, error as? HTTPParserError) + } + + // Must spin the loop. + XCTAssertFalse(channel.isActive) + channel.embeddedEventLoop.run() + } + + func testRequestWithMultipleIdenticalContentLengthFieldsErrors() throws { + // This is another case where http_parser is, if not wrong, then aggressively interpreting + // the spec. Regardless, we match it. + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + let request = "POST / HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 4\r\n\r\n" + XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: request))) { error in + XCTAssertEqual(HTTPParserError.unexpectedContentLength, error as? HTTPParserError) + } + + // Must spin the loop. + XCTAssertFalse(channel.isActive) + channel.embeddedEventLoop.run() + } + + func testRequestWithoutExplicitLengthIsZeroLength() throws { + XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + + let handler = MessageEndHandler() + XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) + + // Send a POST without a length field of any kind. This should be a zero-length request, + // so .end should come immediately. + XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "POST / HTTP/1.1\r\nHost: example.org\r\n\r\n"))) + XCTAssert(handler.seenHead) + XCTAssertFalse(handler.seenBody) + XCTAssert(handler.seenEnd) + + XCTAssertTrue(try channel.finish().isClean) + } +} diff --git a/Tests/NIOPosixTests/SALChannelTests.swift b/Tests/NIOPosixTests/SALChannelTests.swift index 785d7d0d28..c6b4d16cad 100644 --- a/Tests/NIOPosixTests/SALChannelTests.swift +++ b/Tests/NIOPosixTests/SALChannelTests.swift @@ -322,6 +322,48 @@ final class SALChannelTest: XCTestCase, SALTest { } }.salWait()) } + + func testBindFailureClosesChannel() { + guard let channel = try? self.makeSocketChannel() else { + XCTFail("couldn't make a channel") + return + } + let localAddress = try! SocketAddress(ipAddress: "1.2.3.4", port: 5) + let serverAddress = try! SocketAddress(ipAddress: "9.8.7.6", port: 5) + + XCTAssertThrowsError(try channel.eventLoop.runSAL(syscallAssertions: { + try self.assertSetOption(expectedLevel: .tcp, expectedOption: .tcp_nodelay) { value in + return (value as? SocketOptionValue) == 1 + } + try self.assertSetOption(expectedLevel: .socket, expectedOption: .so_reuseaddr) { value in + return (value as? SocketOptionValue) == 1 + } + try self.assertBindFailure(expectedAddress: localAddress) + try self.assertDeregister { selectable in + return true + } + try self.assertClose(expectedFD: .max) + + }) { + ClientBootstrap(group: channel.eventLoop) + .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + .channelOption(ChannelOptions.autoRead, value: false) + .bind(to: localAddress) + .testOnly_connect(injectedChannel: channel, to: serverAddress) + .flatMapError { error in + guard let ioError = error as? IOError else { + XCTFail("Expected IOError, got \(error)") + return channel.eventLoop.makeFailedFuture(error) + } + XCTAssertEqual(ioError.errnoCode, EPERM, "Expected EPERM error code") + XCTAssertTrue(!channel.isActive, "Channel should be closed") + return channel.eventLoop.makeFailedFuture(error) + } + .flatMap { channel in + channel.close() + } + }.salWait()) + } func testAcceptingInboundConnections() throws { final class ConnectionRecorder: ChannelInboundHandler { diff --git a/Tests/NIOPosixTests/SyscallAbstractionLayer.swift b/Tests/NIOPosixTests/SyscallAbstractionLayer.swift index 8da3d76b97..733ab06132 100644 --- a/Tests/NIOPosixTests/SyscallAbstractionLayer.swift +++ b/Tests/NIOPosixTests/SyscallAbstractionLayer.swift @@ -884,7 +884,18 @@ extension SALTest { } } } - + + func assertBindFailure(expectedAddress: SocketAddress, file: StaticString = #filePath, line: UInt = #line) throws { + SAL.printIfDebug("\(#function)") + try self.selector.assertSyscallAndReturn(.error(IOError(errnoCode: EPERM, reason: "bind"))) { syscall in + if case .bind(let address) = syscall { + return address == expectedAddress + } else { + return false + } + } + } + func assertClose(expectedFD: CInt, file: StaticString = #filePath, line: UInt = #line) throws { SAL.printIfDebug("\(#function)") try self.selector.assertSyscallAndReturn(.returnVoid, file: (file), line: line) { syscall in From 3b33cebe9eb77d8f5ac30af55dd3ad182880940f Mon Sep 17 00:00:00 2001 From: dkz2 Date: Wed, 10 Jan 2024 23:52:43 -0600 Subject: [PATCH 2/6] transition to closed state when bind fails but still hitting assertion --- Sources/NIOCore/Channel.swift | 3 --- Sources/NIOPosix/SocketChannel.swift | 19 ++++++++----------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/Sources/NIOCore/Channel.swift b/Sources/NIOCore/Channel.swift index 2925eb0768..d61e3b7dcf 100644 --- a/Sources/NIOCore/Channel.swift +++ b/Sources/NIOCore/Channel.swift @@ -371,9 +371,6 @@ public enum ChannelError: Error { /// An attempt was made to remove a ChannelHandler that is not removable. case unremovableHandler - - /// An attempt to bind a `Channel` failed. - case bindFailed } extension ChannelError: Equatable { } diff --git a/Sources/NIOPosix/SocketChannel.swift b/Sources/NIOPosix/SocketChannel.swift index 46a7b83ea6..271c78cd4d 100644 --- a/Sources/NIOPosix/SocketChannel.swift +++ b/Sources/NIOPosix/SocketChannel.swift @@ -263,21 +263,18 @@ final class ServerSocketChannel: BaseSocketChannel { // It's important to call the methods before we actually notify the original promise for ordering reasons. self.becomeActive0(promise: promise) }.whenFailure{ error in + self.close0(error: error, mode: .all, promise: nil) promise?.fail(error) } executeAndComplete(p) { - do { - switch target { - case .socketAddress(let address): - try socket.bind(to: address) - case .vsockAddress(let address): - try socket.bind(to: address) - } - self.updateCachedAddressesFromSocket(updateRemote: false) - try self.socket.listen(backlog: backlog) - } catch { - promise?.fail(ChannelError.bindFailed) + switch target { + case .socketAddress(let address): + try socket.bind(to: address) + case .vsockAddress(let address): + try socket.bind(to: address) } + self.updateCachedAddressesFromSocket(updateRemote: false) + try self.socket.listen(backlog: backlog) } } From e13a5b2083fce660955369498b1b5b2c231d83db Mon Sep 17 00:00:00 2001 From: dkz2 Date: Thu, 11 Jan 2024 13:22:07 -0600 Subject: [PATCH 3/6] propagate bind up if it fails --- Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift b/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift index 015c6382ed..5c83ea8658 100644 --- a/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift +++ b/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift @@ -265,7 +265,7 @@ public final class AcceptBackoffHandlerTest: XCTestCase { name: self.acceptHandlerName) }.wait()) - XCTAssertNoThrow(try eventLoop.flatSubmit { + let bindFuture = eventLoop.flatSubmit { // this is pretty delicate at the moment: // `bind` must be _synchronously_ follow `register`, otherwise in our current implementation, `epoll` will // send us `EPOLLHUP`. To have it run synchronously, we need to invoke the `flatMap` on the eventloop that the @@ -273,7 +273,13 @@ public final class AcceptBackoffHandlerTest: XCTestCase { serverChannel.register().flatMap { () -> EventLoopFuture<()> in return serverChannel.bind(to: try! SocketAddress(ipAddress: "127.0.0.1", port: 0)) } - }.wait() as Void) + } + + // If bind fails, the error will propagate up + try bindFuture.wait() + return serverChannel } + + } From a4863a8d90acbf42384d7f9c5bc7d51958fdf7eb Mon Sep 17 00:00:00 2001 From: dkz2 Date: Thu, 11 Jan 2024 13:44:41 -0600 Subject: [PATCH 4/6] cleanup --- Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift b/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift index 5c83ea8658..7c69d951c6 100644 --- a/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift +++ b/Tests/NIOPosixTests/AcceptBackoffHandlerTest.swift @@ -280,6 +280,4 @@ public final class AcceptBackoffHandlerTest: XCTestCase { return serverChannel } - - } From 85bfd59a4eb790afcf8912fd6feba9a975d43c03 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Fri, 12 Jan 2024 20:32:26 -0600 Subject: [PATCH 5/6] fold assertBindFailure --- Tests/NIOPosixTests/SALChannelTests.swift | 2 +- .../SyscallAbstractionLayer.swift | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Tests/NIOPosixTests/SALChannelTests.swift b/Tests/NIOPosixTests/SALChannelTests.swift index c6b4d16cad..0e893fa543 100644 --- a/Tests/NIOPosixTests/SALChannelTests.swift +++ b/Tests/NIOPosixTests/SALChannelTests.swift @@ -338,7 +338,7 @@ final class SALChannelTest: XCTestCase, SALTest { try self.assertSetOption(expectedLevel: .socket, expectedOption: .so_reuseaddr) { value in return (value as? SocketOptionValue) == 1 } - try self.assertBindFailure(expectedAddress: localAddress) + try self.assertBind(expectedAddress: localAddress, errorReturn: IOError(errnoCode: EPERM, reason: "bind")) try self.assertDeregister { selectable in return true } diff --git a/Tests/NIOPosixTests/SyscallAbstractionLayer.swift b/Tests/NIOPosixTests/SyscallAbstractionLayer.swift index 733ab06132..06c382ec47 100644 --- a/Tests/NIOPosixTests/SyscallAbstractionLayer.swift +++ b/Tests/NIOPosixTests/SyscallAbstractionLayer.swift @@ -874,20 +874,10 @@ extension SALTest { } } - func assertBind(expectedAddress: SocketAddress, file: StaticString = #filePath, line: UInt = #line) throws { + func assertBind(expectedAddress: SocketAddress, errorReturn: IOError? = nil, file: StaticString = #filePath, line: UInt = #line) throws { SAL.printIfDebug("\(#function)") - try self.selector.assertSyscallAndReturn(.returnVoid, file: (file), line: line) { syscall in - if case .bind(let address) = syscall { - return address == expectedAddress - } else { - return false - } - } - } - - func assertBindFailure(expectedAddress: SocketAddress, file: StaticString = #filePath, line: UInt = #line) throws { - SAL.printIfDebug("\(#function)") - try self.selector.assertSyscallAndReturn(.error(IOError(errnoCode: EPERM, reason: "bind"))) { syscall in + + try self.selector.assertSyscallAndReturn(errorReturn != nil ? .error(errorReturn!) : .returnVoid, file: file, line: line) { syscall in if case .bind(let address) = syscall { return address == expectedAddress } else { @@ -895,7 +885,7 @@ extension SALTest { } } } - + func assertClose(expectedFD: CInt, file: StaticString = #filePath, line: UInt = #line) throws { SAL.printIfDebug("\(#function)") try self.selector.assertSyscallAndReturn(.returnVoid, file: (file), line: line) { syscall in From 57266e928c717bbe34dff6994a5a998097ba3f57 Mon Sep 17 00:00:00 2001 From: dkz2 Date: Fri, 12 Jan 2024 20:34:00 -0600 Subject: [PATCH 6/6] removed unneeded file --- .../NIOHTTPTests/HTTPDecoderLengthTest.swift | 556 ------------------ 1 file changed, 556 deletions(-) delete mode 100644 Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift diff --git a/Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift b/Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift deleted file mode 100644 index 13ec391776..0000000000 --- a/Tests/NIOHTTPTests/HTTPDecoderLengthTest.swift +++ /dev/null @@ -1,556 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftNIO open source project -// -// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftNIO project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import XCTest -import NIOCore -import NIOEmbedded -import NIOHTTP1 - -private class MessageEndHandler: ChannelInboundHandler { - typealias InboundIn = HTTPPart - - var seenEnd = false - var seenBody = false - var seenHead = false - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case .head: - XCTAssertFalse(self.seenHead) - self.seenHead = true - case .body: - XCTAssertFalse(self.seenBody) - self.seenBody = true - case .end: - XCTAssertFalse(self.seenEnd) - self.seenEnd = true - } - } -} - -/// Tests for the HTTP decoder's handling of message body framing. -/// -/// Mostly tests assertions in [RFC 7230 § 3.3.3](https://tools.ietf.org/html/rfc7230#section-3.3.3). -class HTTPDecoderLengthTest: XCTestCase { - private var channel: EmbeddedChannel! - private var loop: EmbeddedEventLoop { - return self.channel.embeddedEventLoop - } - - override func setUp() { - self.channel = EmbeddedChannel() - } - - override func tearDown() { - XCTAssertNoThrow(try self.channel?.finish(acceptAlreadyClosed: true)) - self.channel = nil - } - - /// The mechanism by which EOF is being sent. - enum EOFMechanism { - case channelInactive - case halfClosure - } - - /// The various header fields that can be used to frame a response. - enum FramingField { - case contentLength - case transferEncoding - case neither - } - - private func assertSemanticEOFOnChannelInactiveResponse(version: HTTPVersion, how eofMechanism: EOFMechanism) throws { - print("yes") - class ChannelInactiveHandler: ChannelInboundHandler { - typealias InboundIn = HTTPClientResponsePart - var response: HTTPResponseHead? - var receivedEnd = false - var eof = false - var body: [UInt8]? - private let eofMechanism: EOFMechanism - - init(_ eofMechanism: EOFMechanism) { - self.eofMechanism = eofMechanism - } - - func channelRead(context: ChannelHandlerContext, data: NIOAny) { - switch self.unwrapInboundIn(data) { - case .head(let h): - self.response = h - case .end: - self.receivedEnd = true - case .body(var b): - XCTAssertNil(self.body) - self.body = b.readBytes(length: b.readableBytes)! - } - } - - func channelInactive(context: ChannelHandlerContext) { - if case .channelInactive = self.eofMechanism { - XCTAssert(self.receivedEnd, "Received channelInactive before response end!") - self.eof = true - } else { - XCTAssert(self.eof, "Did not receive .inputClosed") - } - } - - func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { - guard case .halfClosure = self.eofMechanism else { - XCTFail("Got half closure when not expecting it") - return - } - - guard let evt = event as? ChannelEvent, case .inputClosed = evt else { - context.fireUserInboundEventTriggered(event) - return - } - - XCTAssert(self.receivedEnd, "Received inputClosed before response end!") - self.eof = true - } - } - - XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) - - let handler = ChannelInactiveHandler(eofMechanism) - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Prime the decoder with a GET and consume it. - XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: version, method: .GET, uri: "/"))).isFull) - XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) - - // We now want to send a HTTP/1.1 response. This response has no content-length, no transfer-encoding, - // is not a response to a HEAD request, is not a 2XX response to CONNECT, and is not 1XX, 204, or 304. - // That means, per RFC 7230 § 3.3.3, the body is framed by EOF. Because this is a response, that EOF - // may be transmitted by channelInactive. - let response = "HTTP/\(version.major).\(version.minor) 200 OK\r\nServer: example\r\n\r\n" - XCTAssertNoThrow(try channel.writeInbound(IOData.byteBuffer(channel.allocator.buffer(string: response)))) - - // We should have a response but no body. - XCTAssertNotNil(handler.response) - XCTAssertNil(handler.body) - XCTAssertFalse(handler.receivedEnd) - XCTAssertFalse(handler.eof) - - // Send a body chunk. This should be immediately passed on. Still no end or EOF. - XCTAssertNoThrow(try channel.writeInbound(IOData.byteBuffer(channel.allocator.buffer(string: "some body data")))) - XCTAssertNotNil(handler.response) - XCTAssertEqual(handler.body!, Array("some body data".utf8)) - XCTAssertFalse(handler.receivedEnd) - XCTAssertFalse(handler.eof) - - // Now we send EOF. This should cause a response end. The handler will enforce ordering. - if case .halfClosure = eofMechanism { - channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - } else { - channel.pipeline.fireChannelInactive() - } - XCTAssertNotNil(handler.response) - XCTAssertEqual(handler.body!, Array("some body data".utf8)) - XCTAssertTrue(handler.receivedEnd) - XCTAssertTrue(handler.eof) - - XCTAssertTrue(try channel.finish().isClean) - } - - func testHTTP11SemanticEOFOnChannelInactive() throws { - try assertSemanticEOFOnChannelInactiveResponse(version: .http1_1, how: .channelInactive) - } - - func testHTTP10SemanticEOFOnChannelInactive() throws { - try assertSemanticEOFOnChannelInactiveResponse(version: .http1_0, how: .channelInactive) - } - - func testHTTP11SemanticEOFOnHalfClosure() throws { - try assertSemanticEOFOnChannelInactiveResponse(version: .http1_1, how: .halfClosure) - } - - func testHTTP10SemanticEOFOnHalfClosure() throws { - try assertSemanticEOFOnChannelInactiveResponse(version: .http1_0, how: .halfClosure) - } - - private func assertIgnoresLengthFields(requestMethod: HTTPMethod, - responseStatus: HTTPResponseStatus, - responseFramingField: FramingField) throws { - print("asyn") - XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) - let decoder = HTTPResponseDecoder(leftOverBytesStrategy: .dropBytes, informationalResponseStrategy: .forward) - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(decoder)).wait()) - - let handler = MessageEndHandler() - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Prime the decoder with a request and consume it. - XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, - method: requestMethod, - uri: "/"))).isFull) - XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) - - // We now want to send a HTTP/1.1 response. This response may contain some length framing fields that RFC 7230 says MUST - // be ignored. - var response = channel.allocator.buffer(capacity: 256) - response.writeString("HTTP/1.1 \(responseStatus.code) \(responseStatus.reasonPhrase)\r\nServer: example\r\n") - - switch responseFramingField { - case .contentLength: - response.writeStaticString("Content-Length: 16\r\n") - case .transferEncoding: - response.writeStaticString("Transfer-Encoding: chunked\r\n") - case .neither: - break - } - response.writeStaticString("\r\n") - - XCTAssertNoThrow(try channel.writeInbound(IOData.byteBuffer(response))) - - // We should have a response, no body, and immediately see EOF. - XCTAssert(handler.seenHead) - switch responseStatus.code { - case 100, 102..<200: - // If an informational response header is tested, we expect another "real" header to - // follow. For this reason, we don't expect an `.end` here. - XCTAssertFalse(handler.seenBody) - XCTAssertFalse(handler.seenEnd) - - default: - XCTAssertFalse(handler.seenBody) - XCTAssert(handler.seenEnd) - } - - XCTAssertTrue(try channel.finish().isClean) - } - - func testIgnoresTransferEncodingFieldOnCONNECTResponses() throws { - try assertIgnoresLengthFields(requestMethod: .CONNECT, responseStatus: .ok, responseFramingField: .transferEncoding) - } - - func testIgnoresContentLengthFieldOnCONNECTResponses() throws { - try assertIgnoresLengthFields(requestMethod: .CONNECT, responseStatus: .ok, responseFramingField: .contentLength) - } - - func testEarlyFinishWithoutLengthAtAllOnCONNECTResponses() throws { - try assertIgnoresLengthFields(requestMethod: .CONNECT, responseStatus: .ok, responseFramingField: .neither) - } - - func testIgnoresTransferEncodingFieldOnHEADResponses() throws { - try assertIgnoresLengthFields(requestMethod: .HEAD, responseStatus: .ok, responseFramingField: .transferEncoding) - } - - func testIgnoresContentLengthFieldOnHEADResponses() throws { - try assertIgnoresLengthFields(requestMethod: .HEAD, responseStatus: .ok, responseFramingField: .contentLength) - } - - func testEarlyFinishWithoutLengthAtAllOnHEADResponses() throws { - try assertIgnoresLengthFields(requestMethod: .HEAD, responseStatus: .ok, responseFramingField: .neither) - } - - func testIgnoresTransferEncodingFieldOn1XXResponses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, - responseStatus: .custom(code: 103, reasonPhrase: "Early Hints"), - responseFramingField: .transferEncoding) - } - - func testIgnoresContentLengthFieldOn1XXResponses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, - responseStatus: .custom(code: 103, reasonPhrase: "Early Hints"), - responseFramingField: .contentLength) - } - - func testEarlyFinishWithoutLengthAtAllOn1XXResponses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, - responseStatus: .custom(code: 103, reasonPhrase: "Early Hints"), - responseFramingField: .neither) - } - - func testIgnoresTransferEncodingFieldOn204Responses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .noContent, responseFramingField: .transferEncoding) - } - - func testIgnoresContentLengthFieldOn204Responses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .noContent, responseFramingField: .contentLength) - } - - func testEarlyFinishWithoutLengthAtAllOn204Responses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .noContent, responseFramingField: .neither) - } - - func testIgnoresTransferEncodingFieldOn304Responses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .transferEncoding) - } - - func testIgnoresContentLengthFieldOn304Responses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .contentLength) - } - - func testEarlyFinishWithoutLengthAtAllOn304Responses() throws { - try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .neither) - } - - private func assertRequestTransferEncodingInError(transferEncodingHeader: String) throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - let handler = MessageEndHandler() - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Send a GET with the appropriate Transfer Encoding header. - XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) { error in - XCTAssertEqual(error as? HTTPParserError, .unknown) - } - } - - func testMultipleTEWithChunkedLastWorksFine() throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - let handler = MessageEndHandler() - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Send a GET with the appropriate Transfer Encoding header. - XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: gzip, chunked\r\n\r\n0\r\n\r\n"))) - - // We should have a request, no body, and immediately see end of request. - XCTAssert(handler.seenHead) - XCTAssertFalse(handler.seenBody) - XCTAssert(handler.seenEnd) - - XCTAssertTrue(try channel.finish().isClean) - } - - func testMultipleTEWithChunkedFirstHasNoBodyOnRequest() throws { - try assertRequestTransferEncodingInError(transferEncodingHeader: "chunked, gzip") - } - - func testMultipleTEWithChunkedInTheMiddleHasNoBodyOnRequest() throws { - try assertRequestTransferEncodingInError(transferEncodingHeader: "gzip, chunked, deflate") - } - - private func assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: String, eofMechanism: EOFMechanism) throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) - - let handler = MessageEndHandler() - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Prime the decoder with a request and consume it. - XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, - method: .GET, - uri: "/"))).isFull) - XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) - - // Send a 200 with the appropriate Transfer Encoding header. We should see the request, - // but no body or end. - XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "HTTP/1.1 200 OK\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) - XCTAssert(handler.seenHead) - XCTAssertFalse(handler.seenBody) - XCTAssertFalse(handler.seenEnd) - - // Now send body. Note that this is *not* chunk encoded. We should also see a body. - XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "caribbean"))) - XCTAssert(handler.seenHead) - XCTAssert(handler.seenBody) - XCTAssertFalse(handler.seenEnd) - - // Now send EOF. This should send the end as well. - if case .halfClosure = eofMechanism { - channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - } else { - channel.pipeline.fireChannelInactive() - } - XCTAssert(handler.seenHead) - XCTAssert(handler.seenBody) - XCTAssert(handler.seenEnd) - - XCTAssertTrue(try channel.finish().isClean) - } - - private func assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: String, eofMechanism: EOFMechanism) throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) - - let handler = MessageEndHandler() - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Prime the decoder with a request and consume it. - XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, - method: .GET, - uri: "/"))).isFull) - XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self))) - - // Send a 200 with the appropriate Transfer Encoding header. We should see the request. - XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "HTTP/1.1 200 OK\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) - XCTAssert(handler.seenHead) - XCTAssertFalse(handler.seenBody) - XCTAssertFalse(handler.seenEnd) - - // Now send body. Note that this *is* chunk encoded. We should also see a body. - XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "9\r\ncaribbean\r\n"))) - XCTAssert(handler.seenHead) - XCTAssert(handler.seenBody) - XCTAssertFalse(handler.seenEnd) - - // Now send EOF. This should error, as we're expecting the end chunk. - if case .halfClosure = eofMechanism { - channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed) - } else { - channel.pipeline.fireChannelInactive() - } - - XCTAssert(handler.seenHead) - XCTAssert(handler.seenBody) - XCTAssertFalse(handler.seenEnd) - - XCTAssertThrowsError(try channel.throwIfErrorCaught()) { error in - XCTAssertEqual(error as? HTTPParserError, .invalidEOFState) - } - - XCTAssertTrue(try channel.finish().isClean) - } - - func testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithChannelInactive() throws { - try assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: "gzip, chunked", eofMechanism: .channelInactive) - } - - func testMultipleTEWithChunkedFirstHasEOFBodyOnResponseWithChannelInactive() throws { - // Here http_parser is right, and this is EOF terminated. - try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "chunked, gzip", eofMechanism: .channelInactive) - } - - func testMultipleTEWithChunkedInTheMiddleHasEOFBodyOnResponseWithChannelInactive() throws { - // Here http_parser is right, and this is EOF terminated. - try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "gzip, chunked, deflate", eofMechanism: .channelInactive) - } - - func testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithHalfClosure() throws { - try assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: "gzip, chunked", eofMechanism: .halfClosure) - } - - func testMultipleTEWithChunkedFirstHasEOFBodyOnResponseWithHalfClosure() throws { - // Here http_parser is right, and this is EOF terminated. - try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "chunked, gzip", eofMechanism: .halfClosure) - } - - func testMultipleTEWithChunkedInTheMiddleHasEOFBodyOnResponseWithHalfClosure() throws { - // Here http_parser is right, and this is EOF terminated. - try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "gzip, chunked, deflate", eofMechanism: .halfClosure) - } - - func testRequestWithTEAndContentLengthErrors() throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - // Send a GET with the invalid headers. - let request = channel.allocator.buffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n") - XCTAssertThrowsError(try channel.writeInbound(request)) { error in - XCTAssertEqual(HTTPParserError.unexpectedContentLength, error as? HTTPParserError) - } - - // Must spin the loop. - XCTAssertFalse(channel.isActive) - channel.embeddedEventLoop.run() - } - - func testResponseWithTEAndContentLengthErrors() throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait()) - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait()) - - // Prime the decoder with a request. - XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .http1_1, - method: .GET, - uri: "/"))).isFull) - - // Send a 200 OK with the invalid headers. - let response = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 4\r\n\r\n" - XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: response))) { error in - XCTAssertEqual(HTTPParserError.unexpectedContentLength, error as? HTTPParserError) - } - - // Must spin the loop. - XCTAssertFalse(channel.isActive) - channel.embeddedEventLoop.run() - } - - private func assertRequestWithInvalidCLErrors(contentLengthField: String) throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - // Send a GET with the invalid headers. - let request = "POST / HTTP/1.1\r\nContent-Length: \(contentLengthField)\r\n\r\n" - XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: request))) { error in - XCTAssert(HTTPParserError.unexpectedContentLength == error as? HTTPParserError || - HTTPParserError.invalidContentLength == error as? HTTPParserError) - } - - // Must spin the loop. - XCTAssertFalse(channel.isActive) - channel.embeddedEventLoop.run() - } - - func testRequestWithMultipleDifferentContentLengthsFails() throws { - try assertRequestWithInvalidCLErrors(contentLengthField: "4, 5") - } - - func testRequestWithMultipleDifferentContentLengthsOnDifferentLinesFails() throws { - try assertRequestWithInvalidCLErrors(contentLengthField: "4\r\nContent-Length: 5") - } - - func testRequestWithInvalidContentLengthFails() throws { - try assertRequestWithInvalidCLErrors(contentLengthField: "pie") - } - - func testRequestWithIdenticalContentLengthRepeatedErrors() throws { - // This is another case where http_parser is, if not wrong, then aggressively interpreting - // the spec. Regardless, we match it. - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - // Send two POSTs with repeated content length, one with one field and one with two. - // Both should error. - let request = "POST / HTTP/1.1\r\nContent-Length: 4, 4\r\n\r\n" - XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: request))) { error in - XCTAssertEqual(HTTPParserError.invalidContentLength, error as? HTTPParserError) - } - - // Must spin the loop. - XCTAssertFalse(channel.isActive) - channel.embeddedEventLoop.run() - } - - func testRequestWithMultipleIdenticalContentLengthFieldsErrors() throws { - // This is another case where http_parser is, if not wrong, then aggressively interpreting - // the spec. Regardless, we match it. - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - let request = "POST / HTTP/1.1\r\nContent-Length: 4\r\nContent-Length: 4\r\n\r\n" - XCTAssertThrowsError(try channel.writeInbound(channel.allocator.buffer(string: request))) { error in - XCTAssertEqual(HTTPParserError.unexpectedContentLength, error as? HTTPParserError) - } - - // Must spin the loop. - XCTAssertFalse(channel.isActive) - channel.embeddedEventLoop.run() - } - - func testRequestWithoutExplicitLengthIsZeroLength() throws { - XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) - - let handler = MessageEndHandler() - XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait()) - - // Send a POST without a length field of any kind. This should be a zero-length request, - // so .end should come immediately. - XCTAssertNoThrow(try channel.writeInbound(channel.allocator.buffer(string: "POST / HTTP/1.1\r\nHost: example.org\r\n\r\n"))) - XCTAssert(handler.seenHead) - XCTAssertFalse(handler.seenBody) - XCTAssert(handler.seenEnd) - - XCTAssertTrue(try channel.finish().isClean) - } -}