HTTPClient   F
last analyzed

Complexity

Total Complexity 165

Size/Duplication

Total Lines 865
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 865
rs 1.735
c 0
b 0
f 0
wmc 165
lcom 1
cbo 1

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 2
A get() 0 6 6
A dget() 0 9 2
A post() 0 5 4
C ssltunnel() 0 51 12
B sendData() 0 29 6
B readData() 0 37 10
A readLine() 0 25 5
A debug() 0 8 3
A debugHtml() 0 10 2
A debugText() 0 5 2
A time() 0 4 1
A parseHeaders() 0 22 5
A buildHeaders() 0 8 3
A getCookies() 0 9 3
A postEncode() 0 3 1
A postMultipartEncode() 0 23 5
A uniqueConnectionId() 0 3 1
A useProxyForUrl() 0 3 3
F sendRequest() 0 334 89

How to fix   Complexity   

Complex Class

Complex classes like HTTPClient often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HTTPClient, and based on these observations, apply Extract Interface, too.

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