From c4596e5377f63464ebb1e60989f96ae82c81135a Mon Sep 17 00:00:00 2001 From: gggeek Date: Sun, 5 Nov 2017 19:21:54 +0000 Subject: [PATCH] allow http auth to work in curl mode; add tests for basic and digest auth; refactor client to allow better switch between curl and sowket mode --- demo/server/server.php | 31 ++++- src/Client.php | 209 ++++++++++++++++++++++------------ src/Helper/Http.php | 14 ++- tests/4LocalhostMultiTest.php | 26 ++++- tests/ci/travis/apache_vhost | 10 ++ 5 files changed, 211 insertions(+), 79 deletions(-) diff --git a/demo/server/server.php b/demo/server/server.php index 1af2554..d836674 100644 --- a/demo/server/server.php +++ b/demo/server/server.php @@ -953,15 +953,15 @@ $signatures = array( $signatures = array_merge($signatures, $moreSignatures); -// enable support for the NULL extension +// Enable support for the NULL extension PhpXmlRpc\PhpXmlRpc::$xmlrpc_null_extension = true; $s = new PhpXmlRpc\Server($signatures, false); $s->setdebug(3); $s->compress_response = true; -// out-of-band information: let the client manipulate the server operations. -// we do this to help the testsuite script: do not reproduce in production! +// Out-of-band information: let the client manipulate the server operations. +// We do this to help the testsuite script: do not reproduce in production! if (isset($_GET['RESPONSE_ENCODING'])) { $s->response_charset_encoding = $_GET['RESPONSE_ENCODING']; } @@ -971,11 +971,30 @@ if (isset($_GET['DETECT_ENCODINGS'])) { if (isset($_GET['EXCEPTION_HANDLING'])) { $s->exception_handling = $_GET['EXCEPTION_HANDLING']; } +if (isset($_GET['FORCE_AUTH'])) { + // We implement both Basic and Digest auth in php to avoid having to set it up in a vhost. + // Code taken from php.net + // NB: we do NOT check for valid credentials! + if ($_GET['FORCE_AUTH'] == 'Basic') { + if (!isset($_SERVER['PHP_AUTH_USER']) && !isset($_SERVER['REMOTE_USER']) && !isset($_SERVER['REDIRECT_REMOTE_USER'])) { + header('HTTP/1.0 401 Unauthorized'); + header('WWW-Authenticate: Basic realm="Phpxmlrpc Basic Realm"'); + die('Text visible if user hits Cancel button'); + } + } elseif ($_GET['FORCE_AUTH'] == 'Digest') { + if (empty($_SERVER['PHP_AUTH_DIGEST'])) { + header('HTTP/1.1 401 Unauthorized'); + header('WWW-Authenticate: Digest realm="Phpxmlrpc Digest Realm",qop="auth",nonce="'.uniqid().'",opaque="'.md5('Phpxmlrpc Digest Realm').'"'); + die('Text visible if user hits Cancel button'); + } + } +} + $s->service(); -// that should do all we need! +// That should do all we need! -// out-of-band information: let the client manipulate the server operations. -// we do this to help the testsuite script: do not reproduce in production! +// Out-of-band information: let the client manipulate the server operations. +// We do this to help the testsuite script: do not reproduce in production! if (isset($_COOKIE['PHPUNIT_SELENIUM_TEST_ID']) && extension_loaded('xdebug')) { include_once __DIR__ . "/../../vendor/phpunit/phpunit-selenium/PHPUnit/Extensions/SeleniumCommon/append.php"; } diff --git a/src/Client.php b/src/Client.php index a33fe65..ede6a68 100644 --- a/src/Client.php +++ b/src/Client.php @@ -448,7 +448,6 @@ class Client * @param string $method valid values are 'http', 'http11' and 'https'. If left unspecified, the http protocol * chosen during creation of the object will be used. * - * * @return Response|Response[] Note that the client will always return a Response object, even if the call fails */ public function send($req, $timeout = 0, $method = '') @@ -473,8 +472,12 @@ class Client // where req is a Request $req->setDebug($this->debug); - if ($method == 'https') { - $r = $this->sendPayloadHTTPS( + /// @todo we could be smarter about this and force usage of curl in scenarios where it is both available and + /// needed, such as digest or ntlm auth + $useCurl = ($method == 'https' || $method == 'http11'); + + if ($useCurl) { + $r = $this->sendPayloadCURL( $req, $this->server, $this->port, @@ -491,34 +494,16 @@ class Client $this->proxy_user, $this->proxy_pass, $this->proxy_authtype, + // bc + $method == 'http11' ? 'http' : $method, $this->keepalive, $this->key, $this->keypass, $this->sslversion ); - } elseif ($method == 'http11') { - $r = $this->sendPayloadCURL( - $req, - $this->server, - $this->port, - $timeout, - $this->username, - $this->password, - $this->authtype, - null, - null, - null, - null, - $this->proxy, - $this->proxyport, - $this->proxy_user, - $this->proxy_pass, - $this->proxy_authtype, - 'http', - $this->keepalive - ); } else { - $r = $this->sendPayloadHTTP10( + // plain 'http 1.0': default to using socket + $r = $this->sendPayloadSocket( $req, $this->server, $this->port, @@ -526,12 +511,20 @@ class Client $this->username, $this->password, $this->authtype, + $this->cert, + $this->certpass, + $this->cacert, + $this->cacertdir, $this->proxy, $this->proxyport, $this->proxy_user, $this->proxy_pass, $this->proxy_authtype, - $method + $method, + $this->keepalive, + $this->key, + $this->keypass, + $this->sslversion ); } @@ -539,6 +532,7 @@ class Client } /** + * @deprecated * @param Request $req * @param string $server * @param int $port @@ -557,9 +551,75 @@ class Client protected function sendPayloadHTTP10($req, $server, $port, $timeout = 0, $username = '', $password = '', $authType = 1, $proxyHost = '', $proxyPort = 0, $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method='http') + { + return $this->sendPayloadSocket($req, $server, $port, $timeout, $username, $password, $authType, null, null, + null, null, $proxyHost, $proxyPort, $proxyUsername, $proxyPassword, $proxyAuthType); + } + + /** + * @deprecated + * @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 bool $keepAlive + * @param string $key + * @param string $keyPass + * @param int $sslVersion + * @return Response + */ + protected function sendPayloadHTTPS($req, $server, $port, $timeout = 0, $username = '', $password = '', + $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0, + $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $keepAlive = false, $key = '', $keyPass = '', + $sslVersion = 0) + { + return $this->sendPayloadCURL($req, $server, $port, $timeout, $username, + $password, $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort, + $proxyUsername, $proxyPassword, $proxyAuthType, 'https', $keepAlive, $key, $keyPass, $sslVersion); + } + + /** + * @param Request $req + * @param string $server + * @param int $port + * @param int $timeout + * @param string $username + * @param string $password + * @param int $authType only value supported is 1 + * @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 only value supported is 1 + * @param string $method 'http' (synonym for 'http10'), 'http10' or 'https' + * @param string $key + * @param string $keyPass @todo not implemented yet. + * @param int $sslVersion @todo not implemented yet. See http://php.net/manual/en/migration56.openssl.php + * @return Response + */ + protected function sendPayloadSocket($req, $server, $port, $timeout = 0, $username = '', $password = '', + $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0, + $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $method='http', $key = '', $keyPass = '', + $sslVersion = 0) { if ($port == 0) { - $port = ( $method === "https" ) ? 443 : 80; + $port = ( $method === 'https' ) ? 443 : 80; } // Only create the payload if it was not created previously @@ -608,7 +668,7 @@ class Client } $connectServer = $proxyHost; $connectPort = $proxyPort; - $transport = "tcp"; + $transport = 'tcp'; $uri = 'http://' . $server . ':' . $port . $this->path; if ($proxyUsername != '') { if ($proxyAuthType != 1) { @@ -620,7 +680,7 @@ class Client $connectServer = $server; $connectPort = $port; /// @todo if supporting https, we should support all its current options as well: peer name verification etc... - $transport = ( $method === "https" ) ? "tls" : "tcp"; + $transport = ( $method === 'https' ) ? 'tls' : 'tcp'; $uri = $this->path; } @@ -649,8 +709,12 @@ class Client $cookieHeader = 'Cookie:' . $version . substr($cookieHeader, 0, -1) . "\r\n"; } - // omit port if 80 - $port = ($port == 80) ? '' : (':' . $port); + // omit port if default + if (($port == 80 && in_array($method, array('http', 'http10'))) || ($port == 443 && $method == 'https')) { + $port = ''; + } else { + $port = ':' . $port; + } $op = 'POST ' . $uri . " HTTP/1.0\r\n" . 'User-Agent: ' . $this->user_agent . "\r\n" . @@ -669,11 +733,36 @@ class Client Logger::instance()->debugMessage("---SENDING---\n$op\n---END---"); } - if ($timeout > 0) { - $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, $timeout); + $contextOptions = array(); + if ($method == 'https') { + if ($cert != '') { + $contextOptions['ssl']['local_cert'] = $cert; + if ($certPass != '') { + $contextOptions['ssl']['passphrase'] = $certPass; + } + } + if ($caCert != '') { + $contextOptions['ssl']['cafile'] = $caCert; + } + if ($caCertDir != '') { + $contextOptions['ssl']['capath'] = $caCertDir; + } + if ($key != '') { + $contextOptions['ssl']['local_pk'] = $key; + } + $contextOptions['ssl']['verify_peer'] = $this->verifypeer; + + } + $context = stream_context_create($contextOptions); + + if ($timeout <= 0) { + $connectTimeout = ini_get('default_socket_timeout'); } else { - $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr); + $connectTimeout = $timeout; } + + $fp = @stream_socket_client("$transport://$connectServer:$connectPort", $this->errno, $this->errstr, $connectTimeout, + STREAM_CLIENT_CONNECT, $context); if ($fp) { if ($timeout > 0) { stream_set_timeout($fp, $timeout); @@ -709,39 +798,6 @@ class Client return $r; } - /** - * @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 bool $keepAlive - * @param string $key - * @param string $keyPass - * @param int $sslVersion - * @return Response - */ - protected function sendPayloadHTTPS($req, $server, $port, $timeout = 0, $username = '', $password = '', - $authType = 1, $cert = '', $certPass = '', $caCert = '', $caCertDir = '', $proxyHost = '', $proxyPort = 0, - $proxyUsername = '', $proxyPassword = '', $proxyAuthType = 1, $keepAlive = false, $key = '', $keyPass = '', - $sslVersion = 0) - { - return $this->sendPayloadCURL($req, $server, $port, $timeout, $username, - $password, $authType, $cert, $certPass, $caCert, $caCertDir, $proxyHost, $proxyPort, - $proxyUsername, $proxyPassword, $proxyAuthType, 'https', $keepAlive, $key, $keyPass, $sslVersion); - } - /** * Contributed by Justin Miller * Requires curl to be built into PHP @@ -763,7 +819,7 @@ class Client * @param string $proxyUsername * @param string $proxyPassword * @param int $proxyAuthType - * @param string $method + * @param string $method 'http' (let curl decide), 'http10', 'http11' or 'https' * @param bool $keepAlive * @param string $key * @param string $keyPass @@ -789,7 +845,7 @@ class Client } if ($port == 0) { - if ($method == 'http') { + if (in_array($method, array('http', 'http10', 'http11'))) { $port = 80; } else { $port = 443; @@ -826,7 +882,12 @@ class Client } if (!$keepAlive || !$this->xmlrpc_curl_handle) { - $curl = curl_init($method . '://' . $server . ':' . $port . $this->path); + if ($method == 'http11' || $method == 'http10') { + $protocol = 'http'; + } else { + $protocol = $method; + } + $curl = curl_init($protocol . '://' . $server . ':' . $port . $this->path); if ($keepAlive) { $this->xmlrpc_curl_handle = $curl; } @@ -883,6 +944,12 @@ class Client curl_setopt($curl, CURLOPT_TIMEOUT, $timeout == 1 ? 1 : $timeout - 1); } + if ($method == 'http10') { + curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + } elseif ($method == 'http11') { + curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + } + if ($username && $password) { curl_setopt($curl, CURLOPT_USERPWD, $username . ':' . $password); if (defined('CURLOPT_HTTPAUTH')) { @@ -965,7 +1032,7 @@ class Client } $message .= $name . ': ' . $val . "\n"; } - $message .= "---END---"; + $message .= '---END---'; Logger::instance()->debugMessage($message); } diff --git a/src/Helper/Http.php b/src/Helper/Http.php index 03a1ab6..41eec4a 100644 --- a/src/Helper/Http.php +++ b/src/Helper/Http.php @@ -72,7 +72,7 @@ class Http { $httpResponse = array('raw_data' => $data, 'headers'=> array(), 'cookies' => array()); - // Support "web-proxy-tunelling" connections for https through proxies + // Support "web-proxy-tunnelling" 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) @@ -110,6 +110,15 @@ class Http } $data = substr($data, $pos); } + + // When using Curl to query servers using Digest Auth, we get back a double set of http headers. + // We strip out the 1st... + if ($headersProcessed && preg_match('/^HTTP\/[0-9.]+ 401 /', $data)) { + if (preg_match('/(\r?\n){2}HTTP\/[0-9.]+ 200 /', $data)) { + $data = preg_replace('/^HTTP\/[0-9.]+ 401 .+?(?:\r?\n){2}(HTTP\/[0-9.]+ 200 )/s', '$1', $data, 1); + } + } + 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); @@ -131,8 +140,10 @@ class Http $bd = 0; } } + // be tolerant to line endings, and extra empty lines $ar = preg_split("/\r?\n/", trim(substr($data, 0, $pos))); + foreach($ar as $line) { // take care of multi-line headers and cookies $arr = explode(':', $line, 2); @@ -203,6 +214,7 @@ class Http // 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)) { diff --git a/tests/4LocalhostMultiTest.php b/tests/4LocalhostMultiTest.php index e5d365a..8788ddc 100644 --- a/tests/4LocalhostMultiTest.php +++ b/tests/4LocalhostMultiTest.php @@ -19,7 +19,10 @@ class LocalhostMultiTest extends LocalhostTest */ function _runtests() { - $unsafeMethods = array('testHttps', 'testCatchExceptions', 'testUtf8Method', 'testServerComments', 'testExoticCharsetsRequests', 'testExoticCharsetsRequests2', 'testExoticCharsetsRequests3'); + $unsafeMethods = array('testHttps', 'testCatchExceptions', 'testUtf8Method', 'testServerComments', 'testExoticCharsetsRequests', + 'testExoticCharsetsRequests2', 'testExoticCharsetsRequests3', + // @todo the following are currently not compatible w Digest Auth (most likely because of client copy) and should be fixed + 'testcatchWarnings', 'testWrappedMethodAsSource', 'testTransferOfObjectViaWrapping'); foreach(get_class_methods('LocalhostTest') as $method) { if(strpos($method, 'test') === 0 && !in_array($method, $unsafeMethods)) @@ -212,4 +215,25 @@ class LocalhostMultiTest extends LocalhostTest $this->client->request_charset_encoding = 'ISO-8859-1'; $this->_runtests(); } + + function testBasicAuth() + { + $this->client->setCredentials('test', 'test'); + $this->client->path = $this->args['URI'].'?FORCE_AUTH=Basic'; + $this->_runtests(); + } + + function testDigestAuth() + { + if (!function_exists('curl_init')) + { + $this->markTestSkipped('CURL missing: cannot test digest auth functionality'); + return; + } + $this->client->setCredentials('test', 'test', CURLAUTH_DIGEST); + $this->client->path = $this->args['URI'].'?FORCE_AUTH=Digest'; + $this->method = 'http11'; + $this->client->method = 'http11'; + $this->_runtests(); + } } diff --git a/tests/ci/travis/apache_vhost b/tests/ci/travis/apache_vhost index 87841d6..ee294fd 100644 --- a/tests/ci/travis/apache_vhost +++ b/tests/ci/travis/apache_vhost @@ -13,6 +13,11 @@ AllowOverride All Order deny,allow Allow from all + + # needed for basic auth (PHP_AUTH_USER and PHP_AUTH_PW) + RewriteEngine on + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}] # Wire up Apache to use Travis CI's php-fpm. @@ -39,6 +44,11 @@ AllowOverride All Order deny,allow Allow from all + + # needed for basic auth (PHP_AUTH_USER and PHP_AUTH_PW) + RewriteEngine on + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization}] # Wire up Apache to use Travis CI's php-fpm. -- 2.43.0