mirror of
https://github.com/swift-server/async-http-client.git
synced 2026-05-03 07:32:29 +00:00
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:
@@ -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)) }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -29,5 +29,6 @@ import XCTest
|
||||
testCase(HTTPClientCookieTests.allTests),
|
||||
testCase(HTTPClientInternalTests.allTests),
|
||||
testCase(HTTPClientTests.allTests),
|
||||
testCase(RequestValidationTests.allTests),
|
||||
])
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user