* @revision 05 * @license http://creativecommons.org/licenses/by-sa/3.0/de/ Creative Commons Attribution-Share Alike 3.0 Germany * @homepage http://oss.tiggerswelt.net/xmpp * @copyright Copyright © 2009 tiggersWelt.net **/ require_once ('phpEvents/socket/stream/xml.php'); require_once ('phpEvents/socket/stream/xml/tag.php'); require_once ('tiggerXMPP/error.php'); /** * XMPP Stream Initiator * --------------------- * Create a basic XMPP-Stream and handle low-level functions like * Stream-Features and encryption * * @class tiggerXMPP_Initiator * @extends phpEvents_Socket_Stream_XML * @author Bernd Holzmueller **/ class tiggerXMPP_Initiator extends phpEvents_Socket_Stream_XML { /* Stream constants */ const STREAM_OMIT_NAMESPACE = false; const STREAM_OMIT_DESTINATION = false; /* Stream types */ const STREAM_TYPE_CLIENT = 'jabber:client'; const STREAM_TYPE_SERVER = 'jabber:server'; const STREAM_TYPE_COMPONENT = 'jabber:component:accept'; // As defined in XEP-0114 /* Stream settings */ protected $Domain = ''; protected $Namespace = ''; protected $Language = 'de'; protected $connectionID = ''; private $connectionEncrypted = false; /* Stream Features */ private $authMechs = null; private $saslCount = 0; private $Authenticator = null; // {{{ setNamespace /** * Set the namespace for this stream * * @param string $Namespace * * @access public * @return void **/ public function setNamespace ($Namespace) { $this->Namespace = $Namespace; } // }}} // {{{ setDestination /** * Set the domain we are connecting to * * @param string $Domain * * @access public * @return void **/ public function setDestination ($Domain) { $this->Domain = $Domain; } // }}} // {{{ getLanguage /** * Retrive the language for this stream * * @access public * @return string **/ public function getLanguage () { return $this->Language; } // }}} // {{{ setLanguage /** * Set the basic language for this stream * * @param string $Language * * @access public * @return void **/ public function setLanguage ($Lang) { $this->Language = $Lang; } // }}} // {{{ createConnection /** * Create a connection with our destination-server * * @param string $Server * @param int $Port * * @access public * @return bool **/ public function createConnection ($Server, $Port) { // Reset the connection $this->connectionEncrypted = false; $this->saslCount = 0; // Setup our root-tag $Stream = new tiggerXMPP_Packet ('stream:stream'); $Stream->setAttribute ('version', '1.0'); $Stream->setNamespace ('http://etherx.jabber.org/streams', 'stream'); $Stream->setLanguage ($this->Language); if ($this->streamSetNamespace ()) $Stream->setNamespace ($this->Namespace); if ($this->streamSetDestination ()) $Stream->setAttribute ('to', $this->Domain); $this->setRootTag ($Stream); $this->registerCallback ('stream:features', array ($this, 'handleStreamFeatures'), false); // Try to connect to server return $this->connect (self::MODE_TCP, $Server, $Port); } // {{{ closeConnection /** * Tell our server to stop the stream and close the connection * * @access public * @return bool **/ public function closeConnection () { self::sendXML (''); # fclose ($this->_FD); return true; } // }}} // {{{ streamSetNamespace /** * Determine if stream-initiator has to set a namespace on initial packet * * @access protected * @return bool **/ protected function streamSetNamespace () { return (constant (get_class ($this) . '::STREAM_OMIT_NAMESPACE') == false); } // }}} // {{{ streamSetDestination /** * Determine if stream-initiator should present a stream-destination upon connect * * @access protected * @return bool **/ protected function streamSetDestination () { return (constant (get_class ($this) . '::STREAM_OMIT_DESTINATION') == false); } // }}} // {{{ receiveRoot /** * Callback: Initial root-element was received * * @param object $Tag The root-element * * @access protected * @return void * throws xmppException if no valid stream could be established **/ protected function receiveRoot ($Tag) { // Check the name of the root-element if ($Tag->getName () != 'stream:stream') throw new xmppException ('Could not establish valid stream'); // Remeber the stream-ID $this->connectionID = $Tag->getAttribute ('id'); // Handle the stream-Features $Continue = true; $rc = true; if (is_object ($Features = $Tag->getSubtagByName ('stream:features'))) $this->handleStreamFeatures (0, $Features, $Continue, $this); } // }}} // {{{ handleStreamFeatures /** * Handler for stream:features * * @access protected * @return bool * @todo Handle Stream-Features **/ protected function handleStreamFeatures ($ID, $Packet, &$Continue, &$XMPP) { self::__debug (self::DEBUG_DEBUG, 'called', __FUNCTION__, __LINE__, __CLASS__, __FILE__); $StartTLS = $Packet->haveSubtags ('starttls'); $Continue = !$StartTLS; $Mechs = array (); if ($Packet->haveSubtags ('mechanisms') && (($tagMechs = array_shift ($Packet->getSubtagsByName ('mechanisms'))) !== null)) { $Mechanisms = $tagMechs->getSubtagsByName ('mechanism'); foreach ($Mechanisms as $Def) $Mechs [$Def->getValue ()] = $Def->getValue (); if (count ($Mechs) > 0) $this->authMechs = $Mechs; } // Try to start encryption if ($StartTLS && !$this->connectionEncrypted && (($rc = $this->startTLS ()) !== null)) return $rc; // Fire the callback $this->streamInitiated (); // Check if there is an authenticator registered if ($this->Authenticator !== null) call_user_func_array ($this->Authenticator [0], $this->Authenticator [1]); return true; } // }}} // {{{ setAuthenticator /** * Register an authenticator for this stream * * @param callback $Callback * @param ... * * @access public * @return void **/ public function setAuthenticator ($Callback) { $Args = func_get_args (); array_shift ($Args); $this->Authenticator = array ( $Callback, $Args, ); } // }}} // {{{ startTLS /** * Start TLS-negotiation * * @access public * @return bool */ public function startTLS () { self::__debug (self::DEBUG_DEBUG, 'called.', __FUNCTION__, __LINE__, __CLASS__, __FILE__); // Check if our php-setup is capable of this stuff ;) if (!function_exists ('stream_socket_enable_crypto')) return self::__debug (self::DEBUG_ERROR, 'Encryption not available', __FUNCTION__, __LINE__, __CLASS__, __FILE__, null); if (!is_resource ($fd = parent::getStream ())) return null; // Tell the server that we want TLS $Resp = new phpEvents_Socket_Stream_XML_Tag ('starttls'); $Resp->setNamespace ('urn:ietf:params:xml:ns:xmpp-tls'); parent::sendXML ($Resp); // Check the response $Resp = parent::waitBlock (array ('proceed', 'failure')); if ($Resp->getName () != 'proceed') { self::closeConnection (); return false; } // Set socket to blocking mode stream_set_blocking ($fd, true); // Initiate TLS if (!stream_socket_enable_crypto ($fd, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { self::__debug (self::DEBUG_FATAL, 'TLS could not be negotiated', __FUNCTION__, __LINE__, __CLASS__, __FILE__); throw new xmppException ('TLS-negotiation failed'); } // Remove the blocking again stream_set_blocking ($fd, false); self::__debug (self::DEBUG_NOTICE, 'Stream is now TLS negotiated', __FUNCTION__, __LINE__, __CLASS__, __FILE__); // Restart the stream $this->connectionEncrypted = true; $this->restartStream (); return true; } // }}} // {{{ authenticateSASL /** * Perform a SASL-authentication * * @param string $Username * @param string $Password * * @access public * @return bool **/ public function authenticateSASL ($Username, $Password) { $this->__debug (self::DEBUG_DEBUG, 'called.', __FUNCTION__, __LINE__, __CLASS__, __FILE__); if (!is_array ($this->authMechs)) return $this->__debug (self::DEBUG_ERROR, 'Remote side did not offer any SASL-Mechanisms', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); if (in_array ('DIGEST-MD5', $this->authMechs)) return $this->authenticateSASL_DigestMD5 ($Username, $Password); if (in_array ('PLAIN', $this->authMechs)) return $this->authenticateSASL_Plain ($Username, $Password); return $this->__debug (self::DEBUG_ERROR, 'Did not find any suitable authentication-mechanisms', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } // }}} // {{{ authenticateSASL_Plain /** * Perform PLAIN SASL Authentication * * @param string $Username * @param string $Password * * @access private * @return bool **/ private function authenticateSASL_Plain ($Username, $Password) { $this->__debug (self::DEBUG_DEBUG, 'called.', __FUNCTION__, __LINE__, __CLASS__, __FILE__); // Select PLAIN Mechanism and submit username and password $Mechanism = new phpEvents_Socket_Stream_XML_Tag ('auth', null, chr (0) . $Username . chr (0) . $Password); $Mechanism->setNamespace ('urn:ietf:params:xml:ns:xmpp-sasl'); $Mechanism->setAttribute ('mechanism', 'PLAIN'); $this->sendXML ($Mechanism); // Read the response if (!is_object ($Resp = $this->tagReadNext ())) { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Failed to read next tag', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () == 'failure') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'DIGEST-MD5-Authentication failed', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () != 'success') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Server sent unexpected response', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } $this->restartStream (); $this->authenticationSuccess (); return true; } // }}} // {{{ authenticateSASL_DigestMD5 /** * Perform DIGEST-MD5 SASL Authentication * * @param string $Username * @param string $Password * * @access private * @return bool **/ private function authenticateSASL_DigestMD5 ($Username, $Password) { $this->__debug (self::DEBUG_DEBUG, 'called.', __FUNCTION__, __LINE__, __CLASS__, __FILE__); // #4 Select DIGEST-MD5 Mechanism $Mechanism = new phpEvents_Socket_Stream_XML_Tag ('auth'); $Mechanism->setNamespace ('urn:ietf:params:xml:ns:xmpp-sasl'); $Mechanism->setAttribute ('mechanism', 'DIGEST-MD5'); $this->sendXML ($Mechanism); // #5 Read the response if (!is_object ($Resp = $this->tagReadNext ())) { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Failed to read next tag', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () == 'failure') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Server declined DIGEST-MD5-Request', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () != 'challenge') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Server sent unexpected response', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } // Handle the challenge if (!is_array ($Challenge = $this->parseSASLChallenge ($Resp->getValue ()))) { $this->authenticationFailure (); return false; } // #6 Send the response $Value = 'username="' . $Username . '",' . 'realm="' . $Challenge ['realm'] . '",' . 'nonce="' . $Challenge ['nonce'] . '",' . 'cnonce="' . ($cnonce = md5 ($Challenge ['nonce'] . $this->saslCount)) . '",' . 'nc=' . sprintf ('%08x', ++$this->saslCount) . ',' . 'qop=' . $Challenge ['qop'] . ',' . 'digest-uri="xmpp/' . $this->Domain . '",' . 'response='; $Value .= md5 ( md5 ( md5 ($Username . ':' . $Challenge ['realm'] . ':' . $Password, true) . ':' . $Challenge ['nonce'] . ':' . $cnonce . (isset ($Challenge ['authzid']) ? ':' . $Challenge ['authzid'] : '') ) . ':' . $Challenge ['nonce'] . ':' . sprintf ('%08x', $this->saslCount) . ':' . $cnonce . ':' . $Challenge ['qop'] . ':' . md5 ('AUTHENTICATE:xmpp/' . $this->Domain . (($Challenge ['qop'] == 'auth-int') || ($Challenge ['qop'] == 'auth-conf') ? ':00000000000000000000000000000000' : '')) ); $Value .= ','; if (isset ($Challenge ['charset'])) $Value .= 'charset=' . $Challenge ['charset'] . ','; $Value = substr ($Value, 0, -1); $Response = new phpEvents_Socket_Stream_XML_Tag ('response', null, base64_encode ($Value)); $Response->setNamespace ('urn:ietf:params:xml:ns:xmpp-sasl'); $this->sendXML ($Response); // #7 Read the response if (!is_object ($Resp = $this->tagReadNext ())) { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Failed to read next tag', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () == 'failure') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'DIGEST-MD5-Authentication failed', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () != 'challenge') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Server sent unexpected response', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } // #8 Respond to the new challenge $Response->unsetValue (); $this->sendXML ($Response); // #9 Read the response if (!is_object ($Resp = $this->tagReadNext ())) { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Failed to read next tag', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () == 'failure') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'DIGEST-MD5-Authentication failed', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } if ($Resp->getName () != 'success') { $this->authenticationFailure (); return $this->__debug (self::DEBUG_ERROR, 'Server sent unexpected response', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); } $this->restartStream (); $this->authenticationSuccess (); return true; } // }}} // {{{ parseSASLChallenge /** * Parse a SASL-Challenge-Response * * @param string $Data * * @access private * @return array **/ private function parseSASLChallenge ($Data) { // Decode Input $Data = base64_decode ($Data); // Create the result $Result = array (); while (strlen ($Data) > 0) { // Find end of the current property if (($p = strpos ($Data, '=')) === false) return $this->__debug (self::DEBUG_ERROR, 'Could not parse SASL-Challenge', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); // Get the name of the property $Name = substr ($Data, 0, $p); $Data = substr ($Data, $p + 1); // Get the value of the property if (($Data [0] == '"') || ($Data [0] == "'")) { if (($p = strpos ($Data, $Data [0], 1)) === false) return $this->__debug (self::DEBUG_ERROR, 'Could not parse SASL-Challenge', __FUNCTION__, __LINE__, __CLASS__, __FILE__, false); $Value = substr ($Data, 1, $p - 1); if (($p = strpos ($Data, ',', $p)) === false) $Data = ''; else $Data = ltrim (substr ($Data, $p + 1)); } elseif (($p = strpos ($Data, ',')) !== false) { $Value = substr ($Data, 0, $p); $Data = ltrim (substr ($Data, $p + 1)); } else { $Value = $Data; $Data = ''; } $Result [$Name] = $Value; } return $Result; } // }}} // {{{ streamInitiated /** * Callback: Stream was successfully initiated * * @access protected * @return void **/ protected function streamInitiated () { } // }}} } ?>