Verify header field names (#191)

* HTTPRequest without body: Content-Length shall not be send
* Verify field names comply with RFC7230
This commit is contained in:
Fabian Fett
2020-04-01 17:48:01 +02:00
committed by GitHub
parent 2fcf0a9fdd
commit 2d88de3eb6
7 changed files with 238 additions and 32 deletions
+6
View File
@@ -754,6 +754,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
case redirectLimitReached
case redirectCycleDetected
case uncleanShutdown
case traceRequestWithBody
case invalidHeaderFieldNames([String])
}
private var code: Code
@@ -798,4 +800,8 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
public static let redirectCycleDetected = HTTPClientError(code: .redirectCycleDetected)
/// Unclean shutdown
public static let uncleanShutdown = HTTPClientError(code: .uncleanShutdown)
/// A body was sent in a request with method TRACE
public static let traceRequestWithBody = HTTPClientError(code: .traceRequestWithBody)
/// Header field names contain invalid characters
public static func invalidHeaderFieldNames(_ names: [String]) -> HTTPClientError { return HTTPClientError(code: .invalidHeaderFieldNames(names)) }
}
+1 -1
View File
@@ -771,7 +771,7 @@ extension TaskHandler: ChannelDuplexHandler {
}
do {
try headers.validate(body: request.body)
try headers.validate(method: request.method, body: request.body)
} catch {
promise?.fail(error)
context.fireErrorCaught(error)
+79 -19
View File
@@ -2,7 +2,7 @@
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors
// Copyright (c) 2018-2020 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
@@ -16,7 +16,7 @@ import NIO
import NIOHTTP1
extension HTTPHeaders {
mutating func validate(body: HTTPClient.Body?) throws {
mutating func validate(method: HTTPMethod, body: HTTPClient.Body?) throws {
// validate transfer encoding and content length (https://tools.ietf.org/html/rfc7230#section-3.3.1)
var transferEncoding: String?
var contentLength: Int?
@@ -29,34 +29,94 @@ extension HTTPHeaders {
self.remove(name: "Transfer-Encoding")
self.remove(name: "Content-Length")
if let body = body {
guard (encodings.filter { $0 == "chunked" }.count <= 1) else {
throw HTTPClientError.chunkedSpecifiedMultipleTimes
}
try self.validateFieldNames()
if encodings.isEmpty {
guard let body = body else {
// if we don't have a body we might not need to send the Content-Length field
// https://tools.ietf.org/html/rfc7230#section-3.3.2
switch method {
case .GET, .HEAD, .DELETE, .CONNECT, .TRACE:
// A user agent SHOULD NOT send a Content-Length header field when the request
// message does not contain a payload body and the method semantics do not
// anticipate such a body.
return
default:
// A user agent SHOULD send a Content-Length in a request message when
// no Transfer-Encoding is sent and the request method defines a meaning
// for an enclosed payload body.
self.add(name: "Content-Length", value: "0")
return
}
}
if case .TRACE = method {
// A client MUST NOT send a message body in a TRACE request.
// https://tools.ietf.org/html/rfc7230#section-4.3.8
throw HTTPClientError.traceRequestWithBody
}
guard (encodings.filter { $0 == "chunked" }.count <= 1) else {
throw HTTPClientError.chunkedSpecifiedMultipleTimes
}
if encodings.isEmpty {
guard let length = body.length else {
throw HTTPClientError.contentLengthMissing
}
contentLength = length
} else {
transferEncoding = encodings.joined(separator: ", ")
if !encodings.contains("chunked") {
guard let length = body.length else {
throw HTTPClientError.contentLengthMissing
}
contentLength = length
} else {
transferEncoding = encodings.joined(separator: ", ")
if !encodings.contains("chunked") {
guard let length = body.length else {
throw HTTPClientError.contentLengthMissing
}
contentLength = length
}
}
} else {
contentLength = 0
}
// add headers if required
if let enc = transferEncoding {
self.add(name: "Transfer-Encoding", value: enc)
}
if let length = contentLength {
} else if let length = contentLength {
// A sender MUST NOT send a Content-Length header field in any message
// that contains a Transfer-Encoding header field.
self.add(name: "Content-Length", value: String(length))
}
}
func validateFieldNames() throws {
let invalidFieldNames = self.compactMap { (name, _) -> String? in
let satisfy = name.utf8.allSatisfy { (char) -> Bool in
switch char {
case UInt8(ascii: "a")...UInt8(ascii: "z"),
UInt8(ascii: "A")...UInt8(ascii: "Z"),
UInt8(ascii: "0")...UInt8(ascii: "9"),
UInt8(ascii: "!"),
UInt8(ascii: "#"),
UInt8(ascii: "$"),
UInt8(ascii: "%"),
UInt8(ascii: "&"),
UInt8(ascii: "'"),
UInt8(ascii: "*"),
UInt8(ascii: "+"),
UInt8(ascii: "-"),
UInt8(ascii: "."),
UInt8(ascii: "^"),
UInt8(ascii: "_"),
UInt8(ascii: "`"),
UInt8(ascii: "|"),
UInt8(ascii: "~"):
return true
default:
return false
}
}
return satisfy ? nil : name
}
guard invalidFieldNames.count == 0 else {
throw HTTPClientError.invalidHeaderFieldNames(invalidFieldNames)
}
}
}
@@ -830,8 +830,7 @@ class HTTPClientTests: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/foo",
headers: HTTPHeaders([("Host", "localhost"),
("Content-Length", "0")]))),
headers: HTTPHeaders([("Host", "localhost")]))),
try web.readInbound()))
XCTAssertNoThrow(XCTAssertEqual(.end(nil),
try web.readInbound()))
@@ -860,8 +859,7 @@ class HTTPClientTests: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/foo",
headers: HTTPHeaders([("Host", "localhost"),
("Content-Length", "0")]))),
headers: HTTPHeaders([("Host", "localhost")]))),
try web.readInbound()))
XCTAssertNoThrow(XCTAssertEqual(.end(nil),
try web.readInbound()))
@@ -887,8 +885,7 @@ class HTTPClientTests: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/foo",
headers: HTTPHeaders([("Host", "localhost"),
("Content-Length", "0")]))),
headers: HTTPHeaders([("Host", "localhost")]))),
try web.readInbound()))
XCTAssertNoThrow(XCTAssertEqual(.end(nil),
try web.readInbound()))
@@ -916,8 +913,7 @@ class HTTPClientTests: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/foo",
headers: HTTPHeaders([("Host", "localhost"),
("Content-Length", "0")]))),
headers: HTTPHeaders([("Host", "localhost")]))),
try web.readInbound()))
XCTAssertNoThrow(XCTAssertEqual(.end(nil),
try web.readInbound()))
@@ -950,8 +946,7 @@ class HTTPClientTests: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/foo",
headers: HTTPHeaders([("Host", "localhost"),
("Content-Length", "0")]))),
headers: HTTPHeaders([("Host", "localhost")]))),
try web.readInbound()))
XCTAssertNoThrow(XCTAssertEqual(.end(nil),
try web.readInbound()))
@@ -1166,8 +1161,7 @@ class HTTPClientTests: XCTestCase {
XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
method: .GET,
uri: "/foo",
headers: HTTPHeaders([("Host", "localhost"),
("Content-Length", "0")]))),
headers: HTTPHeaders([("Host", "localhost")]))),
try web.readInbound()))
XCTAssertNoThrow(XCTAssertEqual(.end(nil),
try web.readInbound()))
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2018-2019 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
//
// RequestValidationTests+XCTest.swift
//
import XCTest
///
/// NOTE: This file was generated by generate_linux_tests.rb
///
/// Do NOT edit this file directly as it will be regenerated automatically when needed.
///
extension RequestValidationTests {
static var allTests: [(String, (RequestValidationTests) -> () throws -> Void)] {
return [
("testContentLengthHeaderIsRemovedFromGETIfNoBody", testContentLengthHeaderIsRemovedFromGETIfNoBody),
("testContentLengthHeaderIsAddedToPOSTAndPUTWithNoBody", testContentLengthHeaderIsAddedToPOSTAndPUTWithNoBody),
("testContentLengthHeaderIsChangedIfBodyHasDifferentLength", testContentLengthHeaderIsChangedIfBodyHasDifferentLength),
("testChunkedEncodingDoesNotHaveContentLengthHeader", testChunkedEncodingDoesNotHaveContentLengthHeader),
("testTRACERequestMustNotHaveBody", testTRACERequestMustNotHaveBody),
("testGET_HEAD_DELETE_CONNECTRequestCanHaveBody", testGET_HEAD_DELETE_CONNECTRequestCanHaveBody),
("testInvalidHeaderFieldNames", testInvalidHeaderFieldNames),
("testValidHeaderFieldNames", testValidHeaderFieldNames),
]
}
}
@@ -0,0 +1,107 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2020 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
@testable import AsyncHTTPClient
import NIO
import NIOHTTP1
import XCTest
class RequestValidationTests: XCTestCase {
func testContentLengthHeaderIsRemovedFromGETIfNoBody() {
var headers = HTTPHeaders([("Content-Length", "0")])
XCTAssertNoThrow(try headers.validate(method: .GET, body: .none))
XCTAssertNil(headers.first(name: "Content-Length"))
}
func testContentLengthHeaderIsAddedToPOSTAndPUTWithNoBody() {
var putHeaders = HTTPHeaders()
XCTAssertNoThrow(try putHeaders.validate(method: .PUT, body: .none))
XCTAssertEqual(putHeaders.first(name: "Content-Length"), "0")
var postHeaders = HTTPHeaders()
XCTAssertNoThrow(try postHeaders.validate(method: .POST, body: .none))
XCTAssertEqual(postHeaders.first(name: "Content-Length"), "0")
}
func testContentLengthHeaderIsChangedIfBodyHasDifferentLength() {
var headers = HTTPHeaders([("Content-Length", "0")])
var buffer = ByteBufferAllocator().buffer(capacity: 200)
buffer.writeBytes([UInt8](repeating: 12, count: 200))
XCTAssertNoThrow(try headers.validate(method: .PUT, body: .byteBuffer(buffer)))
XCTAssertEqual(headers.first(name: "Content-Length"), "200")
}
func testChunkedEncodingDoesNotHaveContentLengthHeader() {
var headers = HTTPHeaders([
("Content-Length", "200"),
("Transfer-Encoding", "chunked"),
])
var buffer = ByteBufferAllocator().buffer(capacity: 200)
buffer.writeBytes([UInt8](repeating: 12, count: 200))
XCTAssertNoThrow(try headers.validate(method: .PUT, body: .byteBuffer(buffer)))
// https://tools.ietf.org/html/rfc7230#section-3.3.2
// A sender MUST NOT send a Content-Length header field in any message
// that contains a Transfer-Encoding header field.
XCTAssertNil(headers.first(name: "Content-Length"))
XCTAssertEqual(headers.first(name: "Transfer-Encoding"), "chunked")
}
func testTRACERequestMustNotHaveBody() {
var headers = HTTPHeaders([
("Content-Length", "200"),
("Transfer-Encoding", "chunked"),
])
var buffer = ByteBufferAllocator().buffer(capacity: 200)
buffer.writeBytes([UInt8](repeating: 12, count: 200))
XCTAssertThrowsError(try headers.validate(method: .TRACE, body: .byteBuffer(buffer))) {
XCTAssertEqual($0 as? HTTPClientError, .traceRequestWithBody)
}
}
func testGET_HEAD_DELETE_CONNECTRequestCanHaveBody() {
var buffer = ByteBufferAllocator().buffer(capacity: 100)
buffer.writeBytes([UInt8](repeating: 12, count: 100))
// GET, HEAD, DELETE and CONNECT requests can have a payload. (though uncommon)
let allowedMethods: [HTTPMethod] = [.GET, .HEAD, .DELETE, .CONNECT]
var headers = HTTPHeaders()
for method in allowedMethods {
XCTAssertNoThrow(try headers.validate(method: method, body: .byteBuffer(buffer)))
}
}
func testInvalidHeaderFieldNames() {
var headers = HTTPHeaders([
("Content-Length", "200"),
("User Agent", "Haha"),
])
XCTAssertThrowsError(try headers.validate(method: .GET, body: nil)) { error in
XCTAssertEqual(error as? HTTPClientError, HTTPClientError.invalidHeaderFieldNames(["User Agent"]))
}
}
func testValidHeaderFieldNames() {
var headers = HTTPHeaders([
("abcdefghijklmnopqrstuvwxyz", "Haha"),
("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "Haha"),
("0123456789", "Haha"),
("!#$%&'*+-.^_`|~", "Haha"),
])
XCTAssertNoThrow(try headers.validate(method: .GET, body: nil))
}
}
+1
View File
@@ -29,5 +29,6 @@ import XCTest
testCase(HTTPClientCookieTests.allTests),
testCase(HTTPClientInternalTests.allTests),
testCase(HTTPClientTests.allTests),
testCase(RequestValidationTests.allTests),
])
#endif