Passed
Push — master ( 593257...e1cd2d )
by Gaetano
03:24
created

Http::decodeChunked()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 24
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 43
rs 9.536
ccs 0
cts 25
cp 0
crap 20
1
<?php
2
3
namespace PhpXmlRpc\Helper;
4
5
use PhpXmlRpc\PhpXmlRpc;
6
7
class Http
8
{
9
    /**
10
     * Decode a string that is encoded with "chunked" transfer encoding as defined in rfc2068 par. 19.4.6
11
     * Code shamelessly stolen from nusoap library by Dietrich Ayala.
12
     *
13
     * @param string $buffer the string to be decoded
14
     *
15
     * @return string
16
     */
17
    public static function decodeChunked($buffer)
18
    {
19
        // length := 0
20
        $length = 0;
21
        $new = '';
22
23
        // read chunk-size, chunk-extension (if any) and crlf
24
        // get the position of the linebreak
25
        $chunkEnd = strpos($buffer, "\r\n") + 2;
26
        $temp = substr($buffer, 0, $chunkEnd);
27
        $chunkSize = hexdec(trim($temp));
28
        $chunkStart = $chunkEnd;
29
        while ($chunkSize > 0) {
30
            $chunkEnd = strpos($buffer, "\r\n", $chunkStart + $chunkSize);
0 ignored issues
show
Bug introduced by
$chunkStart + $chunkSize of type double is incompatible with the type integer expected by parameter $offset of strpos(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

30
            $chunkEnd = strpos($buffer, "\r\n", /** @scrutinizer ignore-type */ $chunkStart + $chunkSize);
Loading history...
31
32
            // just in case we got a broken connection
33
            if ($chunkEnd == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $chunkEnd of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
34
                $chunk = substr($buffer, $chunkStart);
35
                // append chunk-data to entity-body
36
                $new .= $chunk;
37
                $length += strlen($chunk);
38
                break;
39
            }
40
41
            // read chunk-data and crlf
42
            $chunk = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
43
            // append chunk-data to entity-body
44
            $new .= $chunk;
45
            // length := length + chunk-size
46
            $length += strlen($chunk);
47
            // read chunk-size and crlf
48
            $chunkStart = $chunkEnd + 2;
49
50
            $chunkEnd = strpos($buffer, "\r\n", $chunkStart) + 2;
51
            if ($chunkEnd == false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $chunkEnd of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
52
                break; //just in case we got a broken connection
53
            }
54
            $temp = substr($buffer, $chunkStart, $chunkEnd - $chunkStart);
55
            $chunkSize = hexdec(trim($temp));
56
            $chunkStart = $chunkEnd;
57
        }
58
59
        return $new;
60
    }
61
62
    /**
63
     * Parses HTTP an http response headers and separates them from the body.
64
     *
65
     * @param string $data the http response,headers and body. It will be stripped of headers
66
     * @param bool $headersProcessed when true, we assume that response inflating and dechunking has been already carried out
67
     *
68
     * @return array with keys 'headers' and 'cookies'
69
     * @throws \Exception
70
     */
71 498
    public function parseResponseHeaders(&$data, $headersProcessed = false, $debug=0)
72
    {
73 498
        $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array());
74
75
        // Support "web-proxy-tunnelling" connections for https through proxies
76 498
        if (preg_match('/^HTTP\/1\.[0-1] 200 Connection established/', $data)) {
77
            // Look for CR/LF or simple LF as line separator,
78
            // (even though it is not valid http)
79
            $pos = strpos($data, "\r\n\r\n");
80
            if ($pos || is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
81
                $bd = $pos + 4;
82
            } else {
83
                $pos = strpos($data, "\n\n");
84
                if ($pos || is_int($pos)) {
85
                    $bd = $pos + 2;
86
                } else {
87
                    // No separation between response headers and body: fault?
88
                    $bd = 0;
89
                }
90
            }
91
            if ($bd) {
92
                // this filters out all http headers from proxy.
93
                // maybe we could take them into account, too?
94
                $data = substr($data, $bd);
95
            } else {
96
                Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed');
97
                throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']);
98
            }
99
        }
100
101
        // Strip HTTP 1.1 100 Continue header if present
102 498
        while (preg_match('/^HTTP\/1\.1 1[0-9]{2} /', $data)) {
103 1
            $pos = strpos($data, 'HTTP', 12);
104
            // server sent a Continue header without any (valid) content following...
105
            // give the client a chance to know it
106 1
            if (!$pos && !is_int($pos)) {
107
                // works fine in php 3, 4 and 5
108
109
                break;
110
            }
111 1
            $data = substr($data, $pos);
112
        }
113
114
        // When using Curl to query servers using Digest Auth, we get back a double set of http headers.
115
        // We strip out the 1st...
116 498
        if ($headersProcessed && preg_match('/^HTTP\/[0-9.]+ 401 /', $data)) {
117 30
            if (preg_match('/(\r?\n){2}HTTP\/[0-9.]+ 200 /', $data)) {
118 30
                $data = preg_replace('/^HTTP\/[0-9.]+ 401 .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1);
119
            }
120
        }
121
122 498
        if (!preg_match('/^HTTP\/[0-9.]+ 200 /', $data)) {
123 1
            $errstr = substr($data, 0, strpos($data, "\n") - 1);
124 1
            Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr);
125 1
            throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error']);
126
        }
127
128
        // be tolerant to usage of \n instead of \r\n to separate headers and data
129
        // (even though it is not valid http)
130 497
        $pos = strpos($data, "\r\n\r\n");
131 497
        if ($pos || is_int($pos)) {
0 ignored issues
show
introduced by
The condition is_int($pos) is always true.
Loading history...
132 496
            $bd = $pos + 4;
133
        } else {
134 1
            $pos = strpos($data, "\n\n");
135 1
            if ($pos || is_int($pos)) {
136 1
                $bd = $pos + 2;
137
            } else {
138
                // No separation between response headers and body: fault?
139
                // we could take some action here instead of going on...
140
                $bd = 0;
141
            }
142
        }
143
144
        // be tolerant to line endings, and extra empty lines
145 497
        $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos)));
146
147 497
        foreach($ar as $line) {
148
            // take care of multi-line headers and cookies
149 497
            $arr = explode(':', $line, 2);
150 497
            if (count($arr) > 1) {
151 497
                $headerName = strtolower(trim($arr[0]));
152
                /// @todo some other headers (the ones that allow a CSV list of values)
153
                /// do allow many values to be passed using multiple header lines.
154
                /// We should add content to $xmlrpc->_xh['headers'][$headerName]
155
                /// instead of replacing it for those...
156 497
                if ($headerName == 'set-cookie' || $headerName == 'set-cookie2') {
157 17
                    if ($headerName == 'set-cookie2') {
158
                        // version 2 cookies:
159
                        // there could be many cookies on one line, comma separated
160
                        $cookies = explode(',', $arr[1]);
161
                    } else {
162 17
                        $cookies = array($arr[1]);
163
                    }
164 17
                    foreach ($cookies as $cookie) {
165
                        // glue together all received cookies, using a comma to separate them
166
                        // (same as php does with getallheaders())
167 17
                        if (isset($httpResponse['headers'][$headerName])) {
168 17
                            $httpResponse['headers'][$headerName] .= ', ' . trim($cookie);
169
                        } else {
170 17
                            $httpResponse['headers'][$headerName] = trim($cookie);
171
                        }
172
                        // parse cookie attributes, in case user wants to correctly honour them
173
                        // feature creep: only allow rfc-compliant cookie attributes?
174
                        // @todo support for server sending multiple time cookie with same name, but using different PATHs
175 17
                        $cookie = explode(';', $cookie);
176 17
                        foreach ($cookie as $pos => $val) {
177 17
                            $val = explode('=', $val, 2);
178 17
                            $tag = trim($val[0]);
179 17
                            $val = trim(@$val[1]);
180
                            /// @todo with version 1 cookies, we should strip leading and trailing " chars
181 17
                            if ($pos == 0) {
182 17
                                $cookiename = $tag;
183 17
                                $httpResponse['cookies'][$tag] = array();
184 17
                                $httpResponse['cookies'][$cookiename]['value'] = urldecode($val);
185
                            } else {
186 17
                                if ($tag != 'value') {
187 17
                                    $httpResponse['cookies'][$cookiename][$tag] = $val;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cookiename does not seem to be defined for all execution paths leading up to this point.
Loading history...
188
                                }
189
                            }
190
                        }
191
                    }
192
                } else {
193 497
                    $httpResponse['headers'][$headerName] = trim($arr[1]);
194
                }
195
            } elseif (isset($headerName)) {
196
                /// @todo version1 cookies might span multiple lines, thus breaking the parsing above
197 1
                $httpResponse['headers'][$headerName] .= ' ' . trim($line);
198
            }
199
        }
200
201 497
        $data = substr($data, $bd);
202
203 497
        if ($debug && count($httpResponse['headers'])) {
204
            $msg = '';
205
            foreach ($httpResponse['headers'] as $header => $value) {
206
                $msg .= "HEADER: $header: $value\n";
207
            }
208
            foreach ($httpResponse['cookies'] as $header => $value) {
209
                $msg .= "COOKIE: $header={$value['value']}\n";
210
            }
211
            Logger::instance()->debugMessage($msg);
212
        }
213
214
        // if CURL was used for the call, http headers have been processed,
215
        // and dechunking + reinflating have been carried out
216 497
        if (!$headersProcessed) {
217
218
            // Decode chunked encoding sent by http 1.1 servers
219 315
            if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') {
220
                if (!$data = Http::decodeChunked($data)) {
221
                    Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server');
222
                    throw new \Exception(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail']);
223
                }
224
            }
225
226
            // Decode gzip-compressed stuff
227
            // code shamelessly inspired from nusoap library by Dietrich Ayala
228 315
            if (isset($httpResponse['headers']['content-encoding'])) {
229 60
                $httpResponse['headers']['content-encoding'] = str_replace('x-', '', $httpResponse['headers']['content-encoding']);
230 60
                if ($httpResponse['headers']['content-encoding'] == 'deflate' || $httpResponse['headers']['content-encoding'] == 'gzip') {
231
                    // if decoding works, use it. else assume data wasn't gzencoded
232 60
                    if (function_exists('gzinflate')) {
233 60
                        if ($httpResponse['headers']['content-encoding'] == 'deflate' && $degzdata = @gzuncompress($data)) {
234 30
                            $data = $degzdata;
235 30
                            if ($debug) {
236 30
                                Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
237
                            }
238 30
                        } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && $degzdata = @gzinflate(substr($data, 10))) {
239 30
                            $data = $degzdata;
240 30
                            if ($debug) {
241 30
                                Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n$data\n---END---");
242
                            }
243
                        } else {
244
                            Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server');
245 60
                            throw new \Exception(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail']);
246
                        }
247
                    } else {
248
                        Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.');
249
                        throw new \Exception(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress']);
250
                    }
251
                }
252
            }
253
        } // end of 'if needed, de-chunk, re-inflate response'
254
255 497
        return $httpResponse;
256
    }
257
}
258