<?php
/**
* Jaxl (Jabber XMPP Library)
*
* Copyright (c) 2009-2012, Abhinav Singh <hide@address.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) {
return '<stream:stream xmlns:stream="'.NS_XMPP.'" version="1.0" from="'.$jid->bare.'" to="'.$jid->domain.'" xmlns="'.NS_JABBER_CLIENT.'" xml:lang="en" xmlns:xml="'.NS_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':
$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) {
$this->trans->crypt();
$this->xml->reset_parser();
$this->send_start_stream($this->jid);
return "wait_for_stream_start";
}
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;
}
}
}
?>