]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/HTTP/Request2/Adapter/Socket.php
fb2940a2ee352a8c46125f1b3eb086f66e7d80c6
[quix0rs-gnu-social.git] / extlib / HTTP / Request2 / Adapter / Socket.php
1 <?php\r
2 /**\r
3  * Socket-based adapter for HTTP_Request2\r
4  *\r
5  * PHP version 5\r
6  *\r
7  * LICENSE:\r
8  *\r
9  * Copyright (c) 2008-2012, Alexey Borzov <avb@php.net>\r
10  * All rights reserved.\r
11  *\r
12  * Redistribution and use in source and binary forms, with or without\r
13  * modification, are permitted provided that the following conditions\r
14  * are met:\r
15  *\r
16  *    * Redistributions of source code must retain the above copyright\r
17  *      notice, this list of conditions and the following disclaimer.\r
18  *    * Redistributions in binary form must reproduce the above copyright\r
19  *      notice, this list of conditions and the following disclaimer in the\r
20  *      documentation and/or other materials provided with the distribution.\r
21  *    * The names of the authors may not be used to endorse or promote products\r
22  *      derived from this software without specific prior written permission.\r
23  *\r
24  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS\r
25  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\r
26  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
27  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\r
28  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
29  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
30  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
31  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\r
32  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\r
33  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
34  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
35  *\r
36  * @category HTTP\r
37  * @package  HTTP_Request2\r
38  * @author   Alexey Borzov <avb@php.net>\r
39  * @license  http://opensource.org/licenses/bsd-license.php New BSD License\r
40  * @version  SVN: $Id: Socket.php 324953 2012-04-08 07:24:12Z avb $\r
41  * @link     http://pear.php.net/package/HTTP_Request2\r
42  */\r
43 \r
44 /** Base class for HTTP_Request2 adapters */\r
45 require_once 'HTTP/Request2/Adapter.php';\r
46 \r
47 /** Socket wrapper class */\r
48 require_once 'HTTP/Request2/SocketWrapper.php';\r
49 \r
50 /**\r
51  * Socket-based adapter for HTTP_Request2\r
52  *\r
53  * This adapter uses only PHP sockets and will work on almost any PHP\r
54  * environment. Code is based on original HTTP_Request PEAR package.\r
55  *\r
56  * @category HTTP\r
57  * @package  HTTP_Request2\r
58  * @author   Alexey Borzov <avb@php.net>\r
59  * @license  http://opensource.org/licenses/bsd-license.php New BSD License\r
60  * @version  Release: 2.1.1\r
61  * @link     http://pear.php.net/package/HTTP_Request2\r
62  */\r
63 class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter\r
64 {\r
65     /**\r
66      * Regular expression for 'token' rule from RFC 2616\r
67      */\r
68     const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';\r
69 \r
70     /**\r
71      * Regular expression for 'quoted-string' rule from RFC 2616\r
72      */\r
73     const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"';\r
74 \r
75     /**\r
76      * Connected sockets, needed for Keep-Alive support\r
77      * @var  array\r
78      * @see  connect()\r
79      */\r
80     protected static $sockets = array();\r
81 \r
82     /**\r
83      * Data for digest authentication scheme\r
84      *\r
85      * The keys for the array are URL prefixes.\r
86      *\r
87      * The values are associative arrays with data (realm, nonce, nonce-count,\r
88      * opaque...) needed for digest authentication. Stored here to prevent making\r
89      * duplicate requests to digest-protected resources after we have already\r
90      * received the challenge.\r
91      *\r
92      * @var  array\r
93      */\r
94     protected static $challenges = array();\r
95 \r
96     /**\r
97      * Connected socket\r
98      * @var  HTTP_Request2_SocketWrapper\r
99      * @see  connect()\r
100      */\r
101     protected $socket;\r
102 \r
103     /**\r
104      * Challenge used for server digest authentication\r
105      * @var  array\r
106      */\r
107     protected $serverChallenge;\r
108 \r
109     /**\r
110      * Challenge used for proxy digest authentication\r
111      * @var  array\r
112      */\r
113     protected $proxyChallenge;\r
114 \r
115     /**\r
116      * Remaining length of the current chunk, when reading chunked response\r
117      * @var  integer\r
118      * @see  readChunked()\r
119      */\r
120     protected $chunkLength = 0;\r
121 \r
122     /**\r
123      * Remaining amount of redirections to follow\r
124      *\r
125      * Starts at 'max_redirects' configuration parameter and is reduced on each\r
126      * subsequent redirect. An Exception will be thrown once it reaches zero.\r
127      *\r
128      * @var  integer\r
129      */\r
130     protected $redirectCountdown = null;\r
131 \r
132     /**\r
133      * Sends request to the remote server and returns its response\r
134      *\r
135      * @param HTTP_Request2 $request HTTP request message\r
136      *\r
137      * @return   HTTP_Request2_Response\r
138      * @throws   HTTP_Request2_Exception\r
139      */\r
140     public function sendRequest(HTTP_Request2 $request)\r
141     {\r
142         $this->request = $request;\r
143 \r
144         try {\r
145             $keepAlive = $this->connect();\r
146             $headers   = $this->prepareHeaders();\r
147             $this->socket->write($headers);\r
148             // provide request headers to the observer, see request #7633\r
149             $this->request->setLastEvent('sentHeaders', $headers);\r
150             $this->writeBody();\r
151 \r
152             $response = $this->readResponse();\r
153 \r
154             if ($jar = $request->getCookieJar()) {\r
155                 $jar->addCookiesFromResponse($response, $request->getUrl());\r
156             }\r
157 \r
158             if (!$this->canKeepAlive($keepAlive, $response)) {\r
159                 $this->disconnect();\r
160             }\r
161 \r
162             if ($this->shouldUseProxyDigestAuth($response)) {\r
163                 return $this->sendRequest($request);\r
164             }\r
165             if ($this->shouldUseServerDigestAuth($response)) {\r
166                 return $this->sendRequest($request);\r
167             }\r
168             if ($authInfo = $response->getHeader('authentication-info')) {\r
169                 $this->updateChallenge($this->serverChallenge, $authInfo);\r
170             }\r
171             if ($proxyInfo = $response->getHeader('proxy-authentication-info')) {\r
172                 $this->updateChallenge($this->proxyChallenge, $proxyInfo);\r
173             }\r
174 \r
175         } catch (Exception $e) {\r
176             $this->disconnect();\r
177         }\r
178 \r
179         unset($this->request, $this->requestBody);\r
180 \r
181         if (!empty($e)) {\r
182             $this->redirectCountdown = null;\r
183             throw $e;\r
184         }\r
185 \r
186         if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {\r
187             $this->redirectCountdown = null;\r
188             return $response;\r
189         } else {\r
190             return $this->handleRedirect($request, $response);\r
191         }\r
192     }\r
193 \r
194     /**\r
195      * Connects to the remote server\r
196      *\r
197      * @return   bool    whether the connection can be persistent\r
198      * @throws   HTTP_Request2_Exception\r
199      */\r
200     protected function connect()\r
201     {\r
202         $secure  = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https');\r
203         $tunnel  = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();\r
204         $headers = $this->request->getHeaders();\r
205         $reqHost = $this->request->getUrl()->getHost();\r
206         if (!($reqPort = $this->request->getUrl()->getPort())) {\r
207             $reqPort = $secure? 443: 80;\r
208         }\r
209 \r
210         $httpProxy = $socksProxy = false;\r
211         if (!($host = $this->request->getConfig('proxy_host'))) {\r
212             $host = $reqHost;\r
213             $port = $reqPort;\r
214         } else {\r
215             if (!($port = $this->request->getConfig('proxy_port'))) {\r
216                 throw new HTTP_Request2_LogicException(\r
217                     'Proxy port not provided',\r
218                     HTTP_Request2_Exception::MISSING_VALUE\r
219                 );\r
220             }\r
221             if ('http' == ($type = $this->request->getConfig('proxy_type'))) {\r
222                 $httpProxy = true;\r
223             } elseif ('socks5' == $type) {\r
224                 $socksProxy = true;\r
225             } else {\r
226                 throw new HTTP_Request2_NotImplementedException(\r
227                     "Proxy type '{$type}' is not supported"\r
228                 );\r
229             }\r
230         }\r
231 \r
232         if ($tunnel && !$httpProxy) {\r
233             throw new HTTP_Request2_LogicException(\r
234                 "Trying to perform CONNECT request without proxy",\r
235                 HTTP_Request2_Exception::MISSING_VALUE\r
236             );\r
237         }\r
238         if ($secure && !in_array('ssl', stream_get_transports())) {\r
239             throw new HTTP_Request2_LogicException(\r
240                 'Need OpenSSL support for https:// requests',\r
241                 HTTP_Request2_Exception::MISCONFIGURATION\r
242             );\r
243         }\r
244 \r
245         // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive\r
246         // connection token to a proxy server...\r
247         if ($httpProxy && !$secure && !empty($headers['connection'])\r
248             && 'Keep-Alive' == $headers['connection']\r
249         ) {\r
250             $this->request->setHeader('connection');\r
251         }\r
252 \r
253         $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&\r
254                       empty($headers['connection'])) ||\r
255                      (!empty($headers['connection']) &&\r
256                       'Keep-Alive' == $headers['connection']);\r
257 \r
258         $options = array();\r
259         if ($secure || $tunnel) {\r
260             foreach ($this->request->getConfig() as $name => $value) {\r
261                 if ('ssl_' == substr($name, 0, 4) && null !== $value) {\r
262                     if ('ssl_verify_host' == $name) {\r
263                         if ($value) {\r
264                             $options['CN_match'] = $reqHost;\r
265                         }\r
266                     } else {\r
267                         $options[substr($name, 4)] = $value;\r
268                     }\r
269                 }\r
270             }\r
271             ksort($options);\r
272         }\r
273 \r
274         // Use global request timeout if given, see feature requests #5735, #8964\r
275         if ($timeout = $this->request->getConfig('timeout')) {\r
276             $deadline = time() + $timeout;\r
277         } else {\r
278             $deadline = null;\r
279         }\r
280 \r
281         // Changing SSL context options after connection is established does *not*\r
282         // work, we need a new connection if options change\r
283         $remote    = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://')\r
284                      . $host . ':' . $port;\r
285         $socketKey = $remote . (\r
286                         ($secure && $httpProxy || $socksProxy)\r
287                         ? "->{$reqHost}:{$reqPort}" : ''\r
288                      ) . (empty($options)? '': ':' . serialize($options));\r
289         unset($this->socket);\r
290 \r
291         // We use persistent connections and have a connected socket?\r
292         // Ensure that the socket is still connected, see bug #16149\r
293         if ($keepAlive && !empty(self::$sockets[$socketKey])\r
294             && !self::$sockets[$socketKey]->eof()\r
295         ) {\r
296             $this->socket =& self::$sockets[$socketKey];\r
297 \r
298         } else {\r
299             if ($socksProxy) {\r
300                 require_once 'HTTP/Request2/SOCKS5.php';\r
301 \r
302                 $this->socket = new HTTP_Request2_SOCKS5(\r
303                     $remote, $this->request->getConfig('connect_timeout'),\r
304                     $options, $this->request->getConfig('proxy_user'),\r
305                     $this->request->getConfig('proxy_password')\r
306                 );\r
307                 // handle request timeouts ASAP\r
308                 $this->socket->setDeadline($deadline, $this->request->getConfig('timeout'));\r
309                 $this->socket->connect($reqHost, $reqPort);\r
310                 if (!$secure) {\r
311                     $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}";\r
312                 } else {\r
313                     $this->socket->enableCrypto();\r
314                     $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";\r
315                 }\r
316 \r
317             } elseif ($secure && $httpProxy && !$tunnel) {\r
318                 $this->establishTunnel();\r
319                 $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}";\r
320 \r
321             } else {\r
322                 $this->socket = new HTTP_Request2_SocketWrapper(\r
323                     $remote, $this->request->getConfig('connect_timeout'), $options\r
324                 );\r
325             }\r
326             $this->request->setLastEvent('connect', empty($conninfo)? $remote: $conninfo);\r
327             self::$sockets[$socketKey] =& $this->socket;\r
328         }\r
329         $this->socket->setDeadline($deadline, $this->request->getConfig('timeout'));\r
330         return $keepAlive;\r
331     }\r
332 \r
333     /**\r
334      * Establishes a tunnel to a secure remote server via HTTP CONNECT request\r
335      *\r
336      * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP\r
337      * sees that we are connected to a proxy server (duh!) rather than the server\r
338      * that presents its certificate.\r
339      *\r
340      * @link     http://tools.ietf.org/html/rfc2817#section-5.2\r
341      * @throws   HTTP_Request2_Exception\r
342      */\r
343     protected function establishTunnel()\r
344     {\r
345         $donor   = new self;\r
346         $connect = new HTTP_Request2(\r
347             $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT,\r
348             array_merge($this->request->getConfig(), array('adapter' => $donor))\r
349         );\r
350         $response = $connect->send();\r
351         // Need any successful (2XX) response\r
352         if (200 > $response->getStatus() || 300 <= $response->getStatus()) {\r
353             throw new HTTP_Request2_ConnectionException(\r
354                 'Failed to connect via HTTPS proxy. Proxy response: ' .\r
355                 $response->getStatus() . ' ' . $response->getReasonPhrase()\r
356             );\r
357         }\r
358         $this->socket = $donor->socket;\r
359         $this->socket->enableCrypto();\r
360     }\r
361 \r
362     /**\r
363      * Checks whether current connection may be reused or should be closed\r
364      *\r
365      * @param boolean                $requestKeepAlive whether connection could\r
366      *                               be persistent in the first place\r
367      * @param HTTP_Request2_Response $response         response object to check\r
368      *\r
369      * @return   boolean\r
370      */\r
371     protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response)\r
372     {\r
373         // Do not close socket on successful CONNECT request\r
374         if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod()\r
375             && 200 <= $response->getStatus() && 300 > $response->getStatus()\r
376         ) {\r
377             return true;\r
378         }\r
379 \r
380         $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))\r
381                        || null !== $response->getHeader('content-length')\r
382                        // no body possible for such responses, see also request #17031\r
383                        || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()\r
384                        || in_array($response->getStatus(), array(204, 304));\r
385         $persistent  = 'keep-alive' == strtolower($response->getHeader('connection')) ||\r
386                        (null === $response->getHeader('connection') &&\r
387                         '1.1' == $response->getVersion());\r
388         return $requestKeepAlive && $lengthKnown && $persistent;\r
389     }\r
390 \r
391     /**\r
392      * Disconnects from the remote server\r
393      */\r
394     protected function disconnect()\r
395     {\r
396         if (!empty($this->socket)) {\r
397             $this->socket = null;\r
398             $this->request->setLastEvent('disconnect');\r
399         }\r
400     }\r
401 \r
402     /**\r
403      * Handles HTTP redirection\r
404      *\r
405      * This method will throw an Exception if redirect to a non-HTTP(S) location\r
406      * is attempted, also if number of redirects performed already is equal to\r
407      * 'max_redirects' configuration parameter.\r
408      *\r
409      * @param HTTP_Request2          $request  Original request\r
410      * @param HTTP_Request2_Response $response Response containing redirect\r
411      *\r
412      * @return   HTTP_Request2_Response      Response from a new location\r
413      * @throws   HTTP_Request2_Exception\r
414      */\r
415     protected function handleRedirect(\r
416         HTTP_Request2 $request, HTTP_Request2_Response $response\r
417     ) {\r
418         if (is_null($this->redirectCountdown)) {\r
419             $this->redirectCountdown = $request->getConfig('max_redirects');\r
420         }\r
421         if (0 == $this->redirectCountdown) {\r
422             $this->redirectCountdown = null;\r
423             // Copying cURL behaviour\r
424             throw new HTTP_Request2_MessageException(\r
425                 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',\r
426                 HTTP_Request2_Exception::TOO_MANY_REDIRECTS\r
427             );\r
428         }\r
429         $redirectUrl = new Net_URL2(\r
430             $response->getHeader('location'),\r
431             array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))\r
432         );\r
433         // refuse non-HTTP redirect\r
434         if ($redirectUrl->isAbsolute()\r
435             && !in_array($redirectUrl->getScheme(), array('http', 'https'))\r
436         ) {\r
437             $this->redirectCountdown = null;\r
438             throw new HTTP_Request2_MessageException(\r
439                 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),\r
440                 HTTP_Request2_Exception::NON_HTTP_REDIRECT\r
441             );\r
442         }\r
443         // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),\r
444         // but in practice it is often not\r
445         if (!$redirectUrl->isAbsolute()) {\r
446             $redirectUrl = $request->getUrl()->resolve($redirectUrl);\r
447         }\r
448         $redirect = clone $request;\r
449         $redirect->setUrl($redirectUrl);\r
450         if (303 == $response->getStatus()\r
451             || (!$request->getConfig('strict_redirects')\r
452                 && in_array($response->getStatus(), array(301, 302)))\r
453         ) {\r
454             $redirect->setMethod(HTTP_Request2::METHOD_GET);\r
455             $redirect->setBody('');\r
456         }\r
457 \r
458         if (0 < $this->redirectCountdown) {\r
459             $this->redirectCountdown--;\r
460         }\r
461         return $this->sendRequest($redirect);\r
462     }\r
463 \r
464     /**\r
465      * Checks whether another request should be performed with server digest auth\r
466      *\r
467      * Several conditions should be satisfied for it to return true:\r
468      *   - response status should be 401\r
469      *   - auth credentials should be set in the request object\r
470      *   - response should contain WWW-Authenticate header with digest challenge\r
471      *   - there is either no challenge stored for this URL or new challenge\r
472      *     contains stale=true parameter (in other case we probably just failed\r
473      *     due to invalid username / password)\r
474      *\r
475      * The method stores challenge values in $challenges static property\r
476      *\r
477      * @param HTTP_Request2_Response $response response to check\r
478      *\r
479      * @return   boolean whether another request should be performed\r
480      * @throws   HTTP_Request2_Exception in case of unsupported challenge parameters\r
481      */\r
482     protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response)\r
483     {\r
484         // no sense repeating a request if we don't have credentials\r
485         if (401 != $response->getStatus() || !$this->request->getAuth()) {\r
486             return false;\r
487         }\r
488         if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) {\r
489             return false;\r
490         }\r
491 \r
492         $url    = $this->request->getUrl();\r
493         $scheme = $url->getScheme();\r
494         $host   = $scheme . '://' . $url->getHost();\r
495         if ($port = $url->getPort()) {\r
496             if ((0 == strcasecmp($scheme, 'http') && 80 != $port)\r
497                 || (0 == strcasecmp($scheme, 'https') && 443 != $port)\r
498             ) {\r
499                 $host .= ':' . $port;\r
500             }\r
501         }\r
502 \r
503         if (!empty($challenge['domain'])) {\r
504             $prefixes = array();\r
505             foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) {\r
506                 // don't bother with different servers\r
507                 if ('/' == substr($prefix, 0, 1)) {\r
508                     $prefixes[] = $host . $prefix;\r
509                 }\r
510             }\r
511         }\r
512         if (empty($prefixes)) {\r
513             $prefixes = array($host . '/');\r
514         }\r
515 \r
516         $ret = true;\r
517         foreach ($prefixes as $prefix) {\r
518             if (!empty(self::$challenges[$prefix])\r
519                 && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))\r
520             ) {\r
521                 // probably credentials are invalid\r
522                 $ret = false;\r
523             }\r
524             self::$challenges[$prefix] =& $challenge;\r
525         }\r
526         return $ret;\r
527     }\r
528 \r
529     /**\r
530      * Checks whether another request should be performed with proxy digest auth\r
531      *\r
532      * Several conditions should be satisfied for it to return true:\r
533      *   - response status should be 407\r
534      *   - proxy auth credentials should be set in the request object\r
535      *   - response should contain Proxy-Authenticate header with digest challenge\r
536      *   - there is either no challenge stored for this proxy or new challenge\r
537      *     contains stale=true parameter (in other case we probably just failed\r
538      *     due to invalid username / password)\r
539      *\r
540      * The method stores challenge values in $challenges static property\r
541      *\r
542      * @param HTTP_Request2_Response $response response to check\r
543      *\r
544      * @return   boolean whether another request should be performed\r
545      * @throws   HTTP_Request2_Exception in case of unsupported challenge parameters\r
546      */\r
547     protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response)\r
548     {\r
549         if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) {\r
550             return false;\r
551         }\r
552         if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) {\r
553             return false;\r
554         }\r
555 \r
556         $key = 'proxy://' . $this->request->getConfig('proxy_host') .\r
557                ':' . $this->request->getConfig('proxy_port');\r
558 \r
559         if (!empty(self::$challenges[$key])\r
560             && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale']))\r
561         ) {\r
562             $ret = false;\r
563         } else {\r
564             $ret = true;\r
565         }\r
566         self::$challenges[$key] = $challenge;\r
567         return $ret;\r
568     }\r
569 \r
570     /**\r
571      * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value\r
572      *\r
573      * There is a problem with implementation of RFC 2617: several of the parameters\r
574      * are defined as quoted-string there and thus may contain backslash escaped\r
575      * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as\r
576      * just value of quoted-string X without surrounding quotes, it doesn't speak\r
577      * about removing backslash escaping.\r
578      *\r
579      * Now realm parameter is user-defined and human-readable, strange things\r
580      * happen when it contains quotes:\r
581      *   - Apache allows quotes in realm, but apparently uses realm value without\r
582      *     backslashes for digest computation\r
583      *   - Squid allows (manually escaped) quotes there, but it is impossible to\r
584      *     authorize with either escaped or unescaped quotes used in digest,\r
585      *     probably it can't parse the response (?)\r
586      *   - Both IE and Firefox display realm value with backslashes in\r
587      *     the password popup and apparently use the same value for digest\r
588      *\r
589      * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in\r
590      * quoted-string handling, unfortunately that means failure to authorize\r
591      * sometimes\r
592      *\r
593      * @param string $headerValue value of WWW-Authenticate or Proxy-Authenticate header\r
594      *\r
595      * @return   mixed   associative array with challenge parameters, false if\r
596      *                   no challenge is present in header value\r
597      * @throws   HTTP_Request2_NotImplementedException in case of unsupported challenge parameters\r
598      */\r
599     protected function parseDigestChallenge($headerValue)\r
600     {\r
601         $authParam   = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .\r
602                        self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')';\r
603         $challenge   = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!";\r
604         if (!preg_match($challenge, $headerValue, $matches)) {\r
605             return false;\r
606         }\r
607 \r
608         preg_match_all('!' . $authParam . '!', $matches[0], $params);\r
609         $paramsAry   = array();\r
610         $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale',\r
611                              'algorithm', 'qop');\r
612         for ($i = 0; $i < count($params[0]); $i++) {\r
613             // section 3.2.1: Any unrecognized directive MUST be ignored.\r
614             if (in_array($params[1][$i], $knownParams)) {\r
615                 if ('"' == substr($params[2][$i], 0, 1)) {\r
616                     $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);\r
617                 } else {\r
618                     $paramsAry[$params[1][$i]] = $params[2][$i];\r
619                 }\r
620             }\r
621         }\r
622         // we only support qop=auth\r
623         if (!empty($paramsAry['qop'])\r
624             && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))\r
625         ) {\r
626             throw new HTTP_Request2_NotImplementedException(\r
627                 "Only 'auth' qop is currently supported in digest authentication, " .\r
628                 "server requested '{$paramsAry['qop']}'"\r
629             );\r
630         }\r
631         // we only support algorithm=MD5\r
632         if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {\r
633             throw new HTTP_Request2_NotImplementedException(\r
634                 "Only 'MD5' algorithm is currently supported in digest authentication, " .\r
635                 "server requested '{$paramsAry['algorithm']}'"\r
636             );\r
637         }\r
638 \r
639         return $paramsAry;\r
640     }\r
641 \r
642     /**\r
643      * Parses [Proxy-]Authentication-Info header value and updates challenge\r
644      *\r
645      * @param array  &$challenge  challenge to update\r
646      * @param string $headerValue value of [Proxy-]Authentication-Info header\r
647      *\r
648      * @todo     validate server rspauth response\r
649      */\r
650     protected function updateChallenge(&$challenge, $headerValue)\r
651     {\r
652         $authParam   = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .\r
653                        self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!';\r
654         $paramsAry   = array();\r
655 \r
656         preg_match_all($authParam, $headerValue, $params);\r
657         for ($i = 0; $i < count($params[0]); $i++) {\r
658             if ('"' == substr($params[2][$i], 0, 1)) {\r
659                 $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1);\r
660             } else {\r
661                 $paramsAry[$params[1][$i]] = $params[2][$i];\r
662             }\r
663         }\r
664         // for now, just update the nonce value\r
665         if (!empty($paramsAry['nextnonce'])) {\r
666             $challenge['nonce'] = $paramsAry['nextnonce'];\r
667             $challenge['nc']    = 1;\r
668         }\r
669     }\r
670 \r
671     /**\r
672      * Creates a value for [Proxy-]Authorization header when using digest authentication\r
673      *\r
674      * @param string $user       user name\r
675      * @param string $password   password\r
676      * @param string $url        request URL\r
677      * @param array  &$challenge digest challenge parameters\r
678      *\r
679      * @return   string  value of [Proxy-]Authorization request header\r
680      * @link     http://tools.ietf.org/html/rfc2617#section-3.2.2\r
681      */\r
682     protected function createDigestResponse($user, $password, $url, &$challenge)\r
683     {\r
684         if (false !== ($q = strpos($url, '?'))\r
685             && $this->request->getConfig('digest_compat_ie')\r
686         ) {\r
687             $url = substr($url, 0, $q);\r
688         }\r
689 \r
690         $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password);\r
691         $a2 = md5($this->request->getMethod() . ':' . $url);\r
692 \r
693         if (empty($challenge['qop'])) {\r
694             $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2);\r
695         } else {\r
696             $challenge['cnonce'] = 'Req2.' . rand();\r
697             if (empty($challenge['nc'])) {\r
698                 $challenge['nc'] = 1;\r
699             }\r
700             $nc     = sprintf('%08x', $challenge['nc']++);\r
701             $digest = md5(\r
702                 $a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' .\r
703                 $challenge['cnonce'] . ':auth:' . $a2\r
704             );\r
705         }\r
706         return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' .\r
707                'realm="' . $challenge['realm'] . '", ' .\r
708                'nonce="' . $challenge['nonce'] . '", ' .\r
709                'uri="' . $url . '", ' .\r
710                'response="' . $digest . '"' .\r
711                (!empty($challenge['opaque'])?\r
712                 ', opaque="' . $challenge['opaque'] . '"':\r
713                 '') .\r
714                (!empty($challenge['qop'])?\r
715                 ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"':\r
716                 '');\r
717     }\r
718 \r
719     /**\r
720      * Adds 'Authorization' header (if needed) to request headers array\r
721      *\r
722      * @param array  &$headers    request headers\r
723      * @param string $requestHost request host (needed for digest authentication)\r
724      * @param string $requestUrl  request URL (needed for digest authentication)\r
725      *\r
726      * @throws   HTTP_Request2_NotImplementedException\r
727      */\r
728     protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)\r
729     {\r
730         if (!($auth = $this->request->getAuth())) {\r
731             return;\r
732         }\r
733         switch ($auth['scheme']) {\r
734         case HTTP_Request2::AUTH_BASIC:\r
735             $headers['authorization'] = 'Basic ' . base64_encode(\r
736                 $auth['user'] . ':' . $auth['password']\r
737             );\r
738             break;\r
739 \r
740         case HTTP_Request2::AUTH_DIGEST:\r
741             unset($this->serverChallenge);\r
742             $fullUrl = ('/' == $requestUrl[0])?\r
743                        $this->request->getUrl()->getScheme() . '://' .\r
744                         $requestHost . $requestUrl:\r
745                        $requestUrl;\r
746             foreach (array_keys(self::$challenges) as $key) {\r
747                 if ($key == substr($fullUrl, 0, strlen($key))) {\r
748                     $headers['authorization'] = $this->createDigestResponse(\r
749                         $auth['user'], $auth['password'],\r
750                         $requestUrl, self::$challenges[$key]\r
751                     );\r
752                     $this->serverChallenge =& self::$challenges[$key];\r
753                     break;\r
754                 }\r
755             }\r
756             break;\r
757 \r
758         default:\r
759             throw new HTTP_Request2_NotImplementedException(\r
760                 "Unknown HTTP authentication scheme '{$auth['scheme']}'"\r
761             );\r
762         }\r
763     }\r
764 \r
765     /**\r
766      * Adds 'Proxy-Authorization' header (if needed) to request headers array\r
767      *\r
768      * @param array  &$headers   request headers\r
769      * @param string $requestUrl request URL (needed for digest authentication)\r
770      *\r
771      * @throws   HTTP_Request2_NotImplementedException\r
772      */\r
773     protected function addProxyAuthorizationHeader(&$headers, $requestUrl)\r
774     {\r
775         if (!$this->request->getConfig('proxy_host')\r
776             || !($user = $this->request->getConfig('proxy_user'))\r
777             || (0 == strcasecmp('https', $this->request->getUrl()->getScheme())\r
778                 && HTTP_Request2::METHOD_CONNECT != $this->request->getMethod())\r
779         ) {\r
780             return;\r
781         }\r
782 \r
783         $password = $this->request->getConfig('proxy_password');\r
784         switch ($this->request->getConfig('proxy_auth_scheme')) {\r
785         case HTTP_Request2::AUTH_BASIC:\r
786             $headers['proxy-authorization'] = 'Basic ' . base64_encode(\r
787                 $user . ':' . $password\r
788             );\r
789             break;\r
790 \r
791         case HTTP_Request2::AUTH_DIGEST:\r
792             unset($this->proxyChallenge);\r
793             $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') .\r
794                         ':' . $this->request->getConfig('proxy_port');\r
795             if (!empty(self::$challenges[$proxyUrl])) {\r
796                 $headers['proxy-authorization'] = $this->createDigestResponse(\r
797                     $user, $password,\r
798                     $requestUrl, self::$challenges[$proxyUrl]\r
799                 );\r
800                 $this->proxyChallenge =& self::$challenges[$proxyUrl];\r
801             }\r
802             break;\r
803 \r
804         default:\r
805             throw new HTTP_Request2_NotImplementedException(\r
806                 "Unknown HTTP authentication scheme '" .\r
807                 $this->request->getConfig('proxy_auth_scheme') . "'"\r
808             );\r
809         }\r
810     }\r
811 \r
812 \r
813     /**\r
814      * Creates the string with the Request-Line and request headers\r
815      *\r
816      * @return   string\r
817      * @throws   HTTP_Request2_Exception\r
818      */\r
819     protected function prepareHeaders()\r
820     {\r
821         $headers = $this->request->getHeaders();\r
822         $url     = $this->request->getUrl();\r
823         $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod();\r
824         $host    = $url->getHost();\r
825 \r
826         $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80;\r
827         if (($port = $url->getPort()) && $port != $defaultPort || $connect) {\r
828             $host .= ':' . (empty($port)? $defaultPort: $port);\r
829         }\r
830         // Do not overwrite explicitly set 'Host' header, see bug #16146\r
831         if (!isset($headers['host'])) {\r
832             $headers['host'] = $host;\r
833         }\r
834 \r
835         if ($connect) {\r
836             $requestUrl = $host;\r
837 \r
838         } else {\r
839             if (!$this->request->getConfig('proxy_host')\r
840                 || 'http' != $this->request->getConfig('proxy_type')\r
841                 || 0 == strcasecmp($url->getScheme(), 'https')\r
842             ) {\r
843                 $requestUrl = '';\r
844             } else {\r
845                 $requestUrl = $url->getScheme() . '://' . $host;\r
846             }\r
847             $path        = $url->getPath();\r
848             $query       = $url->getQuery();\r
849             $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query);\r
850         }\r
851 \r
852         if ('1.1' == $this->request->getConfig('protocol_version')\r
853             && extension_loaded('zlib') && !isset($headers['accept-encoding'])\r
854         ) {\r
855             $headers['accept-encoding'] = 'gzip, deflate';\r
856         }\r
857         if (($jar = $this->request->getCookieJar())\r
858             && ($cookies = $jar->getMatching($this->request->getUrl(), true))\r
859         ) {\r
860             $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;\r
861         }\r
862 \r
863         $this->addAuthorizationHeader($headers, $host, $requestUrl);\r
864         $this->addProxyAuthorizationHeader($headers, $requestUrl);\r
865         $this->calculateRequestLength($headers);\r
866 \r
867         $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' .\r
868                       $this->request->getConfig('protocol_version') . "\r\n";\r
869         foreach ($headers as $name => $value) {\r
870             $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));\r
871             $headersStr   .= $canonicalName . ': ' . $value . "\r\n";\r
872         }\r
873         return $headersStr . "\r\n";\r
874     }\r
875 \r
876     /**\r
877      * Sends the request body\r
878      *\r
879      * @throws   HTTP_Request2_MessageException\r
880      */\r
881     protected function writeBody()\r
882     {\r
883         if (in_array($this->request->getMethod(), self::$bodyDisallowed)\r
884             || 0 == $this->contentLength\r
885         ) {\r
886             return;\r
887         }\r
888 \r
889         $position   = 0;\r
890         $bufferSize = $this->request->getConfig('buffer_size');\r
891         while ($position < $this->contentLength) {\r
892             if (is_string($this->requestBody)) {\r
893                 $str = substr($this->requestBody, $position, $bufferSize);\r
894             } elseif (is_resource($this->requestBody)) {\r
895                 $str = fread($this->requestBody, $bufferSize);\r
896             } else {\r
897                 $str = $this->requestBody->read($bufferSize);\r
898             }\r
899             $this->socket->write($str);\r
900             // Provide the length of written string to the observer, request #7630\r
901             $this->request->setLastEvent('sentBodyPart', strlen($str));\r
902             $position += strlen($str);\r
903         }\r
904         $this->request->setLastEvent('sentBody', $this->contentLength);\r
905     }\r
906 \r
907     /**\r
908      * Reads the remote server's response\r
909      *\r
910      * @return   HTTP_Request2_Response\r
911      * @throws   HTTP_Request2_Exception\r
912      */\r
913     protected function readResponse()\r
914     {\r
915         $bufferSize = $this->request->getConfig('buffer_size');\r
916 \r
917         do {\r
918             $response = new HTTP_Request2_Response(\r
919                 $this->socket->readLine($bufferSize), true, $this->request->getUrl()\r
920             );\r
921             do {\r
922                 $headerLine = $this->socket->readLine($bufferSize);\r
923                 $response->parseHeaderLine($headerLine);\r
924             } while ('' != $headerLine);\r
925         } while (in_array($response->getStatus(), array(100, 101)));\r
926 \r
927         $this->request->setLastEvent('receivedHeaders', $response);\r
928 \r
929         // No body possible in such responses\r
930         if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod()\r
931             || (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod()\r
932                 && 200 <= $response->getStatus() && 300 > $response->getStatus())\r
933             || in_array($response->getStatus(), array(204, 304))\r
934         ) {\r
935             return $response;\r
936         }\r
937 \r
938         $chunked = 'chunked' == $response->getHeader('transfer-encoding');\r
939         $length  = $response->getHeader('content-length');\r
940         $hasBody = false;\r
941         if ($chunked || null === $length || 0 < intval($length)) {\r
942             // RFC 2616, section 4.4:\r
943             // 3. ... If a message is received with both a\r
944             // Transfer-Encoding header field and a Content-Length header field,\r
945             // the latter MUST be ignored.\r
946             $toRead = ($chunked || null === $length)? null: $length;\r
947             $this->chunkLength = 0;\r
948 \r
949             while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) {\r
950                 if ($chunked) {\r
951                     $data = $this->readChunked($bufferSize);\r
952                 } elseif (is_null($toRead)) {\r
953                     $data = $this->socket->read($bufferSize);\r
954                 } else {\r
955                     $data    = $this->socket->read(min($toRead, $bufferSize));\r
956                     $toRead -= strlen($data);\r
957                 }\r
958                 if ('' == $data && (!$this->chunkLength || $this->socket->eof())) {\r
959                     break;\r
960                 }\r
961 \r
962                 $hasBody = true;\r
963                 if ($this->request->getConfig('store_body')) {\r
964                     $response->appendBody($data);\r
965                 }\r
966                 if (!in_array($response->getHeader('content-encoding'), array('identity', null))) {\r
967                     $this->request->setLastEvent('receivedEncodedBodyPart', $data);\r
968                 } else {\r
969                     $this->request->setLastEvent('receivedBodyPart', $data);\r
970                 }\r
971             }\r
972         }\r
973 \r
974         if ($hasBody) {\r
975             $this->request->setLastEvent('receivedBody', $response);\r
976         }\r
977         return $response;\r
978     }\r
979 \r
980     /**\r
981      * Reads a part of response body encoded with chunked Transfer-Encoding\r
982      *\r
983      * @param int $bufferSize buffer size to use for reading\r
984      *\r
985      * @return   string\r
986      * @throws   HTTP_Request2_MessageException\r
987      */\r
988     protected function readChunked($bufferSize)\r
989     {\r
990         // at start of the next chunk?\r
991         if (0 == $this->chunkLength) {\r
992             $line = $this->socket->readLine($bufferSize);\r
993             if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {\r
994                 throw new HTTP_Request2_MessageException(\r
995                     "Cannot decode chunked response, invalid chunk length '{$line}'",\r
996                     HTTP_Request2_Exception::DECODE_ERROR\r
997                 );\r
998             } else {\r
999                 $this->chunkLength = hexdec($matches[1]);\r
1000                 // Chunk with zero length indicates the end\r
1001                 if (0 == $this->chunkLength) {\r
1002                     $this->socket->readLine($bufferSize);\r
1003                     return '';\r
1004                 }\r
1005             }\r
1006         }\r
1007         $data = $this->socket->read(min($this->chunkLength, $bufferSize));\r
1008         $this->chunkLength -= strlen($data);\r
1009         if (0 == $this->chunkLength) {\r
1010             $this->socket->readLine($bufferSize); // Trailing CRLF\r
1011         }\r
1012         return $data;\r
1013     }\r
1014 }\r
1015 \r
1016 ?>