@@ -3114,4 +3114,113 @@ class HTTPClientTests: XCTestCase {
31143114
31153115 XCTAssertNoThrow ( try client. execute ( request: request) . wait ( ) )
31163116 }
3117+
3118+ func testRejectsInvalidCharactersInHeaderFieldNames_http1( ) throws {
3119+ try self . _rejectsInvalidCharactersInHeaderFieldNames ( mode: . http1_1( ssl: true ) )
3120+ }
3121+
3122+ func testRejectsInvalidCharactersInHeaderFieldNames_http2( ) throws {
3123+ try self . _rejectsInvalidCharactersInHeaderFieldNames ( mode: . http2( compress: false ) )
3124+ }
3125+
3126+ private func _rejectsInvalidCharactersInHeaderFieldNames( mode: HTTPBin < HTTPBinHandler > . Mode ) throws {
3127+ let group = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
3128+ defer { XCTAssertNoThrow ( try group. syncShutdownGracefully ( ) ) }
3129+ let client = HTTPClient ( eventLoopGroupProvider: . shared( group) )
3130+ defer { XCTAssertNoThrow ( try client. syncShutdown ( ) ) }
3131+ let bin = HTTPBin ( mode)
3132+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
3133+
3134+ // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid
3135+ // characters as the following:
3136+ //
3137+ // ```
3138+ // field-name = token
3139+ //
3140+ // token = 1*tchar
3141+ //
3142+ // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
3143+ // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
3144+ // / DIGIT / ALPHA
3145+ // ; any VCHAR, except delimiters
3146+ let weirdAllowedFieldName = " !#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "
3147+
3148+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3149+ request. headers. add ( name: weirdAllowedFieldName, value: " present " )
3150+
3151+ // This should work fine.
3152+ let response = try client. execute ( request: request) . wait ( )
3153+ XCTAssertEqual ( response. status, . ok)
3154+
3155+ // 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.
3156+ for byte in UInt8 ( 0 ) ... UInt8 ( 127 ) {
3157+ // Skip bytes that we already believe are allowed.
3158+ if weirdAllowedFieldName. utf8. contains ( byte) {
3159+ continue
3160+ }
3161+ let forbiddenFieldName = weirdAllowedFieldName + String( decoding: [ byte] , as: UTF8 . self)
3162+
3163+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3164+ request. headers. add ( name: forbiddenFieldName, value: " present " )
3165+
3166+ XCTAssertThrowsError ( try client. execute ( request: request) . wait ( ) ) { error in
3167+ XCTAssertEqual ( error as? HTTPClientError , . invalidHeaderFieldNames( [ forbiddenFieldName] ) )
3168+ }
3169+ }
3170+ }
3171+
3172+ func testRejectsInvalidCharactersInHeaderFieldValues_http1( ) throws {
3173+ try self . _rejectsInvalidCharactersInHeaderFieldValues ( mode: . http1_1( ssl: true ) )
3174+ }
3175+
3176+ func testRejectsInvalidCharactersInHeaderFieldValues_http2( ) throws {
3177+ try self . _rejectsInvalidCharactersInHeaderFieldValues ( mode: . http2( compress: false ) )
3178+ }
3179+
3180+ private func _rejectsInvalidCharactersInHeaderFieldValues( mode: HTTPBin < HTTPBinHandler > . Mode ) throws {
3181+ let group = MultiThreadedEventLoopGroup ( numberOfThreads: 1 )
3182+ defer { XCTAssertNoThrow ( try group. syncShutdownGracefully ( ) ) }
3183+ let client = HTTPClient ( eventLoopGroupProvider: . shared( group) )
3184+ defer { XCTAssertNoThrow ( try client. syncShutdown ( ) ) }
3185+ let bin = HTTPBin ( mode)
3186+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
3187+
3188+ // We reject all ASCII control characters except HTAB and tolerate everything else.
3189+ let weirdAllowedFieldValue = " ! \" \t #$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[ \\ ]^_`abcdefghijklmnopqrstuvwxyz{|}~ "
3190+
3191+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3192+ request. headers. add ( name: " Weird-Value " , value: weirdAllowedFieldValue)
3193+
3194+ // This should work fine.
3195+ let response = try client. execute ( request: request) . wait ( )
3196+ XCTAssertEqual ( response. status, . ok)
3197+
3198+ // Now, let's confirm all other bytes in the ASCII range ar rejected
3199+ for byte in UInt8 ( 0 ) ... UInt8 ( 127 ) {
3200+ // Skip bytes that we already believe are allowed.
3201+ if weirdAllowedFieldValue. utf8. contains ( byte) {
3202+ continue
3203+ }
3204+ let forbiddenFieldValue = weirdAllowedFieldValue + String( decoding: [ byte] , as: UTF8 . self)
3205+
3206+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3207+ request. headers. add ( name: " Weird-Value " , value: forbiddenFieldValue)
3208+
3209+ XCTAssertThrowsError ( try client. execute ( request: request) . wait ( ) ) { error in
3210+ XCTAssertEqual ( error as? HTTPClientError , . invalidHeaderFieldValues( [ forbiddenFieldValue] ) )
3211+ }
3212+ }
3213+
3214+ // All the bytes outside the ASCII range are fine though.
3215+ for byte in UInt8 ( 128 ) ... UInt8 ( 255 ) {
3216+ let evenWeirderAllowedValue = weirdAllowedFieldValue + String( decoding: [ byte] , as: UTF8 . self)
3217+
3218+ var request = try Request ( url: " \( self . defaultHTTPBinURLPrefix) get " )
3219+ request. headers. add ( name: " Weird-Value " , value: evenWeirderAllowedValue)
3220+
3221+ // This should work fine.
3222+ let response = try client. execute ( request: request) . wait ( )
3223+ XCTAssertEqual ( response. status, . ok)
3224+ }
3225+ }
31173226}
0 commit comments