]> git.mxchange.org Git - quix0rs-gnu-social.git/blobdiff - extlib/HTTP/Request2/Adapter/Socket.php
PEAR::HTTP_Request2 updated to 2.2.1
[quix0rs-gnu-social.git] / extlib / HTTP / Request2 / Adapter / Socket.php
index fb2940a2ee352a8c46125f1b3eb086f66e7d80c6..7946b0a37410cb8af78078431813dbc97017cb1a 100644 (file)
@@ -4,41 +4,18 @@
  *\r
  * PHP version 5\r
  *\r
- * LICENSE:\r
+ * LICENSE\r
  *\r
- * Copyright (c) 2008-2012, Alexey Borzov <avb@php.net>\r
- * All rights reserved.\r
+ * This source file is subject to BSD 3-Clause License that is bundled\r
+ * with this package in the file LICENSE and available at the URL\r
+ * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE\r
  *\r
- * Redistribution and use in source and binary forms, with or without\r
- * modification, are permitted provided that the following conditions\r
- * are met:\r
- *\r
- *    * Redistributions of source code must retain the above copyright\r
- *      notice, this list of conditions and the following disclaimer.\r
- *    * Redistributions in binary form must reproduce the above copyright\r
- *      notice, this list of conditions and the following disclaimer in the\r
- *      documentation and/or other materials provided with the distribution.\r
- *    * The names of the authors may not be used to endorse or promote products\r
- *      derived from this software without specific prior written permission.\r
- *\r
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS\r
- * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\r
- * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\r
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY\r
- * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\r
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
- *\r
- * @category HTTP\r
- * @package  HTTP_Request2\r
- * @author   Alexey Borzov <avb@php.net>\r
- * @license  http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version  SVN: $Id: Socket.php 324953 2012-04-08 07:24:12Z avb $\r
- * @link     http://pear.php.net/package/HTTP_Request2\r
+ * @category  HTTP\r
+ * @package   HTTP_Request2\r
+ * @author    Alexey Borzov <avb@php.net>\r
+ * @copyright 2008-2014 Alexey Borzov <avb@php.net>\r
+ * @license   http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
+ * @link      http://pear.php.net/package/HTTP_Request2\r
  */\r
 \r
 /** Base class for HTTP_Request2 adapters */\r
@@ -56,8 +33,8 @@ require_once 'HTTP/Request2/SocketWrapper.php';
  * @category HTTP\r
  * @package  HTTP_Request2\r
  * @author   Alexey Borzov <avb@php.net>\r
- * @license  http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version  Release: 2.1.1\r
+ * @license  http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License\r
+ * @version  Release: 2.2.1\r
  * @link     http://pear.php.net/package/HTTP_Request2\r
  */\r
 class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter\r
@@ -70,7 +47,7 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
     /**\r
      * Regular expression for 'quoted-string' rule from RFC 2616\r
      */\r
-    const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"';\r
+    const REGEXP_QUOTED_STRING = '"(?>[^"\\\\]+|\\\\.)*"';\r
 \r
     /**\r
      * Connected sockets, needed for Keep-Alive support\r
@@ -129,6 +106,12 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
      */\r
     protected $redirectCountdown = null;\r
 \r
+    /**\r
+     * Whether to wait for "100 Continue" response before sending request body\r
+     * @var bool\r
+     */\r
+    protected $expect100Continue = false;\r
+\r
     /**\r
      * Sends request to the remote server and returns its response\r
      *\r
@@ -147,9 +130,21 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
             $this->socket->write($headers);\r
             // provide request headers to the observer, see request #7633\r
             $this->request->setLastEvent('sentHeaders', $headers);\r
-            $this->writeBody();\r
 \r
-            $response = $this->readResponse();\r
+            if (!$this->expect100Continue) {\r
+                $this->writeBody();\r
+                $response = $this->readResponse();\r
+\r
+            } else {\r
+                $response = $this->readResponse();\r
+                if (!$response || 100 == $response->getStatus()) {\r
+                    $this->expect100Continue = false;\r
+                    // either got "100 Continue" or timed out -> send body\r
+                    $this->writeBody();\r
+                    $response = $this->readResponse();\r
+                }\r
+            }\r
+\r
 \r
             if ($jar = $request->getCookieJar()) {\r
                 $jar->addCookiesFromResponse($response, $request->getUrl());\r
@@ -256,19 +251,25 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
                       'Keep-Alive' == $headers['connection']);\r
 \r
         $options = array();\r
+        if ($ip = $this->request->getConfig('local_ip')) {\r
+            $options['socket'] = array(\r
+                'bindto' => (false === strpos($ip, ':') ? $ip : '[' . $ip . ']') . ':0'\r
+            );\r
+        }\r
         if ($secure || $tunnel) {\r
+            $options['ssl'] = array();\r
             foreach ($this->request->getConfig() as $name => $value) {\r
                 if ('ssl_' == substr($name, 0, 4) && null !== $value) {\r
                     if ('ssl_verify_host' == $name) {\r
                         if ($value) {\r
-                            $options['CN_match'] = $reqHost;\r
+                            $options['ssl']['CN_match'] = $reqHost;\r
                         }\r
                     } else {\r
-                        $options[substr($name, 4)] = $value;\r
+                        $options['ssl'][substr($name, 4)] = $value;\r
                     }\r
                 }\r
             }\r
-            ksort($options);\r
+            ksort($options['ssl']);\r
         }\r
 \r
         // Use global request timeout if given, see feature requests #5735, #8964\r
@@ -863,6 +864,11 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
         $this->addAuthorizationHeader($headers, $host, $requestUrl);\r
         $this->addProxyAuthorizationHeader($headers, $requestUrl);\r
         $this->calculateRequestLength($headers);\r
+        if ('1.1' == $this->request->getConfig('protocol_version')) {\r
+            $this->updateExpectHeader($headers);\r
+        } else {\r
+            $this->expect100Continue = false;\r
+        }\r
 \r
         $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' .\r
                       $this->request->getConfig('protocol_version') . "\r\n";\r
@@ -873,6 +879,78 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
         return $headersStr . "\r\n";\r
     }\r
 \r
+    /**\r
+     * Adds or removes 'Expect: 100-continue' header from request headers\r
+     *\r
+     * Also sets the $expect100Continue property. Parsing of existing header\r
+     * is somewhat needed due to its complex structure and due to the\r
+     * requirement in section 8.2.3 of RFC 2616:\r
+     * > A client MUST NOT send an Expect request-header field (section\r
+     * > 14.20) with the "100-continue" expectation if it does not intend\r
+     * > to send a request body.\r
+     *\r
+     * @param array &$headers Array of headers prepared for the request\r
+     *\r
+     * @throws HTTP_Request2_LogicException\r
+     * @link http://pear.php.net/bugs/bug.php?id=19233\r
+     * @link http://tools.ietf.org/html/rfc2616#section-8.2.3\r
+     */\r
+    protected function updateExpectHeader(&$headers)\r
+    {\r
+        $this->expect100Continue = false;\r
+        $expectations = array();\r
+        if (isset($headers['expect'])) {\r
+            if ('' === $headers['expect']) {\r
+                // empty 'Expect' header is technically invalid, so just get rid of it\r
+                unset($headers['expect']);\r
+                return;\r
+            }\r
+            // build regexp to parse the value of existing Expect header\r
+            $expectParam     = ';\s*' . self::REGEXP_TOKEN . '(?:\s*=\s*(?:'\r
+                               . self::REGEXP_TOKEN . '|'\r
+                               . self::REGEXP_QUOTED_STRING . '))?\s*';\r
+            $expectExtension = self::REGEXP_TOKEN . '(?:\s*=\s*(?:'\r
+                               . self::REGEXP_TOKEN . '|'\r
+                               . self::REGEXP_QUOTED_STRING . ')\s*(?:'\r
+                               . $expectParam . ')*)?';\r
+            $expectItem      = '!(100-continue|' . $expectExtension . ')!A';\r
+\r
+            $pos    = 0;\r
+            $length = strlen($headers['expect']);\r
+\r
+            while ($pos < $length) {\r
+                $pos += strspn($headers['expect'], " \t", $pos);\r
+                if (',' === substr($headers['expect'], $pos, 1)) {\r
+                    $pos++;\r
+                    continue;\r
+\r
+                } elseif (!preg_match($expectItem, $headers['expect'], $m, 0, $pos)) {\r
+                    throw new HTTP_Request2_LogicException(\r
+                        "Cannot parse value '{$headers['expect']}' of Expect header",\r
+                        HTTP_Request2_Exception::INVALID_ARGUMENT\r
+                    );\r
+\r
+                } else {\r
+                    $pos += strlen($m[0]);\r
+                    if (strcasecmp('100-continue', $m[0])) {\r
+                        $expectations[]  = $m[0];\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        if (1024 < $this->contentLength) {\r
+            $expectations[] = '100-continue';\r
+            $this->expect100Continue = true;\r
+        }\r
+\r
+        if (empty($expectations)) {\r
+            unset($headers['expect']);\r
+        } else {\r
+            $headers['expect'] = implode(',', $expectations);\r
+        }\r
+    }\r
+\r
     /**\r
      * Sends the request body\r
      *\r
@@ -888,6 +966,8 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
 \r
         $position   = 0;\r
         $bufferSize = $this->request->getConfig('buffer_size');\r
+        $headers    = $this->request->getHeaders();\r
+        $chunked    = isset($headers['transfer-encoding']);\r
         while ($position < $this->contentLength) {\r
             if (is_string($this->requestBody)) {\r
                 $str = substr($this->requestBody, $position, $bufferSize);\r
@@ -896,11 +976,20 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
             } else {\r
                 $str = $this->requestBody->read($bufferSize);\r
             }\r
-            $this->socket->write($str);\r
+            if (!$chunked) {\r
+                $this->socket->write($str);\r
+            } else {\r
+                $this->socket->write(dechex(strlen($str)) . "\r\n{$str}\r\n");\r
+            }\r
             // Provide the length of written string to the observer, request #7630\r
             $this->request->setLastEvent('sentBodyPart', strlen($str));\r
             $position += strlen($str);\r
         }\r
+\r
+        // write zero-length chunk\r
+        if ($chunked) {\r
+            $this->socket->write("0\r\n\r\n");\r
+        }\r
         $this->request->setLastEvent('sentBody', $this->contentLength);\r
     }\r
 \r
@@ -913,15 +1002,31 @@ class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter
     protected function readResponse()\r
     {\r
         $bufferSize = $this->request->getConfig('buffer_size');\r
+        // http://tools.ietf.org/html/rfc2616#section-8.2.3\r
+        // ...the client SHOULD NOT wait for an indefinite period before sending the request body\r
+        $timeout    = $this->expect100Continue ? 1 : null;\r
 \r
         do {\r
-            $response = new HTTP_Request2_Response(\r
-                $this->socket->readLine($bufferSize), true, $this->request->getUrl()\r
-            );\r
-            do {\r
-                $headerLine = $this->socket->readLine($bufferSize);\r
-                $response->parseHeaderLine($headerLine);\r
-            } while ('' != $headerLine);\r
+            try {\r
+                $response = new HTTP_Request2_Response(\r
+                    $this->socket->readLine($bufferSize, $timeout), true, $this->request->getUrl()\r
+                );\r
+                do {\r
+                    $headerLine = $this->socket->readLine($bufferSize);\r
+                    $response->parseHeaderLine($headerLine);\r
+                } while ('' != $headerLine);\r
+\r
+            } catch (HTTP_Request2_MessageException $e) {\r
+                if (HTTP_Request2_Exception::TIMEOUT === $e->getCode()\r
+                    && $this->expect100Continue\r
+                ) {\r
+                    return null;\r
+                }\r
+                throw $e;\r
+            }\r
+            if ($this->expect100Continue && 100 == $response->getStatus()) {\r
+                return $response;\r
+            }\r
         } while (in_array($response->getStatus(), array(100, 101)));\r
 \r
         $this->request->setLastEvent('receivedHeaders', $response);\r