Initial commit
This commit is contained in:
15
src/AuthTypes/Authenticable.php
Normal file
15
src/AuthTypes/Authenticable.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\AuthTypes;
|
||||
|
||||
interface Authenticable
|
||||
{
|
||||
/**
|
||||
* Based on auth type, return the right format of credentials to be sent to the server.
|
||||
*/
|
||||
public function encodedCredentials(): string;
|
||||
|
||||
public function getName(): string;
|
||||
}
|
24
src/AuthTypes/Authentication.php
Normal file
24
src/AuthTypes/Authentication.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\AuthTypes;
|
||||
|
||||
use Pdahal\Xmpp\Options;
|
||||
use Pdahal\Xmpp\Xml\Xml;
|
||||
|
||||
abstract class Authentication implements Authenticable
|
||||
{
|
||||
use Xml;
|
||||
|
||||
protected string $name;
|
||||
|
||||
public function __construct(protected Options $options)
|
||||
{
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
16
src/AuthTypes/DigestMD5.php
Normal file
16
src/AuthTypes/DigestMD5.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\AuthTypes;
|
||||
|
||||
class DigestMD5 extends Authentication
|
||||
{
|
||||
protected string $name = 'DIGEST-MD5';
|
||||
|
||||
public function encodedCredentials(): string
|
||||
{
|
||||
$credentials = "\x00{$this->options->getUsername()}\x00{$this->options->getPassword()}";
|
||||
return self::quote(sha1($credentials));
|
||||
}
|
||||
}
|
16
src/AuthTypes/Plain.php
Normal file
16
src/AuthTypes/Plain.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\AuthTypes;
|
||||
|
||||
class Plain extends Authentication
|
||||
{
|
||||
protected string $name = 'PLAIN';
|
||||
|
||||
public function encodedCredentials(): string
|
||||
{
|
||||
$credentials = "\x00{$this->options->getUsername()}\x00{$this->options->getPassword()}";
|
||||
return self::quote(base64_encode($credentials));
|
||||
}
|
||||
}
|
18
src/Buffers/Buffer.php
Normal file
18
src/Buffers/Buffer.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Buffers;
|
||||
|
||||
interface Buffer
|
||||
{
|
||||
/**
|
||||
* Write to buffer (add to array of values)
|
||||
*/
|
||||
public function write(string $data): void;
|
||||
|
||||
/**
|
||||
* Read from buffer and delete the data
|
||||
*/
|
||||
public function read(): string;
|
||||
}
|
35
src/Buffers/Response.php
Normal file
35
src/Buffers/Response.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Buffers;
|
||||
|
||||
class Response implements Buffer
|
||||
{
|
||||
protected array $response = [];
|
||||
|
||||
public function write(?string $data = null): void
|
||||
{
|
||||
if ($data) {
|
||||
$this->response[] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
public function read(): string
|
||||
{
|
||||
$implodedResponse = $this->response != [] ? implode('', $this->response) : '';
|
||||
$this->flush();
|
||||
|
||||
return $implodedResponse;
|
||||
}
|
||||
|
||||
protected function flush(): void
|
||||
{
|
||||
$this->response = [];
|
||||
}
|
||||
|
||||
public function getCurrentBufferData(): array
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
18
src/Exceptions/DeadSocket.php
Normal file
18
src/Exceptions/DeadSocket.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class DeadSocket extends Exception
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$errorCode = socket_last_error();
|
||||
$errorMsg = socket_strerror($errorCode);
|
||||
|
||||
parent::__construct("Couldn't create socket: [$errorCode] $errorMsg");
|
||||
}
|
||||
}
|
15
src/Exceptions/StreamError.php
Normal file
15
src/Exceptions/StreamError.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class StreamError extends Exception
|
||||
{
|
||||
public function __construct(string $streamErrorType)
|
||||
{
|
||||
parent::__construct("Unrecoverable stream error ({$streamErrorType}), trying to reconnect...");
|
||||
}
|
||||
}
|
33
src/Loggers/Loggable.php
Normal file
33
src/Loggers/Loggable.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Loggers;
|
||||
|
||||
interface Loggable
|
||||
{
|
||||
/**
|
||||
* Standard log message
|
||||
*/
|
||||
public function log(string $message): void;
|
||||
|
||||
/**
|
||||
* Shorthand method for logging with prepended "REQUEST" string
|
||||
*/
|
||||
public function logRequest(string $message): void;
|
||||
|
||||
/**
|
||||
* Shorthand method for logging with prepended "RESPONSE" string
|
||||
*/
|
||||
public function logResponse(string $message): void;
|
||||
|
||||
/**
|
||||
* Shorthand method for logging with prepended "ERROR" string
|
||||
*/
|
||||
public function error(string $message): void;
|
||||
|
||||
/**
|
||||
* Returns relative path from given resource
|
||||
*/
|
||||
public function getFilePathFromResource(mixed $resource): string;
|
||||
}
|
69
src/Loggers/Logger.php
Normal file
69
src/Loggers/Logger.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Loggers;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Logger implements Loggable
|
||||
{
|
||||
public mixed $log;
|
||||
|
||||
public const LOG_FOLDER = "logs";
|
||||
public const LOG_FILE = "xmpp.log";
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createLogFile();
|
||||
$this->log = fopen(self::LOG_FOLDER . '/' . self::LOG_FILE, 'a');
|
||||
}
|
||||
|
||||
protected function createLogFile(): void
|
||||
{
|
||||
if (!file_exists(self::LOG_FOLDER)) {
|
||||
mkdir(self::LOG_FOLDER, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function log(string $message): void
|
||||
{
|
||||
$this->writeToLog($message);
|
||||
}
|
||||
|
||||
public function logRequest(string $message): void
|
||||
{
|
||||
$this->writeToLog($message, "REQUEST");
|
||||
}
|
||||
|
||||
public function logResponse(string $message): void
|
||||
{
|
||||
$this->writeToLog($message, "RESPONSE");
|
||||
}
|
||||
|
||||
public function error(string $message): void
|
||||
{
|
||||
$this->writeToLog($message, "ERROR");
|
||||
}
|
||||
|
||||
protected function writeToLog(string $message, string $type = ''): void
|
||||
{
|
||||
$prefix = date("Y.m.d H:i:s") . " " . session_id() . ($type ? " {$type}::" : " ");
|
||||
$this->writeToFile($this->log, $prefix . "$message\n");
|
||||
}
|
||||
|
||||
protected function writeToFile(mixed $file, string $message): void
|
||||
{
|
||||
try {
|
||||
fwrite($file, $message);
|
||||
} catch (Exception) {
|
||||
// silent fail
|
||||
}
|
||||
}
|
||||
|
||||
public function getFilePathFromResource(mixed $resource): string
|
||||
{
|
||||
$metaData = stream_get_meta_data($resource);
|
||||
return $metaData["uri"];
|
||||
}
|
||||
}
|
259
src/Options.php
Normal file
259
src/Options.php
Normal file
@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp;
|
||||
|
||||
use Pdahal\Xmpp\AuthTypes\Authenticable;
|
||||
use Pdahal\Xmpp\AuthTypes\Plain;
|
||||
use Pdahal\Xmpp\Loggers\Loggable;
|
||||
use Pdahal\Xmpp\Loggers\Logger;
|
||||
use Psr\Log\InvalidArgumentException;
|
||||
|
||||
class Options
|
||||
{
|
||||
/**
|
||||
* Hostname of XMPP server.
|
||||
*/
|
||||
protected string $host = '';
|
||||
|
||||
/**
|
||||
* Domain Name of XMPP server.
|
||||
*/
|
||||
protected string $domain = '';
|
||||
|
||||
/**
|
||||
* XMPP server port. Usually 5222.
|
||||
*/
|
||||
protected int $port = 5222;
|
||||
|
||||
/**
|
||||
* Protocol used for socket connection, defaults to TCP.
|
||||
*/
|
||||
protected string $protocol = 'tcp';
|
||||
|
||||
/**
|
||||
* Username to authenticate on XMPP server.
|
||||
*/
|
||||
protected string $username = '';
|
||||
|
||||
/**
|
||||
* Password to authenticate on XMPP server.
|
||||
*/
|
||||
protected string $password = '';
|
||||
|
||||
/**
|
||||
* XMPP resource.
|
||||
*/
|
||||
protected string $resource = '';
|
||||
|
||||
/**
|
||||
* Custom logger interface.
|
||||
*/
|
||||
protected ?Loggable $logger = null;
|
||||
|
||||
/**
|
||||
* Use TLS if available.
|
||||
*/
|
||||
protected bool $useTls = true;
|
||||
|
||||
/**
|
||||
* Auth type (Authentication/AuthTypes/).
|
||||
*/
|
||||
protected ?Authenticable $authType = null;
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
if (!$this->host) {
|
||||
$this->getLogger()->error(__METHOD__.'::'.__LINE__.
|
||||
' No host found, please set the host variable');
|
||||
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function setHost(string $host): Options
|
||||
{
|
||||
$this->host = trim($host);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDomain(string $domain): Options
|
||||
{
|
||||
$this->domain = $domain;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDomain(): string
|
||||
{
|
||||
if ($this->domain == '') {
|
||||
return $this->getHost();
|
||||
}
|
||||
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function setPort(int $port): Options
|
||||
{
|
||||
$this->port = $port;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
if ($this->username == '') {
|
||||
$this->getLogger()->error(__METHOD__.'::'.__LINE__.
|
||||
' No username found, please set the username variable');
|
||||
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to assign a resource if it exists. If bare JID is forwarded, this will default to your username.
|
||||
*/
|
||||
public function setUsername(string $username): Options
|
||||
{
|
||||
$usernameResource = explode('/', $username);
|
||||
|
||||
if (count($usernameResource) > 1) {
|
||||
$this->setResource($usernameResource[1]);
|
||||
$username = $usernameResource[0];
|
||||
}
|
||||
|
||||
$this->username = trim($username);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getPassword(): string
|
||||
{
|
||||
if ($this->password == '') {
|
||||
$this->getLogger()->error(__METHOD__.'::'.__LINE__.
|
||||
' No password found, please set the password variable');
|
||||
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): Options
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getResource(): string
|
||||
{
|
||||
if ($this->resource == '') {
|
||||
$this->resource = 'Pdahal_machine_'.time();
|
||||
}
|
||||
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
public function setResource(string $resource): Options
|
||||
{
|
||||
$this->resource = trim($resource);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProtocol(): string
|
||||
{
|
||||
return $this->protocol;
|
||||
}
|
||||
|
||||
public function setProtocol(string $protocol): Options
|
||||
{
|
||||
$this->protocol = $protocol;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function fullSocketAddress(): string
|
||||
{
|
||||
$protocol = $this->getProtocol();
|
||||
$host = $this->getHost();
|
||||
$port = $this->getPort();
|
||||
|
||||
return "{$protocol}://{$host}:{$port}";
|
||||
}
|
||||
|
||||
public function fullJid(): string
|
||||
{
|
||||
$username = $this->getUsername();
|
||||
$resource = $this->getResource();
|
||||
$host = $this->getHost();
|
||||
|
||||
return "{$username}@{$host}/{$resource}";
|
||||
}
|
||||
|
||||
public function bareJid(): string
|
||||
{
|
||||
$username = $this->getUsername();
|
||||
$host = $this->getHost();
|
||||
|
||||
return "{$username}@{$host}";
|
||||
}
|
||||
|
||||
public function setLogger(Loggable $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function getLogger(): Loggable
|
||||
{
|
||||
if ($this->logger === null) {
|
||||
$this->logger = new Logger();
|
||||
}
|
||||
|
||||
return $this->logger;
|
||||
}
|
||||
|
||||
public function setUseTls(bool $enable): void
|
||||
{
|
||||
$this->useTls = $enable;
|
||||
}
|
||||
|
||||
public function usingTls(): bool
|
||||
{
|
||||
return $this->useTls;
|
||||
}
|
||||
|
||||
public function getAuthType(): Authenticable
|
||||
{
|
||||
if ($this->authType === null) {
|
||||
$this->setAuthType(new Plain($this));
|
||||
}
|
||||
|
||||
return $this->authType;
|
||||
}
|
||||
|
||||
public function setAuthType(Authenticable $authType): Options
|
||||
{
|
||||
$this->authType = $authType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
111
src/Socket.php
Normal file
111
src/Socket.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp;
|
||||
|
||||
use Pdahal\Xmpp\Buffers\Response;
|
||||
use Pdahal\Xmpp\Exceptions\DeadSocket;
|
||||
|
||||
class Socket
|
||||
{
|
||||
public mixed $connection;
|
||||
protected Response $responseBuffer;
|
||||
protected Options $options;
|
||||
|
||||
/**
|
||||
* Period in microseconds for imposed timeout while doing socket_read().
|
||||
*/
|
||||
protected int $timeout = 150000;
|
||||
|
||||
/**
|
||||
* Socket constructor.
|
||||
*
|
||||
* @throws DeadSocket
|
||||
*/
|
||||
public function __construct(Options $options)
|
||||
{
|
||||
$this->responseBuffer = new Response();
|
||||
$this->connection = stream_socket_client($options->fullSocketAddress());
|
||||
|
||||
if (!$this->isAlive($this->connection)) {
|
||||
throw new DeadSocket();
|
||||
}
|
||||
|
||||
// stream_set_blocking($this->connection, true);
|
||||
stream_set_timeout($this->connection, 0, $this->timeout);
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
fclose($this->connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending XML stanzas to open socket.
|
||||
*/
|
||||
public function send(string $xml)
|
||||
{
|
||||
try {
|
||||
fwrite($this->connection, $xml);
|
||||
$this->options->getLogger()->logRequest(__METHOD__.'::'.__LINE__." {$xml}");
|
||||
// $this->checkSocketStatus();
|
||||
} catch (\Exception $e) {
|
||||
$this->options->getLogger()->error(__METHOD__.'::'.__LINE__.' fwrite() failed '.$e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->receive();
|
||||
}
|
||||
|
||||
public function receive(): void
|
||||
{
|
||||
$response = '';
|
||||
while ($out = fgets($this->connection)) {
|
||||
$response .= $out;
|
||||
}
|
||||
|
||||
if (!$response) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->responseBuffer->write($response);
|
||||
$this->options->getLogger()->logResponse(__METHOD__.'::'.__LINE__." {$response}");
|
||||
}
|
||||
|
||||
protected function isAlive($socket): bool
|
||||
{
|
||||
return $socket !== false;
|
||||
}
|
||||
|
||||
public function setTimeout($timeout): void
|
||||
{
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
public function getResponseBuffer(): Response
|
||||
{
|
||||
return $this->responseBuffer;
|
||||
}
|
||||
|
||||
public function getOptions(): Options
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
protected function checkSocketStatus(): void
|
||||
{
|
||||
$status = socket_get_status($this->connection);
|
||||
|
||||
// echo print_r($status);
|
||||
|
||||
if ($status['eof']) {
|
||||
$this->options->getLogger()->logResponse(
|
||||
__METHOD__.'::'.__LINE__.
|
||||
" ---Probably a broken pipe, restart connection\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
51
src/Xml/Stanzas/Auth.php
Normal file
51
src/Xml/Stanzas/Auth.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||
|
||||
use Pdahal\Xmpp\AuthTypes\Authenticable;
|
||||
|
||||
class Auth extends Stanza
|
||||
{
|
||||
public function authenticate(): void
|
||||
{
|
||||
$response = $this->socket->getResponseBuffer()->read();
|
||||
$options = $this->socket->getOptions();
|
||||
|
||||
$tlsSupported = self::isTlsSupported($response);
|
||||
$tlsRequired = self::isTlsRequired($response);
|
||||
|
||||
if ($tlsSupported && ($tlsRequired || (!$tlsRequired && $options->usingTls()))) {
|
||||
$this->startTls();
|
||||
$this->socket->send(self::openXmlStream($options->getDomain()));
|
||||
}
|
||||
|
||||
$xml = $this->generateAuthXml($options->getAuthType());
|
||||
$this->socket->send($xml);
|
||||
$this->socket->send(self::openXmlStream($options->getDomain()));
|
||||
}
|
||||
|
||||
protected function startTls(): void
|
||||
{
|
||||
$this->socket->send("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
|
||||
$response = $this->socket->getResponseBuffer()->read();
|
||||
|
||||
if (!self::canProceed($response)) {
|
||||
$this->socket->getOptions()->getLogger()->error(__METHOD__ . '::' . __LINE__ .
|
||||
" TLS authentication failed. Trying to continue but will most likely fail.");
|
||||
return;
|
||||
}
|
||||
|
||||
stream_socket_enable_crypto($this->socket->connection, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT);
|
||||
}
|
||||
|
||||
protected function generateAuthXml(Authenticable $authType): string
|
||||
{
|
||||
$mechanism = $authType->getName();
|
||||
$encodedCredentials = $authType->encodedCredentials();
|
||||
$nameSpace = "urn:ietf:params:xml:ns:xmpp-sasl";
|
||||
|
||||
return "<auth xmlns='{$nameSpace}' mechanism='{$mechanism}'>{$encodedCredentials}</auth>";
|
||||
}
|
||||
}
|
98
src/Xml/Stanzas/Iq.php
Normal file
98
src/Xml/Stanzas/Iq.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||
|
||||
class Iq extends Stanza
|
||||
{
|
||||
public function getRoster(): void
|
||||
{
|
||||
$query = "<query xmlns='jabber:iq:roster'/>";
|
||||
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function addToRoster(string $name, string $forJid, string $from, ?string $groupName = null): void
|
||||
{
|
||||
$group = $groupName ? "<group>{$groupName}</group>" : null;
|
||||
$item = "<item jid='{$forJid}' name='{$name}'>{$group}</item>";
|
||||
$query = "<query xmlns='jabber:iq:roster'>{$item}</query>";
|
||||
$xml = "<iq type='set' id='{$this->uniqueId()}' from='{$from}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function removeFromRoster(string $jid, string $myJid): void
|
||||
{
|
||||
$item = "<item jid='{$jid}' subscription='remove'/>";
|
||||
$query = "<query xmlns='jabber:iq:roster'>{$item}</query>";
|
||||
$xml = "<iq type='set' id='{$this->uniqueId()}' from='{$myJid}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function setResource(string $name): void
|
||||
{
|
||||
if (!trim($name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = "<resource>{$name}</resource>";
|
||||
$bind = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>{$resource}</bind>";
|
||||
$xml = "<iq type='set' id='{$this->uniqueId()}'>{$bind}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function setGroup(string $name, string $forJid): void
|
||||
{
|
||||
$group = "<group>{$name}</group>";
|
||||
$item = "<item jid='{$forJid}'>{$group}</item>";
|
||||
$query = "<query xmlns='jabber:iq:roster'>{$item}</query>";
|
||||
$xml = "<iq type='set' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function getServerVersion(): void
|
||||
{
|
||||
$query = "<query xmlns='jabber:iq:version'/>";
|
||||
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function getServerFeatures(): void
|
||||
{
|
||||
$query = "<query xmlns='http://jabber.org/protocol/disco#info'></query>";
|
||||
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function getServerTime(): void
|
||||
{
|
||||
$query = "<query xmlns='urn:xmpp:time'/>";
|
||||
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function getFeatures(string $forJid): void
|
||||
{
|
||||
$query = "<query xmlns='http://jabber.org/protocol/disco#info'></query>";
|
||||
$xml = "<iq type='get' to='{$forJid}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function ping(): void
|
||||
{
|
||||
$query = "<query xmlns='urn:xmpp:ping'/>";
|
||||
$xml = "<iq type='get' id='{$this->uniqueId()}'>{$query}</iq>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
}
|
31
src/Xml/Stanzas/Message.php
Normal file
31
src/Xml/Stanzas/Message.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||
|
||||
class Message extends Stanza
|
||||
{
|
||||
public function send(string $body, string $to, string $type = "chat"): void
|
||||
{
|
||||
$xml = $this->generateMessageXml($body, $to, $type);
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function receive(): array
|
||||
{
|
||||
$this->socket->receive();
|
||||
$rawResponse = $this->socket->getResponseBuffer()->read();
|
||||
return self::parseTag($rawResponse, "message");
|
||||
}
|
||||
|
||||
protected function generateMessageXml(string $body, string $to, string $type): string
|
||||
{
|
||||
$to = self::quote($to);
|
||||
$body = self::quote($body);
|
||||
|
||||
$bodyXml = "<body>{$body}</body>";
|
||||
|
||||
return "<message to='{$to}' type='{$type}'>{$bodyXml}</message>";
|
||||
}
|
||||
}
|
67
src/Xml/Stanzas/Presence.php
Normal file
67
src/Xml/Stanzas/Presence.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||
|
||||
class Presence extends Stanza
|
||||
{
|
||||
public const PRIORITY_UPPER_BOUND = 127;
|
||||
public const PRIORITY_LOWER_BOUND = -128;
|
||||
|
||||
public function subscribe(string $to): void
|
||||
{
|
||||
$this->setPresence($to, 'subscribe');
|
||||
}
|
||||
|
||||
public function unsubscribe(string $from): void
|
||||
{
|
||||
$this->setPresence($from, 'unsubscribe');
|
||||
}
|
||||
|
||||
public function acceptSubscription(string $from): void
|
||||
{
|
||||
$this->setPresence($from, 'subscribed');
|
||||
}
|
||||
|
||||
public function declineSubscription(string $from): void
|
||||
{
|
||||
$this->setPresence($from, 'unsubscribed');
|
||||
}
|
||||
|
||||
protected function setPresence(string $to, string $type = "subscribe"): void
|
||||
{
|
||||
$xml = "<presence from='{$this->socket->getOptions()->bareJid()}' to='{$to}' type='{$type}'/>";
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set priority to current resource by default, or optional other resource tied to the
|
||||
* current username
|
||||
* @param string|null $forResource
|
||||
*/
|
||||
public function setPriority(int $value, string $forResource = null): void
|
||||
{
|
||||
$from = self::quote($this->socket->getOptions()->fullJid());
|
||||
|
||||
if ($forResource) {
|
||||
$from = $this->socket->getOptions()->getUsername() . "/$forResource";
|
||||
}
|
||||
|
||||
$priority = "<priority>{$this->limitPriority($value)}</priority>";
|
||||
$xml = "<presence from='{$from}'>{$priority}</presence>";
|
||||
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
protected function limitPriority(int $value): int
|
||||
{
|
||||
if ($value > self::PRIORITY_UPPER_BOUND) {
|
||||
return self::PRIORITY_UPPER_BOUND;
|
||||
} elseif ($value < self::PRIORITY_LOWER_BOUND) {
|
||||
return self::PRIORITY_LOWER_BOUND;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
31
src/Xml/Stanzas/Stanza.php
Normal file
31
src/Xml/Stanzas/Stanza.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Xml\Stanzas;
|
||||
|
||||
use Pdahal\Xmpp\Socket;
|
||||
use Pdahal\Xmpp\Xml\Xml;
|
||||
|
||||
abstract class Stanza
|
||||
{
|
||||
use Xml;
|
||||
|
||||
public function __construct(protected Socket $socket)
|
||||
{
|
||||
}
|
||||
|
||||
protected function uniqueId(): string
|
||||
{
|
||||
return uniqid();
|
||||
}
|
||||
|
||||
protected function readResponseFile(): string|false
|
||||
{
|
||||
$logger = $this->socket->getOptions()->getLogger();
|
||||
$responseFilePath = $logger->getFilePathFromResource($logger->log);
|
||||
$responseFile = fopen($responseFilePath, 'r');
|
||||
|
||||
return fread($responseFile, filesize($responseFilePath));
|
||||
}
|
||||
}
|
123
src/Xml/Xml.php
Normal file
123
src/Xml/Xml.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp\Xml;
|
||||
|
||||
use Pdahal\Xmpp\Exceptions\StreamError;
|
||||
|
||||
trait Xml
|
||||
{
|
||||
/**
|
||||
* Opening tag for starting a XMPP stream exchange.
|
||||
*/
|
||||
public static function openXmlStream(string $host): string
|
||||
{
|
||||
$xmlOpen = "<?xml version='1.0' encoding='UTF-8'?>";
|
||||
$to = "to='{$host}'";
|
||||
$stream = "xmlns:stream='http://etherx.jabber.org/streams'";
|
||||
$client = "xmlns='jabber:client'";
|
||||
$version = "version='1.0'";
|
||||
|
||||
return "{$xmlOpen}<stream:stream {$to} {$stream} {$client} {$version}>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Closing tag for one XMPP stream session.
|
||||
*/
|
||||
public static function closeXmlStream(): string
|
||||
{
|
||||
return '</stream:stream>';
|
||||
}
|
||||
|
||||
public static function quote(string $input): string
|
||||
{
|
||||
return htmlspecialchars($input, ENT_XML1, 'utf-8');
|
||||
}
|
||||
|
||||
public static function parseTag(string $rawResponse, string $tag): array
|
||||
{
|
||||
preg_match_all("#(<{$tag}.*?>.*?<\\/{$tag}>)#si", $rawResponse, $matched);
|
||||
|
||||
return count($matched) <= 1 ? [] : array_map(fn ($match): false|\SimpleXMLElement => @simplexml_load_string($match), $matched[1]);
|
||||
}
|
||||
|
||||
public static function parseFeatures(string $xml): string
|
||||
{
|
||||
return self::matchInsideOfTag($xml, 'stream:features');
|
||||
}
|
||||
|
||||
public static function isTlsSupported(string $xml): bool
|
||||
{
|
||||
$matchTag = self::matchCompleteTag($xml, 'starttls');
|
||||
|
||||
return !empty($matchTag);
|
||||
}
|
||||
|
||||
public static function isTlsRequired(string $xml): bool
|
||||
{
|
||||
if (!self::isTlsSupported($xml)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tls = self::matchCompleteTag($xml, 'starttls');
|
||||
preg_match('#required#', $tls, $match);
|
||||
|
||||
return count($match) > 0;
|
||||
}
|
||||
|
||||
public static function matchCompleteTag(string $xml, string $tag): string
|
||||
{
|
||||
$matches = self::matchTag($xml, $tag);
|
||||
|
||||
return is_array($matches) && count($matches) > 0 ? $matches[0] : '';
|
||||
}
|
||||
|
||||
public static function matchInsideOfTag(string $xml, string $tag): string
|
||||
{
|
||||
$match = self::matchTag($xml, $tag);
|
||||
|
||||
return is_array($match) && count($match) > 1 ? $match[1] : '';
|
||||
}
|
||||
|
||||
private static function matchTag(string $xml, string $tag): array
|
||||
{
|
||||
$matches = null;
|
||||
preg_match("#<{$tag}.*?>(.*)<\\/{$tag}>#", $xml, $matches);
|
||||
|
||||
return count($matches) < 1 ? [] : $matches;
|
||||
}
|
||||
|
||||
public static function canProceed($xml): bool
|
||||
{
|
||||
preg_match("#<proceed xmlns=[\\'|\"]urn:ietf:params:xml:ns:xmpp-tls[\\'|\"]\\/>#", (string) $xml, $match);
|
||||
|
||||
return count($match) > 0;
|
||||
}
|
||||
|
||||
public static function supportedAuthMethods($xml): array
|
||||
{
|
||||
preg_match_all('#<mechanism>(.*?)<\\/mechanism>#', (string) $xml, $match);
|
||||
|
||||
return count($match) < 1 ? [] : $match[1];
|
||||
}
|
||||
|
||||
public static function roster(string $xml): array
|
||||
{
|
||||
preg_match_all("#<iq.*?type=[\\'|\"]result[\\'|\"]>(.*?)<\\/iq>#", $xml, $match);
|
||||
|
||||
return count($match) < 1 ? [] : $match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StreamError
|
||||
*/
|
||||
public static function checkForUnrecoverableErrors(string $response): void
|
||||
{
|
||||
preg_match_all('#<stream:error>(<(.*?) (.*?)\\/>)<\\/stream:error>#', $response, $streamErrors);
|
||||
|
||||
if ((!empty($streamErrors[0])) && count($streamErrors[2]) > 0) {
|
||||
throw new StreamError($streamErrors[2][0]);
|
||||
}
|
||||
}
|
||||
}
|
131
src/XmppClient.php
Normal file
131
src/XmppClient.php
Normal file
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Pdahal\Xmpp;
|
||||
|
||||
use Pdahal\Xmpp\Exceptions\StreamError;
|
||||
use Pdahal\Xmpp\Xml\Stanzas\Auth;
|
||||
use Pdahal\Xmpp\Xml\Stanzas\Iq;
|
||||
use Pdahal\Xmpp\Xml\Stanzas\Message;
|
||||
use Pdahal\Xmpp\Xml\Stanzas\Presence;
|
||||
use Pdahal\Xmpp\Xml\Xml;
|
||||
|
||||
class XmppClient
|
||||
{
|
||||
use Xml;
|
||||
|
||||
protected Socket $socket;
|
||||
|
||||
public Auth $auth;
|
||||
|
||||
public Iq $iq;
|
||||
|
||||
public Presence $presence;
|
||||
|
||||
public Message $message;
|
||||
|
||||
public function __construct(protected Options $options)
|
||||
{
|
||||
$this->initDependencies();
|
||||
$this->initSession();
|
||||
}
|
||||
|
||||
protected function initDependencies(): void
|
||||
{
|
||||
$this->socket = $this->initSocket();
|
||||
$this->initStanzas($this->socket);
|
||||
}
|
||||
|
||||
public function connect(): void
|
||||
{
|
||||
$this->openStream();
|
||||
$this->auth->authenticate();
|
||||
$this->iq->setResource($this->options->getResource());
|
||||
$this->sendInitialPresenceStanza();
|
||||
}
|
||||
|
||||
public function send(string $xml): void
|
||||
{
|
||||
$this->socket->send($xml);
|
||||
}
|
||||
|
||||
public function getResponse(): string
|
||||
{
|
||||
$this->socket->receive();
|
||||
$response = $this->socket->getResponseBuffer()->read();
|
||||
|
||||
return $this->checkForErrors($response);
|
||||
}
|
||||
|
||||
public function prettyPrint(string $response): void
|
||||
{
|
||||
if ($response) {
|
||||
$separator = "\n-------------\n";
|
||||
echo "{$separator} {$response} {$separator}";
|
||||
}
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->socket->send(self::closeXmlStream());
|
||||
$this->socket->disconnect();
|
||||
}
|
||||
|
||||
protected function openStream(): void
|
||||
{
|
||||
$openStreamXml = self::openXmlStream($this->options->getDomain());
|
||||
$this->socket->send($openStreamXml);
|
||||
}
|
||||
|
||||
protected function sendInitialPresenceStanza(): void
|
||||
{
|
||||
$this->socket->send('<presence/>');
|
||||
}
|
||||
|
||||
protected function initStanzas(Socket $socket): void
|
||||
{
|
||||
$this->auth = new Auth($socket);
|
||||
$this->iq = new Iq($socket);
|
||||
$this->presence = new Presence($socket);
|
||||
$this->message = new Message($socket);
|
||||
}
|
||||
|
||||
protected function initSession(): void
|
||||
{
|
||||
if (!headers_sent($filename, $linenum)) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function initSocket(): Socket
|
||||
{
|
||||
return new Socket($this->options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws StreamError
|
||||
*/
|
||||
protected function checkForErrors(string $response): string
|
||||
{
|
||||
try {
|
||||
self::checkForUnrecoverableErrors($response);
|
||||
} catch (StreamError $e) {
|
||||
$this->options->getLogger()->logResponse(__METHOD__.'::'.__LINE__." {$response}");
|
||||
$this->options->getLogger()->error(__METHOD__.'::'.__LINE__.' '.$e->getMessage());
|
||||
$this->reconnect();
|
||||
$response = '';
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function reconnect(): void
|
||||
{
|
||||
$this->disconnect();
|
||||
$this->initDependencies();
|
||||
$this->connect();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user