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