PageRenderTime 62ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/Missus.php

https://bitbucket.org/Neewok/missus
PHP | 984 lines | 502 code | 64 blank | 418 comment | 87 complexity | 327c25f7bbf2a331e3815d510a8a38ba MD5 | raw file
  1. <?php
  2. /*
  3. *
  4. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  5. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  6. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  7. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  8. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  9. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  10. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  11. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  12. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  13. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  14. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  15. *
  16. * This software consists of voluntary contributions made by many individuals
  17. * and is licensed under the LGPL. For more information, see
  18. * <http://www.gnu.org/copyleft/lesser.html>.
  19. */
  20. /**
  21. * @package Missus
  22. * @author Denis Hovart <dhovart@gmail.com>
  23. * @license www.gnu.org/copyleft/lesser.html LGPL
  24. * @version 0.1
  25. */
  26. class Missus {
  27. // =======
  28. // = Log =
  29. // =======
  30. /**
  31. * @var boolean
  32. */
  33. private $log;
  34. /**
  35. * @var boolean
  36. */
  37. private $log_html;
  38. // =================
  39. // = Configuration =
  40. // =================
  41. /**
  42. * @var boolean
  43. */
  44. protected $use_tls;
  45. /**
  46. * @var boolean
  47. */
  48. protected $use_bosh;
  49. /**
  50. * @var boolean
  51. */
  52. protected $timeout;
  53. // ====================
  54. // = Connection infos =
  55. // ====================
  56. /**
  57. * @var string
  58. */
  59. protected $host;
  60. /**
  61. * @var Integer
  62. */
  63. protected $port;
  64. /**
  65. * @var boolean
  66. */
  67. protected $is_bot;
  68. /**
  69. * @var string
  70. */
  71. protected $username;
  72. /**
  73. * @var string
  74. */
  75. protected $domain;
  76. /**
  77. * @var string
  78. */
  79. protected $password;
  80. // ==============
  81. // = XMPP infos =
  82. // ==============
  83. /**
  84. * @var string
  85. */
  86. protected $bare_jid;
  87. /**
  88. * @var string
  89. */
  90. protected $full_jid;
  91. /**
  92. * @var Integer
  93. */
  94. protected $id;
  95. /**
  96. * @var array
  97. */
  98. protected $roster;
  99. // ===================
  100. // = Events handling =
  101. // ===================
  102. /**
  103. * @var array
  104. */
  105. protected $events;
  106. /**
  107. * @var boolean
  108. */
  109. protected $authenticated;
  110. /**
  111. * @var boolean
  112. */
  113. protected $connected;
  114. /**
  115. * @var boolean
  116. */
  117. protected $tls_is_set;
  118. /**
  119. * @var boolean
  120. */
  121. protected $id_is_set;
  122. // ==========
  123. // = Stream =
  124. // ==========
  125. /**
  126. * @var string
  127. */
  128. protected $received;
  129. // =========
  130. // = Timer =
  131. // =========
  132. /**
  133. * @var integer
  134. */
  135. protected $time_start;
  136. // ======================================
  137. // = Missus main API and public methods =
  138. // ======================================
  139. /**
  140. * Constructor
  141. * @param string $username
  142. * @param string $password
  143. * @param string $domain
  144. * @param string $host If null, same as $domain
  145. * @param boolean $use_tls If set to true, will attempt a secured encypted connection over TLS (if proposed by server)
  146. * @param boolean $use_bosh (Not yet implemented)
  147. * @param Integer $port
  148. * @return Missus
  149. */
  150. public function __construct($username, $password, $domain, $host = null, $use_tls = true, $use_bosh = false, $port = 5222) {
  151. if(null === $username || null === $password || null === $domain)
  152. throw new Exception("Username, password and host need to be specified");
  153. if(null === $host) $this->host = $domain;
  154. else $this->host = $host;
  155. $this->port = $port;
  156. $this->username = $username;
  157. $this->domain = $domain;
  158. $this->password = $password;
  159. $this->resource = "Missus";
  160. $this->full_jid = sprintf("%s@%s/%s", $this->username, $this->host, $this->resource);
  161. $this->bare_jid = sprintf("%s@%s", $this->username, $this->host);
  162. $this->received = '';
  163. $this->timeout = 20;
  164. $this->use_tls = $use_tls;
  165. $this->use_bosh = $use_bosh;
  166. $this->tls_is_set = false;
  167. $this->authenticated = false;
  168. $this->connected = false;
  169. $this->id_is_set = false;
  170. $this->events = array();
  171. $this->log = false;
  172. $this->lang = 'en';
  173. $this->is_bot = false;
  174. $this->roster= array();
  175. if($this->use_bosh) throw new Exception("Bosh is not supported yet");
  176. $this->time_start = array_sum(explode(' ', microtime()));
  177. }
  178. /**
  179. * Performed when connecting
  180. */
  181. protected function onConnect() {}
  182. /**
  183. * Performed while Missus is running
  184. */
  185. protected function mainLoop() {}
  186. /**
  187. * Performed when receiving something new
  188. */
  189. protected function onReceivedContent() {}
  190. /**
  191. * Performed when a contact is composing a message
  192. * @param string $from the user composing
  193. */
  194. protected function onUserComposing($from) {}
  195. /**
  196. * Performed when a contact paused the composition of his message
  197. * @param string $from the user composing
  198. */
  199. protected function onUserPaused($from) {} # todo
  200. /**
  201. * Performed when a message is received
  202. * @param string $body the contents of the message
  203. * @param string $from the user sending the message
  204. * @param string $type the type of message
  205. * @param string $xml the raw xml of the message
  206. */
  207. protected function onReceivedMessage($body, $from, $type, $xml) {}
  208. /**
  209. * Performed when a contact send a presence stanza
  210. * @param string $from the user
  211. * @param string $status the user status
  212. * @param string $availability the user availability
  213. */
  214. protected function onPresence($from, $status, $availability) {}
  215. /**
  216. * Performed when a contact send a subscription request
  217. * @param string $from the user
  218. */
  219. protected function onSubscribe($from) {}
  220. /**
  221. * Performed when a contact unsubscribe from the user
  222. * @param string $from the user
  223. */
  224. protected function onUnsubscribe($from) {}
  225. /**
  226. * Activate events log
  227. * @return Missus
  228. */
  229. public function logEvents() {
  230. $this->log = true;
  231. return $this;
  232. }
  233. /**
  234. * Set client lang
  235. * @param string $lang The lang you wish to set your content to
  236. * @link http://www.w3.org/TR/xml/#sec-lang-tag
  237. * @return Missus
  238. */
  239. public function setLang($lang) {
  240. $this->lang = $lang;
  241. return $this;
  242. }
  243. /**
  244. * Set the connection as persistent
  245. * @return Missus
  246. */
  247. public function setPersistent() {
  248. $this->is_bot = true;
  249. return $this;
  250. }
  251. /**
  252. * Open a new stream to the specified host on the specified port, then attempt authentication
  253. */
  254. public function connect() {
  255. $socket = sprintf('tcp://%s:%d/', $this->host, $this->port);
  256. if($this->log) Logger::log(sprintf('Connecting to %s...', $socket), $this->log_html);
  257. $stream = @stream_socket_client($socket, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT);
  258. if(!$stream) throw new Exception(sprintf('Unable to open stream. Error %d: %s', $errno, $errstr));
  259. stream_set_blocking($stream, 0);
  260. $this->stream = $stream;
  261. $this->initiateXMLStream();
  262. $this->addEventListener('checkFeatures');
  263. $this->connected = true;
  264. $this->onConnect();
  265. if($this->is_bot) $this->addEventListener('checkStreamContent');
  266. $this->disconnect();
  267. }
  268. /**
  269. * Close XML stream and shut down socket
  270. */
  271. public function disconnect() {
  272. $this->writeToStream('</stream:stream>');
  273. stream_socket_shutdown($this->stream, STREAM_SHUT_RDWR);
  274. }
  275. /**
  276. * Defines user presence
  277. * @param string $type
  278. * @param string $to
  279. * @param string $from
  280. * @param string $availability
  281. * @param string $status
  282. * @param string $priority
  283. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.5.5
  284. * @return Missus
  285. */
  286. public function sendPresence($type = null, $to = null, $from = null, $availability = null, $status = null, $priority = null) {
  287. $presence = new SimpleXMLElement('<presence/>');
  288. if(null !== $type) $presence->addAttribute('type', $type);
  289. if(null !== $to) $presence->addAttribute('to', $to);
  290. if(null !== $from) $presence->addAttribute('from', $from);
  291. if(null !== $availability) $presence->addChild('show', $availability);
  292. if(null !== $status) $presence->addChild('status', $status);
  293. if(null !== $priority) $presence->addChild('priority', $priority);
  294. $this->writeToStream($this->as_xml($presence));
  295. unset($presence);
  296. return $this;
  297. }
  298. /**
  299. * Set user availibility
  300. * @param string $availability
  301. * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-show
  302. * @uses sendPresence
  303. * @return Missus
  304. */
  305. public function setAvailability($availability) {
  306. $this->sendPresence(null, null, null, $availability);
  307. return $this;
  308. }
  309. /**
  310. * Set user status
  311. * @param string $status
  312. * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-status
  313. * @uses sendPresence
  314. * @return Missus
  315. */
  316. public function setStatus($status) {
  317. $this->sendPresence(null, null, null, null, $status);
  318. return $this;
  319. }
  320. /**
  321. * Set user priority
  322. * @param integer $priority
  323. * @link http://xmpp.org/rfcs/rfc3921.html#stanzas-presence-children-priority
  324. * @uses sendPresence
  325. * @return Missus
  326. */
  327. public function setPriority($priority) {
  328. $this->sendPresence(null, null, null, null, null, $priority);
  329. return $this;
  330. }
  331. /**
  332. * Send a subscription request to a user
  333. * @param string $to the jid of the user you wish to subscribe to
  334. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.1
  335. * @uses sendPresence
  336. * @return Missus
  337. */
  338. public function subscribe($to, $name = null, $group = null) {
  339. $this->addToRoster($to);
  340. $this->sendPresence('subscribe', $to);
  341. $this->askRoster();
  342. return $this;
  343. }
  344. /**
  345. * Subscribe back to a user
  346. * @param string $to the jid of the user you wish to subscribe back to
  347. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.8.3
  348. * @uses subscribe
  349. * @uses sendPresence
  350. * @return Missus
  351. */
  352. public function subscribeBack($to, $name = null, $group = null) {
  353. $this->sendPresence('subscribed', $to);
  354. $this->subscribe($to);
  355. return $this;
  356. }
  357. /**
  358. * Decline subscription request
  359. * @param string $to
  360. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.8.2.1
  361. * @uses sendPresence
  362. * @return Missus
  363. */
  364. public function declineSubscriptionRequest($from) {
  365. $this->sendPresence('unsubscribed', $from);
  366. $this->askRoster();
  367. return $this;
  368. }
  369. /**
  370. * Unsubscribe from a user
  371. * @param string $to the jid of the user you wish to unsubscribe from
  372. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.6.1
  373. * @return Missus
  374. */
  375. public function unsubscribe($to) {
  376. $this->sendPresence('unsubscribe', $to);
  377. # TODO verify if the user is in the contact's roster before
  378. $this->removeFromRoster($to);
  379. $this->askRoster();
  380. return $this;
  381. }
  382. /**
  383. * Returns user roster
  384. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.5.5
  385. * @return Array
  386. */
  387. public function getRoster() {
  388. return $this->roster;
  389. }
  390. /**
  391. * Send a message to a user
  392. * @param string $to The jid of the user you wish to send a message to
  393. * @param string $body The content of your message
  394. * @param string $subject If any, the subject of the message
  395. * @param string $thread If any, the thread the message belongs to
  396. * @param string $type The type of message. If null, set as 'chat'
  397. * @param string $payload Eventual XML payload
  398. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.2.1
  399. */
  400. public function message($to, $body, $subject = null, $thread = null, $type = null, $payload = null) {
  401. $type = (null === $type) ? 'normal' : $type;
  402. $this->writeToStream(sprintf(
  403. '<message from="%s" to="%s" type="%s"><body>%s</body>%s</message>',
  404. $this->full_jid, $to, $type, $body, $payload
  405. ));
  406. return $this;
  407. }
  408. /**
  409. * Send an HTML message to a user
  410. * @uses message
  411. * @param string $contents The content of your message
  412. * @param string $subject If any, the subject of the message
  413. * @param string $thread If any, the thread the message belongs to
  414. * @param string $type The type of message. If null, set as 'chat'
  415. * @link http://xmpp.org/extensions/xep-0071.html
  416. */
  417. public function messageHTML($to, $contents, $subject = null, $thread = null, $type = null) {
  418. $html = new SimpleXMLElement('<html/>', LIBXML_NOXMLDECL, false, 'http://jabber.org/protocol/xhtml-im');
  419. $html->addChild('body', $contents, 'http://www.w3.org/1999/xhtml');
  420. $body = filter_var($contents, FILTER_SANITIZE_STRING); # strip HTML tags to include default message also
  421. $this->message($to, $body, $subject, $thread, $type, $this->as_xml($html));
  422. }
  423. /**
  424. * Get a username (if any) by its jid
  425. */
  426. public function getUserByJID($jid) {} # TODO
  427. // ===================
  428. // = XML operations =
  429. // ===================
  430. /**
  431. * Loads an xml string and turns it into a SimpleXMLElement object.
  432. * @return SimpleXMLElement|false
  433. */
  434. protected function load_xml_string($string, $wrap = false, $strip_ns = false) {
  435. if($wrap) $string = '<xml>' . $string . '</xml>';
  436. if($strip_ns) $string = $this->strip_ns($string);
  437. # we don't want to validate the (possibly broken) xml parsed (thus the use of ~LIBXML_DTDVALID)
  438. return @simplexml_load_string($string, 'SimpleXMLElement', ~LIBXML_DTDVALID);
  439. }
  440. /**
  441. * Output a simpleXMLObject as an xml string
  442. * I had to create my own method since the option LIBXML_NOXMLDECL wasn't ever implemented
  443. * http://bugs.php.net/bug.php?id=47137 | http://bugs.php.net/bug.php?id=50989
  444. * @return string
  445. */
  446. protected function as_xml($sxe) {
  447. return preg_replace('/^\<\?xml[^>]*>/', '', $sxe->asXML());
  448. }
  449. /**
  450. * Strip namespaces declarations; not very standard but it can cause some
  451. * problem for parsing with SimpleXML otherwise.
  452. */
  453. protected function strip_ns($string) {
  454. return preg_replace('/\s(xmlns=(?:"|\')[^"\']*(?:"|\'))/', '', $string);
  455. }
  456. // ==================
  457. // = Events methods =
  458. // ==================
  459. /**
  460. * Get elapsed time
  461. */
  462. protected function getElapsedTime() {
  463. $time = array_sum(explode(' ', microtime()));
  464. return round($time - $this->time_start, 5);
  465. }
  466. /**
  467. * Adds a "listener", that will call a custom-defined function that checks if an event occured.
  468. * You can then trigger whatever function you want as a callback in the function you just defined.
  469. * @param string $eventName The name of the event and of the function (must be of protected access) that you want to call
  470. */
  471. protected function addEventListener($eventName) {
  472. if($this->log) Logger::log('Adding event listener : ' . $eventName, $this->log_html);
  473. array_unshift($this->events, $eventName);
  474. $this->readFromStream();
  475. }
  476. /**
  477. * Remove last event
  478. */
  479. protected function removeEventListener() {
  480. if($this->log) Logger::log('Removing event listener : ' . $this->events[0], $this->log_html);
  481. array_shift($this->events);
  482. }
  483. /**
  484. * Execute the listeners in the events stack
  485. */
  486. private function listen() {
  487. if(preg_match("#^[\s]*$#", $this->received)) return;
  488. foreach($this->events as $event) call_user_func(array($this, $event));
  489. }
  490. // ==============================
  491. // = Stream read/write methods =
  492. // ==============================
  493. /**
  494. * Write to the stream
  495. * @param string $contents
  496. */
  497. protected function writeToStream($contents) {
  498. if($this->log) Logger::log('Sending : ' . $contents, $this->log_html);
  499. stream_socket_sendto($this->stream, $contents);
  500. }
  501. /**
  502. * Read from the stream
  503. * @param integer $buffer The length of the max amount of bytes you want to read from the stream
  504. */
  505. protected function readFromStream($buffer = 8192) {
  506. $this->emptyReceivedContent();
  507. if($this->log) Logger::log('Waiting for new content', $this->log_html);
  508. $write = $except = null;
  509. while(count($this->events)) {
  510. $read = array($this->stream);
  511. $updated_streams = stream_select($read, $write, $except, $this->is_bot ? 0 : $this->timeout);
  512. if($updated_streams === 0 && !$this->is_bot) throw new Exception('Timeout - server did not answer');
  513. $this->received .= stream_socket_recvfrom($this->stream, $buffer);
  514. if($this->connected) $this->mainLoop();
  515. $this->listen();
  516. usleep(0020000);
  517. }
  518. return true;
  519. }
  520. /**
  521. * Clean "buffer"
  522. */
  523. protected function emptyReceivedContent() {
  524. $this->received = '';
  525. }
  526. // ================
  527. // = XMPP Methods =
  528. // ================
  529. /**
  530. * Sends a new XML stream to server
  531. * @link http://tools.ietf.org/html/rfc3920#section-4
  532. */
  533. protected function initiateXMLStream() {
  534. $this->writeToStream(sprintf(
  535. "<?xml version='1.0'?><stream:stream xmlns='jabber:client' ".
  536. "xmlns:stream='http://etherx.jabber.org/streams' to='%s' xml:lang='%s' version='1.0'>",
  537. $this->domain, $this->lang
  538. ));
  539. }
  540. /**
  541. * Handle <stream:features/> tag
  542. * @link http://tools.ietf.org/html/rfc3920#section-4.6
  543. */
  544. private function checkFeatures() {
  545. $xml = $this->load_xml_string($this->received . '</stream:stream>');
  546. if(!$xml) return;
  547. $xml->registerXPathNamespace('stream', 'http://etherx.jabber.org/streams');
  548. if($features_query = $xml->xpath('/stream:stream/stream:features')) {
  549. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  550. $this->removeEventListener();
  551. $features = $features_query[0];
  552. if(count($features->bind) && $this->authenticated) $this->bindResource();
  553. else if(count($features->starttls) && $this->use_tls && !$this->tls_is_set) $this->askTLSConnection();
  554. else if(count($features->mechanisms)) {
  555. $mechs = array();
  556. foreach($features->mechanisms[0] as $mech) $mechs[] = strval($mech);
  557. $this->handleMechs($mechs);
  558. }
  559. else throw new Exception('No authentication mechanisms found');
  560. }
  561. unset($xml);
  562. }
  563. /**
  564. * Handle authentication mechanisms defined in stream features
  565. * Currently supported : PLAIN, DIGEST-MD5, ANONYMOUS
  566. * @link http://tools.ietf.org/html/rfc3920#section-5.3
  567. */
  568. private function handleMechs($mechs) {
  569. if(empty($mechs)) throw new Exception('No authentication mechanisms found');
  570. if($this->log) Logger::log('Authentification mechanisms are : ' . implode('; ', $mechs), $this->log_html);
  571. if(in_array('DIGEST-MD5', $mechs)) $this->digestAuth();
  572. else if(in_array('PLAIN', $mechs)) $this->plainTextAuth();
  573. #else if(in_array('ANONYMOUS', $mechs)) $this->anonymousAuth(); # TODO
  574. }
  575. /**
  576. * Ask for TLS connection
  577. * @link http://tools.ietf.org/html/rfc3920#section-5.1
  578. */
  579. private function askTLSConnection() {
  580. if($this->log) Logger::log('Asking for TLS connection...', $this->log_html);
  581. $this->writeToStream("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'><required /></starttls>");
  582. $this->addEventListener('checkTLSNegotiation');
  583. }
  584. /**
  585. * Custom "listener" to check if server allow us to proceed to stream encryption
  586. * @link http://tools.ietf.org/html/rfc3920#section-5.1
  587. */
  588. private function checkTLSNegotiation() {
  589. $xml = $this->load_xml_string($this->received);
  590. switch($xml->getName()) {
  591. case 'proceed' :
  592. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  593. $this->removeEventListener();
  594. if($this->log) Logger::log('Proceeding to TLS encrypted connection', $this->log_html);
  595. $this->startTLSConnection();
  596. break;
  597. default:
  598. case 'failure' :
  599. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  600. $this->removeEventListener();
  601. throw new Exception('Unable to connect over TLS');
  602. break;
  603. }
  604. unset($xml);
  605. }
  606. /**
  607. * Starts stream encryption
  608. * @link http://tools.ietf.org/html/rfc3920#section-5.1
  609. */
  610. private function startTLSConnection() {
  611. stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
  612. $this->tls_is_set = true;
  613. $this->initiateXMLStream();
  614. $this->addEventListener('checkFeatures');
  615. }
  616. /**
  617. * Ask for digest-md5 authentication
  618. */
  619. private function digestAuth() {
  620. if($this->log) Logger::log('Attempting DIGEST-MD5 authentication... ', $this->log_html);
  621. $this->writeToStream('<auth xmlns="urn:ietf:params:xml:ns:xmpp-sasl" mechanism="DIGEST-MD5"/>');
  622. $this->addEventListener('checkChallenge');
  623. }
  624. /**
  625. * Check if we received a challenge from the server (second part of the digest-md5 authentication routine)
  626. */
  627. private function checkChallenge() {
  628. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  629. $xml = $this->load_xml_string($this->received);
  630. $this->removeEventListener();
  631. $this->processChallenge(base64_decode(strval($xml[0])));
  632. }
  633. /**
  634. * Process the challenge received and send a response to the server (third part of the digest-md5 authentication routine)
  635. * @param $challenge
  636. */
  637. private function processChallenge($challenge) {
  638. if($this->log) Logger::log('Processing received challenge...', $this->log_html);
  639. $challenge_contents = explode(',', $challenge);
  640. $challenge_assoc = array();
  641. foreach($challenge_contents as $content) {
  642. preg_match('#(?P<key>.+)=(?P<value>.+)#', $content, $matches);
  643. $challenge_assoc[$matches['key']] = str_replace('"', '', $matches['value']);
  644. }
  645. $cnonce = md5(rand().time());
  646. $digest_uri = sprintf('xmpp/%s', $this->host);
  647. $realm = isset($challenge_assoc['realm']) ? $challenge_assoc['realm'] : '';
  648. $response = $this->generateDigestResponse(
  649. $this->username,
  650. $this->password,
  651. $realm,
  652. $challenge_assoc['nonce'],
  653. $cnonce,
  654. $digest_uri,
  655. $this->domain == 'chat.facebook.com' ? false : $this->full_jid
  656. );
  657. if(!isset($challenge_assoc['realm'])) $response_content = sprintf(
  658. 'username="%s",nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,charset=utf-8',
  659. $this->username, $challenge_assoc['nonce'], $cnonce, $digest_uri, $response, $this->full_jid
  660. );
  661. else $response_content = sprintf(
  662. 'username="%s",realm="%s",nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,charset=utf-8',
  663. $this->username, $challenge_assoc['realm'], $challenge_assoc['nonce'], $cnonce, $digest_uri, $response, $this->full_jid
  664. );
  665. if($this->domain != 'chat.facebook.com') $response_content .= sprintf(',authzid="%s"', $this->full_jid);
  666. $this->writeToStream(sprintf(
  667. '<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl">%s</response>',
  668. base64_encode($response_content)
  669. ));
  670. $this->addEventListener('checkChallenge2');
  671. }
  672. /**
  673. * Helper method that generates the 'response' part of our answer to the server
  674. */
  675. private function generateDigestResponse($username, $pass, $realm, $nonce, $cnonce, $digest_uri, $authzid = false) {
  676. if($authzid) $A1 = sprintf('%s:%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $realm, $pass))), $nonce, $cnonce, $authzid);
  677. else $A1 = sprintf('%s:%s:%s', pack('H32', md5(sprintf('%s:%s:%s', $username, $realm, $pass))), $nonce, $cnonce);
  678. $A2 = 'AUTHENTICATE:' . $digest_uri;
  679. return md5(sprintf('%s:%s:00000001:%s:auth:%s', md5($A1), $nonce, $cnonce, md5($A2)));
  680. }
  681. /**
  682. * Check if we received a second challenge from the server then wait for authentication confirmation
  683. */
  684. private function checkChallenge2() {
  685. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  686. $xml = $this->load_xml_string($this->received);
  687. $this->removeEventListener();
  688. if($xml[0]->getName() === 'failure') throw new Exception('Authentication failed');
  689. $this->writeToStream('<response xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></response>');
  690. $this->addEventListener('checkAuthConfirmation');
  691. }
  692. /**
  693. * Send authentication credentials as plain-text then wait for authentication confirmation (fourth part of the digest-md5 authentication routine)
  694. */
  695. private function plainTextAuth() {
  696. if($this->log) Logger::log('Attempting plain-text authentication... ', $this->log_html);
  697. $this->writeToStream(sprintf(
  698. "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>",
  699. base64_encode(chr(0) . $this->username . chr(0) . $this->password)
  700. ));
  701. $this->addEventListener('checkAuthConfirmation');
  702. }
  703. /**
  704. * Check if the server successfully authenticated the user, whatever the mechanism used was
  705. */
  706. private function checkAuthConfirmation() {
  707. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  708. $xml = $this->load_xml_string($this->received);
  709. switch($xml[0]->getName()) {
  710. default:
  711. case 'failure':
  712. $children = $xml[0]->children();
  713. throw new Exception('Unable to authenticate : ' . $children[0]->getName());
  714. break;
  715. case 'success':
  716. if($this->log) Logger::log('Successfully authenticated ', $this->log_html);
  717. $this->authenticated = true;
  718. $this->removeEventListener();
  719. $this->initiateXMLStream();
  720. $this->addEventListener('checkFeatures');
  721. break;
  722. }
  723. unset($xml);
  724. }
  725. /**
  726. * Helper method that generates a unique id for the user and register it in the class attributes
  727. */
  728. private function generateId() {
  729. $this->id = uniqid();
  730. $this->id_is_set = true;
  731. }
  732. /**
  733. * Ask for binding resource id on the server
  734. * @link http://tools.ietf.org/html/rfc3920#section-7
  735. */
  736. private function bindResource() {
  737. $this->generateId();
  738. if($this->log) Logger::log('Attempting to bind resource on server...', $this->log_html);
  739. $this->writeToStream(sprintf(
  740. "<iq type='set' id='%s'>".
  741. "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>".
  742. "<resource>%s</resource>" .
  743. "</bind>" .
  744. "</iq>",
  745. $this->id,
  746. $this->resource
  747. ));
  748. $this->addEventListener('checkBoundResourceResult');
  749. }
  750. /**
  751. * Check if resource identifier was successfully bound on the server
  752. * @link http://tools.ietf.org/html/rfc3920#section-7
  753. */
  754. private function checkBoundResourceResult() {
  755. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  756. $xml = $this->load_xml_string($this->received);
  757. $iq_type_query = $xml->xpath('@type');
  758. switch($iq_type_query[0]) {
  759. case 'result':
  760. if($this->log) Logger::log('Resource identifier successfully bound on server', $this->log_html);
  761. $this->removeEventListener();
  762. $this->full_jid = strval($xml[0]->bind->jid);
  763. $this->startSession();
  764. break;
  765. default:
  766. case 'error':
  767. $error_query = $xml[0]->error->children();
  768. $error = $error_query[0];
  769. switch($error->getName()) {
  770. case 'bad-request':
  771. throw new Exception('Resource identifier cannot be processed');
  772. break;
  773. case 'conflict':
  774. throw new Exception('Resource identifier id already in use');
  775. break;
  776. case 'not-allowed':
  777. throw new Exception('Not allowed to bind a resource');
  778. break;
  779. }
  780. break;
  781. }
  782. unset($xml);
  783. }
  784. /**
  785. * Ask for a new session, then wait confirmation from server
  786. * @link http://xmpp.org/rfcs/rfc3921.html#session
  787. */
  788. private function startSession() {
  789. $this->generateId();
  790. $this->writeToStream(sprintf(
  791. "<iq xmlns='jabber:client' type='set' id='%s'>" .
  792. "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />" .
  793. "</iq>",
  794. $this->id
  795. ));
  796. $this->addEventListener('checkSessionStarted');
  797. }
  798. /**
  799. * Check if session started
  800. * @link http://xmpp.org/rfcs/rfc3921.html#session
  801. */
  802. private function checkSessionStarted() {
  803. $xml = $this->load_xml_string($this->received);
  804. if(!$xml) return;
  805. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  806. $result = strval($xml->attributes()->type);
  807. if($result == 'result') {
  808. if($this->log) Logger::log('Session started', $this->log_html);
  809. $this->removeEventListener();
  810. $this->askRoster();
  811. }
  812. else {
  813. # TODO : get error type
  814. throw new Exception('Unable to initiate session');
  815. }
  816. }
  817. /**
  818. * Ask for user roster
  819. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
  820. */
  821. private function askRoster() {
  822. $this->writeToStream(sprintf(
  823. "<iq from='%s' type='get' id='roster_1'><query xmlns='jabber:iq:roster'/></iq>",
  824. $this->full_jid
  825. ));
  826. $this->addEventListener('getRosterContents');
  827. }
  828. /**
  829. * Store user roster in an associative array
  830. * @link http://xmpp.org/rfcs/rfc3921.html#rfc.section.7.3
  831. */
  832. private function getRosterContents() {
  833. $xml = $this->load_xml_string($this->received, true, true);
  834. if(!$xml) return;
  835. if($this->log) Logger::log('Received : ' . $this->received, $this->log_html);
  836. $result = $xml->xpath("//iq[@type='result']/query");
  837. if(!$result) return;
  838. else $contacts = $result[0]->children();
  839. foreach($contacts as $contact) {
  840. $attrs = $contact->attributes();
  841. $this->roster[strval($attrs->jid)] = array(
  842. 'subscription' => strval($attrs->subscription),
  843. 'name' => isset($attrs->name) ? strval($attrs->name) : null
  844. );
  845. }
  846. $this->removeEventListener();
  847. }
  848. /**
  849. *
  850. * @link
  851. */
  852. private function addToRoster($jid, $name = null, $group = null) {
  853. $name = ($name !== null) ? $name : $jid;
  854. $group_str = '';
  855. if($group !== null) {
  856. if(is_array($group)) foreach($group as $g) $group_str .= '<group>' . $g . '</group>';
  857. else $group_str = $group;
  858. }
  859. $this->writeToStream(sprintf(
  860. "<iq from='%s' type='set' id='set1'><query xmlns='jabber:iq:roster'>" .
  861. "<item jid='%s' name='%s'>%s</item></query></iq>",
  862. $this->full_jid, $jid, $name, $group_str
  863. ));
  864. }
  865. /**
  866. *
  867. * @link
  868. */
  869. private function removeFromRoster($jid) {
  870. $this->writeToStream(sprintf(
  871. "<iq from='%s' id='roster_4' type='set'><query xmlns='jabber:iq:roster'><item subscription='remove' jid='%s'/></query></iq>",
  872. $this->full_jid, $jid
  873. ));
  874. }
  875. // =================
  876. // = Bot behaviour =
  877. // =================
  878. /**
  879. * This function will be called in the main loop for persistent connections
  880. */
  881. private function checkStreamContent() {
  882. # Logger::log($this->received, false);
  883. $xml = $this->load_xml_string($this->received, true, true);
  884. $messages = $xml->xpath("//message");
  885. foreach($messages as $msg) {
  886. $attrs = $msg->attributes();
  887. $children = $msg->children();
  888. $body = $children->body;
  889. if(!empty($body)) {
  890. $this->onReceivedMessage(strval($body), strval($attrs->from), strval($attrs->type), $msg->asXML());
  891. continue;
  892. }
  893. if(isset($children->composing)) $this->onUserComposing(strval($attrs->from));
  894. else if(isset($children->paused)) $this->onUserPaused(strval($attrs->from));
  895. }
  896. $presences = $xml->xpath("//presence");
  897. foreach($presences as $presence) {
  898. $attrs = $presence->attributes();
  899. $children = $presence->children();
  900. $status = isset($children->status) ? strval($children->status) : null;
  901. $availability = isset($children->show) ? strval($children->show) : null;
  902. if($attrs->type == 'subscribe') $this->onSubscribe(strval($attrs->from));
  903. else if($attrs->type == 'unsubscribe') $this->onUnsubscribe(strval($attrs->from));
  904. else $this->onPresence(strval($attrs->from), $status, $availability);
  905. }
  906. $this->onReceivedContent();
  907. $this->emptyReceivedContent();
  908. }
  909. }
  910. /**
  911. * A very simple logger.
  912. * @package Logger
  913. */
  914. class Logger {
  915. static public function log($log, $html = false) {
  916. echo '__________'. ($html ? "\n<br />" : "\n");
  917. echo '['. strftime('%H:%M:%S') . '] ' . ($html ? htmlentities($log) : $log) . ($html ? "\n<br />" : "\n");
  918. }
  919. }
  920. ?>