From 181241cd5aba1b10f51bfa965ac6f0251c74404f Mon Sep 17 00:00:00 2001 From: Fabian Grutschus Date: Mon, 27 Jan 2014 16:08:34 +0100 Subject: [PATCH] Added Digist-MD5 support --- .../Stream/Authentication/DigestMd5.php | 100 ++++++++- .../Xmpp/EventListener/Stream/Session.php | 2 +- src/Fabiang/Xmpp/Util/XML.php | 23 +++ tests/bootstrap.php | 14 +- tests/features/authentication.feature | 6 + .../bootstrap/AuthenticationContext.php | 38 ++++ tests/features/bootstrap/SessionContext.php | 2 +- .../Stream/Authentication/DigestMd5Test.php | 191 ++++++++++++++++++ .../Xmpp/EventListener/Stream/SessionTest.php | 2 +- 9 files changed, 372 insertions(+), 6 deletions(-) create mode 100644 tests/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5Test.php diff --git a/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5.php b/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5.php index c75eb0e..3708dd5 100644 --- a/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5.php +++ b/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5.php @@ -38,6 +38,7 @@ use Fabiang\Xmpp\EventListener\AbstractEventListener; use Fabiang\Xmpp\Event\XMLEvent; +use Fabiang\Xmpp\Util\XML; /** * Handler for "digest md5" authentication mechanism. @@ -54,6 +55,18 @@ class DigestMd5 extends AbstractEventListener implements AuthenticationInterface */ protected $blocking = false; + /** + * + * @var string + */ + protected $username; + + /** + * + * @var string + */ + protected $password; + /** * {@inheritDoc} */ @@ -61,6 +74,7 @@ public function attachEvents() { $input = $this->getConnection()->getInputStream()->getEventManager(); $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}challenge', array($this, 'challenge')); + $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success')); $output = $this->getConnection()->getOutputStream()->getEventManager(); $output->attach('{urn:ietf:params:xml:ns:xmpp-sasl}auth', array($this, 'auth')); @@ -70,7 +84,8 @@ public function attachEvents() * {@inheritDoc} */ public function authenticate($username, $password) - { + { + $this->setUsername($username)->setPassword($password); $auth = ''; $this->getConnection()->send($auth); } @@ -95,11 +110,70 @@ public function challenge(XMLEvent $event) { if (false === $event->isStartTag()) { list($element) = $event->getParameters(); - $challenge = $element->nodeValue; + + $challenge = XML::base64Decode($element->nodeValue); + + if ($challenge) { + $matches = array(); + + preg_match_all('#(\w+)\=(?:"([^"]+)"|([^,]+))#', $challenge, $matches); + list(, $variables, $quoted, $unquoted) = $matches; + // filter empty strings; preserve keys + $quoted = array_filter($quoted); + $unquoted = array_filter($unquoted); + // replace "unquoted" values into "quoted" array and combine variables array with it + $values = array_combine($variables, array_replace($quoted, $unquoted)); + + $values['cnonce'] = uniqid(mt_rand(), false); + $values['nc'] = '00000001'; + $values['qop'] = 'auth'; + + if (!isset($values['digest-uri'])) { + $values['digest-uri'] = 'xmpp/' . $this->getOptions()->getTo(); + } + + $a1 = sprintf('%s:%s:%s', $this->getUsername(), $values['realm'], $this->getPassword()); + + if ('md5-sess' === $values['algorithm']) { + $a1 = pack('H32', md5($a1)) . ':' . $values['nonce'] . ':' . $values['cnonce']; + } + + $a2 = "AUTHENTICATE:" . $values['digest-uri']; + + $password = md5($a1) . ':' . $values['nonce'] . ':' . $values['nc'] . ':' + . $values['cnonce'] . ':' . $values['qop'] . ':' . md5($a2); + $password = md5($password); + + $response = sprintf( + 'username="%s",realm="%s",nonce="%s",cnonce="%s",nc=%s,qop=%s,digest-uri="%s",response=%s,charset=utf-8', + $this->getUsername(), + $values['realm'], + $values['nonce'], + $values['cnonce'], + $values['nc'], + $values['qop'], + $values['digest-uri'], + $password + ); + + $this->getConnection()->send( + '' . XML::base64Encode($response) . '' + ); + } $this->blocking = false; } } + /** + * Handle success event. + * + * @return void + */ + public function success() + { + $this->blocking = false; + } + /** * {@inheritDoc} */ @@ -108,4 +182,26 @@ public function isBlocking() return $this->blocking; } + public function getUsername() + { + return $this->username; + } + + public function setUsername($username) + { + $this->username = $username; + return $this; + } + + public function getPassword() + { + return $this->password; + } + + public function setPassword($password) + { + $this->password = $password; + return $this; + } + } diff --git a/src/Fabiang/Xmpp/EventListener/Stream/Session.php b/src/Fabiang/Xmpp/EventListener/Stream/Session.php index d83fb4f..32039f5 100644 --- a/src/Fabiang/Xmpp/EventListener/Stream/Session.php +++ b/src/Fabiang/Xmpp/EventListener/Stream/Session.php @@ -70,7 +70,7 @@ public function attachEvents() { $input = $this->getConnection()->getInputStream()->getEventManager(); $input->attach('{urn:ietf:params:xml:ns:xmpp-session}session', array($this, 'session')); - $input->attach('{http://etherx.jabber.org/streams}iq', array($this, 'iq')); + $input->attach('{jabber:client}iq', array($this, 'iq')); } /** diff --git a/src/Fabiang/Xmpp/Util/XML.php b/src/Fabiang/Xmpp/Util/XML.php index e202491..37610d8 100644 --- a/src/Fabiang/Xmpp/Util/XML.php +++ b/src/Fabiang/Xmpp/Util/XML.php @@ -72,4 +72,27 @@ public static function generateId() return static::quote('fabiang_xmpp_' . uniqid()); } + /** + * Encode a string with Base64 and quote it. + * + * @param string $data + * @param string $encoding + * @return string + */ + public static function base64Encode($data, $encoding = 'UTF-8') + { + return static::quote(base64_encode($data), $encoding); + } + + /** + * Decode a Base64 encoded string. + * + * @param string $data + * @return string + */ + public static function base64Decode($data) + { + return base64_decode($data); + } + } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index abe056a..917216f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -34,6 +34,18 @@ * @link http://github.com/fabiang/dependency-composer */ +$autoloaderFile = __DIR__ . '/../vendor/autoload.php'; + +if (!file_exists($autoloaderFile)) { + die( + 'You need to set up the project dependencies using the following commands:' . PHP_EOL . + 'wget http://getcomposer.org/composer.phar' . PHP_EOL . + 'php composer.phar install' . PHP_EOL + ); +} + /* @var $autoloader \Composer\Autoload\ClassLoader */ -$autoloader = require __DIR__ . '/../vendor/autoload.php'; +$autoloader = require $autoloaderFile; $autoloader->add('Fabiang\\Xmpp\\', __DIR__ . '/src/'); + +unset($autoloaderFile, $autoloader); diff --git a/tests/features/authentication.feature b/tests/features/authentication.feature index 3e6b80d..9df4268 100644 --- a/tests/features/authentication.feature +++ b/tests/features/authentication.feature @@ -15,3 +15,9 @@ Feature: Authentication And exceptions are catched when connecting When connecting Then a authorization exception should be catched + + Scenario: digest-md5 authentication + Given Test connection adapter + And Test response data for digest-md5 auth + When connecting + Then digest-md5 authentication element should be send diff --git a/tests/features/bootstrap/AuthenticationContext.php b/tests/features/bootstrap/AuthenticationContext.php index f595ca5..773d2bd 100644 --- a/tests/features/bootstrap/AuthenticationContext.php +++ b/tests/features/bootstrap/AuthenticationContext.php @@ -38,6 +38,7 @@ use Behat\Behat\Context\BehatContext; use Behat\Behat\Exception\PendingException; +use Fabiang\Xmpp\Util\XML; require_once 'PHPUnit/Framework/Assert/Functions.php'; @@ -73,6 +74,31 @@ public function testResponseDataForAuthenticationFailure() "" )); } + + /** + * @Given /^Test response data for digest-md5 auth$/ + */ + public function testResponseDataForDigestMdAuth() + { + $this->getConnection()->setData(array( + "" + . "DIGEST-MD5" + . "", + '' + . XML::base64Encode( + 'realm="localhost",nonce="abcdefghijklmnopqrstuvw",' + . 'qop="auth",charset=utf-8,algorithm=md5-sess' + ) + . '', + '' + . XML::base64Encode('rspauth=7fb0ac7ac1ff501a330a76e89a0f1633') + . '', + "" + )); + } + /** * @Then /^plain authentication element should be send$/ @@ -84,6 +110,18 @@ public function plainAuthenticationElementShouldBeSend() $this->getConnection()->getBuffer() ); } + + /** + * @Then /^digest-md(\d+) authentication element should be send$/ + */ + public function digestMdAuthenticationElementShouldBeSend($arg1) + { + assertContains( + '', + $this->getConnection()->getBuffer() + ); + } + /** * @Given /^should be authenticated$/ diff --git a/tests/features/bootstrap/SessionContext.php b/tests/features/bootstrap/SessionContext.php index 4d00407..291df27 100644 --- a/tests/features/bootstrap/SessionContext.php +++ b/tests/features/bootstrap/SessionContext.php @@ -83,7 +83,7 @@ public function testResponseDataForEmptySession() public function manipulatingId() { $listeners = $this->getConnection()->getInputStream()->getEventManager()->getEventList(); - $listener = array_filter($listeners['{http://etherx.jabber.org/streams}iq'], function ($listener) { + $listener = array_filter($listeners['{jabber:client}iq'], function ($listener) { return ($listener[0] instanceof Session); }); diff --git a/tests/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5Test.php b/tests/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5Test.php new file mode 100644 index 0000000..3d2207c --- /dev/null +++ b/tests/src/Fabiang/Xmpp/EventListener/Stream/Authentication/DigestMd5Test.php @@ -0,0 +1,191 @@ + + * @copyright 2014 Fabian Grutschus. All rights reserved. + * @license BSD + * @link http://github.com/fabiang/xmpp + */ + +namespace Fabiang\Xmpp\EventListener\Stream\Authentication; + +use Fabiang\Xmpp\Event\XMLEvent; +use Fabiang\Xmpp\Connection\Test; +use Fabiang\Xmpp\Event\EventManager; +use Fabiang\Xmpp\Options; +use Fabiang\Xmpp\Util\XML; + +/** + * Generated by PHPUnit_SkeletonGenerator 1.2.1 on 2014-01-27 at 12:11:12. + */ +class DigestMd5Test extends \PHPUnit_Framework_TestCase +{ + + /** + * @var DigestMd5 + */ + protected $object; + + /** + * + * @var Test + */ + protected $connection; + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + * + * @return void + */ + protected function setUp() + { + $this->object = new DigestMd5; + $this->connection = new Test; + + $options = new Options; + $options->setConnection($this->connection); + $this->object->setOptions($options); + $this->connection->setReady(true); + } + + /** + * Test attaching events. + * + * @covers Fabiang\Xmpp\EventListener\Stream\Authentication\DigestMd5::attachEvents + * @return void + */ + public function testAttachEvents() + { + $this->object->attachEvents(); + $this->assertSame( + array( + '*' => array(), + '{urn:ietf:params:xml:ns:xmpp-sasl}challenge' => array(array($this->object, 'challenge')), + '{urn:ietf:params:xml:ns:xmpp-sasl}success' => array(array($this->object, 'success')) + ), + $this->connection->getInputStream()->getEventManager()->getEventList() + ); + + $this->assertSame( + array( + '*' => array(), + '{urn:ietf:params:xml:ns:xmpp-sasl}auth' => array(array($this->object, 'auth')), + ), + $this->connection->getOutputStream()->getEventManager()->getEventList() + ); + } + + /** + * Test authentication. + * + * @covers Fabiang\Xmpp\EventListener\Stream\Authentication\DigestMd5::authenticate + * @return void + */ + public function testAuthenticate() + { + $this->object->authenticate('aaa', 'bbb'); + + $this->assertContains( + '', + $this->connection->getBuffer() + ); + $this->assertSame('aaa', $this->object->getUsername()); + $this->assertSame('bbb', $this->object->getPassword()); + } + + /** + * Test blocking when authentication element is send. + * + * @covers Fabiang\Xmpp\EventListener\Stream\Authentication\DigestMd5::auth + * @return void + */ + public function testAuth() + { + $this->assertFalse($this->object->isBlocking()); + $this->object->auth(); + $this->assertTrue($this->object->isBlocking()); + } + + /** + * Test parsing challenge and sending response. + * + * @covers Fabiang\Xmpp\EventListener\Stream\Authentication\DigestMd5::challenge + * @return void + */ + public function testChallenge() + { + $this->object->setUsername('aaa')->setPassword('bbb'); + $this->object->getOptions()->setTo('localhost'); + + $document = new \DOMDocument; + $document->loadXML( + '' + . XML::quote(base64_encode( + 'realm="localhost",nonce="abcdefghijklmnopqrstuvw",' + . 'qop="auth",charset=utf-8,algorithm=md5-sess' + )) + . '' + ); + + $event = new XMLEvent; + $event->setParameters(array($document->documentElement)); + $this->object->challenge($event); + + $buffer = $this->connection->getBuffer(); + $this->assertCount(1, $buffer); + $response = $buffer[0]; + $this->assertRegExp('#^.+$#', $response); + + $parser = new \DOMDocument; + $parser->loadXML($response); + $value = base64_decode($parser->documentElement->textContent); + $this->assertRegExp( + '#^username="aaa",realm="localhost",nonce="abcdefghijklmnopqrstuvw",cnonce="[^"]+",nc=00000001,' + . 'qop=auth,digest-uri="xmpp/localhost",response=[^,]+,charset=utf-8$#', + $value + ); + } + + /** + * Test handling success event. + * + * @covers Fabiang\Xmpp\EventListener\Stream\Authentication\DigestMd5::success + * @covers Fabiang\Xmpp\EventListener\Stream\Authentication\DigestMd5::isBlocking + * @return void + */ + public function testSuccess() + { + $this->object->auth(); + $this->assertTrue($this->object->isBlocking()); + $this->object->success(); + $this->assertFalse($this->object->isBlocking()); + } +} diff --git a/tests/src/Fabiang/Xmpp/EventListener/Stream/SessionTest.php b/tests/src/Fabiang/Xmpp/EventListener/Stream/SessionTest.php index a12dd62..745ce80 100644 --- a/tests/src/Fabiang/Xmpp/EventListener/Stream/SessionTest.php +++ b/tests/src/Fabiang/Xmpp/EventListener/Stream/SessionTest.php @@ -89,7 +89,7 @@ public function testAttachEvents() array( '*' => array(), '{urn:ietf:params:xml:ns:xmpp-session}session' => array(array($this->object, 'session')), - '{http://etherx.jabber.org/streams}iq' => array(array($this->object, 'iq')) + '{jabber:client}iq' => array(array($this->object, 'iq')) ), $this->connection->getInputStream()->getEventManager()->getEventList() );