@@ -3468,4 +3468,113 @@ class HTTPClientTests: XCTestCase {
34683468 let httpClient = HTTPClient ( eventLoopGroupProvider: . shared( self . clientGroup) )
34693469 XCTAssertNoThrow ( try httpClient. shutdown ( ) . wait ( ) )
34703470 }
3471+
3472+ func testRejectsInvalidCharactersInHeaderFieldNames_http1( ) throws {
3473+ try self . _rejectsInvalidCharactersInHeaderFieldNames ( mode: . http1_1( ssl: true ) )
3474+ }
3475+
3476+ func testRejectsInvalidCharactersInHeaderFieldNames_http2( ) throws {
3477+ try self . _rejectsInvalidCharactersInHeaderFieldNames ( mode: . http2( compress: false ) )
3478+ }
3479+
3480+ private func _rejectsInvalidCharactersInHeaderFieldNames( mode: HTTPBin < HTTPBinHandler > . Mode ) throws {
3481+ let group = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
3482+ defer { XCTAssertNoThrow ( try group. syncShutdownGracefully ( ) ) }
3483+ let client = HTTPClient ( eventLoopGroupProvider: . shared( group) )
3484+ defer { XCTAssertNoThrow ( try client. syncShutdown ( ) ) }
3485+ let bin = HTTPBin ( mode)
3486+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
3487+
3488+ // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid
3489+ // characters as the following:
3490+ //
3491+ // ```
3492+ // field-name = token
3493+ //
3494+ // token = 1*tchar
3495+ //
3496+ // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
3497+ // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
3498+ // / DIGIT / ALPHA
3499+ // ; any VCHAR, except delimiters
3500+ let weirdAllowedFieldName = " !#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "
3501+
3502+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3503+ request. headers. add ( name: weirdAllowedFieldName, value: " present " )
3504+
3505+ // This should work fine.
3506+ let response = try client. execute ( request: request) . wait ( )
3507+ XCTAssertEqual ( response. status, . ok)
3508+
3509+ // Now, let's confirm all other bytes are rejected. We want to stay within the ASCII space as the HTTPHeaders type will forbid anything else.
3510+ for byte in UInt8 ( 0 ) ... UInt8 ( 127 ) {
3511+ // Skip bytes that we already believe are allowed.
3512+ if weirdAllowedFieldName. utf8. contains ( byte) {
3513+ continue
3514+ }
3515+ let forbiddenFieldName = weirdAllowedFieldName + String( decoding: [ byte] , as: UTF8 . self)
3516+
3517+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3518+ request. headers. add ( name: forbiddenFieldName, value: " present " )
3519+
3520+ XCTAssertThrowsError ( try client. execute ( request: request) . wait ( ) ) { error in
3521+ XCTAssertEqual ( error as? HTTPClientError , . invalidHeaderFieldNames( [ forbiddenFieldName] ) )
3522+ }
3523+ }
3524+ }
3525+
3526+ func testRejectsInvalidCharactersInHeaderFieldValues_http1( ) throws {
3527+ try self . _rejectsInvalidCharactersInHeaderFieldValues ( mode: . http1_1( ssl: true ) )
3528+ }
3529+
3530+ func testRejectsInvalidCharactersInHeaderFieldValues_http2( ) throws {
3531+ try self . _rejectsInvalidCharactersInHeaderFieldValues ( mode: . http2( compress: false ) )
3532+ }
3533+
3534+ private func _rejectsInvalidCharactersInHeaderFieldValues( mode: HTTPBin < HTTPBinHandler > . Mode ) throws {
3535+ let group = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
3536+ defer { XCTAssertNoThrow ( try group. syncShutdownGracefully ( ) ) }
3537+ let client = HTTPClient ( eventLoopGroupProvider: . shared( group) )
3538+ defer { XCTAssertNoThrow ( try client. syncShutdown ( ) ) }
3539+ let bin = HTTPBin ( mode)
3540+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
3541+
3542+ // We reject all ASCII control characters except HTAB and tolerate everything else.
3543+ let weirdAllowedFieldValue = " ! \" \t #$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[ \\ ]^_`abcdefghijklmnopqrstuvwxyz{|}~ "
3544+
3545+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3546+ request. headers. add ( name: " Weird-Value " , value: weirdAllowedFieldValue)
3547+
3548+ // This should work fine.
3549+ let response = try client. execute ( request: request) . wait ( )
3550+ XCTAssertEqual ( response. status, . ok)
3551+
3552+ // Now, let's confirm all other bytes in the ASCII range ar rejected
3553+ for byte in UInt8 ( 0 ) ... UInt8 ( 127 ) {
3554+ // Skip bytes that we already believe are allowed.
3555+ if weirdAllowedFieldValue. utf8. contains ( byte) {
3556+ continue
3557+ }
3558+ let forbiddenFieldValue = weirdAllowedFieldValue + String( decoding: [ byte] , as: UTF8 . self)
3559+
3560+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3561+ request. headers. add ( name: " Weird-Value " , value: forbiddenFieldValue)
3562+
3563+ XCTAssertThrowsError ( try client. execute ( request: request) . wait ( ) ) { error in
3564+ XCTAssertEqual ( error as? HTTPClientError , . invalidHeaderFieldValues( [ forbiddenFieldValue] ) )
3565+ }
3566+ }
3567+
3568+ // All the bytes outside the ASCII range are fine though.
3569+ for byte in UInt8 ( 128 ) ... UInt8 ( 255 ) {
3570+ let evenWeirderAllowedValue = weirdAllowedFieldValue + String( decoding: [ byte] , as: UTF8 . self)
3571+
3572+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3573+ request. headers. add ( name: " Weird-Value " , value: evenWeirderAllowedValue)
3574+
3575+ // This should work fine.
3576+ let response = try client. execute ( request: request) . wait ( )
3577+ XCTAssertEqual ( response. status, . ok)
3578+ }
3579+ }
34713580}
0 commit comments