PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/xmpp/xmpp_stream.php

https://github.com/onurdegerli/JAXL
PHP | 670 lines | 477 code | 91 blank | 102 comment | 71 complexity | d52c07b5f6cbeff8816682c5952d6add MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Jaxl (Jabber XMPP Library)
  4. *
  5. * Copyright (c) 2009-2012, Abhinav Singh <me@abhinavsingh.com>.
  6. * All rights reserved.
  7. *
  8. * Redistribution and use in source and binary forms, with or without
  9. * modification, are permitted provided that the following conditions
  10. * are met:
  11. *
  12. * * Redistributions of source code must retain the above copyright
  13. * notice, this list of conditions and the following disclaimer.
  14. *
  15. * * Redistributions in binary form must reproduce the above copyright
  16. * notice, this list of conditions and the following disclaimer in
  17. * the documentation and/or other materials provided with the
  18. * distribution.
  19. *
  20. * * Neither the name of Abhinav Singh nor the names of his
  21. * contributors may be used to endorse or promote products derived
  22. * from this software without specific prior written permission.
  23. *
  24. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  25. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  26. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  27. * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  28. * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  29. * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  30. * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  31. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  32. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRIC
  33. * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  34. * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  35. * POSSIBILITY OF SUCH DAMAGE.
  36. *
  37. */
  38. require_once JAXL_CWD.'/core/jaxl_fsm.php';
  39. require_once JAXL_CWD.'/core/jaxl_xml.php';
  40. require_once JAXL_CWD.'/core/jaxl_xml_stream.php';
  41. require_once JAXL_CWD.'/core/jaxl_util.php';
  42. require_once JAXL_CWD.'/core/jaxl_socket_client.php';
  43. require_once JAXL_CWD.'/xmpp/xmpp_nss.php';
  44. require_once JAXL_CWD.'/xmpp/xmpp_jid.php';
  45. require_once JAXL_CWD.'/xmpp/xmpp_msg.php';
  46. require_once JAXL_CWD.'/xmpp/xmpp_pres.php';
  47. require_once JAXL_CWD.'/xmpp/xmpp_iq.php';
  48. /**
  49. *
  50. * Enter description here ...
  51. * @author abhinavsingh
  52. *
  53. */
  54. abstract class XMPPStream extends JAXLFsm {
  55. // jid with binding resource value
  56. public $full_jid = null;
  57. // input parameters
  58. public $jid = null;
  59. public $pass = null;
  60. public $resource = null;
  61. public $force_tls = false;
  62. // underlying socket/bosh and xml stream ref
  63. protected $trans = null;
  64. protected $xml = null;
  65. // stanza id
  66. protected $last_id = 0;
  67. //
  68. // abstract methods
  69. //
  70. abstract public function handle_stream_start($stanza);
  71. abstract public function handle_auth_mechs($stanza, $mechs);
  72. abstract public function handle_auth_success();
  73. abstract public function handle_auth_failure($reason);
  74. abstract public function handle_iq($stanza);
  75. abstract public function handle_presence($stanza);
  76. abstract public function handle_message($stanza);
  77. abstract public function handle_other($event, $args);
  78. //
  79. // public api
  80. //
  81. public function __construct($transport, $jid, $pass=null, $resource=null, $force_tls=false) {
  82. $this->jid = $jid;
  83. $this->pass = $pass;
  84. $this->resource = $resource ? $resource : md5(time());
  85. $this->force_tls = $force_tls;
  86. $this->trans = $transport;
  87. $this->xml = new JAXLXmlStream();
  88. $this->trans->set_callback(array(&$this->xml, "parse"));
  89. $this->xml->set_callback(array(&$this, "start_cb"), array(&$this, "end_cb"), array(&$this, "stanza_cb"));
  90. parent::__construct("setup");
  91. }
  92. public function __destruct() {
  93. //_debug("cleaning up xmpp stream...");
  94. }
  95. public function handle_invalid_state($r) {
  96. _error("got invalid return value from state handler '".$this->state."', sending end stream...");
  97. $this->send_end_stream();
  98. $this->state = "logged_out";
  99. _notice("state handler '".$this->state."' returned ".serialize($r).", kindly report this to developers");
  100. }
  101. public function send($stanza) {
  102. $this->trans->send($stanza->to_string());
  103. }
  104. public function send_raw($data) {
  105. $this->trans->send($data);
  106. }
  107. //
  108. // pkt creation utilities
  109. //
  110. public function get_start_stream($jid) {
  111. $xml = '<stream:stream xmlns:stream="'.NS_XMPP.'" version="1.0" ';
  112. //if(isset($jid->bare)) $xml .= 'from="'.$jid->bare.'" ';
  113. if(isset($jid->domain)) $xml .= 'to="'.$jid->domain.'" ';
  114. $xml .= 'xmlns="'.NS_JABBER_CLIENT.'" xml:lang="en" xmlns:xml="'.NS_XML.'">';
  115. return $xml;
  116. }
  117. public function get_end_stream() {
  118. return '</stream:stream>';
  119. }
  120. public function get_starttls_pkt() {
  121. $stanza = new JAXLXml('starttls', NS_TLS);
  122. return $stanza;
  123. }
  124. public function get_compress_pkt($method) {
  125. $stanza = new JAXLXml('compress', NS_COMPRESSION_PROTOCOL);
  126. $stanza->c('method')->t($method);
  127. return $stanza;
  128. }
  129. // someday this all needs to go inside jaxl_sasl_auth
  130. public function get_auth_pkt($mechanism, $user, $pass) {
  131. $stanza = new JAXLXml('auth', NS_SASL, array('mechanism'=>$mechanism));
  132. switch($mechanism) {
  133. case 'PLAIN':
  134. case 'X-OAUTH2':
  135. $stanza->t(base64_encode("\x00".$user."\x00".$pass));
  136. break;
  137. case 'DIGEST-MD5':
  138. break;
  139. case 'CRAM-MD5':
  140. break;
  141. case 'SCRAM-SHA-1':
  142. // client first message always starts with n, y or p for GS2 extensibility
  143. $stanza->t(base64_encode("n,,n=".$user.",r=".JAXLUtil::get_nonce(false)));
  144. break;
  145. case 'ANONYMOUS':
  146. break;
  147. case 'EXTERNAL':
  148. // If no password, then we are probably doing certificate auth, so follow RFC 6120 form and pass '='.
  149. if(strlen($pass) == 0)
  150. $stanza->t('=');
  151. break;
  152. default:
  153. break;
  154. }
  155. return $stanza;
  156. }
  157. public function get_challenge_response_pkt($challenge) {
  158. $stanza = new JAXLXml('response', NS_SASL);
  159. $decoded = $this->explode_data(base64_decode($challenge));
  160. if(!isset($decoded['rspauth'])) {
  161. _debug("calculating response to challenge");
  162. $stanza->t($this->get_challenge_response($decoded));
  163. }
  164. return $stanza;
  165. }
  166. public function get_challenge_response($decoded) {
  167. $response = array();
  168. $nc = '00000001';
  169. if(!isset($decoded['digest-uri']))
  170. $decoded['digest-uri'] = 'xmpp/'.$this->jid->domain;
  171. $decoded['cnonce'] = base64_encode(JAXLUtil::get_nonce());
  172. if(isset($decoded['qop']) && $decoded['qop'] != 'auth' && strpos($decoded['qop'], 'auth') !== false)
  173. $decoded['qop'] = 'auth';
  174. $data = array_merge($decoded, array('nc'=>$nc));
  175. $response = array(
  176. 'username'=> $this->jid->node,
  177. 'response' => $this->encrypt_password($data, $this->jid->node, $this->pass),
  178. 'charset' => 'utf-8',
  179. 'nc' => $nc,
  180. 'qop' => 'auth'
  181. );
  182. foreach(array('nonce', 'digest-uri', 'realm', 'cnonce') as $key)
  183. if(isset($decoded[$key]))
  184. $response[$key] = $decoded[$key];
  185. return base64_encode($this->implode_data($response));
  186. }
  187. public function get_bind_pkt($resource) {
  188. $stanza = new JAXLXml('bind', NS_BIND);
  189. $stanza->c('resource')->t($resource);
  190. return $this->get_iq_pkt(array(
  191. 'type' => 'set'
  192. ), $stanza);
  193. }
  194. public function get_session_pkt() {
  195. $stanza = new JAXLXml('session', NS_SESSION);
  196. return $this->get_iq_pkt(array(
  197. 'type' => 'set'
  198. ), $stanza);
  199. }
  200. public function get_msg_pkt($attrs, $body=null, $thread=null, $subject=null, $payload=null) {
  201. $msg = new XMPPMsg($attrs, $body, $thread, $subject);
  202. if(!$msg->id) $msg->id = $this->get_id();
  203. if($payload) $msg->cnode($payload);
  204. return $msg;
  205. }
  206. public function get_pres_pkt($attrs, $status=null, $show=null, $priority=null, $payload=null) {
  207. $pres = new XMPPPres($attrs, $status, $show, $priority);
  208. if(!$pres->id) $pres->id = $this->get_id();
  209. if($payload) $pres->cnode($payload);
  210. return $pres;
  211. }
  212. public function get_iq_pkt($attrs, $payload) {
  213. $iq = new XMPPIq($attrs);
  214. if(!$iq->id) $iq->id = $this->get_id();
  215. if($payload) $iq->cnode($payload);
  216. return $iq;
  217. }
  218. public function get_id() {
  219. ++$this->last_id;
  220. return dechex($this->last_id);
  221. }
  222. public function explode_data($data) {
  223. $data = explode(',', $data);
  224. $pairs = array();
  225. $key = false;
  226. foreach($data as $pair) {
  227. $dd = strpos($pair, '=');
  228. if($dd) {
  229. $key = trim(substr($pair, 0, $dd));
  230. $pairs[$key] = trim(trim(substr($pair, $dd + 1)), '"');
  231. }
  232. else if(strpos(strrev(trim($pair)), '"') === 0 && $key) {
  233. $pairs[$key] .= ',' . trim(trim($pair), '"');
  234. continue;
  235. }
  236. }
  237. return $pairs;
  238. }
  239. public function implode_data($data) {
  240. $return = array();
  241. foreach($data as $key => $value) $return[] = $key . '="' . $value . '"';
  242. return implode(',', $return);
  243. }
  244. public function encrypt_password($data, $user, $pass) {
  245. foreach(array('realm', 'cnonce', 'digest-uri') as $key)
  246. if(!isset($data[$key]))
  247. $data[$key] = '';
  248. $pack = md5($user.':'.$data['realm'].':'.$pass);
  249. if(isset($data['authzid']))
  250. $a1 = pack('H32',$pack).sprintf(':%s:%s:%s',$data['nonce'],$data['cnonce'],$data['authzid']);
  251. else
  252. $a1 = pack('H32',$pack).sprintf(':%s:%s',$data['nonce'],$data['cnonce']);
  253. $a2 = 'AUTHENTICATE:'.$data['digest-uri'];
  254. return md5(sprintf('%s:%s:%s:%s:%s:%s', md5($a1), $data['nonce'], $data['nc'], $data['cnonce'], $data['qop'], md5($a2)));
  255. }
  256. //
  257. // socket senders
  258. //
  259. protected function send_start_stream($jid) {
  260. $this->send_raw($this->get_start_stream($jid));
  261. }
  262. public function send_end_stream() {
  263. $this->send_raw($this->get_end_stream());
  264. }
  265. protected function send_auth_pkt($type, $user, $pass) {
  266. $this->send($this->get_auth_pkt($type, $user, $pass));
  267. }
  268. protected function send_starttls_pkt() {
  269. $this->send($this->get_starttls_pkt());
  270. }
  271. protected function send_compress_pkt($method) {
  272. $this->send($this->get_compress_pkt($method));
  273. }
  274. protected function send_challenge_response($challenge) {
  275. $this->send($this->get_challenge_response_pkt($challenge));
  276. }
  277. protected function send_bind_pkt($resource) {
  278. $this->send($this->get_bind_pkt($resource));
  279. }
  280. protected function send_session_pkt() {
  281. $this->send($this->get_session_pkt());
  282. }
  283. private function do_connect($args) {
  284. $socket_path = @$args[0];
  285. if($this->trans->connect($socket_path)) {
  286. return array("connected", 1);
  287. }
  288. else {
  289. return array("disconnected", 0);
  290. }
  291. }
  292. //
  293. // fsm States
  294. //
  295. public function setup($event, $args) {
  296. switch($event) {
  297. case "connect":
  298. return $this->do_connect($args);
  299. break;
  300. // someone else already started the stream
  301. // even before "connect" was called
  302. // must be bosh
  303. case "start_cb":
  304. $stanza = $args[0];
  305. return $this->handle_stream_start($stanza);
  306. break;
  307. default:
  308. _debug("uncatched $event");
  309. //print_r($args);
  310. return $this->handle_other($event, $args);
  311. //return array("setup", 0);
  312. break;
  313. }
  314. }
  315. public function connected($event, $args) {
  316. switch($event) {
  317. case "start_stream":
  318. $this->send_start_stream($this->jid);
  319. return array("wait_for_stream_start", 1);
  320. break;
  321. // someone else already started the stream before us
  322. // even before "start_stream" was called
  323. // must be component
  324. case "start_cb":
  325. $stanza = $args[0];
  326. return $this->handle_stream_start($stanza);
  327. break;
  328. default:
  329. _debug("uncatched $event");
  330. return $this->handle_other($event, $args);
  331. //return array("connected", 0);
  332. break;
  333. }
  334. }
  335. public function disconnected($event, $args) {
  336. switch($event) {
  337. case "connect":
  338. return $this->do_connect($args);
  339. break;
  340. case "end_stream":
  341. $this->send_end_stream();
  342. return "logged_out";
  343. break;
  344. default:
  345. _debug("uncatched $event");
  346. return $this->handle_other($event, $args);
  347. //return array("disconnected", 0);
  348. break;
  349. }
  350. }
  351. public function wait_for_stream_start($event, $args) {
  352. switch($event) {
  353. case "start_cb":
  354. // TODO: save stream id and other meta info
  355. //_debug("stream started");
  356. return "wait_for_stream_features";
  357. break;
  358. default:
  359. _debug("uncatched $event");
  360. return $this->handle_other($event, $args);
  361. //return array("wait_for_stream_start", 0);
  362. break;
  363. }
  364. }
  365. // XEP-0170: Recommended Order of Stream Feature Negotiation
  366. public function wait_for_stream_features($event, $args) {
  367. switch($event) {
  368. case "stanza_cb":
  369. $stanza = $args[0];
  370. // get starttls requirements
  371. $starttls = $stanza->exists('starttls', NS_TLS);
  372. $required = $starttls ? ($this->force_tls ? true : ($starttls->exists('required') ? true : false)) : false;
  373. if($starttls && $required) {
  374. $this->send_starttls_pkt();
  375. return "wait_for_tls_result";
  376. }
  377. // handle auth mech
  378. $mechs = $stanza->exists('mechanisms', NS_SASL);
  379. if($mechs) {
  380. $new_state = $this->handle_auth_mechs($stanza, $mechs);
  381. return $new_state ? $new_state : "wait_for_sasl_response";
  382. }
  383. // post auth
  384. $bind = $stanza->exists('bind', NS_BIND) ? true : false;
  385. $sess = $stanza->exists('session', NS_SESSION) ? true : false;
  386. $comp = $stanza->exists('compression', NS_COMPRESSION_FEATURE) ? true : false;
  387. if($bind) {
  388. $this->send_bind_pkt($this->resource);
  389. return "wait_for_bind_response";
  390. }
  391. /*// compression not supported due to bug in php stream filters
  392. else if($comp) {
  393. $this->send_compress_pkt("zlib");
  394. return "wait_for_compression_result";
  395. }*/
  396. else {
  397. _debug("no catch");
  398. }
  399. break;
  400. default:
  401. _debug("uncatched $event");
  402. return $this->handle_other($event, $args);
  403. //return array("wait_for_stream_features", 0);
  404. break;
  405. }
  406. }
  407. public function wait_for_tls_result($event, $args) {
  408. switch($event) {
  409. case "stanza_cb":
  410. $stanza = $args[0];
  411. if($stanza->name == 'proceed' && $stanza->ns == NS_TLS) {
  412. if($this->trans->crypt()) {
  413. $this->xml->reset_parser();
  414. $this->send_start_stream($this->jid);
  415. return "wait_for_stream_start";
  416. }
  417. else {
  418. $this->handle_auth_failure("tls-negotiation-failed");
  419. return "logged_out";
  420. }
  421. }
  422. else {
  423. // FIXME: here
  424. }
  425. break;
  426. default:
  427. _debug("uncatched $event");
  428. return $this->handle_other($event, $args);
  429. //return array("wait_for_tls_result", 0);
  430. break;
  431. }
  432. }
  433. public function wait_for_compression_result($event, $args) {
  434. switch($event) {
  435. case "stanza_cb":
  436. $stanza = $args[0];
  437. if($stanza->name == 'compressed' && $stanza->ns == NS_COMPRESSION_PROTOCOL) {
  438. $this->xml->reset_parser();
  439. $this->trans->compress();
  440. $this->send_start_stream($this->jid);
  441. return "wait_for_stream_start";
  442. }
  443. break;
  444. default:
  445. _debug("uncatched $event");
  446. return $this->handle_other($event, $args);
  447. //return array("wait_for_compression_result", 0);
  448. break;
  449. }
  450. }
  451. public function wait_for_sasl_response($event, $args) {
  452. switch($event) {
  453. case "stanza_cb":
  454. $stanza = $args[0];
  455. if($stanza->name == 'failure' && $stanza->ns == NS_SASL) {
  456. $reason = $stanza->childrens[0]->name;
  457. //_debug("sasl failed with reason ".$reason."");
  458. $this->handle_auth_failure($reason);
  459. return "logged_out";
  460. }
  461. else if($stanza->name == 'challenge' && $stanza->ns == NS_SASL) {
  462. $challenge = $stanza->text;
  463. $this->send_challenge_response($challenge);
  464. return "wait_for_sasl_response";
  465. }
  466. else if($stanza->name == 'success' && $stanza->ns == NS_SASL) {
  467. $this->xml->reset_parser();
  468. $this->send_start_stream(@$this->jid);
  469. return "wait_for_stream_start";
  470. }
  471. else {
  472. _debug("got unhandled sasl response");
  473. }
  474. return "wait_for_sasl_response";
  475. break;
  476. default:
  477. _debug("uncatched $event");
  478. return $this->handle_other($event, $args);
  479. //return array("wait_for_sasl_response", 0);
  480. break;
  481. }
  482. }
  483. public function wait_for_bind_response($event, $args) {
  484. switch($event) {
  485. case "stanza_cb":
  486. $stanza = $args[0];
  487. // TODO: chk on id
  488. if($stanza->name == 'iq' && $stanza->attrs['type'] == 'result'
  489. && ($jid = $stanza->exists('bind', NS_BIND)->exists('jid'))) {
  490. $this->full_jid = new XMPPJid($jid->text);
  491. $this->send_session_pkt();
  492. return "wait_for_session_response";
  493. }
  494. else {
  495. // FIXME:
  496. }
  497. break;
  498. default:
  499. _debug("uncatched $event");
  500. return $this->handle_other($event, $args);
  501. //return array("wait_for_bind_response", 0);
  502. break;
  503. }
  504. }
  505. public function wait_for_session_response($event, $args) {
  506. switch($event) {
  507. case "stanza_cb":
  508. $this->handle_auth_success();
  509. return "logged_in";
  510. break;
  511. default:
  512. _debug("uncatched $event");
  513. return $this->handle_other($event, $args);
  514. //return array("wait_for_session_response", 0);
  515. break;
  516. }
  517. }
  518. public function logged_in($event, $args) {
  519. switch($event) {
  520. case "stanza_cb":
  521. $stanza = $args[0];
  522. // call abstract
  523. if($stanza->name == 'message') {
  524. $stanza->type = (@$stanza->type ? $stanza->type : 'normal');
  525. $this->handle_message($stanza);
  526. }
  527. else if($stanza->name == 'presence') {
  528. $this->handle_presence($stanza);
  529. }
  530. else if($stanza->name == 'iq') {
  531. $this->handle_iq($stanza);
  532. }
  533. else {
  534. $this->handle_other($event, $args);
  535. }
  536. return "logged_in";
  537. break;
  538. case "end_cb":
  539. $this->send_end_stream();
  540. return "logged_out";
  541. break;
  542. case "end_stream":
  543. $this->send_end_stream();
  544. return "logged_out";
  545. break;
  546. case "disconnect":
  547. $this->trans->disconnect();
  548. return "disconnected";
  549. break;
  550. default:
  551. _debug("uncatched $event");
  552. return $this->handle_other($event, $args);
  553. //return array("logged_in", 0);
  554. break;
  555. }
  556. }
  557. public function logged_out($event, $args) {
  558. switch($event) {
  559. case "end_cb":
  560. $this->trans->disconnect();
  561. return "disconnected";
  562. break;
  563. case "end_stream":
  564. return "disconnected";
  565. break;
  566. case "disconnect":
  567. $this->trans->disconnect();
  568. return "disconnected";
  569. break;
  570. default:
  571. // exit for any other event in logged_out state
  572. _debug("uncatched $event");
  573. return $this->handle_other($event, $args);
  574. //return array("logged_out", 0);
  575. break;
  576. }
  577. }
  578. }
  579. ?>