@@ -124,32 +124,34 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
124124 {
125125 // additional, stricter safe-guard for request line
126126 // because request parser doesn't properly cope with invalid ones
127- if (!\preg_match ('#^[^ ]+ [^ ]+ HTTP/\d\.\d#m ' , $ headers )) {
127+ $ start = array ();
128+ if (!\preg_match ('#^(?<method>[^ ]+) (?<target>[^ ]+) HTTP/(?<version>\d\.\d)#m ' , $ headers , $ start )) {
128129 throw new \InvalidArgumentException ('Unable to parse invalid request-line ' );
129130 }
130131
131- // parser does not support asterisk-form and authority-form
132- // remember original target and temporarily replace and re-apply below
133- $ originalTarget = null ;
134- if (\strncmp ($ headers , 'OPTIONS * ' , 10 ) === 0 ) {
135- $ originalTarget = '* ' ;
136- $ headers = 'OPTIONS / ' . \substr ($ headers , 10 );
137- } elseif (\strncmp ($ headers , 'CONNECT ' , 8 ) === 0 ) {
138- $ parts = \explode (' ' , $ headers , 3 );
139- $ uri = \parse_url ('tcp:// ' . $ parts [1 ]);
132+ // only support HTTP/1.1 and HTTP/1.0 requests
133+ if ($ start ['version ' ] !== '1.1 ' && $ start ['version ' ] !== '1.0 ' ) {
134+ throw new \InvalidArgumentException ('Received request with invalid protocol version ' , 505 );
135+ }
140136
141- // check this is a valid authority-form request-target (host:port)
142- if (isset ($ uri ['scheme ' ], $ uri ['host ' ], $ uri ['port ' ]) && count ($ uri ) === 3 ) {
143- $ originalTarget = $ parts [1 ];
144- $ parts [1 ] = 'http:// ' . $ parts [1 ] . '/ ' ;
145- $ headers = implode (' ' , $ parts );
146- } else {
147- throw new \InvalidArgumentException ('CONNECT method MUST use authority-form request target ' );
148- }
137+ $ matches = array ();
138+ $ n = \preg_match_all ('/^([^()<>@,;: \\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+/m ' , $ headers , $ matches , \PREG_SET_ORDER );
139+
140+ if (\substr_count ($ headers , "\n" ) !== $ n ) {
141+ throw new \InvalidArgumentException ('Unable to parse invalid request header fields ' );
149142 }
150143
151- // parse request headers into obj implementing RequestInterface
152- $ request = g7 \parse_request ($ headers );
144+ // format all header fields into associative array
145+ $ host = null ;
146+ $ fields = array ();
147+ foreach ($ matches as $ match ) {
148+ $ fields [$ match [1 ]][] = $ match [2 ];
149+
150+ // match `Host` request header
151+ if ($ host === null && \strtolower ($ match [1 ]) === 'host ' ) {
152+ $ host = $ match [2 ];
153+ }
154+ }
153155
154156 // create new obj implementing ServerRequestInterface by preserving all
155157 // previous properties and restoring original request-target
@@ -158,6 +160,48 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
158160 'REQUEST_TIME_FLOAT ' => \microtime (true )
159161 );
160162
163+ // scheme is `http` unless TLS is used
164+ $ localParts = \parse_url ($ localSocketUri );
165+ if (isset ($ localParts ['scheme ' ]) && $ localParts ['scheme ' ] === 'tls ' ) {
166+ $ scheme = 'https:// ' ;
167+ $ serverParams ['HTTPS ' ] = 'on ' ;
168+ } else {
169+ $ scheme = 'http:// ' ;
170+ }
171+
172+ // default host if unset comes from local socket address or defaults to localhost
173+ if ($ host === null ) {
174+ $ host = isset ($ localParts ['host ' ], $ localParts ['port ' ]) ? $ localParts ['host ' ] . ': ' . $ localParts ['port ' ] : '127.0.0.1 ' ;
175+ }
176+
177+ if ($ start ['method ' ] === 'OPTIONS ' && $ start ['target ' ] === '* ' ) {
178+ // support asterisk-form for `OPTIONS *` request line only
179+ $ uri = $ scheme . $ host ;
180+ } elseif ($ start ['method ' ] === 'CONNECT ' ) {
181+ $ parts = \parse_url ('tcp:// ' . $ start ['target ' ]);
182+
183+ // check this is a valid authority-form request-target (host:port)
184+ if (!isset ($ parts ['scheme ' ], $ parts ['host ' ], $ parts ['port ' ]) || \count ($ parts ) !== 3 ) {
185+ throw new \InvalidArgumentException ('CONNECT method MUST use authority-form request target ' );
186+ }
187+ $ uri = $ scheme . $ start ['target ' ];
188+ } else {
189+ // support absolute-form or origin-form for proxy requests
190+ if ($ start ['target ' ][0 ] === '/ ' ) {
191+ $ uri = $ scheme . $ host . $ start ['target ' ];
192+ } else {
193+ // ensure absolute-form request-target contains a valid URI
194+ $ parts = \parse_url ($ start ['target ' ]);
195+
196+ // make sure value contains valid host component (IP or hostname), but no fragment
197+ if (!isset ($ parts ['scheme ' ], $ parts ['host ' ]) || $ parts ['scheme ' ] !== 'http ' || isset ($ parts ['fragment ' ])) {
198+ throw new \InvalidArgumentException ('Invalid absolute-form request-target ' );
199+ }
200+
201+ $ uri = $ start ['target ' ];
202+ }
203+ }
204+
161205 // apply REMOTE_ADDR and REMOTE_PORT if source address is known
162206 // address should always be known, unless this is over Unix domain sockets (UDS)
163207 if ($ remoteSocketUri !== null ) {
@@ -169,51 +213,23 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
169213 // apply SERVER_ADDR and SERVER_PORT if server address is known
170214 // address should always be known, even for Unix domain sockets (UDS)
171215 // but skip UDS as it doesn't have a concept of host/port.
172- if ($ localSocketUri !== null ) {
173- $ localAddress = \parse_url ($ localSocketUri );
174- if (isset ($ localAddress ['host ' ], $ localAddress ['port ' ])) {
175- $ serverParams ['SERVER_ADDR ' ] = $ localAddress ['host ' ];
176- $ serverParams ['SERVER_PORT ' ] = $ localAddress ['port ' ];
177- }
178- if (isset ($ localAddress ['scheme ' ]) && $ localAddress ['scheme ' ] === 'tls ' ) {
179- $ serverParams ['HTTPS ' ] = 'on ' ;
180- }
216+ if ($ localSocketUri !== null && isset ($ localParts ['host ' ], $ localParts ['port ' ])) {
217+ $ serverParams ['SERVER_ADDR ' ] = $ localParts ['host ' ];
218+ $ serverParams ['SERVER_PORT ' ] = $ localParts ['port ' ];
181219 }
182220
183- $ target = $ request ->getRequestTarget ();
184221 $ request = new ServerRequest (
185- $ request -> getMethod () ,
186- $ request -> getUri () ,
187- $ request -> getHeaders () ,
188- $ request -> getBody () ,
189- $ request -> getProtocolVersion () ,
222+ $ start [ ' method ' ] ,
223+ $ uri ,
224+ $ fields ,
225+ null ,
226+ $ start [ ' version ' ] ,
190227 $ serverParams
191228 );
192- $ request = $ request ->withRequestTarget ($ target );
193-
194- // re-apply actual request target from above
195- if ($ originalTarget !== null ) {
196- $ request = $ request ->withUri (
197- $ request ->getUri ()->withPath ('' ),
198- true
199- )->withRequestTarget ($ originalTarget );
200- }
201-
202- // only support HTTP/1.1 and HTTP/1.0 requests
203- $ protocolVersion = $ request ->getProtocolVersion ();
204- if ($ protocolVersion !== '1.1 ' && $ protocolVersion !== '1.0 ' ) {
205- throw new \InvalidArgumentException ('Received request with invalid protocol version ' , 505 );
206- }
207-
208- // ensure absolute-form request-target contains a valid URI
209- $ requestTarget = $ request ->getRequestTarget ();
210- if (\strpos ($ requestTarget , ':// ' ) !== false && \substr ($ requestTarget , 0 , 1 ) !== '/ ' ) {
211- $ parts = \parse_url ($ requestTarget );
212229
213- // make sure value contains valid host component (IP or hostname), but no fragment
214- if (!isset ($ parts ['scheme ' ], $ parts ['host ' ]) || $ parts ['scheme ' ] !== 'http ' || isset ($ parts ['fragment ' ])) {
215- throw new \InvalidArgumentException ('Invalid absolute-form request-target ' );
216- }
230+ // only assign request target if it is not in origin-form (happy path for most normal requests)
231+ if ($ start ['target ' ][0 ] !== '/ ' ) {
232+ $ request = $ request ->withRequestTarget ($ start ['target ' ]);
217233 }
218234
219235 // Optional Host header value MUST be valid (host and optional port)
@@ -252,44 +268,6 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
252268 }
253269 }
254270
255- // set URI components from socket address if not already filled via Host header
256- if ($ request ->getUri ()->getHost () === '' ) {
257- $ parts = \parse_url ($ localSocketUri );
258- if (!isset ($ parts ['host ' ], $ parts ['port ' ])) {
259- $ parts = array ('host ' => '127.0.0.1 ' , 'port ' => 80 );
260- }
261-
262- $ request = $ request ->withUri (
263- $ request ->getUri ()->withScheme ('http ' )->withHost ($ parts ['host ' ])->withPort ($ parts ['port ' ]),
264- true
265- );
266- }
267-
268- // Do not assume this is HTTPS when this happens to be port 443
269- // detecting HTTPS is left up to the socket layer (TLS detection)
270- if ($ request ->getUri ()->getScheme () === 'https ' ) {
271- $ request = $ request ->withUri (
272- $ request ->getUri ()->withScheme ('http ' )->withPort (443 ),
273- true
274- );
275- }
276-
277- // Update request URI to "https" scheme if the connection is encrypted
278- $ parts = \parse_url ($ localSocketUri );
279- if (isset ($ parts ['scheme ' ]) && $ parts ['scheme ' ] === 'tls ' ) {
280- // The request URI may omit default ports here, so try to parse port
281- // from Host header field (if possible)
282- $ port = $ request ->getUri ()->getPort ();
283- if ($ port === null ) {
284- $ port = \parse_url ('tcp:// ' . $ request ->getHeaderLine ('Host ' ), PHP_URL_PORT ); // @codeCoverageIgnore
285- }
286-
287- $ request = $ request ->withUri (
288- $ request ->getUri ()->withScheme ('https ' )->withPort ($ port ),
289- true
290- );
291- }
292-
293271 // always sanitize Host header because it contains critical routing information
294272 $ request = $ request ->withUri ($ request ->getUri ()->withUserInfo ('u ' )->withUserInfo ('' ));
295273
0 commit comments