@@ -3468,4 +3468,113 @@ class HTTPClientTests: XCTestCase {
3468
3468
let httpClient = HTTPClient ( eventLoopGroupProvider: . shared( self . clientGroup) )
3469
3469
XCTAssertNoThrow ( try httpClient. shutdown ( ) . wait ( ) )
3470
3470
}
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
+ }
3471
3580
}
0 commit comments