@@ -21,19 +21,20 @@ extension HTTPHeaders {
21
21
if self . contains ( name: " Transfer-Encoding " ) , self . contains ( name: " Content-Length " ) {
22
22
throw HTTPClientError . incompatibleHeaders
23
23
}
24
-
24
+
25
25
var transferEncoding : String ?
26
26
var contentLength : Int ?
27
27
let encodings = self [ canonicalForm: " Transfer-Encoding " ] . map { $0. lowercased ( ) }
28
-
28
+
29
29
guard !encodings. contains ( " identity " ) else {
30
30
throw HTTPClientError . identityCodingIncorrectlyPresent
31
31
}
32
-
32
+
33
33
self . remove ( name: " Transfer-Encoding " )
34
-
34
+
35
35
try self . validateFieldNames ( )
36
-
36
+ try self . validateFieldValues ( )
37
+
37
38
guard let body = body else {
38
39
self . remove ( name: " Content-Length " )
39
40
// if we don't have a body we might not need to send the Content-Length field
@@ -52,17 +53,17 @@ extension HTTPHeaders {
52
53
return
53
54
}
54
55
}
55
-
56
+
56
57
if case . TRACE = method {
57
58
// A client MUST NOT send a message body in a TRACE request.
58
59
// https://tools.ietf.org/html/rfc7230#section-4.3.8
59
60
throw HTTPClientError . traceRequestWithBody
60
61
}
61
-
62
+
62
63
guard ( encodings. filter { $0 == " chunked " } . count <= 1 ) else {
63
64
throw HTTPClientError . chunkedSpecifiedMultipleTimes
64
65
}
65
-
66
+
66
67
if encodings. isEmpty {
67
68
if let length = body. length {
68
69
self . remove ( name: " Content-Length " )
@@ -72,7 +73,7 @@ extension HTTPHeaders {
72
73
}
73
74
} else {
74
75
self . remove ( name: " Content-Length " )
75
-
76
+
76
77
transferEncoding = encodings. joined ( separator: " , " )
77
78
if !encodings. contains ( " chunked " ) {
78
79
guard let length = body. length else {
@@ -81,7 +82,7 @@ extension HTTPHeaders {
81
82
contentLength = length
82
83
}
83
84
}
84
-
85
+
85
86
// add headers if required
86
87
if let enc = transferEncoding {
87
88
self . add ( name: " Transfer-Encoding " , value: enc)
@@ -91,40 +92,90 @@ extension HTTPHeaders {
91
92
self . add ( name: " Content-Length " , value: String ( length) )
92
93
}
93
94
}
94
-
95
+
95
96
func validateFieldNames( ) throws {
96
97
let invalidFieldNames = self . compactMap { ( name, _) -> String ? in
97
98
let satisfy = name. utf8. allSatisfy { ( char) -> Bool in
98
99
switch char {
99
100
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: " ~ " ) :
117
118
return true
118
119
default :
119
120
return false
120
121
}
121
122
}
122
-
123
+
123
124
return satisfy ? nil : name
124
125
}
125
-
126
+
126
127
guard invalidFieldNames. count == 0 else {
127
128
throw HTTPClientError . invalidHeaderFieldNames ( invalidFieldNames)
128
129
}
129
130
}
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
+ }
130
181
}
0 commit comments