@@ -58,6 +58,8 @@ class Headers implements HeadersInterface, IteratorAggregate, HasHttpRegex
58
58
59
59
private bool $ IsParser ;
60
60
private bool $ HasEmptyLine = false ;
61
+ private bool $ HasBadWhitespace = false ;
62
+ private bool $ HasObsoleteLineFolding = false ;
61
63
62
64
/**
63
65
* Trailing whitespace carried over from the previous line
@@ -101,7 +103,7 @@ public static function from($headersOrPayload): self
101
103
"\r\n" ,
102
104
explode ("\r\n\r\n" , $ headersOrPayload , 2 )[0 ] . "\r\n" ,
103
105
));
104
- // Parse header lines
106
+ // Parse field lines
105
107
$ instance = new static ();
106
108
foreach ($ lines as $ line ) {
107
109
$ instance = $ instance ->addLine ("$ line \r\n" );
@@ -114,7 +116,7 @@ public static function from($headersOrPayload): self
114
116
/**
115
117
* @inheritDoc
116
118
*
117
- * @param bool $strict If `true`, strict \[RFC7230 ] compliance is enforced.
119
+ * @param bool $strict If `true`, strict \[RFC9112 ] compliance is enforced.
118
120
*/
119
121
public function addLine (string $ line , bool $ strict = false )
120
122
{
@@ -138,43 +140,50 @@ private function doAddLine(string $line, bool $strict)
138
140
{
139
141
if ($ strict && substr ($ line , -2 ) !== "\r\n" ) {
140
142
throw new InvalidHeaderException (
141
- 'HTTP header line must end with CRLF ' ,
143
+ 'HTTP field line must end with CRLF ' ,
142
144
);
143
145
}
144
146
145
147
if ($ line === "\r\n" || (!$ strict && trim ($ line ) === '' )) {
146
148
if ($ strict && $ this ->HasEmptyLine ) {
147
149
throw new InvalidHeaderException (
148
- 'HTTP message cannot have empty header line after body ' ,
150
+ 'HTTP message cannot have empty field line after body ' ,
149
151
);
150
152
}
151
153
return $ this ->with ('Carry ' , null )->with ('HasEmptyLine ' , true );
152
154
}
153
155
154
156
if ($ strict ) {
155
157
$ line = substr ($ line , 0 , -2 );
156
- if (
157
- !Regex::match (self ::HTTP_HEADER_FIELD_REGEX , $ line , $ matches , \PREG_UNMATCHED_AS_NULL )
158
- || $ matches ['bad_whitespace ' ] !== null
159
- ) {
158
+ if (!Regex::match (self ::HTTP_FIELD_LINE_REGEX , $ line , $ matches , \PREG_UNMATCHED_AS_NULL )) {
160
159
throw new InvalidHeaderException (
161
- sprintf ('Invalid HTTP header field: %s ' , $ line ),
160
+ sprintf ('Invalid HTTP field line : %s ' , $ line ),
162
161
);
163
162
}
164
163
165
- // As per [RFC7230 ] Section 3.2.4, "replace each received obs-fold
166
- // with one or more SP octets prior to interpreting the field value "
164
+ // As per [RFC9112 ] Section 5.2 ("Obsolete Line Folding"), "replace
165
+ // each received obs-fold with one or more SP octets"
167
166
$ instance = $ this ->with ('Carry ' , (string ) $ matches ['carry ' ]);
168
167
if ($ matches ['extended ' ] !== null ) {
169
168
if ($ this ->Carry === null ) {
170
169
throw new InvalidHeaderException (
171
- sprintf ('Invalid HTTP header line folding: %s ' , $ line ),
170
+ sprintf ('Invalid HTTP field line folding: %s ' , $ line ),
172
171
);
173
172
}
174
173
$ line = $ this ->Carry . ' ' . $ matches ['extended ' ];
175
174
return $ instance ->extendPreviousLine ($ line );
176
175
}
177
176
177
+ // [RFC9112] Section 5.1 ("Field Line Parsing"):
178
+ // - "No whitespace is allowed between the field name and colon"
179
+ // - "A server MUST reject ... any received request message that
180
+ // contains whitespace between a header field name and colon"
181
+ // - "A proxy MUST remove any such whitespace from a response
182
+ // message before forwarding the message downstream"
183
+ if ($ matches ['bad_whitespace ' ] !== null ) {
184
+ $ instance = $ instance ->with ('HasBadWhitespace ' , true );
185
+ }
186
+
178
187
/** @var string */
179
188
$ name = $ matches ['name ' ];
180
189
/** @var string */
@@ -190,7 +199,7 @@ private function doAddLine(string $line, bool $strict)
190
199
if (strpos (" \t" , $ line [0 ]) !== false ) {
191
200
if ($ this ->Carry === null ) {
192
201
throw new InvalidHeaderException (
193
- sprintf ('Invalid HTTP header line folding: %s ' , $ line ),
202
+ sprintf ('Invalid HTTP field line folding: %s ' , $ line ),
194
203
);
195
204
}
196
205
$ line = $ this ->Carry . ' ' . trim ($ line );
@@ -200,16 +209,15 @@ private function doAddLine(string $line, bool $strict)
200
209
$ split = explode (': ' , $ line , 2 );
201
210
if (count ($ split ) !== 2 ) {
202
211
throw new InvalidHeaderException (
203
- sprintf ('Invalid HTTP header field: %s ' , $ line ),
212
+ sprintf ('Invalid HTTP field line : %s ' , $ line ),
204
213
);
205
214
}
206
215
207
- // [RFC7230] Section 3.2.4:
208
- // - "No whitespace is allowed between the header field-name and colon."
209
- // - "A proxy MUST remove any such whitespace from a response message
210
- // before forwarding the message downstream."
211
216
$ name = rtrim ($ split [0 ]);
212
217
$ value = trim ($ split [1 ]);
218
+ if ($ name !== $ split [0 ]) {
219
+ $ instance = $ instance ->with ('HasBadWhitespace ' , true );
220
+ }
213
221
return $ instance ->addValue ($ name , $ value )->maybeIndexLastHeader ();
214
222
}
215
223
@@ -223,7 +231,9 @@ private function extendPreviousLine(string $line)
223
231
$ k = array_key_last ($ headers );
224
232
[, $ value ] = $ this ->Headers [$ k ];
225
233
$ headers [$ k ][1 ] = ltrim ($ value . $ line );
226
- return $ this ->maybeReplaceHeaders ($ headers , $ this ->Index );
234
+ return $ this
235
+ ->with ('HasObsoleteLineFolding ' , true )
236
+ ->maybeReplaceHeaders ($ headers , $ this ->Index );
227
237
}
228
238
229
239
/**
@@ -249,6 +259,22 @@ public function hasEmptyLine(): bool
249
259
return $ this ->HasEmptyLine ;
250
260
}
251
261
262
+ /**
263
+ * @inheritDoc
264
+ */
265
+ public function hasBadWhitespace (): bool
266
+ {
267
+ return $ this ->HasBadWhitespace ;
268
+ }
269
+
270
+ /**
271
+ * @inheritDoc
272
+ */
273
+ public function hasObsoleteLineFolding (): bool
274
+ {
275
+ return $ this ->HasObsoleteLineFolding ;
276
+ }
277
+
252
278
/**
253
279
* @inheritDoc
254
280
*/
@@ -698,7 +724,7 @@ public function getHeaderValues(string $name): array
698
724
$ line = $ this ->getHeaderLine ($ name );
699
725
return $ line === ''
700
726
? []
701
- // [RFC7230 ] Section 7 : "a recipient MUST parse and ignore a
727
+ // [RFC9110 ] Section 5.6.1.2 : "A recipient MUST parse and ignore a
702
728
// reasonable number of empty list elements"
703
729
: Str::splitDelimited (', ' , $ line );
704
730
}
@@ -831,8 +857,8 @@ private function getIndexHeaders(array $index): array
831
857
*/
832
858
private function filterIndex (array $ index ): array
833
859
{
834
- // [RFC7230 ] Section 5.4 : "a user agent SHOULD generate Host as the
835
- // first header field following the request-line "
860
+ // [RFC9110 ] Section 7.2 : "A user agent that sends Host SHOULD send it
861
+ // as the first field in the header section of a request"
836
862
return isset ($ index ['host ' ])
837
863
? ['host ' => $ index ['host ' ]] + $ index
838
864
: $ index ;
@@ -919,7 +945,7 @@ private function filterItems(iterable $items): iterable
919
945
920
946
private function filterName (string $ name ): string
921
947
{
922
- if (!Regex::match (self ::HTTP_HEADER_FIELD_NAME_REGEX , $ name )) {
948
+ if (!Regex::match (self ::HTTP_FIELD_NAME_REGEX , $ name )) {
923
949
throw new InvalidArgumentException (
924
950
sprintf ('Invalid header name: %s ' , $ name ),
925
951
);
@@ -930,7 +956,7 @@ private function filterName(string $name): string
930
956
private function filterValue (string $ value ): string
931
957
{
932
958
$ value = Regex::replace ('/\r\n\h++/ ' , ' ' , trim ($ value , " \t" ));
933
- if (!Regex::match (self ::HTTP_HEADER_FIELD_VALUE_REGEX , $ value )) {
959
+ if (!Regex::match (self ::HTTP_FIELD_VALUE_REGEX , $ value )) {
934
960
throw new InvalidArgumentException (
935
961
sprintf ('Invalid header value: %s ' , $ value ),
936
962
);
0 commit comments