@@ -21,19 +21,20 @@ extension HTTPHeaders {
2121 if self . contains ( name: " Transfer-Encoding " ) , self . contains ( name: " Content-Length " ) {
2222 throw HTTPClientError . incompatibleHeaders
2323 }
24-
24+
2525 var transferEncoding : String ?
2626 var contentLength : Int ?
2727 let encodings = self [ canonicalForm: " Transfer-Encoding " ] . map { $0. lowercased ( ) }
28-
28+
2929 guard !encodings. contains ( " identity " ) else {
3030 throw HTTPClientError . identityCodingIncorrectlyPresent
3131 }
32-
32+
3333 self . remove ( name: " Transfer-Encoding " )
34-
34+
3535 try self . validateFieldNames ( )
36-
36+ try self . validateFieldValues ( )
37+
3738 guard let body = body else {
3839 self . remove ( name: " Content-Length " )
3940 // if we don't have a body we might not need to send the Content-Length field
@@ -52,17 +53,17 @@ extension HTTPHeaders {
5253 return
5354 }
5455 }
55-
56+
5657 if case . TRACE = method {
5758 // A client MUST NOT send a message body in a TRACE request.
5859 // https://tools.ietf.org/html/rfc7230#section-4.3.8
5960 throw HTTPClientError . traceRequestWithBody
6061 }
61-
62+
6263 guard ( encodings. filter { $0 == " chunked " } . count <= 1 ) else {
6364 throw HTTPClientError . chunkedSpecifiedMultipleTimes
6465 }
65-
66+
6667 if encodings. isEmpty {
6768 if let length = body. length {
6869 self . remove ( name: " Content-Length " )
@@ -72,7 +73,7 @@ extension HTTPHeaders {
7273 }
7374 } else {
7475 self . remove ( name: " Content-Length " )
75-
76+
7677 transferEncoding = encodings. joined ( separator: " , " )
7778 if !encodings. contains ( " chunked " ) {
7879 guard let length = body. length else {
@@ -81,7 +82,7 @@ extension HTTPHeaders {
8182 contentLength = length
8283 }
8384 }
84-
85+
8586 // add headers if required
8687 if let enc = transferEncoding {
8788 self . add ( name: " Transfer-Encoding " , value: enc)
@@ -91,40 +92,90 @@ extension HTTPHeaders {
9192 self . add ( name: " Content-Length " , value: String ( length) )
9293 }
9394 }
94-
95+
9596 func validateFieldNames( ) throws {
9697 let invalidFieldNames = self . compactMap { ( name, _) -> String ? in
9798 let satisfy = name. utf8. allSatisfy { ( char) -> Bool in
9899 switch char {
99100 case UInt8 ( ascii: " a " ) ... UInt8 ( ascii: " z " ) ,
100- UInt8 ( ascii: " A " ) ... UInt8 ( ascii: " Z " ) ,
101- UInt8 ( ascii: " 0 " ) ... UInt8 ( ascii: " 9 " ) ,
102- UInt8 ( ascii: " ! " ) ,
103- UInt8 ( ascii: " # " ) ,
104- UInt8 ( ascii: " $ " ) ,
105- UInt8 ( ascii: " % " ) ,
106- UInt8 ( ascii: " & " ) ,
107- UInt8 ( ascii: " ' " ) ,
108- UInt8 ( ascii: " * " ) ,
109- UInt8 ( ascii: " + " ) ,
110- UInt8 ( ascii: " - " ) ,
111- UInt8 ( ascii: " . " ) ,
112- UInt8 ( ascii: " ^ " ) ,
113- UInt8 ( ascii: " _ " ) ,
114- UInt8 ( ascii: " ` " ) ,
115- UInt8 ( ascii: " | " ) ,
116- UInt8 ( ascii: " ~ " ) :
101+ UInt8 ( ascii: " A " ) ... UInt8 ( ascii: " Z " ) ,
102+ UInt8 ( ascii: " 0 " ) ... UInt8 ( ascii: " 9 " ) ,
103+ UInt8 ( ascii: " ! " ) ,
104+ UInt8 ( ascii: " # " ) ,
105+ UInt8 ( ascii: " $ " ) ,
106+ UInt8 ( ascii: " % " ) ,
107+ UInt8 ( ascii: " & " ) ,
108+ UInt8 ( ascii: " ' " ) ,
109+ UInt8 ( ascii: " * " ) ,
110+ UInt8 ( ascii: " + " ) ,
111+ UInt8 ( ascii: " - " ) ,
112+ UInt8 ( ascii: " . " ) ,
113+ UInt8 ( ascii: " ^ " ) ,
114+ UInt8 ( ascii: " _ " ) ,
115+ UInt8 ( ascii: " ` " ) ,
116+ UInt8 ( ascii: " | " ) ,
117+ UInt8 ( ascii: " ~ " ) :
117118 return true
118119 default :
119120 return false
120121 }
121122 }
122-
123+
123124 return satisfy ? nil : name
124125 }
125-
126+
126127 guard invalidFieldNames. count == 0 else {
127128 throw HTTPClientError . invalidHeaderFieldNames ( invalidFieldNames)
128129 }
129130 }
131+
132+ private func validateFieldValues( ) throws {
133+ let invalidValues = self . compactMap { _, value -> String ? in
134+ let satisfy = value. utf8. allSatisfy { char -> Bool in
135+ /// Validates a byte of a given header field value against the definition in RFC 9110.
136+ ///
137+ /// The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid
138+ /// characters as the following:
139+ ///
140+ /// ```
141+ /// field-value = *field-content
142+ /// field-content = field-vchar
143+ /// [ 1*( SP / HTAB / field-vchar ) field-vchar ]
144+ /// field-vchar = VCHAR / obs-text
145+ /// obs-text = %x80-FF
146+ /// ```
147+ ///
148+ /// Additionally, it makes the following note:
149+ ///
150+ /// "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the
151+ /// varying ways that implementations might parse and interpret those characters; a recipient
152+ /// of CR, LF, or NUL within a field value MUST either reject the message or replace each of
153+ /// those characters with SP before further processing or forwarding of that message. Field
154+ /// values containing other CTL characters are also invalid; however, recipients MAY retain
155+ /// such characters for the sake of robustness when they appear within a safe context (e.g.,
156+ /// an application-specific quoted string that will not be processed by any downstream HTTP
157+ /// parser)."
158+ ///
159+ /// As we cannot guarantee the context is safe, this code will reject all ASCII control characters
160+ /// directly _except_ for HTAB, which is explicitly allowed.
161+ switch char {
162+ case UInt8 ( ascii: " \t " ) :
163+ // HTAB, explicitly allowed.
164+ return true
165+ case 0 ... 0x1f , 0x7F :
166+ // ASCII control character, forbidden.
167+ return false
168+ default :
169+ // Printable or non-ASCII, allowed.
170+ return true
171+ }
172+ }
173+
174+ return satisfy ? nil : value
175+ }
176+
177+ guard invalidValues. count == 0 else {
178+ throw HTTPClientError . invalidHeaderFieldValues ( invalidValues)
179+ }
180+ }
130181}
0 commit comments