HTTPClient.php 33 KB

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