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