diff --git a/.gitignore b/.gitignore index 67ccfe7..3489835 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store .svn -.idea \ No newline at end of file +.idea +/nbproject/ +/nbproject/private/ \ No newline at end of file diff --git a/README.md b/README.md index 841948d..99b652e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Usage * @param string $jabberHost Jabber Server Host * @param string $boshUri Full URI to the http-bind * @param string $resource Resource identifier - * @param bool $useSsl Use SSL (not working yet, TODO) + * @param bool $useSsl Use SSL * @param bool $debug Enable debug */ $xmppPrebind = new XmppPrebind('your-jabber-host.tld', 'http://your-jabber-host/http-bind/', 'Your XMPP Clients resource name', false, false); @@ -42,6 +42,10 @@ Other Languages =============== There exist other projects for other languages to support a prebind. Go googling :) +SSL +======== +Actually just set CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST is useSSL is false + Be aware ======== This class is in no way feature complete. There may also be bugs. I'd appreciate it if you contribute or submit bug reports. diff --git a/composer.json b/composer.json index 2a07a62..61e60b1 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "piotr-cz/xmpp-prebind-php", + "name": "matteocacciola/xmpp-prebind-php", "description": "This class is for prebinding a XMPP Session with PHP.", "version": "0.1.0", "type": "library", @@ -7,6 +7,10 @@ "homepage": "http://jolicode.com", "license": "MIT", "authors": [ + { + "name": "Matteo Cacciola", + "email": "matteo.cacciola@gmail.com" + }, { "name": "Michael Weibel" }, diff --git a/lib/XmppPrebind.php b/lib/XmppPrebind.php index 033857f..635453a 100644 --- a/lib/XmppPrebind.php +++ b/lib/XmppPrebind.php @@ -1,11 +1,12 @@ + * @author Matteo Cacciola */ - /** * FirePHP for debugging */ @@ -21,662 +22,661 @@ */ class XmppPrebind { - const XMLNS_BODY = 'http://jabber.org/protocol/httpbind'; - const XMLNS_BOSH = 'urn:xmpp:xbosh'; - const XMLNS_CLIENT = 'jabber:client'; - const XMLNS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'; - const XMLNS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'; - const XMLNS_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'; - const XMLNS_VCARD = 'vcard-temp'; - - const XML_LANG = 'en'; - const CONTENT_TYPE = 'text/xml charset=utf-8'; - - const ENCRYPTION_PLAIN = 'PLAIN'; - const ENCRYPTION_DIGEST_MD5 = 'DIGEST-MD5'; - const ENCRYPTION_CRAM_MD5 = 'CRAM-MD5'; - - const SERVICE_NAME = 'xmpp'; - - protected $jabberHost = ''; - protected $boshUri = ''; - protected $resource = ''; - - protected $debug = false; - /** - * FirePHP Instance - * - * @var FirePHP - */ - protected $firePhp = null; - - protected $useGzip = false; - protected $useSsl = false; - protected $encryption = self::ENCRYPTION_PLAIN; - - protected $jid = ''; - protected $password = ''; - - protected $rid = ''; - protected $sid = ''; - - protected $doSession = false; - protected $doBind = false; - - protected $mechanisms = array(); - - // the Bosh attributes for use in a client using this prebound session - protected $wait; - protected $requests; - protected $ver; - protected $polling; - protected $inactivity; - protected $hold; - protected $to; - protected $ack; - protected $accept; - protected $maxpause; - - /** - * Session creation response - * - * @var DOMDocument - */ - public $response; - - /** - * Create a new XmppPrebind Object with the required params - * - * @param string $jabberHost Jabber Server Host - * @param string $boshUri Full URI to the http-bind - * @param string $resource Resource identifier - * @param bool $useSsl Use SSL (not working yet, TODO) - * @param bool $debug Enable debug - */ - public function __construct($jabberHost, $boshUri, $resource, $useSsl = false, $debug = false) { - $this->jabberHost = $jabberHost; - $this->boshUri = $boshUri; - $this->resource = $resource; - - $this->useSsl = $useSsl; - - $this->debug = $debug; - if ($this->debug === true) { - $this->firePhp = FirePHP::getInstance(true); - $this->firePhp->setEnabled(true); - } - - /* TODO: Not working - if (function_exists('gzinflate')) { - $this->useGzip = true; - }*/ - - /* - * The client MUST generate a large, random, positive integer for the initial 'rid' (see Security Considerations) - * and then increment that value by one for each subsequent request. The client MUST take care to choose an - * initial 'rid' that will never be incremented above 9007199254740991 [21] within the session. - * In practice, a session would have to be extraordinarily long (or involve the exchange of an extraordinary - * number of packets) to exceed the defined limit. - * - * @link http://xmpp.org/extensions/xep-0124.html#rids - */ - if (function_exists('mt_rand')) { - $this->rid = mt_rand(1000000000, 10000000000); - } else { - $this->rid = rand(1000000000, 10000000000); - } - } - - /** - * connect to the jabber server with the supplied username & password - * - * @param string $username Username without jabber host - * @param string $password Password - * @param string $route Route - */ - public function connect($username, $password, $route = false) { - $this->jid = $username . '@' . $this->jabberHost; - - if($this->resource) { - $this->jid .= '/' . $this->resource; - } - - $this->password = $password; - - $response = $this->sendInitialConnection($route); - if(empty($response)) { - throw new XmppPrebindConnectionException("No response from server."); + const XMLNS_BODY = 'http://jabber.org/protocol/httpbind'; + const XMLNS_BOSH = 'urn:xmpp:xbosh'; + const XMLNS_CLIENT = 'jabber:client'; + const XMLNS_SESSION = 'urn:ietf:params:xml:ns:xmpp-session'; + const XMLNS_BIND = 'urn:ietf:params:xml:ns:xmpp-bind'; + const XMLNS_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'; + const XMLNS_VCARD = 'vcard-temp'; + const XML_LANG = 'en'; + const CONTENT_TYPE = 'text/xml charset=utf-8'; + const ENCRYPTION_PLAIN = 'PLAIN'; + const ENCRYPTION_DIGEST_MD5 = 'DIGEST-MD5'; + const ENCRYPTION_CRAM_MD5 = 'CRAM-MD5'; + const SERVICE_NAME = 'xmpp'; + + protected $jabberHost = ''; + protected $boshUri = ''; + protected $resource = ''; + protected $debug = false; + + /** + * FirePHP Instance + * + * @var FirePHP + */ + protected $firePhp = null; + protected $useGzip = false; + protected $useSsl = false; + protected $encryption = self::ENCRYPTION_PLAIN; + protected $jid = ''; + protected $password = ''; + protected $rid = ''; + protected $sid = ''; + protected $doSession = false; + protected $doBind = false; + protected $mechanisms = array(); + // the Bosh attributes for use in a client using this prebound session + protected $wait; + protected $requests; + protected $ver; + protected $polling; + protected $inactivity; + protected $hold; + protected $to; + protected $ack; + protected $accept; + protected $maxpause; + + /** + * Session creation response + * + * @var DOMDocument + */ + public $response; + + /** + * Create a new XmppPrebind Object with the required params + * + * @param string $jabberHost Jabber Server Host + * @param string $boshUri Full URI to the http-bind + * @param string $resource Resource identifier + * @param bool $useSsl Use SSL (not working yet, TODO) + * @param bool $debug Enable debug + */ + public function __construct($jabberHost, $boshUri, $resource, $useSsl = false, $debug = false) { + $this->jabberHost = $jabberHost; + $this->boshUri = $boshUri; + $this->resource = $resource; + + $this->useSsl = $useSsl; + + $this->debug = $debug; + if ($this->debug === true) { + $this->firePhp = FirePHP::getInstance(true); + $this->firePhp->setEnabled(true); + } + + /* TODO: Not working + if (function_exists('gzinflate')) { + $this->useGzip = true; + } */ + + /* + * The client MUST generate a large, random, positive integer for the initial 'rid' (see Security Considerations) + * and then increment that value by one for each subsequent request. The client MUST take care to choose an + * initial 'rid' that will never be incremented above 9007199254740991 [21] within the session. + * In practice, a session would have to be extraordinarily long (or involve the exchange of an extraordinary + * number of packets) to exceed the defined limit. + * + * @link http://xmpp.org/extensions/xep-0124.html#rids + */ + if (function_exists('mt_rand')) { + $this->rid = mt_rand(1000000000, 10000000000); + } else { + $this->rid = rand(1000000000, 10000000000); + } + } + + /** + * connect to the jabber server with the supplied username & password + * + * @param string $username Username without jabber host + * @param string $password Password + * @param string $route Route + */ + public function connect($username, $password, $route = false) { + $this->jid = $username . '@' . $this->jabberHost; + + if ($this->resource) { + $this->jid .= '/' . $this->resource; + } + + $this->password = $password; + + $response = $this->sendInitialConnection($route); + if (empty($response)) { + throw new XmppPrebindConnectionException("No response from server."); + } + + $body = self::getBodyFromXml($response); + if (empty($body)) + throw new XmppPrebindConnectionException("No body could be found in response from server."); + $this->sid = $body->getAttribute('sid'); + + // set the Bosh Attributes + $this->wait = $body->getAttribute('wait'); + $this->requests = $body->getAttribute('requests'); + $this->ver = $body->getAttribute('ver'); + $this->polling = $body->getAttribute('polling'); + $this->inactivity = $body->getAttribute('inactivity'); + $this->hold = $body->getAttribute('hold'); + $this->to = $body->getAttribute('to'); + $this->accept = $body->getAttribute('accept'); + $this->maxpause = $body->getAttribute('maxpause'); + + $this->debug($this->sid, 'sid'); + + if (empty($body->firstChild) || empty($body->firstChild->firstChild)) { + throw new XmppPrebindConnectionException("Child not found in response from server."); + } + $mechanisms = $body->getElementsByTagName('mechanism'); + + foreach ($mechanisms as $value) { + $this->mechanisms[] = $value->nodeValue; + } + + if (in_array(self::ENCRYPTION_DIGEST_MD5, $this->mechanisms)) { + $this->encryption = self::ENCRYPTION_DIGEST_MD5; + } elseif (in_array(self::ENCRYPTION_CRAM_MD5, $this->mechanisms)) { + $this->encryption = self::ENCRYPTION_CRAM_MD5; + } elseif (in_array(self::ENCRYPTION_PLAIN, $this->mechanisms)) { + $this->encryption = self::ENCRYPTION_PLAIN; + } else { + throw new XmppPrebindConnectionException("No encryption supported by the server is supported by this library."); + } + + $this->debug($this->encryption, 'encryption used'); + + // Assign session creation response + $this->response = $body; + } + + /** + * Try to authenticate + * + * @throws XmppPrebindException if invalid login + * @return bool + */ + public function auth() { + $auth = Auth_SASL::factory($this->encryption); + + switch ($this->encryption) { + case self::ENCRYPTION_PLAIN: + $authXml = $this->buildPlainAuth($auth); + break; + case self::ENCRYPTION_DIGEST_MD5: + $authXml = $this->sendChallengeAndBuildDigestMd5Auth($auth); + break; + case self::ENCRYPTION_CRAM_MD5: + $authXml = $this->sendChallengeAndBuildCramMd5Auth($auth); + break; + } + $response = $this->send($authXml); + + $body = self::getBodyFromXml($response); + + if (!$body->hasChildNodes() || $body->firstChild->nodeName !== 'success') { + throw new XmppPrebindException("Invalid login"); + } + + $this->sendRestart(); + $this->sendBindIfRequired(); + $this->sendSessionIfRequired(); + + return true; + } + + /** + * Get BOSH parameters to properly setup the BOSH client + * + * @return array + */ + public function getBoshInfo() { + return array( + 'wait' => $this->wait, + 'requests' => $this->requests, + 'ver' => $this->ver, + 'polling' => $this->polling, + 'inactivity' => $this->inactivity, + 'hold' => $this->hold, + 'to' => $this->to, + 'ack' => $this->ack, + 'accept' => $this->accept, + 'maxpause' => $this->maxpause, + ); + } + + /** + * Get jid, sid and rid for attaching + * + * @return array + */ + public function getSessionInfo() { + return array('jid' => $this->jid, 'sid' => $this->sid, 'rid' => $this->rid); + } + + /** + * Debug if debug enabled + * + * @param string $msg + * @param string $label + */ + protected function debug($msg, $label = null) { + if ($this->firePhp) { + $this->firePhp->log($msg, $label); + } + } + + /** + * Send xmpp restart message after successful auth + * + * @return string Response + */ + protected function sendRestart() { + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + $body->appendChild(self::getNewTextAttribute($domDocument, 'to', $this->jabberHost)); + $body->appendChild(self::getNewTextAttribute($domDocument, 'xmlns:xmpp', self::XMLNS_BOSH)); + $body->appendChild(self::getNewTextAttribute($domDocument, 'xmpp:restart', 'true')); + + $restartResponse = $this->send($domDocument->saveXML()); + + $restartBody = self::getBodyFromXml($restartResponse); + foreach ($restartBody->childNodes as $bodyChildNodes) { + if ($bodyChildNodes->nodeName === 'stream:features') { + foreach ($bodyChildNodes->childNodes as $streamFeatures) { + if ($streamFeatures->nodeName === 'bind') { + $this->doBind = true; + } elseif ($streamFeatures->nodeName === 'session') { + $this->doSession = true; + } + } + } + } + + return $restartResponse; + } + + /** + * Send xmpp bind message after restart + * + * @return string Response + */ + protected function sendBindIfRequired() { + if ($this->doBind) { + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $iq = $domDocument->createElement('iq'); + $iq->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_CLIENT)); + $iq->appendChild(self::getNewTextAttribute($domDocument, 'type', 'set')); + $iq->appendChild(self::getNewTextAttribute($domDocument, 'id', 'bind_' . rand())); + + $bind = $domDocument->createElement('bind'); + $bind->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_BIND)); + + $resource = $domDocument->createElement('resource'); + $resource->appendChild($domDocument->createTextNode($this->resource)); + + $bind->appendChild($resource); + $iq->appendChild($bind); + $body->appendChild($iq); + + return $this->send($domDocument->saveXML()); + } + return false; + } + + /** + * Send session if there's a session node in the restart response (within stream:features) + */ + protected function sendSessionIfRequired() { + if ($this->doSession) { + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $iq = $domDocument->createElement('iq'); + $iq->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_CLIENT)); + $iq->appendChild(self::getNewTextAttribute($domDocument, 'type', 'set')); + $iq->appendChild(self::getNewTextAttribute($domDocument, 'id', 'session_auth_' . rand())); + + $session = $domDocument->createElement('session'); + $session->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SESSION)); + + $iq->appendChild($session); + $body->appendChild($iq); + + return $this->send($domDocument->saveXML()); + } + return false; + } + + /** + * Send initial connection string + * + * @param string $route + * @return string Response + */ + protected function sendInitialConnection($route = false) { + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $waitTime = 60; + + $body->appendChild(self::getNewTextAttribute($domDocument, 'hold', '1')); + $body->appendChild(self::getNewTextAttribute($domDocument, 'to', $this->jabberHost)); + $body->appendChild(self::getNewTextAttribute($domDocument, 'xmlns:xmpp', self::XMLNS_BOSH)); + $body->appendChild(self::getNewTextAttribute($domDocument, 'xmpp:version', '1.0')); + $body->appendChild(self::getNewTextAttribute($domDocument, 'wait', $waitTime)); + + if ($route) { + $body->appendChild(self::getNewTextAttribute($domDocument, 'route', $route)); + } + + return $this->send($domDocument->saveXML()); + } + + /** + * Send challenge request + * + * @return string Challenge + */ + protected function sendChallenge() { + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $auth = $domDocument->createElement('auth'); + $auth->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); + $auth->appendChild(self::getNewTextAttribute($domDocument, 'mechanism', $this->encryption)); + $body->appendChild($auth); + + $response = $this->send($domDocument->saveXML()); + + $body = $this->getBodyFromXml($response); + $challenge = base64_decode($body->firstChild->nodeValue); + + return $challenge; + } + + /** + * Build PLAIN auth string + * + * @param Auth_SASL_Common $auth + * @return string Auth XML to send + */ + protected function buildPlainAuth(Auth_SASL_Common $auth) { + $authString = $auth->getResponse(self::getNodeFromJid($this->jid), $this->password, self::getBareJidFromJid($this->jid)); + $authString = base64_encode($authString); + $this->debug($authString, 'PLAIN Auth String'); + + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $auth = $domDocument->createElement('auth'); + $auth->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); + $auth->appendChild(self::getNewTextAttribute($domDocument, 'mechanism', $this->encryption)); + $auth->appendChild($domDocument->createTextNode($authString)); + $body->appendChild($auth); + + return $domDocument->saveXML(); + } + + /** + * Send challenge request and build DIGEST-MD5 auth string + * + * @param Auth_SASL_Common $auth + * @return string Auth XML to send + */ + protected function sendChallengeAndBuildDigestMd5Auth(Auth_SASL_Common $auth) { + $challenge = $this->sendChallenge(); + + $authString = $auth->getResponse(self::getNodeFromJid($this->jid), $this->password, $challenge, $this->jabberHost, self::SERVICE_NAME); + $this->debug($authString, 'DIGEST-MD5 Auth String'); + + $authString = base64_encode($authString); + + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $response = $domDocument->createElement('response'); + $response->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); + $response->appendChild($domDocument->createTextNode($authString)); + + $body->appendChild($response); + + + $challengeResponse = $this->send($domDocument->saveXML()); + + return $this->replyToChallengeResponse($challengeResponse); + } + + /** + * Send challenge request and build CRAM-MD5 auth string + * + * @param Auth_SASL_Common $auth + * @return string Auth XML to send + */ + protected function sendChallengeAndBuildCramMd5Auth(Auth_SASL_Common $auth) { + $challenge = $this->sendChallenge(); + + $authString = $auth->getResponse(self::getNodeFromJid($this->jid), $this->password, $challenge); + $this->debug($authString, 'CRAM-MD5 Auth String'); + + $authString = base64_encode($authString); + + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + + $response = $domDocument->createElement('response'); + $response->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); + $response->appendChild($domDocument->createTextNode($authString)); + + $body->appendChild($response); + + $challengeResponse = $this->send($domDocument->saveXML()); + + return $this->replyToChallengeResponse($challengeResponse); + } + + /** + * CRAM-MD5 and DIGEST-MD5 reply with an additional challenge response which must be replied to. + * After this additional reply, the server should reply with "success". + */ + protected function replyToChallengeResponse($challengeResponse) { + $body = self::getBodyFromXml($challengeResponse); + $challenge = base64_decode((string) $body->firstChild->nodeValue); + if (strpos($challenge, 'rspauth') === false) { + throw new XmppPrebindConnectionException('Invalid challenge response received'); + } + + $domDocument = $this->buildBody(); + $body = self::getBodyFromDomDocument($domDocument); + $response = $domDocument->createElement('response'); + $response->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); + + $body->appendChild($response); + + return $domDocument->saveXML(); + } + + /** + * Send XML via CURL + * + * @param string $xml + * @return string Response + */ + protected function send($xml) { + $ch = curl_init($this->boshUri); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + + $header = array('Content-Type: ' . self::CONTENT_TYPE); + if ($this->useGzip) { + $header[] = 'Accept-Encoding: gzip, deflate'; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + + curl_setopt($ch, CURLOPT_VERBOSE, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + if (!($this->useSsl)) { + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + } + + $response = curl_exec($ch); + + // Check if curl failed to get response + if ($response === false) { + throw new XmppPrebindConnectionException("Cannot connect to service"); } - $body = self::getBodyFromXml($response); - if ( empty( $body ) ) - throw new XmppPrebindConnectionException("No body could be found in response from server."); - $this->sid = $body->getAttribute('sid'); - - // set the Bosh Attributes - $this->wait = $body->getAttribute('wait'); - $this->requests = $body->getAttribute('requests'); - $this->ver = $body->getAttribute('ver'); - $this->polling = $body->getAttribute('polling'); - $this->inactivity = $body->getAttribute('inactivity'); - $this->hold = $body->getAttribute('hold'); - $this->to = $body->getAttribute('to'); - $this->accept = $body->getAttribute('accept'); - $this->maxpause = $body->getAttribute('maxpause'); - - $this->debug($this->sid, 'sid'); - - if(empty($body->firstChild) || empty($body->firstChild->firstChild)) { - throw new XmppPrebindConnectionException("Child not found in response from server."); + curl_close($ch); + + if ($this->useGzip) { + $response = self::compatibleGzInflate($response); + } + + $this->debug($xml, 'SENT'); + $this->debug($response, 'RECV:'); + + return $response; + } + + /** + * Fix gzdecompress/gzinflate data error warning. + * + * @link http://www.mydigitallife.info/2010/01/17/workaround-to-fix-php-warning-gzuncompress-or-gzinflate-data-error-in-wordpress-http-php/ + * + * @param string $gzData + * @return string|bool + */ + public static function compatibleGzInflate($gzData) { + if (substr($gzData, 0, 3) == "\x1f\x8b\x08") { + $i = 10; + $flg = ord(substr($gzData, 3, 1)); + if ($flg > 0) { + if ($flg & 4) { + list($xlen) = unpack('v', substr($gzData, $i, 2)); + $i = $i + 2 + $xlen; + } + if ($flg & 8) + $i = strpos($gzData, "\0", $i) + 1; + if ($flg & 16) + $i = strpos($gzData, "\0", $i) + 1; + if ($flg & 2) + $i = $i + 2; + } + return gzinflate(substr($gzData, $i, -8)); + } else { + return false; + } + } + + /** + * Build DOMDocument with standard xmpp body child node. + * + * @return DOMDocument + */ + protected function buildBody() { + $xml = new DOMDocument('1.0', 'UTF-8'); + + $body = $xml->createElement('body'); + $xml->appendChild($body); + + $body->appendChild(self::getNewTextAttribute($xml, 'xmlns', self::XMLNS_BODY)); + $body->appendChild(self::getNewTextAttribute($xml, 'content', self::CONTENT_TYPE)); + $body->appendChild(self::getNewTextAttribute($xml, 'rid', $this->getAndIncrementRid())); + $body->appendChild(self::getNewTextAttribute($xml, 'xml:lang', self::XML_LANG)); + + if ($this->sid != '') { + $body->appendChild(self::getNewTextAttribute($xml, 'sid', $this->sid)); + } + + return $xml; + } + + /** + * Get jid in form of username@jabberHost + * + * @param string $jid Jid in form username@jabberHost/Resource + * @return string JID + */ + public static function getBareJidFromJid($jid) { + if ($jid == '') { + return ''; + } + $splittedJid = explode('/', $jid, 1); + return $splittedJid[0]; + } + + /** + * Get node (username) from jid + * + * @param string $jid + * @return string Node + */ + public static function getNodeFromJid($jid) { + $atPos = strpos($jid, '@'); + if ($atPos === false) { + return ''; } - $mechanisms = $body->getElementsByTagName('mechanism'); - - foreach ($mechanisms as $value) { - $this->mechanisms[] = $value->nodeValue; - } - - if (in_array(self::ENCRYPTION_DIGEST_MD5, $this->mechanisms)) { - $this->encryption = self::ENCRYPTION_DIGEST_MD5; - } elseif (in_array(self::ENCRYPTION_CRAM_MD5, $this->mechanisms)) { - $this->encryption = self::ENCRYPTION_CRAM_MD5; - } elseif (in_array(self::ENCRYPTION_PLAIN, $this->mechanisms)) { - $this->encryption = self::ENCRYPTION_PLAIN; - } else { - throw new XmppPrebindConnectionException("No encryption supported by the server is supported by this library."); - } - - $this->debug($this->encryption, 'encryption used'); - - // Assign session creation response - $this->response = $body; - } - - /** - * Try to authenticate - * - * @throws XmppPrebindException if invalid login - * @return bool - */ - public function auth() { - $auth = Auth_SASL::factory($this->encryption); - - switch ($this->encryption) { - case self::ENCRYPTION_PLAIN: - $authXml = $this->buildPlainAuth($auth); - break; - case self::ENCRYPTION_DIGEST_MD5: - $authXml = $this->sendChallengeAndBuildDigestMd5Auth($auth); - break; - case self::ENCRYPTION_CRAM_MD5: - $authXml = $this->sendChallengeAndBuildCramMd5Auth($auth); - break; - } - $response = $this->send($authXml); - - $body = self::getBodyFromXml($response); - - if (!$body->hasChildNodes() || $body->firstChild->nodeName !== 'success') { - throw new XmppPrebindException("Invalid login"); - } - - $this->sendRestart(); - $this->sendBindIfRequired(); - $this->sendSessionIfRequired(); - - return true; - } - - /** - * Get BOSH parameters to properly setup the BOSH client - * - * @return array - */ - public function getBoshInfo() - { - return array( - 'wait' => $this->wait, - 'requests' => $this->requests, - 'ver' => $this->ver, - 'polling' => $this->polling, - 'inactivity' => $this->inactivity, - 'hold' => $this->hold, - 'to' => $this->to, - 'ack' => $this->ack, - 'accept' => $this->accept, - 'maxpause' => $this->maxpause, - ); - } - - /** - * Get jid, sid and rid for attaching - * - * @return array - */ - public function getSessionInfo() { - return array('jid' => $this->jid, 'sid' => $this->sid, 'rid' => $this->rid); - } - - /** - * Debug if debug enabled - * - * @param string $msg - * @param string $label - */ - protected function debug($msg, $label = null) { - if ($this->firePhp) { - $this->firePhp->log($msg, $label); - } - } - - /** - * Send xmpp restart message after successful auth - * - * @return string Response - */ - protected function sendRestart() { - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - $body->appendChild(self::getNewTextAttribute($domDocument, 'to', $this->jabberHost)); - $body->appendChild(self::getNewTextAttribute($domDocument, 'xmlns:xmpp', self::XMLNS_BOSH)); - $body->appendChild(self::getNewTextAttribute($domDocument, 'xmpp:restart', 'true')); - - $restartResponse = $this->send($domDocument->saveXML()); - - $restartBody = self::getBodyFromXml($restartResponse); - foreach ($restartBody->childNodes as $bodyChildNodes) { - if ($bodyChildNodes->nodeName === 'stream:features') { - foreach ($bodyChildNodes->childNodes as $streamFeatures) { - if ($streamFeatures->nodeName === 'bind') { - $this->doBind = true; - } elseif ($streamFeatures->nodeName === 'session') { - $this->doSession = true; - } - } - } - } - - return $restartResponse; - } - - /** - * Send xmpp bind message after restart - * - * @return string Response - */ - protected function sendBindIfRequired() { - if ($this->doBind) { - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $iq = $domDocument->createElement('iq'); - $iq->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_CLIENT)); - $iq->appendChild(self::getNewTextAttribute($domDocument, 'type', 'set')); - $iq->appendChild(self::getNewTextAttribute($domDocument, 'id', 'bind_' . rand())); - - $bind = $domDocument->createElement('bind'); - $bind->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_BIND)); - - $resource = $domDocument->createElement('resource'); - $resource->appendChild($domDocument->createTextNode($this->resource)); - - $bind->appendChild($resource); - $iq->appendChild($bind); - $body->appendChild($iq); - - return $this->send($domDocument->saveXML()); - } - return false; - } - - /** - * Send session if there's a session node in the restart response (within stream:features) - */ - protected function sendSessionIfRequired() { - if ($this->doSession) { - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $iq = $domDocument->createElement('iq'); - $iq->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_CLIENT)); - $iq->appendChild(self::getNewTextAttribute($domDocument, 'type', 'set')); - $iq->appendChild(self::getNewTextAttribute($domDocument, 'id', 'session_auth_' . rand())); - - $session = $domDocument->createElement('session'); - $session->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SESSION)); - - $iq->appendChild($session); - $body->appendChild($iq); - - return $this->send($domDocument->saveXML()); - } - return false; - } - - /** - * Send initial connection string - * - * @param string $route - * @return string Response - */ - protected function sendInitialConnection($route = false) { - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $waitTime = 60; - - $body->appendChild(self::getNewTextAttribute($domDocument, 'hold', '1')); - $body->appendChild(self::getNewTextAttribute($domDocument, 'to', $this->jabberHost)); - $body->appendChild(self::getNewTextAttribute($domDocument, 'xmlns:xmpp', self::XMLNS_BOSH)); - $body->appendChild(self::getNewTextAttribute($domDocument, 'xmpp:version', '1.0')); - $body->appendChild(self::getNewTextAttribute($domDocument, 'wait', $waitTime)); - - if ($route) - { - $body->appendChild(self::getNewTextAttribute($domDocument, 'route', $route)); - } - - return $this->send($domDocument->saveXML()); - } - - /** - * Send challenge request - * - * @return string Challenge - */ - protected function sendChallenge() { - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $auth = $domDocument->createElement('auth'); - $auth->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); - $auth->appendChild(self::getNewTextAttribute($domDocument, 'mechanism', $this->encryption)); - $body->appendChild($auth); - - $response = $this->send($domDocument->saveXML()); - - $body = $this->getBodyFromXml($response); - $challenge = base64_decode($body->firstChild->nodeValue); - - return $challenge; - } - - /** - * Build PLAIN auth string - * - * @param Auth_SASL_Common $auth - * @return string Auth XML to send - */ - protected function buildPlainAuth(Auth_SASL_Common $auth) { - $authString = $auth->getResponse(self::getNodeFromJid($this->jid), $this->password, self::getBareJidFromJid($this->jid)); - $authString = base64_encode($authString); - $this->debug($authString, 'PLAIN Auth String'); - - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $auth = $domDocument->createElement('auth'); - $auth->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); - $auth->appendChild(self::getNewTextAttribute($domDocument, 'mechanism', $this->encryption)); - $auth->appendChild($domDocument->createTextNode($authString)); - $body->appendChild($auth); - - return $domDocument->saveXML(); - } - - /** - * Send challenge request and build DIGEST-MD5 auth string - * - * @param Auth_SASL_Common $auth - * @return string Auth XML to send - */ - protected function sendChallengeAndBuildDigestMd5Auth(Auth_SASL_Common $auth) { - $challenge = $this->sendChallenge(); - - $authString = $auth->getResponse(self::getNodeFromJid($this->jid), $this->password, $challenge, $this->jabberHost, self::SERVICE_NAME); - $this->debug($authString, 'DIGEST-MD5 Auth String'); - - $authString = base64_encode($authString); - - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $response = $domDocument->createElement('response'); - $response->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); - $response->appendChild($domDocument->createTextNode($authString)); - - $body->appendChild($response); - - - $challengeResponse = $this->send($domDocument->saveXML()); - - return $this->replyToChallengeResponse($challengeResponse); - } - - /** - * Send challenge request and build CRAM-MD5 auth string - * - * @param Auth_SASL_Common $auth - * @return string Auth XML to send - */ - protected function sendChallengeAndBuildCramMd5Auth(Auth_SASL_Common $auth) { - $challenge = $this->sendChallenge(); - - $authString = $auth->getResponse(self::getNodeFromJid($this->jid), $this->password, $challenge); - $this->debug($authString, 'CRAM-MD5 Auth String'); - - $authString = base64_encode($authString); - - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - - $response = $domDocument->createElement('response'); - $response->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); - $response->appendChild($domDocument->createTextNode($authString)); - - $body->appendChild($response); - - $challengeResponse = $this->send($domDocument->saveXML()); - - return $this->replyToChallengeResponse($challengeResponse); - } - - /** - * CRAM-MD5 and DIGEST-MD5 reply with an additional challenge response which must be replied to. - * After this additional reply, the server should reply with "success". - */ - protected function replyToChallengeResponse($challengeResponse) { - $body = self::getBodyFromXml($challengeResponse); - $challenge = base64_decode((string)$body->firstChild->nodeValue); - if (strpos($challenge, 'rspauth') === false) { - throw new XmppPrebindConnectionException('Invalid challenge response received'); - } - - $domDocument = $this->buildBody(); - $body = self::getBodyFromDomDocument($domDocument); - $response = $domDocument->createElement('response'); - $response->appendChild(self::getNewTextAttribute($domDocument, 'xmlns', self::XMLNS_SASL)); - - $body->appendChild($response); - - return $domDocument->saveXML(); - } - - /** - * Send XML via CURL - * - * @param string $xml - * @return string Response - */ - protected function send($xml) { - $ch = curl_init($this->boshUri); - curl_setopt($ch, CURLOPT_HEADER, 0); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - - $header = array('Content-Type: ' . self::CONTENT_TYPE); - if ($this->useGzip) { - $header[] = 'Accept-Encoding: gzip, deflate'; - } - curl_setopt($ch, CURLOPT_HTTPHEADER, $header); - - curl_setopt($ch, CURLOPT_VERBOSE, 0); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - - $response = curl_exec($ch); - - // Check if curl failed to get response - if ($response === false) { - throw new XmppPrebindConnectionException("Cannot connect to service"); - } - - curl_close($ch); - - if ($this->useGzip) { - $response = self::compatibleGzInflate($response); - } - - $this->debug($xml, 'SENT'); - $this->debug($response, 'RECV:'); - - return $response; - } - - /** - * Fix gzdecompress/gzinflate data error warning. - * - * @link http://www.mydigitallife.info/2010/01/17/workaround-to-fix-php-warning-gzuncompress-or-gzinflate-data-error-in-wordpress-http-php/ - * - * @param string $gzData - * @return string|bool - */ - public static function compatibleGzInflate($gzData) { - if ( substr($gzData, 0, 3) == "\x1f\x8b\x08" ) { - $i = 10; - $flg = ord( substr($gzData, 3, 1) ); - if ( $flg > 0 ) { - if ( $flg & 4 ) { - list($xlen) = unpack('v', substr($gzData, $i, 2) ); - $i = $i + 2 + $xlen; - } - if ( $flg & 8 ) - $i = strpos($gzData, "\0", $i) + 1; - if ( $flg & 16 ) - $i = strpos($gzData, "\0", $i) + 1; - if ( $flg & 2 ) - $i = $i + 2; - } - return gzinflate( substr($gzData, $i, -8) ); - } else { - return false; - } - } - - /** - * Build DOMDocument with standard xmpp body child node. - * - * @return DOMDocument - */ - protected function buildBody() { - $xml = new DOMDocument('1.0', 'UTF-8'); - - $body = $xml->createElement('body'); - $xml->appendChild($body); - - $body->appendChild(self::getNewTextAttribute($xml, 'xmlns', self::XMLNS_BODY)); - $body->appendChild(self::getNewTextAttribute($xml, 'content', self::CONTENT_TYPE)); - $body->appendChild(self::getNewTextAttribute($xml, 'rid', $this->getAndIncrementRid())); - $body->appendChild(self::getNewTextAttribute($xml, 'xml:lang', self::XML_LANG)); - - if ($this->sid != '') { - $body->appendChild(self::getNewTextAttribute($xml, 'sid', $this->sid)); - } - - return $xml; - } - - /** - * Get jid in form of username@jabberHost - * - * @param string $jid Jid in form username@jabberHost/Resource - * @return string JID - */ - public static function getBareJidFromJid($jid) { - if ($jid == '') { - return ''; - } - $splittedJid = explode('/', $jid, 1); - return $splittedJid[0]; - } - - /** - * Get node (username) from jid - * - * @param string $jid - * @return string Node - */ - public static function getNodeFromJid($jid) { - $atPos = strpos($jid, '@'); - if ($atPos === false) { - return ''; - } - return substr($jid, 0, $atPos); - } - - /** - * Append new attribute to existing DOMDocument. - * - * @param DOMDocument $domDocument - * @param string $attributeName - * @param string $value - * @return DOMNode - */ - protected static function getNewTextAttribute($domDocument, $attributeName, $value) { - $attribute = $domDocument->createAttribute($attributeName); - $attribute->appendChild($domDocument->createTextNode($value)); - - return $attribute; - } - - /** - * Get body node from DOMDocument - * - * @param DOMDocument $domDocument - * @return DOMNode - */ - protected static function getBodyFromDomDocument($domDocument) { - $body = $domDocument->getElementsByTagName('body'); - return $body->item(0); - } - - /** - * Parse XML and return DOMNode of the body - * - * @uses XmppPrebind::getBodyFromDomDocument() - * @param string $xml - * @return DOMNode - */ - protected static function getBodyFromXml($xml) { - $domDocument = new DOMDocument(); - $domDocument->loadXml($xml); - - return self::getBodyFromDomDocument($domDocument); - } - - /** - * Get the rid and increment it by one. - * Required by RFC - * - * @return int - */ - protected function getAndIncrementRid() { - return $this->rid++; - } + return substr($jid, 0, $atPos); + } + + /** + * Append new attribute to existing DOMDocument. + * + * @param DOMDocument $domDocument + * @param string $attributeName + * @param string $value + * @return DOMNode + */ + protected static function getNewTextAttribute($domDocument, $attributeName, $value) { + $attribute = $domDocument->createAttribute($attributeName); + $attribute->appendChild($domDocument->createTextNode($value)); + + return $attribute; + } + + /** + * Get body node from DOMDocument + * + * @param DOMDocument $domDocument + * @return DOMNode + */ + protected static function getBodyFromDomDocument($domDocument) { + $body = $domDocument->getElementsByTagName('body'); + return $body->item(0); + } + + /** + * Parse XML and return DOMNode of the body + * + * @uses XmppPrebind::getBodyFromDomDocument() + * @param string $xml + * @return DOMNode + */ + protected static function getBodyFromXml($xml) { + $domDocument = new DOMDocument(); + $domDocument->loadXml($xml); + + return self::getBodyFromDomDocument($domDocument); + } + + /** + * Get the rid and increment it by one. + * Required by RFC + * + * @return int + */ + protected function getAndIncrementRid() { + return $this->rid++; + } + } /** * Standard XmppPrebind Exception */ -class XmppPrebindException extends Exception{} +class XmppPrebindException extends Exception { + +} -class XmppPrebindConnectionException extends XmppPrebindException{} +class XmppPrebindConnectionException extends XmppPrebindException { + +}