@@ -597,6 +597,134 @@ final class AsyncAwaitEndToEndTests: XCTestCase {
597597 XCTAssertEqual ( body, ByteBuffer ( string: " 1234 " ) )
598598 }
599599 }
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+ }
600728}
601729
602730extension AsyncSequence where Element == ByteBuffer {
0 commit comments