Failed Conditions
Push — stable ( 017e16...b83837 )
by
unknown
07:54 queued 02:55
created

HTTPClient::postMultipartEncode()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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