You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

907 lines
33 KiB

  1. <?php
  2. namespace dokuwiki\HTTP;
  3. define('HTTP_NL', "\r\n");
  4. /**
  5. * This class implements a basic HTTP client
  6. *
  7. * It supports POST and GET, Proxy usage, basic authentication,
  8. * handles cookies and referrers. It is based upon the httpclient
  9. * function from the VideoDB project.
  10. *
  11. * @link https://www.splitbrain.org/projects/videodb
  12. * @author Andreas Goetz <cpuidle@gmx.de>
  13. * @author Andreas Gohr <andi@splitbrain.org>
  14. * @author Tobias Sarnowski <sarnowski@new-thoughts.org>
  15. */
  16. class HTTPClient
  17. {
  18. //set these if you like
  19. public $agent; // User agent
  20. public $http = '1.0'; // HTTP version defaults to 1.0
  21. public $timeout = 15; // read timeout (seconds)
  22. public $cookies = [];
  23. public $referer = '';
  24. public $max_redirect = 3;
  25. public $max_bodysize = 0;
  26. public $max_bodysize_abort = true; // if set, abort if the response body is bigger than max_bodysize
  27. public $header_regexp = ''; // if set this RE must match against the headers, else abort
  28. public $headers = [];
  29. public $debug = false;
  30. public $start = 0.0; // for timings
  31. public $keep_alive = true; // keep alive rocks
  32. // don't set these, read on error
  33. public $error;
  34. public $redirect_count = 0;
  35. // read these after a successful request
  36. public $status = 0;
  37. public $resp_body;
  38. public $resp_headers;
  39. // set these to do basic authentication
  40. public $user;
  41. public $pass;
  42. // set these if you need to use a proxy
  43. public $proxy_host;
  44. public $proxy_port;
  45. public $proxy_user;
  46. public $proxy_pass;
  47. public $proxy_ssl; //boolean set to true if your proxy needs SSL
  48. public $proxy_except; // regexp of URLs to exclude from proxy
  49. // list of kept alive connections
  50. protected static $connections = [];
  51. // what we use as boundary on multipart/form-data posts
  52. protected $boundary = '---DokuWikiHTTPClient--4523452351';
  53. /**
  54. * Constructor.
  55. *
  56. * @author Andreas Gohr <andi@splitbrain.org>
  57. */
  58. public function __construct()
  59. {
  60. $this->agent = 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; ' . PHP_OS . ')';
  61. if (extension_loaded('zlib')) $this->headers['Accept-encoding'] = 'gzip';
  62. $this->headers['Accept'] = 'text/xml,application/xml,application/xhtml+xml,' .
  63. 'text/html,text/plain,image/png,image/jpeg,image/gif,*/*';
  64. $this->headers['Accept-Language'] = 'en-us';
  65. }
  66. /**
  67. * Simple function to do a GET request
  68. *
  69. * Returns the wanted page or false on an error;
  70. *
  71. * @param string $url The URL to fetch
  72. * @param bool $sloppy304 Return body on 304 not modified
  73. * @return false|string response body, false on error
  74. *
  75. * @author Andreas Gohr <andi@splitbrain.org>
  76. */
  77. public function get($url, $sloppy304 = false)
  78. {
  79. if (!$this->sendRequest($url)) return false;
  80. if ($this->status == 304 && $sloppy304) return $this->resp_body;
  81. if ($this->status < 200 || $this->status > 206) return false;
  82. return $this->resp_body;
  83. }
  84. /**
  85. * Simple function to do a GET request with given parameters
  86. *
  87. * Returns the wanted page or false on an error.
  88. *
  89. * This is a convenience wrapper around get(). The given parameters
  90. * will be correctly encoded and added to the given base URL.
  91. *
  92. * @param string $url The URL to fetch
  93. * @param array $data Associative array of parameters
  94. * @param bool $sloppy304 Return body on 304 not modified
  95. * @return false|string response body, false on error
  96. *
  97. * @author Andreas Gohr <andi@splitbrain.org>
  98. */
  99. public function dget($url, $data, $sloppy304 = false)
  100. {
  101. if (strpos($url, '?')) {
  102. $url .= '&';
  103. } else {
  104. $url .= '?';
  105. }
  106. $url .= $this->postEncode($data);
  107. return $this->get($url, $sloppy304);
  108. }
  109. /**
  110. * Simple function to do a POST request
  111. *
  112. * Returns the resulting page or false on an error;
  113. *
  114. * @param string $url The URL to fetch
  115. * @param array $data Associative array of parameters
  116. * @return false|string response body, false on error
  117. * @author Andreas Gohr <andi@splitbrain.org>
  118. */
  119. public function post($url, $data)
  120. {
  121. if (!$this->sendRequest($url, $data, 'POST')) return false;
  122. if ($this->status < 200 || $this->status > 206) return false;
  123. return $this->resp_body;
  124. }
  125. /**
  126. * Send an HTTP request
  127. *
  128. * This method handles the whole HTTP communication. It respects set proxy settings,
  129. * builds the request headers, follows redirects and parses the response.
  130. *
  131. * Post data should be passed as associative array. When passed as string it will be
  132. * sent as is. You will need to setup your own Content-Type header then.
  133. *
  134. * @param string $url - the complete URL
  135. * @param mixed $data - the post data either as array or raw data
  136. * @param string $method - HTTP Method usually GET or POST.
  137. * @return bool - true on success
  138. *
  139. * @author Andreas Goetz <cpuidle@gmx.de>
  140. * @author Andreas Gohr <andi@splitbrain.org>
  141. */
  142. public function sendRequest($url, $data = '', $method = 'GET')
  143. {
  144. $this->start = microtime(true);
  145. $this->error = '';
  146. $this->status = 0;
  147. $this->resp_body = '';
  148. $this->resp_headers = [];
  149. // save unencoded data for recursive call
  150. $unencodedData = $data;
  151. // don't accept gzip if truncated bodies might occur
  152. if (
  153. $this->max_bodysize &&
  154. !$this->max_bodysize_abort &&
  155. isset($this->headers['Accept-encoding']) &&
  156. $this->headers['Accept-encoding'] == 'gzip'
  157. ) {
  158. unset($this->headers['Accept-encoding']);
  159. }
  160. // parse URL into bits
  161. $uri = parse_url($url);
  162. $server = $uri['host'];
  163. $path = empty($uri['path']) ? '/' : $uri['path'];
  164. $uriPort = empty($uri['port']) ? null : $uri['port'];
  165. if (!empty($uri['query'])) $path .= '?' . $uri['query'];
  166. if (isset($uri['user'])) $this->user = $uri['user'];
  167. if (isset($uri['pass'])) $this->pass = $uri['pass'];
  168. // proxy setup
  169. if ($this->useProxyForUrl($url)) {
  170. $request_url = $url;
  171. $server = $this->proxy_host;
  172. $port = $this->proxy_port;
  173. if (empty($port)) $port = 8080;
  174. $use_tls = $this->proxy_ssl;
  175. } else {
  176. $request_url = $path;
  177. $port = $uriPort ?: ($uri['scheme'] == 'https' ? 443 : 80);
  178. $use_tls = ($uri['scheme'] == 'https');
  179. }
  180. // add SSL stream prefix if needed - needs SSL support in PHP
  181. if ($use_tls) {
  182. if (!in_array('ssl', stream_get_transports())) {
  183. $this->status = -200;
  184. $this->error = 'This PHP version does not support SSL - cannot connect to server';
  185. }
  186. $server = 'ssl://' . $server;
  187. }
  188. // prepare headers
  189. $headers = $this->headers;
  190. $headers['Host'] = $uri['host']
  191. . ($uriPort ? ':' . $uriPort : '');
  192. $headers['User-Agent'] = $this->agent;
  193. $headers['Referer'] = $this->referer;
  194. if ($method == 'POST') {
  195. if (is_array($data)) {
  196. if (empty($headers['Content-Type'])) {
  197. $headers['Content-Type'] = null;
  198. }
  199. if ($headers['Content-Type'] == 'multipart/form-data') {
  200. $headers['Content-Type'] = 'multipart/form-data; boundary=' . $this->boundary;
  201. $data = $this->postMultipartEncode($data);
  202. } else {
  203. $headers['Content-Type'] = 'application/x-www-form-urlencoded';
  204. $data = $this->postEncode($data);
  205. }
  206. }
  207. } elseif ($method == 'GET') {
  208. $data = ''; //no data allowed on GET requests
  209. }
  210. $contentlength = strlen($data);
  211. if ($contentlength) {
  212. $headers['Content-Length'] = $contentlength;
  213. }
  214. if ($this->user) {
  215. $headers['Authorization'] = 'Basic ' . base64_encode($this->user . ':' . $this->pass);
  216. }
  217. if ($this->proxy_user) {
  218. $headers['Proxy-Authorization'] = 'Basic ' . base64_encode($this->proxy_user . ':' . $this->proxy_pass);
  219. }
  220. // already connected?
  221. $connectionId = $this->uniqueConnectionId($server, $port);
  222. $this->debug('connection pool', self::$connections);
  223. $socket = null;
  224. if (isset(self::$connections[$connectionId])) {
  225. $this->debug('reusing connection', $connectionId);
  226. $socket = self::$connections[$connectionId];
  227. }
  228. if (is_null($socket) || feof($socket)) {
  229. $this->debug('opening connection', $connectionId);
  230. // open socket
  231. $socket = @fsockopen($server, $port, $errno, $errstr, $this->timeout);
  232. if (!$socket) {
  233. $this->status = -100;
  234. $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
  235. return false;
  236. }
  237. // try to establish a CONNECT tunnel for SSL
  238. try {
  239. if ($this->ssltunnel($socket, $request_url)) {
  240. // no keep alive for tunnels
  241. $this->keep_alive = false;
  242. // tunnel is authed already
  243. if (isset($headers['Proxy-Authentication'])) unset($headers['Proxy-Authentication']);
  244. }
  245. } catch (HTTPClientException $e) {
  246. $this->status = $e->getCode();
  247. $this->error = $e->getMessage();
  248. fclose($socket);
  249. return false;
  250. }
  251. // keep alive?
  252. if ($this->keep_alive) {
  253. self::$connections[$connectionId] = $socket;
  254. } else {
  255. unset(self::$connections[$connectionId]);
  256. }
  257. }
  258. if ($this->keep_alive && !$this->useProxyForUrl($request_url)) {
  259. // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
  260. // connection token to a proxy server. We still do keep the connection the
  261. // proxy alive (well except for CONNECT tunnels)
  262. $headers['Connection'] = 'Keep-Alive';
  263. } else {
  264. $headers['Connection'] = 'Close';
  265. }
  266. try {
  267. //set non-blocking
  268. stream_set_blocking($socket, 0);
  269. // build request
  270. $request = "$method $request_url HTTP/" . $this->http . HTTP_NL;
  271. $request .= $this->buildHeaders($headers);
  272. $request .= $this->getCookies();
  273. $request .= HTTP_NL;
  274. $request .= $data;
  275. $this->debug('request', $request);
  276. $this->sendData($socket, $request, 'request');
  277. // read headers from socket
  278. $r_headers = '';
  279. do {
  280. $r_line = $this->readLine($socket, 'headers');
  281. $r_headers .= $r_line;
  282. } while ($r_line != "\r\n" && $r_line != "\n");
  283. $this->debug('response headers', $r_headers);
  284. // check if expected body size exceeds allowance
  285. if ($this->max_bodysize && preg_match('/\r?\nContent-Length:\s*(\d+)\r?\n/i', $r_headers, $match)) {
  286. if ($match[1] > $this->max_bodysize) {
  287. if ($this->max_bodysize_abort)
  288. throw new HTTPClientException('Reported content length exceeds allowed response size');
  289. else $this->error = 'Reported content length exceeds allowed response size';
  290. }
  291. }
  292. // get Status
  293. if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/s', $r_headers, $m))
  294. throw new HTTPClientException('Server returned bad answer ' . $r_headers);
  295. $this->status = $m[2];
  296. // handle headers and cookies
  297. $this->resp_headers = $this->parseHeaders($r_headers);
  298. if (isset($this->resp_headers['set-cookie'])) {
  299. foreach ((array)$this->resp_headers['set-cookie'] as $cookie) {
  300. [$cookie] = sexplode(';', $cookie, 2, '');
  301. [$key, $val] = sexplode('=', $cookie, 2, '');
  302. $key = trim($key);
  303. if ($val == 'deleted') {
  304. if (isset($this->cookies[$key])) {
  305. unset($this->cookies[$key]);
  306. }
  307. } elseif ($key) {
  308. $this->cookies[$key] = $val;
  309. }
  310. }
  311. }
  312. $this->debug('Object headers', $this->resp_headers);
  313. // check server status code to follow redirect
  314. if (in_array($this->status, [301, 302, 303, 307, 308])) {
  315. if (empty($this->resp_headers['location'])) {
  316. throw new HTTPClientException('Redirect but no Location Header found');
  317. } elseif ($this->redirect_count == $this->max_redirect) {
  318. throw new HTTPClientException('Maximum number of redirects exceeded');
  319. } else {
  320. // close the connection because we don't handle content retrieval here
  321. // that's the easiest way to clean up the connection
  322. fclose($socket);
  323. unset(self::$connections[$connectionId]);
  324. $this->redirect_count++;
  325. $this->referer = $url;
  326. // handle non-RFC-compliant relative redirects
  327. if (!preg_match('/^http/i', $this->resp_headers['location'])) {
  328. if ($this->resp_headers['location'][0] != '/') {
  329. $this->resp_headers['location'] = $uri['scheme'] . '://' . $uri['host'] . ':' . $uriPort .
  330. dirname($path) . '/' . $this->resp_headers['location'];
  331. } else {
  332. $this->resp_headers['location'] = $uri['scheme'] . '://' . $uri['host'] . ':' . $uriPort .
  333. $this->resp_headers['location'];
  334. }
  335. }
  336. if ($this->status == 307 || $this->status == 308) {
  337. // perform redirected request, same method as before (required by RFC)
  338. return $this->sendRequest($this->resp_headers['location'], $unencodedData, $method);
  339. } else {
  340. // perform redirected request, always via GET (required by RFC)
  341. return $this->sendRequest($this->resp_headers['location'], [], 'GET');
  342. }
  343. }
  344. }
  345. // check if headers are as expected
  346. if ($this->header_regexp && !preg_match($this->header_regexp, $r_headers))
  347. throw new HTTPClientException('The received headers did not match the given regexp');
  348. //read body (with chunked encoding if needed)
  349. $r_body = '';
  350. if (
  351. (
  352. isset($this->resp_headers['transfer-encoding']) &&
  353. $this->resp_headers['transfer-encoding'] == 'chunked'
  354. ) || (
  355. isset($this->resp_headers['transfer-coding']) &&
  356. $this->resp_headers['transfer-coding'] == 'chunked'
  357. )
  358. ) {
  359. $abort = false;
  360. do {
  361. $chunk_size = '';
  362. while (preg_match('/^[a-zA-Z0-9]?$/', $byte = $this->readData($socket, 1, 'chunk'))) {
  363. // read chunksize until \r
  364. $chunk_size .= $byte;
  365. if (strlen($chunk_size) > 128) // set an abritrary limit on the size of chunks
  366. throw new HTTPClientException('Allowed response size exceeded');
  367. }
  368. $this->readLine($socket, 'chunk'); // readtrailing \n
  369. $chunk_size = hexdec($chunk_size);
  370. if ($this->max_bodysize && $chunk_size + strlen($r_body) > $this->max_bodysize) {
  371. if ($this->max_bodysize_abort)
  372. throw new HTTPClientException('Allowed response size exceeded');
  373. $this->error = 'Allowed response size exceeded';
  374. $chunk_size = $this->max_bodysize - strlen($r_body);
  375. $abort = true;
  376. }
  377. if ($chunk_size > 0) {
  378. $r_body .= $this->readData($socket, $chunk_size, 'chunk');
  379. $this->readData($socket, 2, 'chunk'); // read trailing \r\n
  380. }
  381. } while ($chunk_size && !$abort);
  382. } elseif (
  383. isset($this->resp_headers['content-length']) &&
  384. !isset($this->resp_headers['transfer-encoding'])
  385. ) {
  386. /* RFC 2616
  387. * If a message is received with both a Transfer-Encoding header field and a Content-Length
  388. * header field, the latter MUST be ignored.
  389. */
  390. // read up to the content-length or max_bodysize
  391. // for keep alive we need to read the whole message to clean up the socket for the next read
  392. if (
  393. !$this->keep_alive &&
  394. $this->max_bodysize &&
  395. $this->max_bodysize < $this->resp_headers['content-length']
  396. ) {
  397. $length = $this->max_bodysize + 1;
  398. } else {
  399. $length = $this->resp_headers['content-length'];
  400. }
  401. $r_body = $this->readData($socket, $length, 'response (content-length limited)', true);
  402. } elseif (!isset($this->resp_headers['transfer-encoding']) && $this->max_bodysize && !$this->keep_alive) {
  403. $r_body = $this->readData($socket, $this->max_bodysize + 1, 'response (content-length limited)', true);
  404. } elseif ((int)$this->status === 204) {
  405. // request has no content
  406. } else {
  407. // read entire socket
  408. while (!feof($socket)) {
  409. $r_body .= $this->readData($socket, 4096, 'response (unlimited)', true);
  410. }
  411. }
  412. // recheck body size, we might have read max_bodysize+1 or even the whole body, so we abort late here
  413. if ($this->max_bodysize) {
  414. if (strlen($r_body) > $this->max_bodysize) {
  415. if ($this->max_bodysize_abort) {
  416. throw new HTTPClientException('Allowed response size exceeded');
  417. } else {
  418. $this->error = 'Allowed response size exceeded';
  419. }
  420. }
  421. }
  422. } catch (HTTPClientException $err) {
  423. $this->error = $err->getMessage();
  424. if ($err->getCode())
  425. $this->status = $err->getCode();
  426. unset(self::$connections[$connectionId]);
  427. fclose($socket);
  428. return false;
  429. }
  430. if (
  431. !$this->keep_alive ||
  432. (isset($this->resp_headers['connection']) && $this->resp_headers['connection'] == 'Close')
  433. ) {
  434. // close socket
  435. fclose($socket);
  436. unset(self::$connections[$connectionId]);
  437. }
  438. // decode gzip if needed
  439. if (
  440. isset($this->resp_headers['content-encoding']) &&
  441. $this->resp_headers['content-encoding'] == 'gzip' &&
  442. strlen($r_body) > 10 && str_starts_with($r_body, "\x1f\x8b\x08")
  443. ) {
  444. $this->resp_body = @gzinflate(substr($r_body, 10));
  445. if ($this->resp_body === false) {
  446. $this->error = 'Failed to decompress gzip encoded content';
  447. $this->resp_body = $r_body;
  448. }
  449. } else {
  450. $this->resp_body = $r_body;
  451. }
  452. $this->debug('response body', $this->resp_body);
  453. $this->redirect_count = 0;
  454. return true;
  455. }
  456. /**
  457. * Tries to establish a CONNECT tunnel via Proxy
  458. *
  459. * Protocol, Servername and Port will be stripped from the request URL when a successful CONNECT happened
  460. *
  461. * @param resource &$socket
  462. * @param string &$requesturl
  463. * @return bool true if a tunnel was established
  464. * @throws HTTPClientException when a tunnel is needed but could not be established
  465. */
  466. protected function ssltunnel(&$socket, &$requesturl)
  467. {
  468. if (!$this->useProxyForUrl($requesturl)) return false;
  469. $requestinfo = parse_url($requesturl);
  470. if ($requestinfo['scheme'] != 'https') return false;
  471. if (empty($requestinfo['port'])) $requestinfo['port'] = 443;
  472. // build request
  473. $request = "CONNECT {$requestinfo['host']}:{$requestinfo['port']} HTTP/1.0" . HTTP_NL;
  474. $request .= "Host: {$requestinfo['host']}" . HTTP_NL;
  475. if ($this->proxy_user) {
  476. $request .= 'Proxy-Authorization: Basic ' .
  477. base64_encode($this->proxy_user . ':' . $this->proxy_pass) . HTTP_NL;
  478. }
  479. $request .= HTTP_NL;
  480. $this->debug('SSL Tunnel CONNECT', $request);
  481. $this->sendData($socket, $request, 'SSL Tunnel CONNECT');
  482. // read headers from socket
  483. $r_headers = '';
  484. do {
  485. $r_line = $this->readLine($socket, 'headers');
  486. $r_headers .= $r_line;
  487. } while ($r_line != "\r\n" && $r_line != "\n");
  488. $this->debug('SSL Tunnel Response', $r_headers);
  489. if (preg_match('/^HTTP\/1\.[01] 200/i', $r_headers)) {
  490. // set correct peer name for verification (enabled since PHP 5.6)
  491. stream_context_set_option($socket, 'ssl', 'peer_name', $requestinfo['host']);
  492. // SSLv3 is broken, use only TLS connections.
  493. // @link https://bugs.php.net/69195
  494. if (PHP_VERSION_ID >= 50600 && PHP_VERSION_ID <= 50606) {
  495. $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
  496. } else {
  497. // actually means neither SSLv2 nor SSLv3
  498. $cryptoMethod = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
  499. }
  500. if (@stream_socket_enable_crypto($socket, true, $cryptoMethod)) {
  501. $requesturl = ($requestinfo['path'] ?? '/') .
  502. (empty($requestinfo['query']) ? '' : '?' . $requestinfo['query']);
  503. return true;
  504. }
  505. throw new HTTPClientException(
  506. 'Failed to set up crypto for secure connection to ' . $requestinfo['host'],
  507. -151
  508. );
  509. }
  510. throw new HTTPClientException('Failed to establish secure proxy connection', -150);
  511. }
  512. /**
  513. * Safely write data to a socket
  514. *
  515. * @param resource $socket An open socket handle
  516. * @param string $data The data to write
  517. * @param string $message Description of what is being read
  518. * @throws HTTPClientException
  519. *
  520. * @author Tom N Harris <tnharris@whoopdedo.org>
  521. */
  522. protected function sendData($socket, $data, $message)
  523. {
  524. // send request
  525. $towrite = strlen($data);
  526. $written = 0;
  527. while ($written < $towrite) {
  528. // check timeout
  529. $time_used = microtime(true) - $this->start;
  530. if ($time_used > $this->timeout)
  531. throw new HTTPClientException(sprintf('Timeout while sending %s (%.3fs)', $message, $time_used), -100);
  532. if (feof($socket))
  533. throw new HTTPClientException("Socket disconnected while writing $message");
  534. // select parameters
  535. $sel_r = null;
  536. $sel_w = [$socket];
  537. $sel_e = null;
  538. // wait for stream ready or timeout (1sec)
  539. if (@stream_select($sel_r, $sel_w, $sel_e, 1) === false) {
  540. usleep(1000);
  541. continue;
  542. }
  543. // write to stream
  544. $nbytes = fwrite($socket, substr($data, $written, 4096));
  545. if ($nbytes === false)
  546. throw new HTTPClientException("Failed writing to socket while sending $message", -100);
  547. $written += $nbytes;
  548. }
  549. }
  550. /**
  551. * Safely read data from a socket
  552. *
  553. * Reads up to a given number of bytes or throws an exception if the
  554. * response times out or ends prematurely.
  555. *
  556. * @param resource $socket An open socket handle in non-blocking mode
  557. * @param int $nbytes Number of bytes to read
  558. * @param string $message Description of what is being read
  559. * @param bool $ignore_eof End-of-file is not an error if this is set
  560. * @return string
  561. *
  562. * @throws HTTPClientException
  563. * @author Tom N Harris <tnharris@whoopdedo.org>
  564. */
  565. protected function readData($socket, $nbytes, $message, $ignore_eof = false)
  566. {
  567. $r_data = '';
  568. // Does not return immediately so timeout and eof can be checked
  569. if ($nbytes < 0) $nbytes = 0;
  570. $to_read = $nbytes;
  571. do {
  572. $time_used = microtime(true) - $this->start;
  573. if ($time_used > $this->timeout)
  574. throw new HTTPClientException(
  575. sprintf(
  576. 'Timeout while reading %s after %d bytes (%.3fs)',
  577. $message,
  578. strlen($r_data),
  579. $time_used
  580. ),
  581. -100
  582. );
  583. if (feof($socket)) {
  584. if (!$ignore_eof)
  585. throw new HTTPClientException("Premature End of File (socket) while reading $message");
  586. break;
  587. }
  588. if ($to_read > 0) {
  589. // select parameters
  590. $sel_r = [$socket];
  591. $sel_w = null;
  592. $sel_e = null;
  593. // wait for stream ready or timeout (1sec)
  594. if (@stream_select($sel_r, $sel_w, $sel_e, 1) === false) {
  595. usleep(1000);
  596. continue;
  597. }
  598. $bytes = fread($socket, $to_read);
  599. if ($bytes === false)
  600. throw new HTTPClientException("Failed reading from socket while reading $message", -100);
  601. $r_data .= $bytes;
  602. $to_read -= strlen($bytes);
  603. }
  604. } while ($to_read > 0 && strlen($r_data) < $nbytes);
  605. return $r_data;
  606. }
  607. /**
  608. * Safely read a \n-terminated line from a socket
  609. *
  610. * Always returns a complete line, including the terminating \n.
  611. *
  612. * @param resource $socket An open socket handle in non-blocking mode
  613. * @param string $message Description of what is being read
  614. * @return string
  615. *
  616. * @throws HTTPClientException
  617. * @author Tom N Harris <tnharris@whoopdedo.org>
  618. */
  619. protected function readLine($socket, $message)
  620. {
  621. $r_data = '';
  622. do {
  623. $time_used = microtime(true) - $this->start;
  624. if ($time_used > $this->timeout)
  625. throw new HTTPClientException(
  626. sprintf('Timeout while reading %s (%.3fs) >%s<', $message, $time_used, $r_data),
  627. -100
  628. );
  629. if (feof($socket))
  630. throw new HTTPClientException("Premature End of File (socket) while reading $message");
  631. // select parameters
  632. $sel_r = [$socket];
  633. $sel_w = null;
  634. $sel_e = null;
  635. // wait for stream ready or timeout (1sec)
  636. if (@stream_select($sel_r, $sel_w, $sel_e, 1) === false) {
  637. usleep(1000);
  638. continue;
  639. }
  640. $r_data = fgets($socket, 1024);
  641. } while (!preg_match('/\n$/', $r_data));
  642. return $r_data;
  643. }
  644. /**
  645. * print debug info
  646. *
  647. * Uses _debug_text or _debug_html depending on the SAPI name
  648. *
  649. * @param string $info
  650. * @param mixed $var
  651. * @author Andreas Gohr <andi@splitbrain.org>
  652. *
  653. */
  654. protected function debug($info, $var = null)
  655. {
  656. if (!$this->debug) return;
  657. if (PHP_SAPI == 'cli') {
  658. $this->debugText($info, $var);
  659. } else {
  660. $this->debugHtml($info, $var);
  661. }
  662. }
  663. /**
  664. * print debug info as HTML
  665. *
  666. * @param string $info
  667. * @param mixed $var
  668. */
  669. protected function debugHtml($info, $var = null)
  670. {
  671. echo '<b>' . $info . '</b> ' . (microtime(true) - $this->start) . 's<br />';
  672. if (!is_null($var)) {
  673. ob_start();
  674. print_r($var);
  675. $content = htmlspecialchars(ob_get_contents());
  676. ob_end_clean();
  677. echo '<pre>' . $content . '</pre>';
  678. }
  679. }
  680. /**
  681. * prints debug info as plain text
  682. *
  683. * @param string $info
  684. * @param mixed $var
  685. */
  686. protected function debugText($info, $var = null)
  687. {
  688. echo '*' . $info . '* ' . (microtime(true) - $this->start) . "s\n";
  689. if (!is_null($var)) print_r($var);
  690. echo "\n-----------------------------------------------\n";
  691. }
  692. /**
  693. * convert given header string to Header array
  694. *
  695. * All Keys are lowercased.
  696. *
  697. * @param string $string
  698. * @return array
  699. * @author Andreas Gohr <andi@splitbrain.org>
  700. *
  701. */
  702. protected function parseHeaders($string)
  703. {
  704. $headers = [];
  705. $lines = explode("\n", $string);
  706. array_shift($lines); //skip first line (status)
  707. foreach ($lines as $line) {
  708. [$key, $val] = sexplode(':', $line, 2, '');
  709. $key = trim($key);
  710. $val = trim($val);
  711. $key = strtolower($key);
  712. if (!$key) continue;
  713. if (isset($headers[$key])) {
  714. if (is_array($headers[$key])) {
  715. $headers[$key][] = $val;
  716. } else {
  717. $headers[$key] = [$headers[$key], $val];
  718. }
  719. } else {
  720. $headers[$key] = $val;
  721. }
  722. }
  723. return $headers;
  724. }
  725. /**
  726. * convert given header array to header string
  727. *
  728. * @param array $headers
  729. * @return string
  730. * @author Andreas Gohr <andi@splitbrain.org>
  731. *
  732. */
  733. protected function buildHeaders($headers)
  734. {
  735. $string = '';
  736. foreach ($headers as $key => $value) {
  737. if ($value === '') continue;
  738. $string .= $key . ': ' . $value . HTTP_NL;
  739. }
  740. return $string;
  741. }
  742. /**
  743. * get cookies as http header string
  744. *
  745. * @return string
  746. * @author Andreas Goetz <cpuidle@gmx.de>
  747. *
  748. */
  749. protected function getCookies()
  750. {
  751. $headers = '';
  752. foreach ($this->cookies as $key => $val) {
  753. $headers .= "$key=$val; ";
  754. }
  755. $headers = substr($headers, 0, -2);
  756. if ($headers) $headers = "Cookie: $headers" . HTTP_NL;
  757. return $headers;
  758. }
  759. /**
  760. * Encode data for posting
  761. *
  762. * @param array $data
  763. * @return string
  764. * @author Andreas Gohr <andi@splitbrain.org>
  765. *
  766. */
  767. protected function postEncode($data)
  768. {
  769. return http_build_query($data, '', '&');
  770. }
  771. /**
  772. * Encode data for posting using multipart encoding
  773. *
  774. * @fixme use of urlencode might be wrong here
  775. * @param array $data
  776. * @return string
  777. * @author Andreas Gohr <andi@splitbrain.org>
  778. *
  779. */
  780. protected function postMultipartEncode($data)
  781. {
  782. $boundary = '--' . $this->boundary;
  783. $out = '';
  784. foreach ($data as $key => $val) {
  785. $out .= $boundary . HTTP_NL;
  786. if (!is_array($val)) {
  787. $out .= 'Content-Disposition: form-data; name="' . urlencode($key) . '"' . HTTP_NL;
  788. $out .= HTTP_NL; // end of headers
  789. $out .= $val;
  790. $out .= HTTP_NL;
  791. } else {
  792. $out .= 'Content-Disposition: form-data; name="' . urlencode($key) . '"';
  793. if ($val['filename']) $out .= '; filename="' . urlencode($val['filename']) . '"';
  794. $out .= HTTP_NL;
  795. if ($val['mimetype']) $out .= 'Content-Type: ' . $val['mimetype'] . HTTP_NL;
  796. $out .= HTTP_NL; // end of headers
  797. $out .= $val['body'];
  798. $out .= HTTP_NL;
  799. }
  800. }
  801. $out .= "$boundary--" . HTTP_NL;
  802. return $out;
  803. }
  804. /**
  805. * Generates a unique identifier for a connection.
  806. *
  807. * @param string $server
  808. * @param string $port
  809. * @return string unique identifier
  810. */
  811. protected function uniqueConnectionId($server, $port)
  812. {
  813. return "$server:$port";
  814. }
  815. /**
  816. * Should the Proxy be used for the given URL?
  817. *
  818. * Checks the exceptions
  819. *
  820. * @param string $url
  821. * @return bool
  822. */
  823. protected function useProxyForUrl($url)
  824. {
  825. return $this->proxy_host && (!$this->proxy_except || !preg_match('/' . $this->proxy_except . '/i', $url));
  826. }
  827. }