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