*/ /** * FirePHP for debugging */ include 'FirePHP/fb.php'; /** * PEAR Auth_SASL */ require 'Auth/SASL.php'; /** * XMPP Library for connecting to jabber server & receiving sid and rid */ 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."); } $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); $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++; } } /** * Standard XmppPrebind Exception */ class XmppPrebindException extends Exception{} class XmppPrebindConnectionException extends XmppPrebindException{}