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