@@ -597,6 +597,134 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
597
597
XCTAssertEqual ( body, ByteBuffer ( string: " 1234 " ) )
598
598
}
599
599
}
600
+
601
+ func testRejectsInvalidCharactersInHeaderFieldNames_http1( ) {
602
+ self . _rejectsInvalidCharactersInHeaderFieldNames ( mode: . http1_1( ssl: true ) )
603
+ }
604
+
605
+ func testRejectsInvalidCharactersInHeaderFieldNames_http2( ) {
606
+ self . _rejectsInvalidCharactersInHeaderFieldNames ( mode: . http2( compress: false ) )
607
+ }
608
+
609
+ private func _rejectsInvalidCharactersInHeaderFieldNames( mode: HTTPBin < HTTPBinHandler > . Mode ) {
610
+ guard #available( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * ) else { return }
611
+ XCTAsyncTest {
612
+ let bin = HTTPBin ( mode)
613
+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
614
+ let client = makeDefaultHTTPClient ( )
615
+ defer { XCTAssertNoThrow ( try client. syncShutdown ( ) ) }
616
+ let logger = Logger ( label: " HTTPClient " , factory: StreamLogHandler . standardOutput ( label: ) )
617
+
618
+ // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid
619
+ // characters as the following:
620
+ //
621
+ // ```
622
+ // field-name = token
623
+ //
624
+ // token = 1*tchar
625
+ //
626
+ // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
627
+ // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
628
+ // / DIGIT / ALPHA
629
+ // ; any VCHAR, except delimiters
630
+ let weirdAllowedFieldName = " !#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz "
631
+
632
+ var request = HTTPClientRequest ( url: " https://localhost: \( bin. port) /get " )
633
+ request. headers. add ( name: weirdAllowedFieldName, value: " present " )
634
+
635
+ // This should work fine.
636
+ guard let response = await XCTAssertNoThrowWithResult (
637
+ try await client. execute ( request, deadline: . now( ) + . seconds( 10 ) , logger: logger)
638
+ ) else {
639
+ return
640
+ }
641
+
642
+ XCTAssertEqual ( response. status, . ok)
643
+
644
+ // 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.
645
+ for byte in UInt8 ( 0 ) ... UInt8 ( 127 ) {
646
+ // Skip bytes that we already believe are allowed.
647
+ if weirdAllowedFieldName. utf8. contains ( byte) {
648
+ continue
649
+ }
650
+ let forbiddenFieldName = weirdAllowedFieldName + String( decoding: [ byte] , as: UTF8 . self)
651
+
652
+ var request = HTTPClientRequest ( url: " https://localhost: \( bin. port) /get " )
653
+ request. headers. add ( name: forbiddenFieldName, value: " present " )
654
+
655
+ await XCTAssertThrowsError ( try await client. execute ( request, deadline: . now( ) + . seconds( 10 ) , logger: logger) ) { error in
656
+ XCTAssertEqual ( error as? HTTPClientError , . invalidHeaderFieldNames( [ forbiddenFieldName] ) )
657
+ }
658
+ }
659
+ }
660
+ }
661
+
662
+ func testRejectsInvalidCharactersInHeaderFieldValues_http1( ) {
663
+ self . _rejectsInvalidCharactersInHeaderFieldValues ( mode: . http1_1( ssl: true ) )
664
+ }
665
+
666
+ func testRejectsInvalidCharactersInHeaderFieldValues_http2( ) {
667
+ self . _rejectsInvalidCharactersInHeaderFieldValues ( mode: . http2( compress: false ) )
668
+ }
669
+
670
+ private func _rejectsInvalidCharactersInHeaderFieldValues( mode: HTTPBin < HTTPBinHandler > . Mode ) {
671
+ guard #available( macOS 10 . 15 , iOS 13 . 0 , watchOS 6 . 0 , tvOS 13 . 0 , * ) else { return }
672
+ XCTAsyncTest {
673
+ let bin = HTTPBin ( mode)
674
+ defer { XCTAssertNoThrow ( try bin. shutdown ( ) ) }
675
+ let client = makeDefaultHTTPClient ( )
676
+ defer { XCTAssertNoThrow ( try client. syncShutdown ( ) ) }
677
+ let logger = Logger ( label: " HTTPClient " , factory: StreamLogHandler . standardOutput ( label: ) )
678
+
679
+ // We reject all ASCII control characters except HTAB and tolerate everything else.
680
+ let weirdAllowedFieldValue = " ! \" \t #$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[ \\ ]^_`abcdefghijklmnopqrstuvwxyz{|}~ "
681
+
682
+ var request = HTTPClientRequest ( url: " https://localhost: \( bin. port) /get " )
683
+ request. headers. add ( name: " Weird-Value " , value: weirdAllowedFieldValue)
684
+
685
+ // This should work fine.
686
+ guard let response = await XCTAssertNoThrowWithResult (
687
+ try await client. execute ( request, deadline: . now( ) + . seconds( 10 ) , logger: logger)
688
+ ) else {
689
+ return
690
+ }
691
+
692
+ XCTAssertEqual ( response. status, . ok)
693
+
694
+ // Now, let's confirm all other bytes in the ASCII range ar rejected
695
+ for byte in UInt8 ( 0 ) ... UInt8 ( 127 ) {
696
+ // Skip bytes that we already believe are allowed.
697
+ if weirdAllowedFieldValue. utf8. contains ( byte) {
698
+ continue
699
+ }
700
+ let forbiddenFieldValue = weirdAllowedFieldValue + String( decoding: [ byte] , as: UTF8 . self)
701
+
702
+ var request = HTTPClientRequest ( url: " https://localhost: \( bin. port) /get " )
703
+ request. headers. add ( name: " Weird-Value " , value: forbiddenFieldValue)
704
+
705
+ await XCTAssertThrowsError ( try await client. execute ( request, deadline: . now( ) + . seconds( 10 ) , logger: logger) ) { error in
706
+ XCTAssertEqual ( error as? HTTPClientError , . invalidHeaderFieldValues( [ forbiddenFieldValue] ) )
707
+ }
708
+ }
709
+
710
+ // All the bytes outside the ASCII range are fine though.
711
+ for byte in UInt8 ( 128 ) ... UInt8 ( 255 ) {
712
+ let evenWeirderAllowedValue = weirdAllowedFieldValue + String( decoding: [ byte] , as: UTF8 . self)
713
+
714
+ var request = HTTPClientRequest ( url: " https://localhost: \( bin. port) /get " )
715
+ request. headers. add ( name: " Weird-Value " , value: evenWeirderAllowedValue)
716
+
717
+ // This should work fine.
718
+ guard let response = await XCTAssertNoThrowWithResult (
719
+ try await client. execute ( request, deadline: . now( ) + . seconds( 10 ) , logger: logger)
720
+ ) else {
721
+ return
722
+ }
723
+
724
+ XCTAssertEqual ( response. status, . ok)
725
+ }
726
+ }
727
+ }
600
728
}
601
729
602
730
extension AsyncSequence where Element == ByteBuffer {
0 commit comments