Completed
Push — sidebaracl ( 7a112d...7c3e4a )
by Andreas
04:38
created

inc/HTTPClient.php (7 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

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
The property proxy_host 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...
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