*\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
* @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
/**\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
*/\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
$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
'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
$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
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
\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
} 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
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