swift-nio/Tests/NIOHTTP1Tests/HTTPServerProtocolErrorHand...

219 lines
10 KiB
Swift

//===----------------------------------------------------------------------===//
//
// 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
class HTTPServerProtocolErrorHandlerTest: XCTestCase {
func testHandlesBasicErrors() throws {
class CloseOnHTTPErrorHandler: ChannelInboundHandler {
typealias InboundIn = Never
func errorCaught(context: ChannelHandlerContext, error: Error) {
if let error = error as? HTTPParserError {
context.fireErrorCaught(error)
context.close(promise: nil)
}
}
}
let channel = EmbeddedChannel()
XCTAssertNoThrow(try channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).wait())
XCTAssertNoThrow(try channel.pipeline.addHandler(CloseOnHTTPErrorHandler()).wait())
var buffer = channel.allocator.buffer(capacity: 1024)
buffer.writeStaticString("GET / HTTP/1.1\r\nContent-Length: -4\r\n\r\n")
do {
try channel.writeInbound(buffer)
} catch HTTPParserError.invalidContentLength {
// This error is expected
}
channel.embeddedEventLoop.run()
// The channel should be closed at this stage.
XCTAssertNoThrow(try channel.closeFuture.wait())
// We expect exactly one ByteBuffer in the output.
guard var written = try channel.readOutbound(as: ByteBuffer.self) else {
XCTFail("No writes")
return
}
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
// Check the response.
assertResponseIs(response: written.readString(length: written.readableBytes)!,
expectedResponseLine: "HTTP/1.1 400 Bad Request",
expectedResponseHeaders: ["Connection: close", "Content-Length: 0"])
}
func testIgnoresNonParserErrors() throws {
enum DummyError: Error {
case error
}
let channel = EmbeddedChannel()
XCTAssertNoThrow(try channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).wait())
channel.pipeline.fireErrorCaught(DummyError.error)
XCTAssertThrowsError(try channel.throwIfErrorCaught()) { error in
XCTAssertEqual(DummyError.error, error as? DummyError)
}
XCTAssertNoThrow(try channel.finish())
}
func testDoesNotSendAResponseIfResponseHasAlreadyStarted() throws {
let channel = EmbeddedChannel()
defer {
XCTAssertNoThrow(try channel.finish())
}
XCTAssertNoThrow(try channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: false, withErrorHandling: true).wait())
let res = HTTPServerResponsePart.head(.init(version: .http1_1,
status: .ok,
headers: .init([("Content-Length", "0")])))
XCTAssertNoThrow(try channel.writeAndFlush(res).wait())
// now we have started a response but it's not complete yet, let's inject a parser error
channel.pipeline.fireErrorCaught(HTTPParserError.invalidEOFState)
var allOutbound = try channel.readAllOutboundBuffers()
let allOutboundString = allOutbound.readString(length: allOutbound.readableBytes)
// there should be no HTTP/1.1 400 or anything in here
XCTAssertEqual("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", allOutboundString)
XCTAssertThrowsError(try channel.throwIfErrorCaught()) { error in
XCTAssertEqual(.invalidEOFState, error as? HTTPParserError)
}
}
func testCanHandleErrorsWhenResponseHasStarted() throws {
enum NextExpectedState {
case head
case end
case none
}
class DelayWriteHandler: ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
typealias OutboundOut = HTTPServerResponsePart
private var nextExpected: NextExpectedState = .head
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let req = self.unwrapInboundIn(data)
switch req {
case .head:
XCTAssertEqual(.head, self.nextExpected)
self.nextExpected = .end
let res = HTTPServerResponsePart.head(.init(version: .http1_1,
status: .ok,
headers: .init([("Content-Length", "0")])))
context.writeAndFlush(self.wrapOutboundOut(res), promise: nil)
default:
XCTAssertEqual(.end, self.nextExpected)
self.nextExpected = .none
}
}
}
let channel = EmbeddedChannel()
XCTAssertNoThrow(try channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap {
channel.pipeline.addHandler(DelayWriteHandler())
}.wait())
var buffer = channel.allocator.buffer(capacity: 1024)
buffer.writeStaticString("GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\n\r\nGET / HT")
XCTAssertNoThrow(try channel.writeInbound(buffer))
XCTAssertNoThrow(try channel.close().wait())
channel.embeddedEventLoop.run()
// The channel should be closed at this stage.
XCTAssertNoThrow(try channel.closeFuture.wait())
// We expect exactly one ByteBuffer in the output.
guard var written = try channel.readOutbound(as: ByteBuffer.self) else {
XCTFail("No writes")
return
}
XCTAssertNoThrow(XCTAssertNil(try channel.readOutbound()))
// Check the response.
assertResponseIs(response: written.readString(length: written.readableBytes)!,
expectedResponseLine: "HTTP/1.1 200 OK",
expectedResponseHeaders: ["Content-Length: 0"])
}
func testDoesSendAResponseIfInformationalHeaderWasSent() throws {
let channel = EmbeddedChannel()
defer { XCTAssertNoThrow(try channel.finish(acceptAlreadyClosed: false)) }
XCTAssertNoThrow(try channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: false, withErrorHandling: true).wait())
XCTAssertNoThrow(try channel.connect(to: .makeAddressResolvingHost("127.0.0.1", port: 0)).wait())
// Send an head that expects a continue informational response
let reqHeadBytes = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n"
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: reqHeadBytes)))
let expectedHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["Transfer-Encoding":"chunked", "Expect":"100-continue"])
XCTAssertEqual(try channel.readInbound(as: HTTPServerRequestPart.self), .head(expectedHead))
// Respond with continue informational response
let continueResponse = HTTPResponseHead(version: .http1_1, status: .continue)
XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(continueResponse)))
XCTAssertEqual(try channel.readOutbound(as: ByteBuffer.self), ByteBuffer(string: "HTTP/1.1 100 Continue\r\n\r\n"))
// Expects a hex digit... But receives garbage
XCTAssertThrowsError(try channel.writeInbound(ByteBuffer(string: "xyz"))) {
XCTAssertEqual($0 as? HTTPParserError, .invalidChunkSize)
}
// Receive a bad request
XCTAssertEqual(try channel.readOutbound(as: ByteBuffer.self), ByteBuffer(string: "HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"))
}
func testDoesNotSendAResponseIfRealHeaderWasSentAfterInformationalHeader() throws {
let channel = EmbeddedChannel()
defer { XCTAssertNoThrow(try channel.finish(acceptAlreadyClosed: false)) }
XCTAssertNoThrow(try channel.connect(to: .makeAddressResolvingHost("127.0.0.1", port: 0)).wait())
XCTAssertNoThrow(try channel.pipeline.configureHTTPServerPipeline(withPipeliningAssistance: false, withErrorHandling: true).wait())
// Send an head that expects a continue informational response
let reqHeadBytes = "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\nExpect: 100-continue\r\n\r\n"
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: reqHeadBytes)))
let expectedHead = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: ["Transfer-Encoding":"chunked", "Expect":"100-continue"])
XCTAssertEqual(try channel.readInbound(as: HTTPServerRequestPart.self), .head(expectedHead))
// Respond with continue informational response
let continueResponse = HTTPResponseHead(version: .http1_1, status: .continue)
XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(continueResponse)))
XCTAssertEqual(try channel.readOutbound(as: ByteBuffer.self), ByteBuffer(string: "HTTP/1.1 100 Continue\r\n\r\n"))
// Send a a chunk
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: "6\r\nfoobar\r\n")))
// Server responds with an actual head, even though request has not finished yet
let acceptedResponse = HTTPResponseHead(version: .http1_1, status: .accepted, headers: ["Content-Length": "20"])
XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(acceptedResponse)))
XCTAssertEqual(try channel.readOutbound(as: ByteBuffer.self), ByteBuffer(string: "HTTP/1.1 202 Accepted\r\nContent-Length: 20\r\n\r\n"))
// Client sends garbage chunk
XCTAssertThrowsError(try channel.writeInbound(ByteBuffer(string: "xyz"))) {
XCTAssertEqual($0 as? HTTPParserError, .invalidChunkSize)
}
XCTAssertNil(try channel.readOutbound(as: ByteBuffer.self))
}
}