diff --git a/Tests/xmppclientTest.php b/Tests/xmppclientTest.php new file mode 100644 index 0000000..5942407 --- /dev/null +++ b/Tests/xmppclientTest.php @@ -0,0 +1,55 @@ + + */ + +/** Test the domframework xmppclient part */ +class xmppclientTest extends PHPUnit_Framework_TestCase +{ + public function test_connection_BADNAME () + { + $this->expectException ("Exception"); + $xmppclient = new xmppclient (); + $res = $xmppclient->connect ("NOTFOUND.fournier38.fr", 5222, "", ""); + } + + public function test_connection_authenticate_1 () + { + $xmppclient = new xmppclient (); + $res = $xmppclient->connect ("xmpp.fournier38.fr", 5222, + "testxmpp@xmpp.fournier38.fr", "LSqmBXDUZWxk"); + $this->assertSame (is_object ($res), true); + } + + public function test_connection_disco_1 () + { + $xmppclient = new xmppclient (); + $xmppclient->connect ("xmpp.fournier38.fr", 5222, + "testxmpp@xmpp.fournier38.fr", "LSqmBXDUZWxk"); + $res = $xmppclient->discoveryService (); + $this->assertSame (is_array ($res) && count ($res) > 5, true); + } + + public function test_connection_sendMessage_1 () + { + $xmppclient = new xmppclient (); + $xmppclient->connect ("xmpp.fournier38.fr", 5222, + "testxmpp@xmpp.fournier38.fr", "LSqmBXDUZWxk"); + $res = $xmppclient->sendMessagePrivate ("dominique@fournier38.fr", + "DFW TEST XMPP : test_connection_sendMessage_1"); + $this->assertSame (is_object ($res), true); + } + + public function test_connection_sendMessage_2 () + { + $xmppclient = new xmppclient (); + $xmppclient->connect ("xmpp.fournier38.fr", 5222, + "testxmpp@xmpp.fournier38.fr", "LSqmBXDUZWxk"); + $res = $xmppclient->sendMessagePrivate ("dominique@fournier38.fr", + "DFW TEST XMPP : test_connection_sendMessage_2 : MESSAGE 1"); + $res = $xmppclient->sendMessagePrivate ("dominique@fournier38.fr", + "DFW TEST XMPP : test_connection_sendMessage_2 : MESSAGE 2"); + $this->assertSame (is_object ($res), true); + } +} diff --git a/xmppclient.php b/xmppclient.php new file mode 100644 index 0000000..3600cc9 --- /dev/null +++ b/xmppclient.php @@ -0,0 +1,241 @@ + + */ + +require_once ("domframework/tcpclient.php"); + +/** This class allow to send XMPP messages to a server. + * If the server supports it, crypt the connection by StartTLS before auth + */ +class xmppclient +{ + // CLASS CONSTANT // + // {{{ + /** One send command each $RATE µs maximum + */ + const RATE = 30000; + // }}} + + // PROPERTIES // + // {{{ + /** The connection socket when connected + */ + private $sock; + + /** The XML stream exchange string + */ + private $xmlStream; + + /** The debug state + */ + private $debug = false; + + /* The object identifier increment + */ + private $id = 0; + + /* The JID provided by the user + */ + private $jid = ""; + + /* Ratelimiter of send commands to server. Store the last timestamp, in + * microseconds. + */ + private $sendLastTime = 0; + // }}} + + // PUBLIC METHODS // + /** Connect to XMPP server $server, on port $port (5222 by default) + * @param string $server The XMPP server + * @param integer $port The port to use (5222 by default) + * @param string $login The login to use + * @param string $password The password to use + * @param boolean|null $debug Debug the socket on stderr + * @return $this + */ + public function connect ($server, $port, $login, $password, + $debug = false) + // {{{ + { + // To have a really one microsecond precision in microtime function + ini_set ("precision", 16); + $this->debug = $debug; + $this->sock = new tcpclient ($server, $port); + $this->sock->readMode ("binary"); + $client = gethostname (); + @list ($user, $domain) = explode ("@", $login, 2); + if ($domain === null) + $domain = $server; + $this->xmlStream = "". + ""; + $this->sock->connect (); + $this->debug ("Info: Connected to '$server:$port'"); + $this->send ($this->xmlStream); + $xml = $this->read (); + // Check if we can/must go in StartTLS + preg_match ("#(.*)<\/starttls>#", $xml, $tls); + if (count ($tls)) + { + $this->send (""); + $xml = $this->read (); + // Can not use the tcpclient part : the certificate is provided by domain, + // not by the XMPP server globally + $options = array ("peer_name" => $domain); + $this->sock->cryptoEnable (true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + $options); + $this->debug ("Info: Crypto enabled"); + } + $this->send ($this->xmlStream); + $xml = $this->read (); + preg_match ("#(.*)<\/mechanism>#", $xml, $auths); + if (! key_exists (1, $auths)) + return null; + $method = strtoupper ($auths[1]); + if ($method === "PLAIN") + { + $val = base64_encode ($login.chr(0).$user.chr(0).$password); + } + else + throw new \Exception (sprintf (dgettext ("domframework", + "XMPP Client : unknown authentication method requested : %s"), + $method)); + $this->send ("$val"); + $xml = $this->read (); + preg_match ("#(.+)#", $xml, $error); + if (count ($match) === 0) + throw new \Exception (sprintf (dgettext ("domframework", + "XMPP Client : Authentication rejected for user '%s' : %s"), + $login, $error[1]), 401); + $this->debug ("Info: User '$login' authenticated"); + $this->send ($this->xmlStream); + $xml = $this->read (); + // $xml : Here we can have the session. + $this->send ("". + "". + "".__CLASS__.""); + $xml = $this->read (); + preg_match ("#(.+)#", $xml, $jids); + if (! key_exists (1, $jids)) + throw new \Exception (dgettext ("domframework", + "XMPP Client : can not get the JID from the server"), 500); + $this->debug ("Info: Get JID => ".$jids[1]); + $this->jid = $jids[1]; + $this->sock->timeout (5); + return $this; + } + // }}} + + /** Disconnect from the XMPP server + */ + public function disconnect () + // {{{ + { + if ($this->sock && $this->sock->getSock ()) + { + $this->send (""); + $this->sock->disconnect (); + } + } + // }}} + + /** Call the Discovery Service to find the available services on the server + * @return array The services available + */ + public function discoveryService () + // {{{ + { + $this->send ("". + ""); + $xml = $this->read (); + preg_match_all ("##U", $xml, $features); + if (! key_exists (1, $features)) + throw new \Exception (dgettext ("domframework", + "XMPP Client : Can not get the discovery service result from server"), + 500); + $this->id++; + return $features[1]; + } + // }}} + + /** Send a direct message to a recipient. The message subject can be omitted, + * as it is not displayed on the clients + * @param string $recipient The recipient of the message + * @param string $message The message to send + * @param string|null $subject The message subject + * @return $this + */ + public function sendMessagePrivate ($recipient, $message, $subject = null) + // {{{ + { + $this->send ("". + "$message$subject". + "Ishmael". + ""); + $this->id++; + return $this; + } + // }}} + + /** In case of destruction, try to disconnect the server properly + */ + public function __destruct () + // {{{ + { + $this->disconnect (); + } + // }}} + + /** Each debug message is sent to stderr if $debug is set + * @param string $msg The message to display + */ + public function debug ($msg) + // {{{ + { + if ($this->debug === false) + return; + file_put_contents ("php://stderr", $msg."\n"); + } + // }}} + + // PRIVATE METHODS // + /** Read fron socket + * @return the read value + */ + private function read () + // {{{ + { + $xml = $this->sock->read (4096); + $this->debug ("Read: $xml"); + return $xml; + } + // }}} + + /** Send the data on socket, add the carriage return + * @param $msg The message to send + */ + private function send ($msg) + // {{{ + { + $start = microtime (true); + $wait = intval (($start - $this->sendLastTime) * 100000); + if ($wait > self::RATE) + $this->debug ("Send: $msg"); + else + { + $sleep = intval (self::RATE - $wait); + $this->debug ("Send: $msg"); + usleep ($sleep); + } + $this->sendLastTime = microtime (true); + return $this->sock->send ($msg."\n"); + } + // }}} +}