xref: /plugin/upgrade/HTTP/HTTPClient.php (revision fc9fd1dacdabb9cf1d802003da12b51682e02ca8)
14e3e87e4SAndreas Gohr<?php
24e3e87e4SAndreas Gohr
34e3e87e4SAndreas Gohrnamespace dokuwiki\plugin\upgrade\HTTP;
44e3e87e4SAndreas Gohr
5eb7336c4SAndreas Gohrif(!defined('HTTP_NL')) define('HTTP_NL',"\r\n");
64e3e87e4SAndreas Gohr
74e3e87e4SAndreas Gohr
84e3e87e4SAndreas Gohr/**
94e3e87e4SAndreas Gohr * This class implements a basic HTTP client
104e3e87e4SAndreas Gohr *
114e3e87e4SAndreas Gohr * It supports POST and GET, Proxy usage, basic authentication,
124e3e87e4SAndreas Gohr * handles cookies and referers. It is based upon the httpclient
134e3e87e4SAndreas Gohr * function from the VideoDB project.
144e3e87e4SAndreas Gohr *
154e3e87e4SAndreas Gohr * @link   http://www.splitbrain.org/go/videodb
164e3e87e4SAndreas Gohr * @author Andreas Goetz <cpuidle@gmx.de>
174e3e87e4SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org>
184e3e87e4SAndreas Gohr * @author Tobias Sarnowski <sarnowski@new-thoughts.org>
194e3e87e4SAndreas Gohr */
204e3e87e4SAndreas Gohrclass HTTPClient {
214e3e87e4SAndreas Gohr    //set these if you like
224e3e87e4SAndreas Gohr    public $agent;         // User agent
234e3e87e4SAndreas Gohr    public $http;          // HTTP version defaults to 1.0
244e3e87e4SAndreas Gohr    public $timeout;       // read timeout (seconds)
254e3e87e4SAndreas Gohr    public $cookies;
264e3e87e4SAndreas Gohr    public $referer;
274e3e87e4SAndreas Gohr    public $max_redirect;
284e3e87e4SAndreas Gohr    public $max_bodysize;
294e3e87e4SAndreas Gohr    public $max_bodysize_abort = true;  // if set, abort if the response body is bigger than max_bodysize
304e3e87e4SAndreas Gohr    public $header_regexp; // if set this RE must match against the headers, else abort
314e3e87e4SAndreas Gohr    public $headers;
324e3e87e4SAndreas Gohr    public $debug;
334e3e87e4SAndreas Gohr    public $start = 0.0; // for timings
344e3e87e4SAndreas Gohr    public $keep_alive = true; // keep alive rocks
354e3e87e4SAndreas Gohr
364e3e87e4SAndreas Gohr    // don't set these, read on error
374e3e87e4SAndreas Gohr    public $error;
384e3e87e4SAndreas Gohr    public $redirect_count;
394e3e87e4SAndreas Gohr
404e3e87e4SAndreas Gohr    // read these after a successful request
414e3e87e4SAndreas Gohr    public $status;
424e3e87e4SAndreas Gohr    public $resp_body;
434e3e87e4SAndreas Gohr    public $resp_headers;
444e3e87e4SAndreas Gohr
454e3e87e4SAndreas Gohr    // set these to do basic authentication
464e3e87e4SAndreas Gohr    public $user;
474e3e87e4SAndreas Gohr    public $pass;
484e3e87e4SAndreas Gohr
494e3e87e4SAndreas Gohr    // set these if you need to use a proxy
504e3e87e4SAndreas Gohr    public $proxy_host;
514e3e87e4SAndreas Gohr    public $proxy_port;
524e3e87e4SAndreas Gohr    public $proxy_user;
534e3e87e4SAndreas Gohr    public $proxy_pass;
544e3e87e4SAndreas Gohr    public $proxy_ssl; //boolean set to true if your proxy needs SSL
554e3e87e4SAndreas Gohr    public $proxy_except; // regexp of URLs to exclude from proxy
564e3e87e4SAndreas Gohr
574e3e87e4SAndreas Gohr    // list of kept alive connections
584e3e87e4SAndreas Gohr    protected static $connections = array();
594e3e87e4SAndreas Gohr
604e3e87e4SAndreas Gohr    // what we use as boundary on multipart/form-data posts
614e3e87e4SAndreas Gohr    protected $boundary = '---DokuWikiHTTPClient--4523452351';
624e3e87e4SAndreas Gohr
634e3e87e4SAndreas Gohr    /**
644e3e87e4SAndreas Gohr     * Constructor.
654e3e87e4SAndreas Gohr     *
664e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
674e3e87e4SAndreas Gohr     */
684e3e87e4SAndreas Gohr    public function __construct(){
694e3e87e4SAndreas Gohr        $this->agent        = 'Mozilla/4.0 (compatible; DokuWiki HTTP Client; '.PHP_OS.')';
704e3e87e4SAndreas Gohr        $this->timeout      = 15;
714e3e87e4SAndreas Gohr        $this->cookies      = array();
724e3e87e4SAndreas Gohr        $this->referer      = '';
734e3e87e4SAndreas Gohr        $this->max_redirect = 3;
744e3e87e4SAndreas Gohr        $this->redirect_count = 0;
754e3e87e4SAndreas Gohr        $this->status       = 0;
764e3e87e4SAndreas Gohr        $this->headers      = array();
774e3e87e4SAndreas Gohr        $this->http         = '1.0';
784e3e87e4SAndreas Gohr        $this->debug        = false;
794e3e87e4SAndreas Gohr        $this->max_bodysize = 0;
804e3e87e4SAndreas Gohr        $this->header_regexp= '';
814e3e87e4SAndreas Gohr        if(extension_loaded('zlib')) $this->headers['Accept-encoding'] = 'gzip';
824e3e87e4SAndreas Gohr        $this->headers['Accept'] = 'text/xml,application/xml,application/xhtml+xml,'.
834e3e87e4SAndreas Gohr            'text/html,text/plain,image/png,image/jpeg,image/gif,*/*';
844e3e87e4SAndreas Gohr        $this->headers['Accept-Language'] = 'en-us';
854e3e87e4SAndreas Gohr    }
864e3e87e4SAndreas Gohr
874e3e87e4SAndreas Gohr
884e3e87e4SAndreas Gohr    /**
894e3e87e4SAndreas Gohr     * Simple function to do a GET request
904e3e87e4SAndreas Gohr     *
914e3e87e4SAndreas Gohr     * Returns the wanted page or false on an error;
924e3e87e4SAndreas Gohr     *
934e3e87e4SAndreas Gohr     * @param  string $url       The URL to fetch
944e3e87e4SAndreas Gohr     * @param  bool   $sloppy304 Return body on 304 not modified
954e3e87e4SAndreas Gohr     * @return false|string  response body, false on error
964e3e87e4SAndreas Gohr     *
974e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
984e3e87e4SAndreas Gohr     */
994e3e87e4SAndreas Gohr    public function get($url,$sloppy304=false){
1004e3e87e4SAndreas Gohr        if(!$this->sendRequest($url)) return false;
1014e3e87e4SAndreas Gohr        if($this->status == 304 && $sloppy304) return $this->resp_body;
1024e3e87e4SAndreas Gohr        if($this->status < 200 || $this->status > 206) return false;
1034e3e87e4SAndreas Gohr        return $this->resp_body;
1044e3e87e4SAndreas Gohr    }
1054e3e87e4SAndreas Gohr
1064e3e87e4SAndreas Gohr    /**
1074e3e87e4SAndreas Gohr     * Simple function to do a GET request with given parameters
1084e3e87e4SAndreas Gohr     *
1094e3e87e4SAndreas Gohr     * Returns the wanted page or false on an error.
1104e3e87e4SAndreas Gohr     *
1114e3e87e4SAndreas Gohr     * This is a convenience wrapper around get(). The given parameters
1124e3e87e4SAndreas Gohr     * will be correctly encoded and added to the given base URL.
1134e3e87e4SAndreas Gohr     *
1144e3e87e4SAndreas Gohr     * @param  string $url       The URL to fetch
1154e3e87e4SAndreas Gohr     * @param  array  $data      Associative array of parameters
1164e3e87e4SAndreas Gohr     * @param  bool   $sloppy304 Return body on 304 not modified
1174e3e87e4SAndreas Gohr     * @return false|string  response body, false on error
1184e3e87e4SAndreas Gohr     *
1194e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
1204e3e87e4SAndreas Gohr     */
1214e3e87e4SAndreas Gohr    public function dget($url,$data,$sloppy304=false){
1224e3e87e4SAndreas Gohr        if(strpos($url,'?')){
1234e3e87e4SAndreas Gohr            $url .= '&';
1244e3e87e4SAndreas Gohr        }else{
1254e3e87e4SAndreas Gohr            $url .= '?';
1264e3e87e4SAndreas Gohr        }
1274e3e87e4SAndreas Gohr        $url .= $this->postEncode($data);
1284e3e87e4SAndreas Gohr        return $this->get($url,$sloppy304);
1294e3e87e4SAndreas Gohr    }
1304e3e87e4SAndreas Gohr
1314e3e87e4SAndreas Gohr    /**
1324e3e87e4SAndreas Gohr     * Simple function to do a POST request
1334e3e87e4SAndreas Gohr     *
1344e3e87e4SAndreas Gohr     * Returns the resulting page or false on an error;
1354e3e87e4SAndreas Gohr     *
1364e3e87e4SAndreas Gohr     * @param  string $url       The URL to fetch
1374e3e87e4SAndreas Gohr     * @param  array  $data      Associative array of parameters
1384e3e87e4SAndreas Gohr     * @return false|string  response body, false on error
1394e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
1404e3e87e4SAndreas Gohr     */
1414e3e87e4SAndreas Gohr    public function post($url,$data){
1424e3e87e4SAndreas Gohr        if(!$this->sendRequest($url,$data,'POST')) return false;
1434e3e87e4SAndreas Gohr        if($this->status < 200 || $this->status > 206) return false;
1444e3e87e4SAndreas Gohr        return $this->resp_body;
1454e3e87e4SAndreas Gohr    }
1464e3e87e4SAndreas Gohr
1474e3e87e4SAndreas Gohr    /**
1484e3e87e4SAndreas Gohr     * Send an HTTP request
1494e3e87e4SAndreas Gohr     *
1504e3e87e4SAndreas Gohr     * This method handles the whole HTTP communication. It respects set proxy settings,
1514e3e87e4SAndreas Gohr     * builds the request headers, follows redirects and parses the response.
1524e3e87e4SAndreas Gohr     *
1534e3e87e4SAndreas Gohr     * Post data should be passed as associative array. When passed as string it will be
1544e3e87e4SAndreas Gohr     * sent as is. You will need to setup your own Content-Type header then.
1554e3e87e4SAndreas Gohr     *
1564e3e87e4SAndreas Gohr     * @param  string $url    - the complete URL
1574e3e87e4SAndreas Gohr     * @param  mixed  $data   - the post data either as array or raw data
1584e3e87e4SAndreas Gohr     * @param  string $method - HTTP Method usually GET or POST.
1594e3e87e4SAndreas Gohr     * @return bool - true on success
1604e3e87e4SAndreas Gohr     *
1614e3e87e4SAndreas Gohr     * @author Andreas Goetz <cpuidle@gmx.de>
1624e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
1634e3e87e4SAndreas Gohr     */
1644e3e87e4SAndreas Gohr    public function sendRequest($url,$data='',$method='GET'){
1654e3e87e4SAndreas Gohr        $this->start  = $this->time();
1664e3e87e4SAndreas Gohr        $this->error  = '';
1674e3e87e4SAndreas Gohr        $this->status = 0;
1684e3e87e4SAndreas Gohr        $this->resp_body = '';
1694e3e87e4SAndreas Gohr        $this->resp_headers = array();
1704e3e87e4SAndreas Gohr
1714e3e87e4SAndreas Gohr        // save unencoded data for recursive call
1724e3e87e4SAndreas Gohr        $unencodedData = $data;
1734e3e87e4SAndreas Gohr
1744e3e87e4SAndreas Gohr        // don't accept gzip if truncated bodies might occur
1754e3e87e4SAndreas Gohr        if($this->max_bodysize &&
1764e3e87e4SAndreas Gohr            !$this->max_bodysize_abort &&
1774e3e87e4SAndreas Gohr            isset($this->headers['Accept-encoding']) &&
1784e3e87e4SAndreas Gohr            $this->headers['Accept-encoding'] == 'gzip'){
1794e3e87e4SAndreas Gohr            unset($this->headers['Accept-encoding']);
1804e3e87e4SAndreas Gohr        }
1814e3e87e4SAndreas Gohr
1824e3e87e4SAndreas Gohr        // parse URL into bits
1834e3e87e4SAndreas Gohr        $uri = parse_url($url);
1844e3e87e4SAndreas Gohr        $server = $uri['host'];
1854e3e87e4SAndreas Gohr        $path   = !empty($uri['path']) ? $uri['path'] : '/';
1864e3e87e4SAndreas Gohr        $uriPort = !empty($uri['port']) ? $uri['port'] : null;
1874e3e87e4SAndreas Gohr        if(!empty($uri['query'])) $path .= '?'.$uri['query'];
1884e3e87e4SAndreas Gohr        if(isset($uri['user'])) $this->user = $uri['user'];
1894e3e87e4SAndreas Gohr        if(isset($uri['pass'])) $this->pass = $uri['pass'];
1904e3e87e4SAndreas Gohr
1914e3e87e4SAndreas Gohr        // proxy setup
1924e3e87e4SAndreas Gohr        if($this->useProxyForUrl($url)){
1934e3e87e4SAndreas Gohr            $request_url = $url;
1944e3e87e4SAndreas Gohr            $server      = $this->proxy_host;
1954e3e87e4SAndreas Gohr            $port        = $this->proxy_port;
1964e3e87e4SAndreas Gohr            if (empty($port)) $port = 8080;
1974e3e87e4SAndreas Gohr            $use_tls     = $this->proxy_ssl;
1984e3e87e4SAndreas Gohr        }else{
1994e3e87e4SAndreas Gohr            $request_url = $path;
2004e3e87e4SAndreas Gohr            $port = $uriPort ?: ($uri['scheme'] == 'https' ? 443 : 80);
2014e3e87e4SAndreas Gohr            $use_tls     = ($uri['scheme'] == 'https');
2024e3e87e4SAndreas Gohr        }
2034e3e87e4SAndreas Gohr
2044e3e87e4SAndreas Gohr        // add SSL stream prefix if needed - needs SSL support in PHP
2054e3e87e4SAndreas Gohr        if($use_tls) {
2064e3e87e4SAndreas Gohr            if(!in_array('ssl', stream_get_transports())) {
2074e3e87e4SAndreas Gohr                $this->status = -200;
2084e3e87e4SAndreas Gohr                $this->error = 'This PHP version does not support SSL - cannot connect to server';
2094e3e87e4SAndreas Gohr            }
2104e3e87e4SAndreas Gohr            $server = 'ssl://'.$server;
2114e3e87e4SAndreas Gohr        }
2124e3e87e4SAndreas Gohr
2134e3e87e4SAndreas Gohr        // prepare headers
2144e3e87e4SAndreas Gohr        $headers               = $this->headers;
2154e3e87e4SAndreas Gohr        $headers['Host']       = $uri['host']
2164e3e87e4SAndreas Gohr            . ($uriPort ? ':' . $uriPort : '');
2174e3e87e4SAndreas Gohr        $headers['User-Agent'] = $this->agent;
2184e3e87e4SAndreas Gohr        $headers['Referer']    = $this->referer;
2194e3e87e4SAndreas Gohr
2204e3e87e4SAndreas Gohr        if($method == 'POST'){
2214e3e87e4SAndreas Gohr            if(is_array($data)){
2224e3e87e4SAndreas Gohr                if (empty($headers['Content-Type'])) {
2234e3e87e4SAndreas Gohr                    $headers['Content-Type'] = null;
2244e3e87e4SAndreas Gohr                }
2254e3e87e4SAndreas Gohr                switch ($headers['Content-Type']) {
2264e3e87e4SAndreas Gohr                    case 'multipart/form-data':
2274e3e87e4SAndreas Gohr                        $headers['Content-Type']   = 'multipart/form-data; boundary=' . $this->boundary;
2284e3e87e4SAndreas Gohr                        $data = $this->postMultipartEncode($data);
2294e3e87e4SAndreas Gohr                        break;
2304e3e87e4SAndreas Gohr                    default:
2314e3e87e4SAndreas Gohr                        $headers['Content-Type']   = 'application/x-www-form-urlencoded';
2324e3e87e4SAndreas Gohr                        $data = $this->postEncode($data);
2334e3e87e4SAndreas Gohr                }
2344e3e87e4SAndreas Gohr            }
2354e3e87e4SAndreas Gohr        }elseif($method == 'GET'){
2364e3e87e4SAndreas Gohr            $data = ''; //no data allowed on GET requests
2374e3e87e4SAndreas Gohr        }
2384e3e87e4SAndreas Gohr
2394e3e87e4SAndreas Gohr        $contentlength = strlen($data);
2404e3e87e4SAndreas Gohr        if($contentlength)  {
2414e3e87e4SAndreas Gohr            $headers['Content-Length'] = $contentlength;
2424e3e87e4SAndreas Gohr        }
2434e3e87e4SAndreas Gohr
2444e3e87e4SAndreas Gohr        if($this->user) {
2454e3e87e4SAndreas Gohr            $headers['Authorization'] = 'Basic '.base64_encode($this->user.':'.$this->pass);
2464e3e87e4SAndreas Gohr        }
2474e3e87e4SAndreas Gohr        if($this->proxy_user) {
2484e3e87e4SAndreas Gohr            $headers['Proxy-Authorization'] = 'Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass);
2494e3e87e4SAndreas Gohr        }
2504e3e87e4SAndreas Gohr
2514e3e87e4SAndreas Gohr        // already connected?
2524e3e87e4SAndreas Gohr        $connectionId = $this->uniqueConnectionId($server,$port);
2534e3e87e4SAndreas Gohr        $this->debug('connection pool', self::$connections);
2544e3e87e4SAndreas Gohr        $socket = null;
2554e3e87e4SAndreas Gohr        if (isset(self::$connections[$connectionId])) {
2564e3e87e4SAndreas Gohr            $this->debug('reusing connection', $connectionId);
2574e3e87e4SAndreas Gohr            $socket = self::$connections[$connectionId];
2584e3e87e4SAndreas Gohr        }
2594e3e87e4SAndreas Gohr        if (is_null($socket) || feof($socket)) {
2604e3e87e4SAndreas Gohr            $this->debug('opening connection', $connectionId);
2614e3e87e4SAndreas Gohr            // open socket
2624e3e87e4SAndreas Gohr            $socket = @fsockopen($server,$port,$errno, $errstr, $this->timeout);
2634e3e87e4SAndreas Gohr            if (!$socket){
2644e3e87e4SAndreas Gohr                $this->status = -100;
2654e3e87e4SAndreas Gohr                $this->error = "Could not connect to $server:$port\n$errstr ($errno)";
2664e3e87e4SAndreas Gohr                return false;
2674e3e87e4SAndreas Gohr            }
2684e3e87e4SAndreas Gohr
2694e3e87e4SAndreas Gohr            // try establish a CONNECT tunnel for SSL
2704e3e87e4SAndreas Gohr            try {
2714e3e87e4SAndreas Gohr                if($this->ssltunnel($socket, $request_url)){
2724e3e87e4SAndreas Gohr                    // no keep alive for tunnels
2734e3e87e4SAndreas Gohr                    $this->keep_alive = false;
2744e3e87e4SAndreas Gohr                    // tunnel is authed already
2754e3e87e4SAndreas Gohr                    if(isset($headers['Proxy-Authentication'])) unset($headers['Proxy-Authentication']);
2764e3e87e4SAndreas Gohr                }
2774e3e87e4SAndreas Gohr            } catch (HTTPClientException $e) {
2784e3e87e4SAndreas Gohr                $this->status = $e->getCode();
2794e3e87e4SAndreas Gohr                $this->error = $e->getMessage();
2804e3e87e4SAndreas Gohr                fclose($socket);
2814e3e87e4SAndreas Gohr                return false;
2824e3e87e4SAndreas Gohr            }
2834e3e87e4SAndreas Gohr
2844e3e87e4SAndreas Gohr            // keep alive?
2854e3e87e4SAndreas Gohr            if ($this->keep_alive) {
2864e3e87e4SAndreas Gohr                self::$connections[$connectionId] = $socket;
2874e3e87e4SAndreas Gohr            } else {
2884e3e87e4SAndreas Gohr                unset(self::$connections[$connectionId]);
2894e3e87e4SAndreas Gohr            }
2904e3e87e4SAndreas Gohr        }
2914e3e87e4SAndreas Gohr
2924e3e87e4SAndreas Gohr        if ($this->keep_alive && !$this->useProxyForUrl($request_url)) {
2934e3e87e4SAndreas Gohr            // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive
2944e3e87e4SAndreas Gohr            // connection token to a proxy server. We still do keep the connection the
2954e3e87e4SAndreas Gohr            // proxy alive (well except for CONNECT tunnels)
2964e3e87e4SAndreas Gohr            $headers['Connection'] = 'Keep-Alive';
2974e3e87e4SAndreas Gohr        } else {
2984e3e87e4SAndreas Gohr            $headers['Connection'] = 'Close';
2994e3e87e4SAndreas Gohr        }
3004e3e87e4SAndreas Gohr
3014e3e87e4SAndreas Gohr        try {
3024e3e87e4SAndreas Gohr            //set non-blocking
3034e3e87e4SAndreas Gohr            stream_set_blocking($socket, 0);
3044e3e87e4SAndreas Gohr
3054e3e87e4SAndreas Gohr            // build request
3064e3e87e4SAndreas Gohr            $request  = "$method $request_url HTTP/".$this->http.HTTP_NL;
3074e3e87e4SAndreas Gohr            $request .= $this->buildHeaders($headers);
3084e3e87e4SAndreas Gohr            $request .= $this->getCookies();
3094e3e87e4SAndreas Gohr            $request .= HTTP_NL;
3104e3e87e4SAndreas Gohr            $request .= $data;
3114e3e87e4SAndreas Gohr
3124e3e87e4SAndreas Gohr            $this->debug('request',$request);
3134e3e87e4SAndreas Gohr            $this->sendData($socket, $request, 'request');
3144e3e87e4SAndreas Gohr
3154e3e87e4SAndreas Gohr            // read headers from socket
3164e3e87e4SAndreas Gohr            $r_headers = '';
3174e3e87e4SAndreas Gohr            do{
3184e3e87e4SAndreas Gohr                $r_line = $this->readLine($socket, 'headers');
3194e3e87e4SAndreas Gohr                $r_headers .= $r_line;
3204e3e87e4SAndreas Gohr            }while($r_line != "\r\n" && $r_line != "\n");
3214e3e87e4SAndreas Gohr
3224e3e87e4SAndreas Gohr            $this->debug('response headers',$r_headers);
3234e3e87e4SAndreas Gohr
3244e3e87e4SAndreas Gohr            // check if expected body size exceeds allowance
3254e3e87e4SAndreas Gohr            if($this->max_bodysize && preg_match('/\r?\nContent-Length:\s*(\d+)\r?\n/i',$r_headers,$match)){
3264e3e87e4SAndreas Gohr                if($match[1] > $this->max_bodysize){
3274e3e87e4SAndreas Gohr                    if ($this->max_bodysize_abort)
3284e3e87e4SAndreas Gohr                        throw new HTTPClientException('Reported content length exceeds allowed response size');
3294e3e87e4SAndreas Gohr                    else
3304e3e87e4SAndreas Gohr                        $this->error = 'Reported content length exceeds allowed response size';
3314e3e87e4SAndreas Gohr                }
3324e3e87e4SAndreas Gohr            }
3334e3e87e4SAndreas Gohr
3344e3e87e4SAndreas Gohr            // get Status
3354e3e87e4SAndreas Gohr            if (!preg_match('/^HTTP\/(\d\.\d)\s*(\d+).*?\n/s', $r_headers, $m))
3364e3e87e4SAndreas Gohr                throw new HTTPClientException('Server returned bad answer '.$r_headers);
3374e3e87e4SAndreas Gohr
3384e3e87e4SAndreas Gohr            $this->status = $m[2];
3394e3e87e4SAndreas Gohr
3404e3e87e4SAndreas Gohr            // handle headers and cookies
3414e3e87e4SAndreas Gohr            $this->resp_headers = $this->parseHeaders($r_headers);
3424e3e87e4SAndreas Gohr            if(isset($this->resp_headers['set-cookie'])){
3434e3e87e4SAndreas Gohr                foreach ((array) $this->resp_headers['set-cookie'] as $cookie){
344*fc9fd1daSAndreas Gohr                    list($cookie) = array_pad(explode(';', $cookie, 2), 2, '');
345*fc9fd1daSAndreas Gohr                    list($key, $val) = array_pad(explode('=', $cookie, 2), 2, '');
3464e3e87e4SAndreas Gohr                    $key = trim($key);
3474e3e87e4SAndreas Gohr                    if($val == 'deleted'){
3484e3e87e4SAndreas Gohr                        if(isset($this->cookies[$key])){
3494e3e87e4SAndreas Gohr                            unset($this->cookies[$key]);
3504e3e87e4SAndreas Gohr                        }
3514e3e87e4SAndreas Gohr                    }elseif($key){
3524e3e87e4SAndreas Gohr                        $this->cookies[$key] = $val;
3534e3e87e4SAndreas Gohr                    }
3544e3e87e4SAndreas Gohr                }
3554e3e87e4SAndreas Gohr            }
3564e3e87e4SAndreas Gohr
3574e3e87e4SAndreas Gohr            $this->debug('Object headers',$this->resp_headers);
3584e3e87e4SAndreas Gohr
3594e3e87e4SAndreas Gohr            // check server status code to follow redirect
3604e3e87e4SAndreas Gohr            if(in_array($this->status, [301, 302, 303, 307, 308])){
3614e3e87e4SAndreas Gohr                if (empty($this->resp_headers['location'])){
3624e3e87e4SAndreas Gohr                    throw new HTTPClientException('Redirect but no Location Header found');
3634e3e87e4SAndreas Gohr                }elseif($this->redirect_count == $this->max_redirect){
3644e3e87e4SAndreas Gohr                    throw new HTTPClientException('Maximum number of redirects exceeded');
3654e3e87e4SAndreas Gohr                }else{
3664e3e87e4SAndreas Gohr                    // close the connection because we don't handle content retrieval here
3674e3e87e4SAndreas Gohr                    // that's the easiest way to clean up the connection
3684e3e87e4SAndreas Gohr                    fclose($socket);
3694e3e87e4SAndreas Gohr                    unset(self::$connections[$connectionId]);
3704e3e87e4SAndreas Gohr
3714e3e87e4SAndreas Gohr                    $this->redirect_count++;
3724e3e87e4SAndreas Gohr                    $this->referer = $url;
3734e3e87e4SAndreas Gohr                    // handle non-RFC-compliant relative redirects
3744e3e87e4SAndreas Gohr                    if (!preg_match('/^http/i', $this->resp_headers['location'])){
3754e3e87e4SAndreas Gohr                        if($this->resp_headers['location'][0] != '/'){
3764e3e87e4SAndreas Gohr                            $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uriPort.
3774e3e87e4SAndreas Gohr                                dirname($path).'/'.$this->resp_headers['location'];
3784e3e87e4SAndreas Gohr                        }else{
3794e3e87e4SAndreas Gohr                            $this->resp_headers['location'] = $uri['scheme'].'://'.$uri['host'].':'.$uriPort.
3804e3e87e4SAndreas Gohr                                $this->resp_headers['location'];
3814e3e87e4SAndreas Gohr                        }
3824e3e87e4SAndreas Gohr                    }
3834e3e87e4SAndreas Gohr                    if($this->status == 307 || $this->status == 308) {
3844e3e87e4SAndreas Gohr                        // perform redirected request, same method as before (required by RFC)
3854e3e87e4SAndreas Gohr                        return $this->sendRequest($this->resp_headers['location'],$unencodedData,$method);
3864e3e87e4SAndreas Gohr                    }else{
3874e3e87e4SAndreas Gohr                        // perform redirected request, always via GET (required by RFC)
3884e3e87e4SAndreas Gohr                        return $this->sendRequest($this->resp_headers['location'],array(),'GET');
3894e3e87e4SAndreas Gohr                    }
3904e3e87e4SAndreas Gohr                }
3914e3e87e4SAndreas Gohr            }
3924e3e87e4SAndreas Gohr
3934e3e87e4SAndreas Gohr            // check if headers are as expected
3944e3e87e4SAndreas Gohr            if($this->header_regexp && !preg_match($this->header_regexp,$r_headers))
3954e3e87e4SAndreas Gohr                throw new HTTPClientException('The received headers did not match the given regexp');
3964e3e87e4SAndreas Gohr
3974e3e87e4SAndreas Gohr            //read body (with chunked encoding if needed)
3984e3e87e4SAndreas Gohr            $r_body    = '';
3994e3e87e4SAndreas Gohr            if(
4004e3e87e4SAndreas Gohr                (
4014e3e87e4SAndreas Gohr                    isset($this->resp_headers['transfer-encoding']) &&
4024e3e87e4SAndreas Gohr                    $this->resp_headers['transfer-encoding'] == 'chunked'
4034e3e87e4SAndreas Gohr                ) || (
4044e3e87e4SAndreas Gohr                    isset($this->resp_headers['transfer-coding']) &&
4054e3e87e4SAndreas Gohr                    $this->resp_headers['transfer-coding'] == 'chunked'
4064e3e87e4SAndreas Gohr                )
4074e3e87e4SAndreas Gohr            ) {
4084e3e87e4SAndreas Gohr                $abort = false;
4094e3e87e4SAndreas Gohr                do {
4104e3e87e4SAndreas Gohr                    $chunk_size = '';
4114e3e87e4SAndreas Gohr                    while (preg_match('/^[a-zA-Z0-9]?$/',$byte=$this->readData($socket,1,'chunk'))){
4124e3e87e4SAndreas Gohr                        // read chunksize until \r
4134e3e87e4SAndreas Gohr                        $chunk_size .= $byte;
4144e3e87e4SAndreas Gohr                        if (strlen($chunk_size) > 128) // set an abritrary limit on the size of chunks
4154e3e87e4SAndreas Gohr                            throw new HTTPClientException('Allowed response size exceeded');
4164e3e87e4SAndreas Gohr                    }
4174e3e87e4SAndreas Gohr                    $this->readLine($socket, 'chunk');     // readtrailing \n
4184e3e87e4SAndreas Gohr                    $chunk_size = hexdec($chunk_size);
4194e3e87e4SAndreas Gohr
4204e3e87e4SAndreas Gohr                    if($this->max_bodysize && $chunk_size+strlen($r_body) > $this->max_bodysize){
4214e3e87e4SAndreas Gohr                        if ($this->max_bodysize_abort)
4224e3e87e4SAndreas Gohr                            throw new HTTPClientException('Allowed response size exceeded');
4234e3e87e4SAndreas Gohr                        $this->error = 'Allowed response size exceeded';
4244e3e87e4SAndreas Gohr                        $chunk_size = $this->max_bodysize - strlen($r_body);
4254e3e87e4SAndreas Gohr                        $abort = true;
4264e3e87e4SAndreas Gohr                    }
4274e3e87e4SAndreas Gohr
4284e3e87e4SAndreas Gohr                    if ($chunk_size > 0) {
4294e3e87e4SAndreas Gohr                        $r_body .= $this->readData($socket, $chunk_size, 'chunk');
4304e3e87e4SAndreas Gohr                        $this->readData($socket, 2, 'chunk'); // read trailing \r\n
4314e3e87e4SAndreas Gohr                    }
4324e3e87e4SAndreas Gohr                } while ($chunk_size && !$abort);
4334e3e87e4SAndreas Gohr            }elseif(isset($this->resp_headers['content-length']) && !isset($this->resp_headers['transfer-encoding'])){
4344e3e87e4SAndreas Gohr                /* RFC 2616
4354e3e87e4SAndreas Gohr                 * If a message is received with both a Transfer-Encoding header field and a Content-Length
4364e3e87e4SAndreas Gohr                 * header field, the latter MUST be ignored.
4374e3e87e4SAndreas Gohr                 */
4384e3e87e4SAndreas Gohr
4394e3e87e4SAndreas Gohr                // read up to the content-length or max_bodysize
4404e3e87e4SAndreas Gohr                // for keep alive we need to read the whole message to clean up the socket for the next read
4414e3e87e4SAndreas Gohr                if(
4424e3e87e4SAndreas Gohr                    !$this->keep_alive &&
4434e3e87e4SAndreas Gohr                    $this->max_bodysize &&
4444e3e87e4SAndreas Gohr                    $this->max_bodysize < $this->resp_headers['content-length']
4454e3e87e4SAndreas Gohr                ) {
4464e3e87e4SAndreas Gohr                    $length = $this->max_bodysize + 1;
4474e3e87e4SAndreas Gohr                }else{
4484e3e87e4SAndreas Gohr                    $length = $this->resp_headers['content-length'];
4494e3e87e4SAndreas Gohr                }
4504e3e87e4SAndreas Gohr
4514e3e87e4SAndreas Gohr                $r_body = $this->readData($socket, $length, 'response (content-length limited)', true);
4524e3e87e4SAndreas Gohr            }elseif( !isset($this->resp_headers['transfer-encoding']) && $this->max_bodysize && !$this->keep_alive){
4534e3e87e4SAndreas Gohr                $r_body = $this->readData($socket, $this->max_bodysize+1, 'response (content-length limited)', true);
4544e3e87e4SAndreas Gohr            } elseif ((int)$this->status === 204) {
4554e3e87e4SAndreas Gohr                // request has no content
4564e3e87e4SAndreas Gohr            } else{
4574e3e87e4SAndreas Gohr                // read entire socket
4584e3e87e4SAndreas Gohr                while (!feof($socket)) {
4594e3e87e4SAndreas Gohr                    $r_body .= $this->readData($socket, 4096, 'response (unlimited)', true);
4604e3e87e4SAndreas Gohr                }
4614e3e87e4SAndreas Gohr            }
4624e3e87e4SAndreas Gohr
4634e3e87e4SAndreas Gohr            // recheck body size, we might have read max_bodysize+1 or even the whole body, so we abort late here
4644e3e87e4SAndreas Gohr            if($this->max_bodysize){
4654e3e87e4SAndreas Gohr                if(strlen($r_body) > $this->max_bodysize){
4664e3e87e4SAndreas Gohr                    if ($this->max_bodysize_abort) {
4674e3e87e4SAndreas Gohr                        throw new HTTPClientException('Allowed response size exceeded');
4684e3e87e4SAndreas Gohr                    } else {
4694e3e87e4SAndreas Gohr                        $this->error = 'Allowed response size exceeded';
4704e3e87e4SAndreas Gohr                    }
4714e3e87e4SAndreas Gohr                }
4724e3e87e4SAndreas Gohr            }
4734e3e87e4SAndreas Gohr
4744e3e87e4SAndreas Gohr        } catch (HTTPClientException $err) {
4754e3e87e4SAndreas Gohr            $this->error = $err->getMessage();
4764e3e87e4SAndreas Gohr            if ($err->getCode())
4774e3e87e4SAndreas Gohr                $this->status = $err->getCode();
4784e3e87e4SAndreas Gohr            unset(self::$connections[$connectionId]);
4794e3e87e4SAndreas Gohr            fclose($socket);
4804e3e87e4SAndreas Gohr            return false;
4814e3e87e4SAndreas Gohr        }
4824e3e87e4SAndreas Gohr
4834e3e87e4SAndreas Gohr        if (!$this->keep_alive ||
4844e3e87e4SAndreas Gohr            (isset($this->resp_headers['connection']) && $this->resp_headers['connection'] == 'Close')) {
4854e3e87e4SAndreas Gohr            // close socket
4864e3e87e4SAndreas Gohr            fclose($socket);
4874e3e87e4SAndreas Gohr            unset(self::$connections[$connectionId]);
4884e3e87e4SAndreas Gohr        }
4894e3e87e4SAndreas Gohr
4904e3e87e4SAndreas Gohr        // decode gzip if needed
4914e3e87e4SAndreas Gohr        if(isset($this->resp_headers['content-encoding']) &&
4924e3e87e4SAndreas Gohr            $this->resp_headers['content-encoding'] == 'gzip' &&
4934e3e87e4SAndreas Gohr            strlen($r_body) > 10 && substr($r_body,0,3)=="\x1f\x8b\x08"){
4944e3e87e4SAndreas Gohr            $this->resp_body = @gzinflate(substr($r_body, 10));
4954e3e87e4SAndreas Gohr            if($this->resp_body === false){
4964e3e87e4SAndreas Gohr                $this->error = 'Failed to decompress gzip encoded content';
4974e3e87e4SAndreas Gohr                $this->resp_body = $r_body;
4984e3e87e4SAndreas Gohr            }
4994e3e87e4SAndreas Gohr        }else{
5004e3e87e4SAndreas Gohr            $this->resp_body = $r_body;
5014e3e87e4SAndreas Gohr        }
5024e3e87e4SAndreas Gohr
5034e3e87e4SAndreas Gohr        $this->debug('response body',$this->resp_body);
5044e3e87e4SAndreas Gohr        $this->redirect_count = 0;
5054e3e87e4SAndreas Gohr        return true;
5064e3e87e4SAndreas Gohr    }
5074e3e87e4SAndreas Gohr
5084e3e87e4SAndreas Gohr    /**
5094e3e87e4SAndreas Gohr     * Tries to establish a CONNECT tunnel via Proxy
5104e3e87e4SAndreas Gohr     *
5114e3e87e4SAndreas Gohr     * Protocol, Servername and Port will be stripped from the request URL when a successful CONNECT happened
5124e3e87e4SAndreas Gohr     *
5134e3e87e4SAndreas Gohr     * @param resource &$socket
5144e3e87e4SAndreas Gohr     * @param string   &$requesturl
5154e3e87e4SAndreas Gohr     * @throws HTTPClientException when a tunnel is needed but could not be established
5164e3e87e4SAndreas Gohr     * @return bool true if a tunnel was established
5174e3e87e4SAndreas Gohr     */
5184e3e87e4SAndreas Gohr    protected function ssltunnel(&$socket, &$requesturl){
5194e3e87e4SAndreas Gohr        if(!$this->useProxyForUrl($requesturl)) return false;
5204e3e87e4SAndreas Gohr        $requestinfo = parse_url($requesturl);
5214e3e87e4SAndreas Gohr        if($requestinfo['scheme'] != 'https') return false;
5224e3e87e4SAndreas Gohr        if(empty($requestinfo['port'])) $requestinfo['port'] = 443;
5234e3e87e4SAndreas Gohr
5244e3e87e4SAndreas Gohr        // build request
5254e3e87e4SAndreas Gohr        $request  = "CONNECT {$requestinfo['host']}:{$requestinfo['port']} HTTP/1.0".HTTP_NL;
5264e3e87e4SAndreas Gohr        $request .= "Host: {$requestinfo['host']}".HTTP_NL;
5274e3e87e4SAndreas Gohr        if($this->proxy_user) {
5284e3e87e4SAndreas Gohr            $request .= 'Proxy-Authorization: Basic '.base64_encode($this->proxy_user.':'.$this->proxy_pass).HTTP_NL;
5294e3e87e4SAndreas Gohr        }
5304e3e87e4SAndreas Gohr        $request .= HTTP_NL;
5314e3e87e4SAndreas Gohr
5324e3e87e4SAndreas Gohr        $this->debug('SSL Tunnel CONNECT',$request);
5334e3e87e4SAndreas Gohr        $this->sendData($socket, $request, 'SSL Tunnel CONNECT');
5344e3e87e4SAndreas Gohr
5354e3e87e4SAndreas Gohr        // read headers from socket
5364e3e87e4SAndreas Gohr        $r_headers = '';
5374e3e87e4SAndreas Gohr        do{
5384e3e87e4SAndreas Gohr            $r_line = $this->readLine($socket, 'headers');
5394e3e87e4SAndreas Gohr            $r_headers .= $r_line;
5404e3e87e4SAndreas Gohr        }while($r_line != "\r\n" && $r_line != "\n");
5414e3e87e4SAndreas Gohr
5424e3e87e4SAndreas Gohr        $this->debug('SSL Tunnel Response',$r_headers);
5434e3e87e4SAndreas Gohr        if(preg_match('/^HTTP\/1\.[01] 200/i',$r_headers)){
5444e3e87e4SAndreas Gohr            // set correct peer name for verification (enabled since PHP 5.6)
5454e3e87e4SAndreas Gohr            stream_context_set_option($socket, 'ssl', 'peer_name', $requestinfo['host']);
5464e3e87e4SAndreas Gohr
5474e3e87e4SAndreas Gohr            // SSLv3 is broken, use only TLS connections.
5484e3e87e4SAndreas Gohr            // @link https://bugs.php.net/69195
5494e3e87e4SAndreas Gohr            if (PHP_VERSION_ID >= 50600 && PHP_VERSION_ID <= 50606) {
5504e3e87e4SAndreas Gohr                $cryptoMethod = STREAM_CRYPTO_METHOD_TLS_CLIENT;
5514e3e87e4SAndreas Gohr            } else {
5524e3e87e4SAndreas Gohr                // actually means neither SSLv2 nor SSLv3
5534e3e87e4SAndreas Gohr                $cryptoMethod = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
5544e3e87e4SAndreas Gohr            }
5554e3e87e4SAndreas Gohr
5564e3e87e4SAndreas Gohr            if (@stream_socket_enable_crypto($socket, true, $cryptoMethod)) {
5574e3e87e4SAndreas Gohr                $requesturl = $requestinfo['path'].
5584e3e87e4SAndreas Gohr                    (!empty($requestinfo['query'])?'?'.$requestinfo['query']:'');
5594e3e87e4SAndreas Gohr                return true;
5604e3e87e4SAndreas Gohr            }
5614e3e87e4SAndreas Gohr
5624e3e87e4SAndreas Gohr            throw new HTTPClientException(
5634e3e87e4SAndreas Gohr                'Failed to set up crypto for secure connection to '.$requestinfo['host'], -151
5644e3e87e4SAndreas Gohr            );
5654e3e87e4SAndreas Gohr        }
5664e3e87e4SAndreas Gohr
5674e3e87e4SAndreas Gohr        throw new HTTPClientException('Failed to establish secure proxy connection', -150);
5684e3e87e4SAndreas Gohr    }
5694e3e87e4SAndreas Gohr
5704e3e87e4SAndreas Gohr    /**
5714e3e87e4SAndreas Gohr     * Safely write data to a socket
5724e3e87e4SAndreas Gohr     *
5734e3e87e4SAndreas Gohr     * @param  resource $socket     An open socket handle
5744e3e87e4SAndreas Gohr     * @param  string   $data       The data to write
5754e3e87e4SAndreas Gohr     * @param  string   $message    Description of what is being read
5764e3e87e4SAndreas Gohr     * @throws HTTPClientException
5774e3e87e4SAndreas Gohr     *
5784e3e87e4SAndreas Gohr     * @author Tom N Harris <tnharris@whoopdedo.org>
5794e3e87e4SAndreas Gohr     */
5804e3e87e4SAndreas Gohr    protected function sendData($socket, $data, $message) {
5814e3e87e4SAndreas Gohr        // send request
5824e3e87e4SAndreas Gohr        $towrite = strlen($data);
5834e3e87e4SAndreas Gohr        $written = 0;
5844e3e87e4SAndreas Gohr        while($written < $towrite){
5854e3e87e4SAndreas Gohr            // check timeout
5864e3e87e4SAndreas Gohr            $time_used = $this->time() - $this->start;
5874e3e87e4SAndreas Gohr            if($time_used > $this->timeout)
5884e3e87e4SAndreas Gohr                throw new HTTPClientException(sprintf('Timeout while sending %s (%.3fs)',$message, $time_used), -100);
5894e3e87e4SAndreas Gohr            if(feof($socket))
5904e3e87e4SAndreas Gohr                throw new HTTPClientException("Socket disconnected while writing $message");
5914e3e87e4SAndreas Gohr
5924e3e87e4SAndreas Gohr            // select parameters
5934e3e87e4SAndreas Gohr            $sel_r = null;
5944e3e87e4SAndreas Gohr            $sel_w = array($socket);
5954e3e87e4SAndreas Gohr            $sel_e = null;
5964e3e87e4SAndreas Gohr            // wait for stream ready or timeout (1sec)
5974e3e87e4SAndreas Gohr            if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
5984e3e87e4SAndreas Gohr                usleep(1000);
5994e3e87e4SAndreas Gohr                continue;
6004e3e87e4SAndreas Gohr            }
6014e3e87e4SAndreas Gohr
6024e3e87e4SAndreas Gohr            // write to stream
6034e3e87e4SAndreas Gohr            $nbytes = fwrite($socket, substr($data,$written,4096));
6044e3e87e4SAndreas Gohr            if($nbytes === false)
6054e3e87e4SAndreas Gohr                throw new HTTPClientException("Failed writing to socket while sending $message", -100);
6064e3e87e4SAndreas Gohr            $written += $nbytes;
6074e3e87e4SAndreas Gohr        }
6084e3e87e4SAndreas Gohr    }
6094e3e87e4SAndreas Gohr
6104e3e87e4SAndreas Gohr    /**
6114e3e87e4SAndreas Gohr     * Safely read data from a socket
6124e3e87e4SAndreas Gohr     *
6134e3e87e4SAndreas Gohr     * Reads up to a given number of bytes or throws an exception if the
6144e3e87e4SAndreas Gohr     * response times out or ends prematurely.
6154e3e87e4SAndreas Gohr     *
6164e3e87e4SAndreas Gohr     * @param  resource $socket     An open socket handle in non-blocking mode
6174e3e87e4SAndreas Gohr     * @param  int      $nbytes     Number of bytes to read
6184e3e87e4SAndreas Gohr     * @param  string   $message    Description of what is being read
6194e3e87e4SAndreas Gohr     * @param  bool     $ignore_eof End-of-file is not an error if this is set
6204e3e87e4SAndreas Gohr     * @throws HTTPClientException
6214e3e87e4SAndreas Gohr     * @return string
6224e3e87e4SAndreas Gohr     *
6234e3e87e4SAndreas Gohr     * @author Tom N Harris <tnharris@whoopdedo.org>
6244e3e87e4SAndreas Gohr     */
6254e3e87e4SAndreas Gohr    protected function readData($socket, $nbytes, $message, $ignore_eof = false) {
6264e3e87e4SAndreas Gohr        $r_data = '';
6274e3e87e4SAndreas Gohr        // Does not return immediately so timeout and eof can be checked
6284e3e87e4SAndreas Gohr        if ($nbytes < 0) $nbytes = 0;
6294e3e87e4SAndreas Gohr        $to_read = $nbytes;
6304e3e87e4SAndreas Gohr        do {
6314e3e87e4SAndreas Gohr            $time_used = $this->time() - $this->start;
6324e3e87e4SAndreas Gohr            if ($time_used > $this->timeout)
6334e3e87e4SAndreas Gohr                throw new HTTPClientException(
6344e3e87e4SAndreas Gohr                    sprintf('Timeout while reading %s after %d bytes (%.3fs)', $message,
6354e3e87e4SAndreas Gohr                        strlen($r_data), $time_used), -100);
6364e3e87e4SAndreas Gohr            if(feof($socket)) {
6374e3e87e4SAndreas Gohr                if(!$ignore_eof)
6384e3e87e4SAndreas Gohr                    throw new HTTPClientException("Premature End of File (socket) while reading $message");
6394e3e87e4SAndreas Gohr                break;
6404e3e87e4SAndreas Gohr            }
6414e3e87e4SAndreas Gohr
6424e3e87e4SAndreas Gohr            if ($to_read > 0) {
6434e3e87e4SAndreas Gohr                // select parameters
6444e3e87e4SAndreas Gohr                $sel_r = array($socket);
6454e3e87e4SAndreas Gohr                $sel_w = null;
6464e3e87e4SAndreas Gohr                $sel_e = null;
6474e3e87e4SAndreas Gohr                // wait for stream ready or timeout (1sec)
6484e3e87e4SAndreas Gohr                if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
6494e3e87e4SAndreas Gohr                    usleep(1000);
6504e3e87e4SAndreas Gohr                    continue;
6514e3e87e4SAndreas Gohr                }
6524e3e87e4SAndreas Gohr
6534e3e87e4SAndreas Gohr                $bytes = fread($socket, $to_read);
6544e3e87e4SAndreas Gohr                if($bytes === false)
6554e3e87e4SAndreas Gohr                    throw new HTTPClientException("Failed reading from socket while reading $message", -100);
6564e3e87e4SAndreas Gohr                $r_data .= $bytes;
6574e3e87e4SAndreas Gohr                $to_read -= strlen($bytes);
6584e3e87e4SAndreas Gohr            }
6594e3e87e4SAndreas Gohr        } while ($to_read > 0 && strlen($r_data) < $nbytes);
6604e3e87e4SAndreas Gohr        return $r_data;
6614e3e87e4SAndreas Gohr    }
6624e3e87e4SAndreas Gohr
6634e3e87e4SAndreas Gohr    /**
6644e3e87e4SAndreas Gohr     * Safely read a \n-terminated line from a socket
6654e3e87e4SAndreas Gohr     *
6664e3e87e4SAndreas Gohr     * Always returns a complete line, including the terminating \n.
6674e3e87e4SAndreas Gohr     *
6684e3e87e4SAndreas Gohr     * @param  resource $socket     An open socket handle in non-blocking mode
6694e3e87e4SAndreas Gohr     * @param  string   $message    Description of what is being read
6704e3e87e4SAndreas Gohr     * @throws HTTPClientException
6714e3e87e4SAndreas Gohr     * @return string
6724e3e87e4SAndreas Gohr     *
6734e3e87e4SAndreas Gohr     * @author Tom N Harris <tnharris@whoopdedo.org>
6744e3e87e4SAndreas Gohr     */
6754e3e87e4SAndreas Gohr    protected function readLine($socket, $message) {
6764e3e87e4SAndreas Gohr        $r_data = '';
6774e3e87e4SAndreas Gohr        do {
6784e3e87e4SAndreas Gohr            $time_used = $this->time() - $this->start;
6794e3e87e4SAndreas Gohr            if ($time_used > $this->timeout)
6804e3e87e4SAndreas Gohr                throw new HTTPClientException(
6814e3e87e4SAndreas Gohr                    sprintf('Timeout while reading %s (%.3fs) >%s<', $message, $time_used, $r_data),
6824e3e87e4SAndreas Gohr                    -100);
6834e3e87e4SAndreas Gohr            if(feof($socket))
6844e3e87e4SAndreas Gohr                throw new HTTPClientException("Premature End of File (socket) while reading $message");
6854e3e87e4SAndreas Gohr
6864e3e87e4SAndreas Gohr            // select parameters
6874e3e87e4SAndreas Gohr            $sel_r = array($socket);
6884e3e87e4SAndreas Gohr            $sel_w = null;
6894e3e87e4SAndreas Gohr            $sel_e = null;
6904e3e87e4SAndreas Gohr            // wait for stream ready or timeout (1sec)
6914e3e87e4SAndreas Gohr            if(@stream_select($sel_r,$sel_w,$sel_e,1) === false){
6924e3e87e4SAndreas Gohr                usleep(1000);
6934e3e87e4SAndreas Gohr                continue;
6944e3e87e4SAndreas Gohr            }
6954e3e87e4SAndreas Gohr
6964e3e87e4SAndreas Gohr            $r_data = fgets($socket, 1024);
6974e3e87e4SAndreas Gohr        } while (!preg_match('/\n$/',$r_data));
6984e3e87e4SAndreas Gohr        return $r_data;
6994e3e87e4SAndreas Gohr    }
7004e3e87e4SAndreas Gohr
7014e3e87e4SAndreas Gohr    /**
7024e3e87e4SAndreas Gohr     * print debug info
7034e3e87e4SAndreas Gohr     *
7044e3e87e4SAndreas Gohr     * Uses _debug_text or _debug_html depending on the SAPI name
7054e3e87e4SAndreas Gohr     *
7064e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
7074e3e87e4SAndreas Gohr     *
7084e3e87e4SAndreas Gohr     * @param string $info
7094e3e87e4SAndreas Gohr     * @param mixed  $var
7104e3e87e4SAndreas Gohr     */
7114e3e87e4SAndreas Gohr    protected function debug($info,$var=null){
7124e3e87e4SAndreas Gohr        if(!$this->debug) return;
7134e3e87e4SAndreas Gohr        if(php_sapi_name() == 'cli'){
7144e3e87e4SAndreas Gohr            $this->debugText($info, $var);
7154e3e87e4SAndreas Gohr        }else{
7164e3e87e4SAndreas Gohr            $this->debugHtml($info, $var);
7174e3e87e4SAndreas Gohr        }
7184e3e87e4SAndreas Gohr    }
7194e3e87e4SAndreas Gohr
7204e3e87e4SAndreas Gohr    /**
7214e3e87e4SAndreas Gohr     * print debug info as HTML
7224e3e87e4SAndreas Gohr     *
7234e3e87e4SAndreas Gohr     * @param string $info
7244e3e87e4SAndreas Gohr     * @param mixed  $var
7254e3e87e4SAndreas Gohr     */
7264e3e87e4SAndreas Gohr    protected function debugHtml($info, $var=null){
7274e3e87e4SAndreas Gohr        print '<b>'.$info.'</b> '.($this->time() - $this->start).'s<br />';
7284e3e87e4SAndreas Gohr        if(!is_null($var)){
7294e3e87e4SAndreas Gohr            ob_start();
7304e3e87e4SAndreas Gohr            print_r($var);
7314e3e87e4SAndreas Gohr            $content = htmlspecialchars(ob_get_contents());
7324e3e87e4SAndreas Gohr            ob_end_clean();
7334e3e87e4SAndreas Gohr            print '<pre>'.$content.'</pre>';
7344e3e87e4SAndreas Gohr        }
7354e3e87e4SAndreas Gohr    }
7364e3e87e4SAndreas Gohr
7374e3e87e4SAndreas Gohr    /**
7384e3e87e4SAndreas Gohr     * prints debug info as plain text
7394e3e87e4SAndreas Gohr     *
7404e3e87e4SAndreas Gohr     * @param string $info
7414e3e87e4SAndreas Gohr     * @param mixed  $var
7424e3e87e4SAndreas Gohr     */
7434e3e87e4SAndreas Gohr    protected function debugText($info, $var=null){
7444e3e87e4SAndreas Gohr        print '*'.$info.'* '.($this->time() - $this->start)."s\n";
7454e3e87e4SAndreas Gohr        if(!is_null($var)) print_r($var);
7464e3e87e4SAndreas Gohr        print "\n-----------------------------------------------\n";
7474e3e87e4SAndreas Gohr    }
7484e3e87e4SAndreas Gohr
7494e3e87e4SAndreas Gohr    /**
7504e3e87e4SAndreas Gohr     * Return current timestamp in microsecond resolution
7514e3e87e4SAndreas Gohr     *
7524e3e87e4SAndreas Gohr     * @return float
7534e3e87e4SAndreas Gohr     */
7544e3e87e4SAndreas Gohr    protected static function time(){
7554e3e87e4SAndreas Gohr        list($usec, $sec) = explode(" ", microtime());
7564e3e87e4SAndreas Gohr        return ((float)$usec + (float)$sec);
7574e3e87e4SAndreas Gohr    }
7584e3e87e4SAndreas Gohr
7594e3e87e4SAndreas Gohr    /**
7604e3e87e4SAndreas Gohr     * convert given header string to Header array
7614e3e87e4SAndreas Gohr     *
7624e3e87e4SAndreas Gohr     * All Keys are lowercased.
7634e3e87e4SAndreas Gohr     *
7644e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
7654e3e87e4SAndreas Gohr     *
7664e3e87e4SAndreas Gohr     * @param string $string
7674e3e87e4SAndreas Gohr     * @return array
7684e3e87e4SAndreas Gohr     */
7694e3e87e4SAndreas Gohr    protected function parseHeaders($string){
7704e3e87e4SAndreas Gohr        $headers = array();
7714e3e87e4SAndreas Gohr        $lines = explode("\n",$string);
7724e3e87e4SAndreas Gohr        array_shift($lines); //skip first line (status)
7734e3e87e4SAndreas Gohr        foreach($lines as $line){
774*fc9fd1daSAndreas Gohr            list($key, $val) = array_pad(explode(':', $line, 2), 2, '');
7754e3e87e4SAndreas Gohr            $key = trim($key);
7764e3e87e4SAndreas Gohr            $val = trim($val);
7774e3e87e4SAndreas Gohr            $key = strtolower($key);
7784e3e87e4SAndreas Gohr            if(!$key) continue;
7794e3e87e4SAndreas Gohr            if(isset($headers[$key])){
7804e3e87e4SAndreas Gohr                if(is_array($headers[$key])){
7814e3e87e4SAndreas Gohr                    $headers[$key][] = $val;
7824e3e87e4SAndreas Gohr                }else{
7834e3e87e4SAndreas Gohr                    $headers[$key] = array($headers[$key],$val);
7844e3e87e4SAndreas Gohr                }
7854e3e87e4SAndreas Gohr            }else{
7864e3e87e4SAndreas Gohr                $headers[$key] = $val;
7874e3e87e4SAndreas Gohr            }
7884e3e87e4SAndreas Gohr        }
7894e3e87e4SAndreas Gohr        return $headers;
7904e3e87e4SAndreas Gohr    }
7914e3e87e4SAndreas Gohr
7924e3e87e4SAndreas Gohr    /**
7934e3e87e4SAndreas Gohr     * convert given header array to header string
7944e3e87e4SAndreas Gohr     *
7954e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
7964e3e87e4SAndreas Gohr     *
7974e3e87e4SAndreas Gohr     * @param array $headers
7984e3e87e4SAndreas Gohr     * @return string
7994e3e87e4SAndreas Gohr     */
8004e3e87e4SAndreas Gohr    protected function buildHeaders($headers){
8014e3e87e4SAndreas Gohr        $string = '';
8024e3e87e4SAndreas Gohr        foreach($headers as $key => $value){
8034e3e87e4SAndreas Gohr            if($value === '') continue;
8044e3e87e4SAndreas Gohr            $string .= $key.': '.$value.HTTP_NL;
8054e3e87e4SAndreas Gohr        }
8064e3e87e4SAndreas Gohr        return $string;
8074e3e87e4SAndreas Gohr    }
8084e3e87e4SAndreas Gohr
8094e3e87e4SAndreas Gohr    /**
8104e3e87e4SAndreas Gohr     * get cookies as http header string
8114e3e87e4SAndreas Gohr     *
8124e3e87e4SAndreas Gohr     * @author Andreas Goetz <cpuidle@gmx.de>
8134e3e87e4SAndreas Gohr     *
8144e3e87e4SAndreas Gohr     * @return string
8154e3e87e4SAndreas Gohr     */
8164e3e87e4SAndreas Gohr    protected function getCookies(){
8174e3e87e4SAndreas Gohr        $headers = '';
8184e3e87e4SAndreas Gohr        foreach ($this->cookies as $key => $val){
8194e3e87e4SAndreas Gohr            $headers .= "$key=$val; ";
8204e3e87e4SAndreas Gohr        }
8214e3e87e4SAndreas Gohr        $headers = substr($headers, 0, -2);
8224e3e87e4SAndreas Gohr        if ($headers) $headers = "Cookie: $headers".HTTP_NL;
8234e3e87e4SAndreas Gohr        return $headers;
8244e3e87e4SAndreas Gohr    }
8254e3e87e4SAndreas Gohr
8264e3e87e4SAndreas Gohr    /**
8274e3e87e4SAndreas Gohr     * Encode data for posting
8284e3e87e4SAndreas Gohr     *
8294e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
8304e3e87e4SAndreas Gohr     *
8314e3e87e4SAndreas Gohr     * @param array $data
8324e3e87e4SAndreas Gohr     * @return string
8334e3e87e4SAndreas Gohr     */
8344e3e87e4SAndreas Gohr    protected function postEncode($data){
8354e3e87e4SAndreas Gohr        return http_build_query($data,'','&');
8364e3e87e4SAndreas Gohr    }
8374e3e87e4SAndreas Gohr
8384e3e87e4SAndreas Gohr    /**
8394e3e87e4SAndreas Gohr     * Encode data for posting using multipart encoding
8404e3e87e4SAndreas Gohr     *
8414e3e87e4SAndreas Gohr     * @fixme use of urlencode might be wrong here
8424e3e87e4SAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
8434e3e87e4SAndreas Gohr     *
8444e3e87e4SAndreas Gohr     * @param array $data
8454e3e87e4SAndreas Gohr     * @return string
8464e3e87e4SAndreas Gohr     */
8474e3e87e4SAndreas Gohr    protected function postMultipartEncode($data){
8484e3e87e4SAndreas Gohr        $boundary = '--'.$this->boundary;
8494e3e87e4SAndreas Gohr        $out = '';
8504e3e87e4SAndreas Gohr        foreach($data as $key => $val){
8514e3e87e4SAndreas Gohr            $out .= $boundary.HTTP_NL;
8524e3e87e4SAndreas Gohr            if(!is_array($val)){
8534e3e87e4SAndreas Gohr                $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"'.HTTP_NL;
8544e3e87e4SAndreas Gohr                $out .= HTTP_NL; // end of headers
8554e3e87e4SAndreas Gohr                $out .= $val;
8564e3e87e4SAndreas Gohr                $out .= HTTP_NL;
8574e3e87e4SAndreas Gohr            }else{
8584e3e87e4SAndreas Gohr                $out .= 'Content-Disposition: form-data; name="'.urlencode($key).'"';
8594e3e87e4SAndreas Gohr                if($val['filename']) $out .= '; filename="'.urlencode($val['filename']).'"';
8604e3e87e4SAndreas Gohr                $out .= HTTP_NL;
8614e3e87e4SAndreas Gohr                if($val['mimetype']) $out .= 'Content-Type: '.$val['mimetype'].HTTP_NL;
8624e3e87e4SAndreas Gohr                $out .= HTTP_NL; // end of headers
8634e3e87e4SAndreas Gohr                $out .= $val['body'];
8644e3e87e4SAndreas Gohr                $out .= HTTP_NL;
8654e3e87e4SAndreas Gohr            }
8664e3e87e4SAndreas Gohr        }
8674e3e87e4SAndreas Gohr        $out .= "$boundary--".HTTP_NL;
8684e3e87e4SAndreas Gohr        return $out;
8694e3e87e4SAndreas Gohr    }
8704e3e87e4SAndreas Gohr
8714e3e87e4SAndreas Gohr    /**
8724e3e87e4SAndreas Gohr     * Generates a unique identifier for a connection.
8734e3e87e4SAndreas Gohr     *
8744e3e87e4SAndreas Gohr     * @param  string $server
8754e3e87e4SAndreas Gohr     * @param  string $port
8764e3e87e4SAndreas Gohr     * @return string unique identifier
8774e3e87e4SAndreas Gohr     */
8784e3e87e4SAndreas Gohr    protected function uniqueConnectionId($server, $port) {
8794e3e87e4SAndreas Gohr        return "$server:$port";
8804e3e87e4SAndreas Gohr    }
8814e3e87e4SAndreas Gohr
8824e3e87e4SAndreas Gohr    /**
8834e3e87e4SAndreas Gohr     * Should the Proxy be used for the given URL?
8844e3e87e4SAndreas Gohr     *
8854e3e87e4SAndreas Gohr     * Checks the exceptions
8864e3e87e4SAndreas Gohr     *
8874e3e87e4SAndreas Gohr     * @param string $url
8884e3e87e4SAndreas Gohr     * @return bool
8894e3e87e4SAndreas Gohr     */
8904e3e87e4SAndreas Gohr    protected function useProxyForUrl($url) {
8914e3e87e4SAndreas Gohr        return $this->proxy_host && (!$this->proxy_except || !preg_match('/' . $this->proxy_except . '/i', $url));
8924e3e87e4SAndreas Gohr    }
8934e3e87e4SAndreas Gohr}
894