/** * Parse the xmlrpc response contained in the string $data and return a Response object. * * When $this->debug has been set to a value greater than 0, will echo debug messages to screen while decoding. * * @param string $data the xmlrpc response, possibly including http headers * @param bool $headersProcessed when true prevents parsing HTTP headers for interpretation of content-encoding and * consequent decoding * @param string $returnType decides return type, i.e. content of response->value(). Either 'xmlrpcvals', 'xml' or * 'phpvals' * * @return Response */ public function parseResponse($data = '', $headersProcessed = false, $returnType = 'xmlrpcvals') { if ($this->debug) { Logger::instance()->debugMessage("---GOT---\n{$data}\n---END---"); } $this->httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array()); if ($data == '') { error_log('XML-RPC: ' . __METHOD__ . ': no response received from server.'); return new Response(0, PhpXmlRpc::$xmlrpcerr['no_data'], PhpXmlRpc::$xmlrpcstr['no_data']); } // parse the HTTP headers of the response, if present, and separate them from data if (substr($data, 0, 4) == 'HTTP') { $httpParser = new Http(); try { $this->httpResponse = $httpParser->parseResponseHeaders($data, $headersProcessed, $this->debug); } catch (\Exception $e) { $r = new Response(0, $e->getCode(), $e->getMessage()); // failed processing of HTTP response headers // save into response obj the full payload received, for debugging $r->raw_data = $data; return $r; } } // be tolerant of extra whitespace in response body $data = trim($data); /// @todo return an error msg if $data=='' ? // be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts) // idea from Luca Mariano <*****@*****.**> originally in PEARified version of the lib $pos = strrpos($data, '</methodResponse>'); if ($pos !== false) { $data = substr($data, 0, $pos + 17); } // try to 'guestimate' the character encoding of the received response $respEncoding = XMLParser::guessEncoding(@$this->httpResponse['headers']['content-type'], $data); if ($this->debug) { $start = strpos($data, '<!-- SERVER DEBUG INFO (BASE64 ENCODED):'); if ($start) { $start += strlen('<!-- SERVER DEBUG INFO (BASE64 ENCODED):'); $end = strpos($data, '-->', $start); $comments = substr($data, $start, $end - $start); Logger::instance()->debugMessage("---SERVER DEBUG INFO (DECODED) ---\n\t" . str_replace("\n", "\n\t", base64_decode($comments)) . "\n---END---", $respEncoding); } } // if user wants back raw xml, give it to him if ($returnType == 'xml') { $r = new Response($data, 0, '', 'xml'); $r->hdrs = $this->httpResponse['headers']; $r->_cookies = $this->httpResponse['cookies']; $r->raw_data = $this->httpResponse['raw_data']; return $r; } if ($respEncoding != '') { // Since parsing will fail if charset is not specified in the xml prologue, // the encoding is not UTF8 and there are non-ascii chars in the text, we try to work round that... // The following code might be better for mb_string enabled installs, but // makes the lib about 200% slower... //if (!is_valid_charset($respEncoding, array('UTF-8'))) if (!in_array($respEncoding, array('UTF-8', 'US-ASCII')) && !XMLParser::hasEncoding($data)) { if ($respEncoding == 'ISO-8859-1') { $data = utf8_encode($data); } else { if (extension_loaded('mbstring')) { $data = mb_convert_encoding($data, 'UTF-8', $respEncoding); } else { error_log('XML-RPC: ' . __METHOD__ . ': invalid charset encoding of received response: ' . $respEncoding); } } } } $parser = xml_parser_create(); xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true); // G. Giunta 2005/02/13: PHP internally uses ISO-8859-1, so we have to tell // the xml parser to give us back data in the expected charset. // What if internal encoding is not in one of the 3 allowed? // we use the broadest one, ie. utf8 // This allows to send data which is native in various charset, // by extending xmlrpc_encode_entities() and setting xmlrpc_internalencoding if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII'))) { xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8'); } else { xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding); } $xmlRpcParser = new XMLParser(); xml_set_object($parser, $xmlRpcParser); if ($returnType == 'phpvals') { xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee_fast'); } else { xml_set_element_handler($parser, 'xmlrpc_se', 'xmlrpc_ee'); } xml_set_character_data_handler($parser, 'xmlrpc_cd'); xml_set_default_handler($parser, 'xmlrpc_dh'); // first error check: xml not well formed if (!xml_parse($parser, $data, count($data))) { // thanks to Peter Kocks <*****@*****.**> if (xml_get_current_line_number($parser) == 1) { $errStr = 'XML error at line 1, check URL'; } else { $errStr = sprintf('XML error: %s at line %d, column %d', xml_error_string(xml_get_error_code($parser)), xml_get_current_line_number($parser), xml_get_current_column_number($parser)); } error_log($errStr); $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' (' . $errStr . ')'); xml_parser_free($parser); if ($this->debug) { print $errStr; } $r->hdrs = $this->httpResponse['headers']; $r->_cookies = $this->httpResponse['cookies']; $r->raw_data = $this->httpResponse['raw_data']; return $r; } xml_parser_free($parser); // second error check: xml well formed but not xml-rpc compliant if ($xmlRpcParser->_xh['isf'] > 1) { if ($this->debug) { /// @todo echo something for user? } $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return'] . ' ' . $xmlRpcParser->_xh['isf_reason']); } elseif ($returnType == 'xmlrpcvals' && !is_object($xmlRpcParser->_xh['value'])) { // something odd has happened // and it's time to generate a client side error // indicating something odd went on $r = new Response(0, PhpXmlRpc::$xmlrpcerr['invalid_return'], PhpXmlRpc::$xmlrpcstr['invalid_return']); } else { if ($this->debug > 1) { Logger::instance()->debugMessage("---PARSED---\n" . var_export($xmlRpcParser->_xh['value'], true) . "\n---END---"); } // note that using =& will raise an error if $xmlRpcParser->_xh['st'] does not generate an object. $v =& $xmlRpcParser->_xh['value']; if ($xmlRpcParser->_xh['isf']) { /// @todo we should test here if server sent an int and a string, and/or coerce them into such... if ($returnType == 'xmlrpcvals') { $errNo_v = $v['faultCode']; $errStr_v = $v['faultString']; $errNo = $errNo_v->scalarval(); $errStr = $errStr_v->scalarval(); } else { $errNo = $v['faultCode']; $errStr = $v['faultString']; } if ($errNo == 0) { // FAULT returned, errno needs to reflect that $errNo = -1; } $r = new Response(0, $errNo, $errStr); } else { $r = new Response($v, 0, '', $returnType); } } $r->hdrs = $this->httpResponse['headers']; $r->_cookies = $this->httpResponse['cookies']; $r->raw_data = $this->httpResponse['raw_data']; return $r; }
/** * Contributed by Justin Miller <*****@*****.**> * Requires curl to be built into PHP * NB: CURL versions before 7.11.10 cannot use proxy to talk to https servers! * * @param Request $req * @param string $server * @param int $port * @param int $timeout * @param string $username * @param string $password * @param int $authType * @param string $cert * @param string $certPass * @param string $caCert * @param string $caCertDir * @param string $proxyHost * @param int $proxyPort * @param string $proxyUsername * @param string $proxyPassword * @param int $proxyAuthType * @param string $method * @param bool $keepAlive * @param string $key * @param string $keyPass * @param int $sslVersion * @return Response */ protected function sendPayloadCURL($req, $server, $port, $timeout = 0, $username = '', $password = '', $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method = 'https', $keepAlive = false, $key = '', $keyPass = '', $sslVersion = 0) { if (!function_exists('curl_init')) { $this->errstr = 'CURL unavailable on this install'; return new Response(0, PhpXmlRpc::$xmlrpcerr['no_curl'], PhpXmlRpc::$xmlrpcstr['no_curl']); } if ($method == 'https') { if (($info = curl_version()) && (is_string($info) && strpos($info, 'OpenSSL') === null || is_array($info) && !isset($info['ssl_version']))) { $this->errstr = 'SSL unavailable on this install'; return new Response(0, PhpXmlRpc::$xmlrpcerr['no_ssl'], PhpXmlRpc::$xmlrpcstr['no_ssl']); } } if ($port == 0) { if ($method == 'http') { $port = 80; } else { $port = 443; } } // Only create the payload if it was not created previously if (empty($req->payload)) { $req->createPayload($this->request_charset_encoding); } // Deflate request body and set appropriate request headers $payload = $req->payload; if (function_exists('gzdeflate') && ($this->request_compression == 'gzip' || $this->request_compression == 'deflate')) { if ($this->request_compression == 'gzip') { $a = @gzencode($payload); if ($a) { $payload = $a; $encodingHdr = 'Content-Encoding: gzip'; } } else { $a = @gzcompress($payload); if ($a) { $payload = $a; $encodingHdr = 'Content-Encoding: deflate'; } } } else { $encodingHdr = ''; } if ($this->debug > 1) { Logger::instance()->debugMessage("---SENDING---\n{$payload}\n---END---"); } if (!$keepAlive || !$this->xmlrpc_curl_handle) { $curl = curl_init($method . '://' . $server . ':' . $port . $this->path); if ($keepAlive) { $this->xmlrpc_curl_handle = $curl; } } else { $curl = $this->xmlrpc_curl_handle; } // results into variable curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); if ($this->debug > 1) { curl_setopt($curl, CURLOPT_VERBOSE, true); /// @todo allow callers to redirect curlopt_stderr to some stream which can be buffered } curl_setopt($curl, CURLOPT_USERAGENT, $this->user_agent); // required for XMLRPC: post the data curl_setopt($curl, CURLOPT_POST, 1); // the data curl_setopt($curl, CURLOPT_POSTFIELDS, $payload); // return the header too curl_setopt($curl, CURLOPT_HEADER, 1); // NB: if we set an empty string, CURL will add http header indicating // ALL methods it is supporting. This is possibly a better option than // letting the user tell what curl can / cannot do... if (is_array($this->accepted_compression) && count($this->accepted_compression)) { //curl_setopt($curl, CURLOPT_ENCODING, implode(',', $this->accepted_compression)); // empty string means 'any supported by CURL' (shall we catch errors in case CURLOPT_SSLKEY undefined ?) if (count($this->accepted_compression) == 1) { curl_setopt($curl, CURLOPT_ENCODING, $this->accepted_compression[0]); } else { curl_setopt($curl, CURLOPT_ENCODING, ''); } } // extra headers $headers = array('Content-Type: ' . $req->content_type, 'Accept-Charset: ' . implode(',', $this->accepted_charset_encodings)); // if no keepalive is wanted, let the server know it in advance if (!$keepAlive) { $headers[] = 'Connection: close'; } // request compression header if ($encodingHdr) { $headers[] = $encodingHdr; } curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); // timeout is borked if ($timeout) { curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1); } if ($username && $password) { curl_setopt($curl, CURLOPT_USERPWD, $username . ':' . $password); if (defined('CURLOPT_HTTPAUTH')) { curl_setopt($curl, CURLOPT_HTTPAUTH, $authType); } elseif ($authType != 1) { error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth is supported by the current PHP/curl install'); } } if ($method == 'https') { // set cert file if ($cert) { curl_setopt($curl, CURLOPT_SSLCERT, $cert); } // set cert password if ($certPass) { curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $certPass); } // whether to verify remote host's cert curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifypeer); // set ca certificates file/dir if ($caCert) { curl_setopt($curl, CURLOPT_CAINFO, $caCert); } if ($caCertDir) { curl_setopt($curl, CURLOPT_CAPATH, $caCertDir); } // set key file (shall we catch errors in case CURLOPT_SSLKEY undefined ?) if ($key) { curl_setopt($curl, CURLOPT_SSLKEY, $key); } // set key password (shall we catch errors in case CURLOPT_SSLKEY undefined ?) if ($keyPass) { curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $keyPass); } // whether to verify cert's common name (CN); 0 for no, 1 to verify that it exists, and 2 to verify that it matches the hostname used curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, $this->verifyhost); // allow usage of different SSL versions curl_setopt($curl, CURLOPT_SSLVERSION, $sslVersion); } // proxy info if ($proxyHost) { if ($proxyPort == 0) { $proxyPort = 8080; // NB: even for HTTPS, local connection is on port 8080 } curl_setopt($curl, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort); if ($proxyUsername) { curl_setopt($curl, CURLOPT_PROXYUSERPWD, $proxyUsername . ':' . $proxyPassword); if (defined('CURLOPT_PROXYAUTH')) { curl_setopt($curl, CURLOPT_PROXYAUTH, $proxyAuthType); } elseif ($proxyAuthType != 1) { error_log('XML-RPC: ' . __METHOD__ . ': warning. Only Basic auth to proxy is supported by the current PHP/curl install'); } } } // NB: should we build cookie http headers by hand rather than let CURL do it? // the following code does not honour 'expires', 'path' and 'domain' cookie attributes // set to client obj the the user... if (count($this->cookies)) { $cookieHeader = ''; foreach ($this->cookies as $name => $cookie) { $cookieHeader .= $name . '=' . $cookie['value'] . '; '; } curl_setopt($curl, CURLOPT_COOKIE, substr($cookieHeader, 0, -2)); } foreach ($this->extracurlopts as $opt => $val) { curl_setopt($curl, $opt, $val); } $result = curl_exec($curl); if ($this->debug > 1) { $message = "---CURL INFO---\n"; foreach (curl_getinfo($curl) as $name => $val) { if (is_array($val)) { $val = implode("\n", $val); } $message .= $name . ': ' . $val . "\n"; } $message .= "---END---"; Logger::instance()->debugMessage($message); } if (!$result) { /// @todo we should use a better check here - what if we get back '' or '0'? $this->errstr = 'no response'; $resp = new Response(0, PhpXmlRpc::$xmlrpcerr['curl_fail'], PhpXmlRpc::$xmlrpcstr['curl_fail'] . ': ' . curl_error($curl)); curl_close($curl); if ($keepAlive) { $this->xmlrpc_curl_handle = null; } } else { if (!$keepAlive) { curl_close($curl); } $resp = $req->parseResponse($result, true, $this->return_type); // if we got back a 302, we can not reuse the curl handle for later calls if ($resp->faultCode() == PhpXmlRpc::$xmlrpcerr['http_error'] && $keepAlive) { curl_close($curl); $this->xmlrpc_curl_handle = null; } } return $resp; }
/** * Parses HTTP an http response headers and separates them from the body. * * @param string $data the http response,headers and body. It will be stripped of headers * @param bool $headersProcessed when true, we assume that response inflating and dechunking has been already carried out * * @return array with keys 'headers' and 'cookies' * @throws \Exception */ public function parseResponseHeaders(&$data, $headersProcessed = false, $debug = 0) { $httpResponse = array('raw_data' => $data, 'headers' => array(), 'cookies' => array()); // Support "web-proxy-tunelling" connections for https through proxies if (preg_match('/^HTTP\\/1\\.[0-1] 200 Connection established/', $data)) { // Look for CR/LF or simple LF as line separator, // (even though it is not valid http) $pos = strpos($data, "\r\n\r\n"); if ($pos || is_int($pos)) { $bd = $pos + 4; } else { $pos = strpos($data, "\n\n"); if ($pos || is_int($pos)) { $bd = $pos + 2; } else { // No separation between response headers and body: fault? $bd = 0; } } if ($bd) { // this filters out all http headers from proxy. // maybe we could take them into account, too? $data = substr($data, $bd); } else { error_log('XML-RPC: ' . __METHOD__ . ': HTTPS via proxy error, tunnel connection possibly failed'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (HTTPS via proxy error, tunnel connection possibly failed)', PhpXmlRpc::$xmlrpcerr['http_error']); } } // Strip HTTP 1.1 100 Continue header if present while (preg_match('/^HTTP\\/1\\.1 1[0-9]{2} /', $data)) { $pos = strpos($data, 'HTTP', 12); // server sent a Continue header without any (valid) content following... // give the client a chance to know it if (!$pos && !is_int($pos)) { // works fine in php 3, 4 and 5 break; } $data = substr($data, $pos); } if (!preg_match('/^HTTP\\/[0-9.]+ 200 /', $data)) { $errstr = substr($data, 0, strpos($data, "\n") - 1); error_log('XML-RPC: ' . __METHOD__ . ': HTTP error, got response: ' . $errstr); throw new \Exception(PhpXmlRpc::$xmlrpcstr['http_error'] . ' (' . $errstr . ')', PhpXmlRpc::$xmlrpcerr['http_error']); } // be tolerant to usage of \n instead of \r\n to separate headers and data // (even though it is not valid http) $pos = strpos($data, "\r\n\r\n"); if ($pos || is_int($pos)) { $bd = $pos + 4; } else { $pos = strpos($data, "\n\n"); if ($pos || is_int($pos)) { $bd = $pos + 2; } else { // No separation between response headers and body: fault? // we could take some action here instead of going on... $bd = 0; } } // be tolerant to line endings, and extra empty lines $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos))); while (list(, $line) = @each($ar)) { // take care of multi-line headers and cookies $arr = explode(':', $line, 2); if (count($arr) > 1) { $headerName = strtolower(trim($arr[0])); /// @todo some other headers (the ones that allow a CSV list of values) /// do allow many values to be passed using multiple header lines. /// We should add content to $xmlrpc->_xh['headers'][$headerName] /// instead of replacing it for those... if ($headerName == 'set-cookie' || $headerName == 'set-cookie2') { if ($headerName == 'set-cookie2') { // version 2 cookies: // there could be many cookies on one line, comma separated $cookies = explode(',', $arr[1]); } else { $cookies = array($arr[1]); } foreach ($cookies as $cookie) { // glue together all received cookies, using a comma to separate them // (same as php does with getallheaders()) if (isset($httpResponse['headers'][$headerName])) { $httpResponse['headers'][$headerName] .= ', ' . trim($cookie); } else { $httpResponse['headers'][$headerName] = trim($cookie); } // parse cookie attributes, in case user wants to correctly honour them // feature creep: only allow rfc-compliant cookie attributes? // @todo support for server sending multiple time cookie with same name, but using different PATHs $cookie = explode(';', $cookie); foreach ($cookie as $pos => $val) { $val = explode('=', $val, 2); $tag = trim($val[0]); $val = trim(@$val[1]); /// @todo with version 1 cookies, we should strip leading and trailing " chars if ($pos == 0) { $cookiename = $tag; $httpResponse['cookies'][$tag] = array(); $httpResponse['cookies'][$cookiename]['value'] = urldecode($val); } else { if ($tag != 'value') { $httpResponse['cookies'][$cookiename][$tag] = $val; } } } } } else { $httpResponse['headers'][$headerName] = trim($arr[1]); } } elseif (isset($headerName)) { /// @todo version1 cookies might span multiple lines, thus breaking the parsing above $httpResponse['headers'][$headerName] .= ' ' . trim($line); } } $data = substr($data, $bd); if ($debug && count($httpResponse['headers'])) { $msg = ''; foreach ($httpResponse['headers'] as $header => $value) { $msg .= "HEADER: {$header}: {$value}\n"; } foreach ($httpResponse['cookies'] as $header => $value) { $msg .= "COOKIE: {$header}={$value['value']}\n"; } Logger::instance()->debugMessage($msg); } // if CURL was used for the call, http headers have been processed, // and dechunking + reinflating have been carried out if (!$headersProcessed) { // Decode chunked encoding sent by http 1.1 servers if (isset($httpResponse['headers']['transfer-encoding']) && $httpResponse['headers']['transfer-encoding'] == 'chunked') { if (!($data = Http::decodeChunked($data))) { error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to rebuild the chunked data received from server'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['dechunk_fail'], PhpXmlRpc::$xmlrpcerr['dechunk_fail']); } } // Decode gzip-compressed stuff // code shamelessly inspired from nusoap library by Dietrich Ayala if (isset($httpResponse['headers']['content-encoding'])) { $httpResponse['headers']['content-encoding'] = str_replace('x-', '', $httpResponse['headers']['content-encoding']); if ($httpResponse['headers']['content-encoding'] == 'deflate' || $httpResponse['headers']['content-encoding'] == 'gzip') { // if decoding works, use it. else assume data wasn't gzencoded if (function_exists('gzinflate')) { if ($httpResponse['headers']['content-encoding'] == 'deflate' && ($degzdata = @gzuncompress($data))) { $data = $degzdata; if ($debug) { Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n{$data}\n---END---"); } } elseif ($httpResponse['headers']['content-encoding'] == 'gzip' && ($degzdata = @gzinflate(substr($data, 10)))) { $data = $degzdata; if ($debug) { Logger::instance()->debugMessage("---INFLATED RESPONSE---[" . strlen($data) . " chars]---\n{$data}\n---END---"); } } else { error_log('XML-RPC: ' . __METHOD__ . ': errors occurred when trying to decode the deflated data received from server'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['decompress_fail'], PhpXmlRpc::$xmlrpcerr['decompress_fail']); } } else { error_log('XML-RPC: ' . __METHOD__ . ': the server sent deflated data. Your php install must have the Zlib extension compiled in to support this.'); throw new \Exception(PhpXmlRpc::$xmlrpcstr['cannot_decompress'], PhpXmlRpc::$xmlrpcerr['cannot_decompress']); } } } } // end of 'if needed, de-chunk, re-inflate response' return $httpResponse; }