SocketWrapper.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <?php
  2. /**
  3. * Socket wrapper class used by Socket Adapter
  4. *
  5. * PHP version 5
  6. *
  7. * LICENSE
  8. *
  9. * This source file is subject to BSD 3-Clause License that is bundled
  10. * with this package in the file LICENSE and available at the URL
  11. * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
  12. *
  13. * @category HTTP
  14. * @package HTTP_Request2
  15. * @author Alexey Borzov <avb@php.net>
  16. * @copyright 2008-2016 Alexey Borzov <avb@php.net>
  17. * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
  18. * @link http://pear.php.net/package/HTTP_Request2
  19. */
  20. /** Exception classes for HTTP_Request2 package */
  21. require_once 'HTTP/Request2/Exception.php';
  22. /**
  23. * Socket wrapper class used by Socket Adapter
  24. *
  25. * Needed to properly handle connection errors, global timeout support and
  26. * similar things. Loosely based on Net_Socket used by older HTTP_Request.
  27. *
  28. * @category HTTP
  29. * @package HTTP_Request2
  30. * @author Alexey Borzov <avb@php.net>
  31. * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
  32. * @version Release: 2.3.0
  33. * @link http://pear.php.net/package/HTTP_Request2
  34. * @link http://pear.php.net/bugs/bug.php?id=19332
  35. * @link http://tools.ietf.org/html/rfc1928
  36. */
  37. class HTTP_Request2_SocketWrapper
  38. {
  39. /**
  40. * PHP warning messages raised during stream_socket_client() call
  41. * @var array
  42. */
  43. protected $connectionWarnings = array();
  44. /**
  45. * Connected socket
  46. * @var resource
  47. */
  48. protected $socket;
  49. /**
  50. * Sum of start time and global timeout, exception will be thrown if request continues past this time
  51. * @var integer
  52. */
  53. protected $deadline;
  54. /**
  55. * Global timeout value, mostly for exception messages
  56. * @var integer
  57. */
  58. protected $timeout;
  59. /**
  60. * Class constructor, tries to establish connection
  61. *
  62. * @param string $address Address for stream_socket_client() call,
  63. * e.g. 'tcp://localhost:80'
  64. * @param int $timeout Connection timeout (seconds)
  65. * @param array $contextOptions Context options
  66. *
  67. * @throws HTTP_Request2_LogicException
  68. * @throws HTTP_Request2_ConnectionException
  69. */
  70. public function __construct($address, $timeout, array $contextOptions = array())
  71. {
  72. if (!empty($contextOptions)
  73. && !isset($contextOptions['socket']) && !isset($contextOptions['ssl'])
  74. ) {
  75. // Backwards compatibility with 2.1.0 and 2.1.1 releases
  76. $contextOptions = array('ssl' => $contextOptions);
  77. }
  78. if (isset($contextOptions['ssl'])) {
  79. $contextOptions['ssl'] += array(
  80. // Using "Intermediate compatibility" cipher bundle from
  81. // https://wiki.mozilla.org/Security/Server_Side_TLS
  82. 'ciphers' => 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:'
  83. . 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:'
  84. . 'DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:'
  85. . 'ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:'
  86. . 'ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:'
  87. . 'ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:'
  88. . 'ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:'
  89. . 'DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:'
  90. . 'DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:'
  91. . 'ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:'
  92. . 'AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:'
  93. . 'AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:'
  94. . '!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'
  95. );
  96. if (version_compare(phpversion(), '5.4.13', '>=')) {
  97. $contextOptions['ssl']['disable_compression'] = true;
  98. if (version_compare(phpversion(), '5.6', '>=')) {
  99. $contextOptions['ssl']['crypto_method'] = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
  100. | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
  101. }
  102. }
  103. }
  104. $context = stream_context_create();
  105. foreach ($contextOptions as $wrapper => $options) {
  106. foreach ($options as $name => $value) {
  107. if (!stream_context_set_option($context, $wrapper, $name, $value)) {
  108. throw new HTTP_Request2_LogicException(
  109. "Error setting '{$wrapper}' wrapper context option '{$name}'"
  110. );
  111. }
  112. }
  113. }
  114. set_error_handler(array($this, 'connectionWarningsHandler'));
  115. $this->socket = stream_socket_client(
  116. $address, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $context
  117. );
  118. restore_error_handler();
  119. // if we fail to bind to a specified local address (see request #19515),
  120. // connection still succeeds, albeit with a warning. Throw an Exception
  121. // with the warning text in this case as that connection is unlikely
  122. // to be what user wants and as Curl throws an error in similar case.
  123. if ($this->connectionWarnings) {
  124. if ($this->socket) {
  125. fclose($this->socket);
  126. }
  127. $error = $errstr ? $errstr : implode("\n", $this->connectionWarnings);
  128. throw new HTTP_Request2_ConnectionException(
  129. "Unable to connect to {$address}. Error: {$error}", 0, $errno
  130. );
  131. }
  132. }
  133. /**
  134. * Destructor, disconnects socket
  135. */
  136. public function __destruct()
  137. {
  138. fclose($this->socket);
  139. }
  140. /**
  141. * Wrapper around fread(), handles global request timeout
  142. *
  143. * @param int $length Reads up to this number of bytes
  144. *
  145. * @return string Data read from socket
  146. * @throws HTTP_Request2_MessageException In case of timeout
  147. */
  148. public function read($length)
  149. {
  150. if ($this->deadline) {
  151. stream_set_timeout($this->socket, max($this->deadline - time(), 1));
  152. }
  153. $data = fread($this->socket, $length);
  154. $this->checkTimeout();
  155. return $data;
  156. }
  157. /**
  158. * Reads until either the end of the socket or a newline, whichever comes first
  159. *
  160. * Strips the trailing newline from the returned data, handles global
  161. * request timeout. Method idea borrowed from Net_Socket PEAR package.
  162. *
  163. * @param int $bufferSize buffer size to use for reading
  164. * @param int $localTimeout timeout value to use just for this call
  165. * (used when waiting for "100 Continue" response)
  166. *
  167. * @return string Available data up to the newline (not including newline)
  168. * @throws HTTP_Request2_MessageException In case of timeout
  169. */
  170. public function readLine($bufferSize, $localTimeout = null)
  171. {
  172. $line = '';
  173. while (!feof($this->socket)) {
  174. if (null !== $localTimeout) {
  175. $timeout = $localTimeout;
  176. } elseif ($this->deadline) {
  177. $timeout = max($this->deadline - time(), 1);
  178. } else {
  179. // "If tv_sec is NULL stream_select() can block
  180. // indefinitely, returning only when an event on one of
  181. // the watched streams occurs (or if a signal interrupts
  182. // the system call)." - http://php.net/stream_select
  183. $timeout = null;
  184. }
  185. $info = stream_get_meta_data($this->socket);
  186. $old_blocking = (bool)$info['blocked'];
  187. stream_set_blocking($this->socket, false);
  188. $r = array($this->socket);
  189. $w = array();
  190. $e = array();
  191. if (stream_select($r, $w, $e, $timeout)) {
  192. $line .= @fgets($this->socket, $bufferSize);
  193. }
  194. stream_set_blocking($this->socket, $old_blocking);
  195. if (null === $localTimeout) {
  196. $this->checkTimeout();
  197. } else {
  198. $info = stream_get_meta_data($this->socket);
  199. // reset socket timeout if we don't have request timeout specified,
  200. // prevents further calls failing with a bogus Exception
  201. if (!$this->deadline) {
  202. $default = (int)@ini_get('default_socket_timeout');
  203. stream_set_timeout($this->socket, $default > 0 ? $default : PHP_INT_MAX);
  204. }
  205. if ($info['timed_out']) {
  206. throw new HTTP_Request2_MessageException(
  207. "readLine() call timed out", HTTP_Request2_Exception::TIMEOUT
  208. );
  209. }
  210. }
  211. if (substr($line, -1) == "\n") {
  212. return rtrim($line, "\r\n");
  213. }
  214. }
  215. return $line;
  216. }
  217. /**
  218. * Wrapper around fwrite(), handles global request timeout
  219. *
  220. * @param string $data String to be written
  221. *
  222. * @return int
  223. * @throws HTTP_Request2_MessageException
  224. */
  225. public function write($data)
  226. {
  227. if ($this->deadline) {
  228. stream_set_timeout($this->socket, max($this->deadline - time(), 1));
  229. }
  230. $written = fwrite($this->socket, $data);
  231. $this->checkTimeout();
  232. // http://www.php.net/manual/en/function.fwrite.php#96951
  233. if ($written < strlen($data)) {
  234. throw new HTTP_Request2_MessageException('Error writing request');
  235. }
  236. return $written;
  237. }
  238. /**
  239. * Tests for end-of-file on a socket
  240. *
  241. * @return bool
  242. */
  243. public function eof()
  244. {
  245. return feof($this->socket);
  246. }
  247. /**
  248. * Sets request deadline
  249. *
  250. * @param int $deadline Exception will be thrown if request continues
  251. * past this time
  252. * @param int $timeout Original request timeout value, to use in
  253. * Exception message
  254. */
  255. public function setDeadline($deadline, $timeout)
  256. {
  257. $this->deadline = $deadline;
  258. $this->timeout = $timeout;
  259. }
  260. /**
  261. * Turns on encryption on a socket
  262. *
  263. * @throws HTTP_Request2_ConnectionException
  264. */
  265. public function enableCrypto()
  266. {
  267. if (version_compare(phpversion(), '5.6', '<')) {
  268. $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
  269. } else {
  270. $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
  271. | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
  272. }
  273. if (!stream_socket_enable_crypto($this->socket, true, $cryptoMethod)) {
  274. throw new HTTP_Request2_ConnectionException(
  275. 'Failed to enable secure connection when connecting through proxy'
  276. );
  277. }
  278. }
  279. /**
  280. * Throws an Exception if stream timed out
  281. *
  282. * @throws HTTP_Request2_MessageException
  283. */
  284. protected function checkTimeout()
  285. {
  286. $info = stream_get_meta_data($this->socket);
  287. if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {
  288. $reason = $this->deadline
  289. ? "after {$this->timeout} second(s)"
  290. : 'due to default_socket_timeout php.ini setting';
  291. throw new HTTP_Request2_MessageException(
  292. "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT
  293. );
  294. }
  295. }
  296. /**
  297. * Error handler to use during stream_socket_client() call
  298. *
  299. * One stream_socket_client() call may produce *multiple* PHP warnings
  300. * (especially OpenSSL-related), we keep them in an array to later use for
  301. * the message of HTTP_Request2_ConnectionException
  302. *
  303. * @param int $errno error level
  304. * @param string $errstr error message
  305. *
  306. * @return bool
  307. */
  308. protected function connectionWarningsHandler($errno, $errstr)
  309. {
  310. if ($errno & E_WARNING) {
  311. array_unshift($this->connectionWarnings, $errstr);
  312. }
  313. return true;
  314. }
  315. }
  316. ?>