Failed Conditions
Push — refctorHTTPCLient ( 0efa8d...5a8d6e )
by Michael
04:15
created

HTTPClient::sendRequest()   F

Complexity

Conditions 91
Paths > 20000

Size

Total Lines 336

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 91
nc 4294967295
nop 3
dl 0
loc 336
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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