/xmpp/xmpp_stream.php
PHP | 670 lines | 477 code | 91 blank | 102 comment | 71 complexity | d52c07b5f6cbeff8816682c5952d6add MD5 | raw file
Possible License(s): BSD-3-Clause
- <?php
- /**
- * Jaxl (Jabber XMPP Library)
- *
- * Copyright (c) 2009-2012, Abhinav Singh <me@abhinavsingh.com>.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in
- * the documentation and/or other materials provided with the
- * distribution.
- *
- * * Neither the name of Abhinav Singh nor the names of his
- * contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
- * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
- * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRIC
- * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
- * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- *
- */
- require_once JAXL_CWD.'/core/jaxl_fsm.php';
- require_once JAXL_CWD.'/core/jaxl_xml.php';
- require_once JAXL_CWD.'/core/jaxl_xml_stream.php';
- require_once JAXL_CWD.'/core/jaxl_util.php';
- require_once JAXL_CWD.'/core/jaxl_socket_client.php';
- require_once JAXL_CWD.'/xmpp/xmpp_nss.php';
- require_once JAXL_CWD.'/xmpp/xmpp_jid.php';
- require_once JAXL_CWD.'/xmpp/xmpp_msg.php';
- require_once JAXL_CWD.'/xmpp/xmpp_pres.php';
- require_once JAXL_CWD.'/xmpp/xmpp_iq.php';
- /**
- *
- * Enter description here ...
- * @author abhinavsingh
- *
- */
- abstract class XMPPStream extends JAXLFsm {
-
- // jid with binding resource value
- public $full_jid = null;
-
- // input parameters
- public $jid = null;
- public $pass = null;
- public $resource = null;
- public $force_tls = false;
-
- // underlying socket/bosh and xml stream ref
- protected $trans = null;
- protected $xml = null;
-
- // stanza id
- protected $last_id = 0;
-
- //
- // abstract methods
- //
-
- abstract public function handle_stream_start($stanza);
- abstract public function handle_auth_mechs($stanza, $mechs);
- abstract public function handle_auth_success();
- abstract public function handle_auth_failure($reason);
- abstract public function handle_iq($stanza);
- abstract public function handle_presence($stanza);
- abstract public function handle_message($stanza);
- abstract public function handle_other($event, $args);
-
- //
- // public api
- //
-
- public function __construct($transport, $jid, $pass=null, $resource=null, $force_tls=false) {
- $this->jid = $jid;
- $this->pass = $pass;
- $this->resource = $resource ? $resource : md5(time());
- $this->force_tls = $force_tls;
-
- $this->trans = $transport;
- $this->xml = new JAXLXmlStream();
-
- $this->trans->set_callback(array(&$this->xml, "parse"));
- $this->xml->set_callback(array(&$this, "start_cb"), array(&$this, "end_cb"), array(&$this, "stanza_cb"));
-
- parent::__construct("setup");
- }
-
- public function __destruct() {
- //_debug("cleaning up xmpp stream...");
- }
-
- public function handle_invalid_state($r) {
- _error("got invalid return value from state handler '".$this->state."', sending end stream...");
- $this->send_end_stream();
- $this->state = "logged_out";
- _notice("state handler '".$this->state."' returned ".serialize($r).", kindly report this to developers");
- }
-
- public function send($stanza) {
- $this->trans->send($stanza->to_string());
- }
-
- public function send_raw($data) {
- $this->trans->send($data);
- }
-
- //
- // pkt creation utilities
- //
-
- public function get_start_stream($jid) {
- $xml = '<stream:stream xmlns:stream="'.NS_XMPP.'" version="1.0" ';
- //if(isset($jid->bare)) $xml .= 'from="'.$jid->bare.'" ';
- if(isset($jid->domain)) $xml .= 'to="'.$jid->domain.'" ';
- $xml .= 'xmlns="'.NS_JABBER_CLIENT.'" xml:lang="en" xmlns:xml="'.NS_XML.'">';
- return $xml;
- }
-
- public function get_end_stream() {
- return '</stream:stream>';
- }
-
- public function get_starttls_pkt() {
- $stanza = new JAXLXml('starttls', NS_TLS);
- return $stanza;
- }
-
- public function get_compress_pkt($method) {
- $stanza = new JAXLXml('compress', NS_COMPRESSION_PROTOCOL);
- $stanza->c('method')->t($method);
- return $stanza;
- }
-
- // someday this all needs to go inside jaxl_sasl_auth
- public function get_auth_pkt($mechanism, $user, $pass) {
- $stanza = new JAXLXml('auth', NS_SASL, array('mechanism'=>$mechanism));
-
- switch($mechanism) {
- case 'PLAIN':
- case 'X-OAUTH2':
- $stanza->t(base64_encode("\x00".$user."\x00".$pass));
- break;
- case 'DIGEST-MD5':
- break;
- case 'CRAM-MD5':
- break;
- case 'SCRAM-SHA-1':
- // client first message always starts with n, y or p for GS2 extensibility
- $stanza->t(base64_encode("n,,n=".$user.",r=".JAXLUtil::get_nonce(false)));
- break;
- case 'ANONYMOUS':
- break;
- case 'EXTERNAL':
- // If no password, then we are probably doing certificate auth, so follow RFC 6120 form and pass '='.
- if(strlen($pass) == 0)
- $stanza->t('=');
- break;
- default:
- break;
- }
-
- return $stanza;
- }
-
- public function get_challenge_response_pkt($challenge) {
- $stanza = new JAXLXml('response', NS_SASL);
- $decoded = $this->explode_data(base64_decode($challenge));
-
- if(!isset($decoded['rspauth'])) {
- _debug("calculating response to challenge");
- $stanza->t($this->get_challenge_response($decoded));
- }
-
- return $stanza;
- }
-
- public function get_challenge_response($decoded) {
- $response = array();
- $nc = '00000001';
-
- if(!isset($decoded['digest-uri']))
- $decoded['digest-uri'] = 'xmpp/'.$this->jid->domain;
-
- $decoded['cnonce'] = base64_encode(JAXLUtil::get_nonce());
-
- if(isset($decoded['qop']) && $decoded['qop'] != 'auth' && strpos($decoded['qop'], 'auth') !== false)
- $decoded['qop'] = 'auth';
-
- $data = array_merge($decoded, array('nc'=>$nc));
-
- $response = array(
- 'username'=> $this->jid->node,
- 'response' => $this->encrypt_password($data, $this->jid->node, $this->pass),
- 'charset' => 'utf-8',
- 'nc' => $nc,
- 'qop' => 'auth'
- );
-
- foreach(array('nonce', 'digest-uri', 'realm', 'cnonce') as $key)
- if(isset($decoded[$key]))
- $response[$key] = $decoded[$key];
-
- return base64_encode($this->implode_data($response));
- }
-
- public function get_bind_pkt($resource) {
- $stanza = new JAXLXml('bind', NS_BIND);
- $stanza->c('resource')->t($resource);
- return $this->get_iq_pkt(array(
- 'type' => 'set'
- ), $stanza);
- }
-
- public function get_session_pkt() {
- $stanza = new JAXLXml('session', NS_SESSION);
- return $this->get_iq_pkt(array(
- 'type' => 'set'
- ), $stanza);
- }
-
- public function get_msg_pkt($attrs, $body=null, $thread=null, $subject=null, $payload=null) {
- $msg = new XMPPMsg($attrs, $body, $thread, $subject);
- if(!$msg->id) $msg->id = $this->get_id();
- if($payload) $msg->cnode($payload);
- return $msg;
- }
-
- public function get_pres_pkt($attrs, $status=null, $show=null, $priority=null, $payload=null) {
- $pres = new XMPPPres($attrs, $status, $show, $priority);
- if(!$pres->id) $pres->id = $this->get_id();
- if($payload) $pres->cnode($payload);
- return $pres;
- }
-
- public function get_iq_pkt($attrs, $payload) {
- $iq = new XMPPIq($attrs);
- if(!$iq->id) $iq->id = $this->get_id();
- if($payload) $iq->cnode($payload);
- return $iq;
- }
-
- public function get_id() {
- ++$this->last_id;
- return dechex($this->last_id);
- }
-
- public function explode_data($data) {
- $data = explode(',', $data);
- $pairs = array();
- $key = false;
-
- foreach($data as $pair) {
- $dd = strpos($pair, '=');
- if($dd) {
- $key = trim(substr($pair, 0, $dd));
- $pairs[$key] = trim(trim(substr($pair, $dd + 1)), '"');
- }
- else if(strpos(strrev(trim($pair)), '"') === 0 && $key) {
- $pairs[$key] .= ',' . trim(trim($pair), '"');
- continue;
- }
- }
-
- return $pairs;
- }
-
- public function implode_data($data) {
- $return = array();
- foreach($data as $key => $value) $return[] = $key . '="' . $value . '"';
- return implode(',', $return);
- }
-
- public function encrypt_password($data, $user, $pass) {
- foreach(array('realm', 'cnonce', 'digest-uri') as $key)
- if(!isset($data[$key]))
- $data[$key] = '';
-
- $pack = md5($user.':'.$data['realm'].':'.$pass);
-
- if(isset($data['authzid']))
- $a1 = pack('H32',$pack).sprintf(':%s:%s:%s',$data['nonce'],$data['cnonce'],$data['authzid']);
- else
- $a1 = pack('H32',$pack).sprintf(':%s:%s',$data['nonce'],$data['cnonce']);
-
- $a2 = 'AUTHENTICATE:'.$data['digest-uri'];
- return md5(sprintf('%s:%s:%s:%s:%s:%s', md5($a1), $data['nonce'], $data['nc'], $data['cnonce'], $data['qop'], md5($a2)));
- }
-
- //
- // socket senders
- //
-
- protected function send_start_stream($jid) {
- $this->send_raw($this->get_start_stream($jid));
- }
-
- public function send_end_stream() {
- $this->send_raw($this->get_end_stream());
- }
-
- protected function send_auth_pkt($type, $user, $pass) {
- $this->send($this->get_auth_pkt($type, $user, $pass));
- }
-
- protected function send_starttls_pkt() {
- $this->send($this->get_starttls_pkt());
- }
-
- protected function send_compress_pkt($method) {
- $this->send($this->get_compress_pkt($method));
- }
-
- protected function send_challenge_response($challenge) {
- $this->send($this->get_challenge_response_pkt($challenge));
- }
-
- protected function send_bind_pkt($resource) {
- $this->send($this->get_bind_pkt($resource));
- }
-
- protected function send_session_pkt() {
- $this->send($this->get_session_pkt());
- }
-
- private function do_connect($args) {
- $socket_path = @$args[0];
- if($this->trans->connect($socket_path)) {
- return array("connected", 1);
- }
- else {
- return array("disconnected", 0);
- }
- }
-
- //
- // fsm States
- //
-
- public function setup($event, $args) {
- switch($event) {
- case "connect":
- return $this->do_connect($args);
- break;
- // someone else already started the stream
- // even before "connect" was called
- // must be bosh
- case "start_cb":
- $stanza = $args[0];
- return $this->handle_stream_start($stanza);
- break;
- default:
- _debug("uncatched $event");
- //print_r($args);
- return $this->handle_other($event, $args);
- //return array("setup", 0);
- break;
- }
- }
-
- public function connected($event, $args) {
- switch($event) {
- case "start_stream":
- $this->send_start_stream($this->jid);
- return array("wait_for_stream_start", 1);
- break;
- // someone else already started the stream before us
- // even before "start_stream" was called
- // must be component
- case "start_cb":
- $stanza = $args[0];
- return $this->handle_stream_start($stanza);
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("connected", 0);
- break;
- }
- }
-
- public function disconnected($event, $args) {
- switch($event) {
- case "connect":
- return $this->do_connect($args);
- break;
- case "end_stream":
- $this->send_end_stream();
- return "logged_out";
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("disconnected", 0);
- break;
- }
- }
-
- public function wait_for_stream_start($event, $args) {
- switch($event) {
- case "start_cb":
- // TODO: save stream id and other meta info
- //_debug("stream started");
- return "wait_for_stream_features";
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_stream_start", 0);
- break;
- }
- }
-
- // XEP-0170: Recommended Order of Stream Feature Negotiation
- public function wait_for_stream_features($event, $args) {
- switch($event) {
- case "stanza_cb":
- $stanza = $args[0];
-
- // get starttls requirements
- $starttls = $stanza->exists('starttls', NS_TLS);
- $required = $starttls ? ($this->force_tls ? true : ($starttls->exists('required') ? true : false)) : false;
-
- if($starttls && $required) {
- $this->send_starttls_pkt();
- return "wait_for_tls_result";
- }
-
- // handle auth mech
- $mechs = $stanza->exists('mechanisms', NS_SASL);
- if($mechs) {
- $new_state = $this->handle_auth_mechs($stanza, $mechs);
- return $new_state ? $new_state : "wait_for_sasl_response";
- }
-
- // post auth
- $bind = $stanza->exists('bind', NS_BIND) ? true : false;
- $sess = $stanza->exists('session', NS_SESSION) ? true : false;
- $comp = $stanza->exists('compression', NS_COMPRESSION_FEATURE) ? true : false;
-
- if($bind) {
- $this->send_bind_pkt($this->resource);
- return "wait_for_bind_response";
- }
- /*// compression not supported due to bug in php stream filters
- else if($comp) {
- $this->send_compress_pkt("zlib");
- return "wait_for_compression_result";
- }*/
- else {
- _debug("no catch");
- }
-
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_stream_features", 0);
- break;
- }
- }
-
- public function wait_for_tls_result($event, $args) {
- switch($event) {
- case "stanza_cb":
- $stanza = $args[0];
-
- if($stanza->name == 'proceed' && $stanza->ns == NS_TLS) {
- if($this->trans->crypt()) {
- $this->xml->reset_parser();
- $this->send_start_stream($this->jid);
- return "wait_for_stream_start";
- }
- else {
- $this->handle_auth_failure("tls-negotiation-failed");
- return "logged_out";
- }
- }
- else {
- // FIXME: here
- }
-
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_tls_result", 0);
- break;
- }
- }
- public function wait_for_compression_result($event, $args) {
- switch($event) {
- case "stanza_cb":
- $stanza = $args[0];
-
- if($stanza->name == 'compressed' && $stanza->ns == NS_COMPRESSION_PROTOCOL) {
- $this->xml->reset_parser();
- $this->trans->compress();
- $this->send_start_stream($this->jid);
- return "wait_for_stream_start";
- }
-
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_compression_result", 0);
- break;
- }
- }
-
- public function wait_for_sasl_response($event, $args) {
- switch($event) {
- case "stanza_cb":
- $stanza = $args[0];
-
- if($stanza->name == 'failure' && $stanza->ns == NS_SASL) {
- $reason = $stanza->childrens[0]->name;
- //_debug("sasl failed with reason ".$reason."");
- $this->handle_auth_failure($reason);
- return "logged_out";
- }
- else if($stanza->name == 'challenge' && $stanza->ns == NS_SASL) {
- $challenge = $stanza->text;
- $this->send_challenge_response($challenge);
- return "wait_for_sasl_response";
- }
- else if($stanza->name == 'success' && $stanza->ns == NS_SASL) {
- $this->xml->reset_parser();
- $this->send_start_stream(@$this->jid);
- return "wait_for_stream_start";
- }
- else {
- _debug("got unhandled sasl response");
- }
-
- return "wait_for_sasl_response";
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_sasl_response", 0);
- break;
- }
- }
-
- public function wait_for_bind_response($event, $args) {
- switch($event) {
- case "stanza_cb":
- $stanza = $args[0];
-
- // TODO: chk on id
- if($stanza->name == 'iq' && $stanza->attrs['type'] == 'result'
- && ($jid = $stanza->exists('bind', NS_BIND)->exists('jid'))) {
- $this->full_jid = new XMPPJid($jid->text);
- $this->send_session_pkt();
- return "wait_for_session_response";
- }
- else {
- // FIXME:
- }
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_bind_response", 0);
- break;
- }
- }
-
- public function wait_for_session_response($event, $args) {
- switch($event) {
- case "stanza_cb":
- $this->handle_auth_success();
- return "logged_in";
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("wait_for_session_response", 0);
- break;
- }
- }
-
- public function logged_in($event, $args) {
- switch($event) {
- case "stanza_cb":
- $stanza = $args[0];
-
- // call abstract
- if($stanza->name == 'message') {
- $stanza->type = (@$stanza->type ? $stanza->type : 'normal');
- $this->handle_message($stanza);
- }
- else if($stanza->name == 'presence') {
- $this->handle_presence($stanza);
- }
- else if($stanza->name == 'iq') {
- $this->handle_iq($stanza);
- }
- else {
- $this->handle_other($event, $args);
- }
-
- return "logged_in";
- break;
- case "end_cb":
- $this->send_end_stream();
- return "logged_out";
- break;
- case "end_stream":
- $this->send_end_stream();
- return "logged_out";
- break;
- case "disconnect":
- $this->trans->disconnect();
- return "disconnected";
- break;
- default:
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("logged_in", 0);
- break;
- }
- }
-
- public function logged_out($event, $args) {
- switch($event) {
- case "end_cb":
- $this->trans->disconnect();
- return "disconnected";
- break;
- case "end_stream":
- return "disconnected";
- break;
- case "disconnect":
- $this->trans->disconnect();
- return "disconnected";
- break;
- default:
- // exit for any other event in logged_out state
- _debug("uncatched $event");
- return $this->handle_other($event, $args);
- //return array("logged_out", 0);
- break;
- }
- }
-
- }
- ?>