Skip to content

Always overwrite Transport-Encoding and Content-Length #479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Sources/AsyncHTTPClient/ConnectionPool/RequestBodyLength.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 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
//
//===----------------------------------------------------------------------===//

enum RequestBodyLength: Hashable {
/// size of the request body is not known before starting the request
case dynamic
/// size of the request body is fixed and exactly `length` bytes
case fixed(length: Int)
}
4 changes: 4 additions & 0 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
case remoteConnectionClosed
case cancelled
case identityCodingIncorrectlyPresent
@available(*, deprecated, message: "AsyncHTTPClient now silently corrects this invalid header.")
case chunkedSpecifiedMultipleTimes
case invalidProxyResponse
case contentLengthMissing
Expand All @@ -894,6 +895,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
case invalidHeaderFieldNames([String])
case bodyLengthMismatch
case writeAfterRequestSent
@available(*, deprecated, message: "AsyncHTTPClient now silently corrects invalid headers.")
case incompatibleHeaders
case connectTimeout
case socksHandshakeTimeout
Expand Down Expand Up @@ -937,6 +939,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
/// Request contains invalid identity encoding.
public static let identityCodingIncorrectlyPresent = HTTPClientError(code: .identityCodingIncorrectlyPresent)
/// Request contains multiple chunks definitions.
@available(*, deprecated, message: "AsyncHTTPClient now silently corrects this invalid header.")
public static let chunkedSpecifiedMultipleTimes = HTTPClientError(code: .chunkedSpecifiedMultipleTimes)
/// Proxy response was invalid.
public static let invalidProxyResponse = HTTPClientError(code: .invalidProxyResponse)
Expand All @@ -959,6 +962,7 @@ public struct HTTPClientError: Error, Equatable, CustomStringConvertible {
/// Body part was written after request was fully sent.
public static let writeAfterRequestSent = HTTPClientError(code: .writeAfterRequestSent)
/// Incompatible headers specified, for example `Transfer-Encoding` and `Content-Length`.
@available(*, deprecated, message: "AsyncHTTPClient now silently corrects invalid headers.")
public static let incompatibleHeaders = HTTPClientError(code: .incompatibleHeaders)
/// Creating a new tcp connection timed out
public static let connectTimeout = HTTPClientError(code: .connectTimeout)
Expand Down
24 changes: 19 additions & 5 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ extension HTTPClient {
}
}

/// Body size. Request validation will be failed with `HTTPClientErrors.contentLengthMissing` if nil,
/// unless `Trasfer-Encoding: chunked` header is set.
/// Body size. if nil,`Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length`
/// header is set with the given `length`.
public var length: Int?
/// Body chunk provider.
public var stream: (StreamWriter) -> EventLoopFuture<Void>
Expand All @@ -62,8 +62,8 @@ extension HTTPClient {
/// Create and stream body using `StreamWriter`.
///
/// - parameters:
/// - length: Body size. Request validation will be failed with `HTTPClientErrors.contentLengthMissing` if nil,
/// unless `Transfer-Encoding: chunked` header is set.
/// - length: Body size. If nil, `Transfer-Encoding` will automatically be set to `chunked`. Otherwise a `Content-Length`
/// header is set with the given `length`.
/// - stream: Body chunk provider.
public static func stream(length: Int? = nil, _ stream: @escaping (StreamWriter) -> EventLoopFuture<Void>) -> Body {
return Body(length: length, stream: stream)
Expand Down Expand Up @@ -309,7 +309,7 @@ extension HTTPClient {
head.headers.add(name: "host", value: host)
}

let metadata = try head.headers.validate(method: self.method, body: self.body)
let metadata = try head.headers.validateAndSetTransportFraming(method: self.method, bodyLength: .init(self.body))

return (head, metadata)
}
Expand Down Expand Up @@ -820,3 +820,17 @@ internal struct RedirectHandler<ResponseType> {
}
}
}

extension RequestBodyLength {
init(_ body: HTTPClient.Body?) {
guard let body = body else {
self = .fixed(length: 0)
return
}
guard let length = body.length else {
self = .dynamic
return
}
self = .fixed(length: length)
}
}
126 changes: 49 additions & 77 deletions Sources/AsyncHTTPClient/RequestValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,90 +16,32 @@ import NIOCore
import NIOHTTP1

extension HTTPHeaders {
mutating func validate(method: HTTPMethod, body: HTTPClient.Body?) throws -> RequestFramingMetadata {
var metadata = RequestFramingMetadata(connectionClose: false, body: .fixedSize(0))

if self[canonicalForm: "connection"].lazy.map({ $0.lowercased() }).contains("close") {
metadata.connectionClose = true
}

// validate transfer encoding and content length (https://tools.ietf.org/html/rfc7230#section-3.3.1)
if self.contains(name: "Transfer-Encoding"), self.contains(name: "Content-Length") {
throw HTTPClientError.incompatibleHeaders
}

var transferEncoding: String?
var contentLength: Int?
let encodings = self[canonicalForm: "Transfer-Encoding"].map { $0.lowercased() }

guard !encodings.contains("identity") else {
throw HTTPClientError.identityCodingIncorrectlyPresent
}

self.remove(name: "Transfer-Encoding")

mutating func validateAndSetTransportFraming(
method: HTTPMethod,
bodyLength: RequestBodyLength
) throws -> RequestFramingMetadata {
try self.validateFieldNames()

guard let body = body else {
self.remove(name: "Content-Length")
// 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 metadata
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 metadata
}
}

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.lazy.filter { $0 == "chunked" }.count <= 1) else {
throw HTTPClientError.chunkedSpecifiedMultipleTimes
}

if encodings.isEmpty {
if let length = body.length {
self.remove(name: "Content-Length")
contentLength = length
} else if !self.contains(name: "Content-Length") {
transferEncoding = "chunked"
}
} else {
self.remove(name: "Content-Length")

transferEncoding = encodings.joined(separator: ", ")
if !encodings.contains("chunked") {
guard let length = body.length else {
throw HTTPClientError.contentLengthMissing
}
contentLength = length
switch bodyLength {
case .fixed(length: 0):
break
case .dynamic, .fixed:
// 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
}
}

// add headers if required
if let enc = transferEncoding {
self.add(name: "Transfer-Encoding", value: enc)
metadata.body = .stream
} 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))
metadata.body = .fixedSize(length)
}
self.setTransportFraming(method: method, bodyLength: bodyLength)

return metadata
let connectionClose = self[canonicalForm: "connection"].lazy.map { $0.lowercased() }.contains("close")
switch bodyLength {
case .dynamic:
return .init(connectionClose: connectionClose, body: .stream)
case .fixed(let length):
return .init(connectionClose: connectionClose, body: .fixedSize(length))
}
}

private func validateFieldNames() throws {
Expand Down Expand Up @@ -137,4 +79,34 @@ extension HTTPHeaders {
throw HTTPClientError.invalidHeaderFieldNames(invalidFieldNames)
}
}

private mutating func setTransportFraming(
method: HTTPMethod,
bodyLength: RequestBodyLength
) {
self.remove(name: "Content-Length")
self.remove(name: "Transfer-Encoding")

switch bodyLength {
case .fixed(0):
// 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.
break
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")
}
case .fixed(let length):
self.add(name: "Content-Length", value: String(length))
case .dynamic:
self.add(name: "Transfer-Encoding", value: "chunked")
}
}
}
2 changes: 1 addition & 1 deletion Tests/AsyncHTTPClientTests/HTTPClientTests+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ extension HTTPClientTests {
("testErrorAfterCloseWhileBackpressureExerted", testErrorAfterCloseWhileBackpressureExerted),
("testRequestSpecificTLS", testRequestSpecificTLS),
("testConnectionPoolSizeConfigValueIsRespected", testConnectionPoolSizeConfigValueIsRespected),
("testRequestWithHeaderTransferEncodingIdentityFails", testRequestWithHeaderTransferEncodingIdentityFails),
("testRequestWithHeaderTransferEncodingIdentityDoesNotFail", testRequestWithHeaderTransferEncodingIdentityDoesNotFail),
]
}
}
11 changes: 6 additions & 5 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2941,22 +2941,23 @@ class HTTPClientTests: XCTestCase {
XCTAssertEqual(httpBin.createdConnections, poolSize)
}

func testRequestWithHeaderTransferEncodingIdentityFails() {
func testRequestWithHeaderTransferEncodingIdentityDoesNotFail() {
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }

let client = HTTPClient(eventLoopGroupProvider: .shared(group))
defer { XCTAssertNoThrow(try client.syncShutdown()) }

guard var request = try? Request(url: "http://localhost/get") else {
let httpBin = HTTPBin()
defer { XCTAssertNoThrow(try httpBin.shutdown()) }

guard var request = try? Request(url: "http://127.0.0.1:\(httpBin.port)/get") else {
return XCTFail("Expected to have a request here.")
}
request.headers.add(name: "X-Test-Header", value: "X-Test-Value")
request.headers.add(name: "Transfer-Encoding", value: "identity")
request.body = .string("1234")

XCTAssertThrowsError(try client.execute(request: request).wait()) {
XCTAssertEqual($0 as? HTTPClientError, .identityCodingIncorrectlyPresent)
}
XCTAssertNoThrow(try client.execute(request: request).wait())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ extension RequestValidationTests {
("testBothHeadersNoBody", testBothHeadersNoBody),
("testBothHeadersHasBody", testBothHeadersHasBody),
("testHostHeaderIsSetCorrectlyInCreateRequestHead", testHostHeaderIsSetCorrectlyInCreateRequestHead),
("testTraceMethodIsNotAllowedToHaveAFixedLengthBody", testTraceMethodIsNotAllowedToHaveAFixedLengthBody),
("testTraceMethodIsNotAllowedToHaveADynamicLengthBody", testTraceMethodIsNotAllowedToHaveADynamicLengthBody),
("testTransferEncodingsAreOverwrittenIfBodyLengthIsFixed", testTransferEncodingsAreOverwrittenIfBodyLengthIsFixed),
("testTransferEncodingsAreOverwrittenIfBodyLengthIsDynamic", testTransferEncodingsAreOverwrittenIfBodyLengthIsDynamic),
]
}
}
Loading