442 lines
12 KiB
PHP
442 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* PHP WebSocketServer Manager.
|
|
*
|
|
* Adapted to PandoraFMS by Fco de Borja Sanchez <fborja.sanchez@artica.es>
|
|
* Compatible with PHP >= 7.0
|
|
*
|
|
* @category External library
|
|
* @package Pandora FMS
|
|
* @subpackage WebSocketServer
|
|
* @version 1.0.0
|
|
* @license See below
|
|
* @filesource https://github.com/ghedipunk/PHP-Websockets
|
|
*
|
|
* 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 PHP WebSockets nor the names of its 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 HOLDER 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, STRICT 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.
|
|
*/
|
|
|
|
// Begin.
|
|
namespace PandoraFMS\Websockets;
|
|
|
|
use \PandoraFMS\Websockets\WebSocketServer;
|
|
use \PandoraFMS\User;
|
|
|
|
require_once __DIR__.'/../../functions.php';
|
|
|
|
/**
|
|
* Redirects ws communication between two endpoints.
|
|
*/
|
|
class WSManager extends WebSocketServer
|
|
{
|
|
|
|
/**
|
|
* 1MB... overkill for an echo server, but potentially plausible for other
|
|
* applications.
|
|
*
|
|
* @var integer
|
|
*/
|
|
public $maxBufferSize = 1048576;
|
|
|
|
/**
|
|
* Interactive mode.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $interative = true;
|
|
|
|
/**
|
|
* Use a timeout of 100 milliseconds to search for messages..
|
|
*
|
|
* @var integer
|
|
*/
|
|
public $timeout = 250;
|
|
|
|
/**
|
|
* Handlers for connected step:
|
|
* 'protocol' => 'function';
|
|
*
|
|
* @var array
|
|
*/
|
|
public $handlerConnected = [];
|
|
|
|
/**
|
|
* Handlers for process step:
|
|
* 'protocol' => 'function';
|
|
*
|
|
* @var array
|
|
*/
|
|
public $handlerProcess = [];
|
|
|
|
/**
|
|
* Handlers for processRaw step:
|
|
* 'protocol' => 'function';
|
|
*
|
|
* @var array
|
|
*/
|
|
public $handlerProcessRaw = [];
|
|
|
|
/**
|
|
* Handlers for tick step:
|
|
* 'protocol' => 'function';
|
|
*
|
|
* @var array
|
|
*/
|
|
public $handlerTick = [];
|
|
|
|
/**
|
|
* Allow only one connection per user session.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
public $socketPerSession = false;
|
|
|
|
|
|
/**
|
|
* Builder.
|
|
*
|
|
* @param string $listen_addr Target address (external).
|
|
* @param integer $listen_port Target port (external).
|
|
* @param array $connected Handlers for <connected> step.
|
|
* @param array $process Handlers for <process> step.
|
|
* @param array $processRaw Handlers for <processRaw> step.
|
|
* @param array $tick Handlers for <tick> step.
|
|
* @param integer $bufferLength Max buffer size.
|
|
* @param boolean $debug Enable traces.
|
|
*/
|
|
public function __construct(
|
|
$listen_addr,
|
|
int $listen_port,
|
|
$connected=[],
|
|
$process=[],
|
|
$processRaw=[],
|
|
$tick=[],
|
|
$bufferLength=1048576,
|
|
$debug=false
|
|
) {
|
|
$this->maxBufferSize = $bufferLength;
|
|
$this->debug = $debug;
|
|
|
|
// Configure handlers.
|
|
$this->handlerConnected = $connected;
|
|
$this->handlerProcess = $process;
|
|
$this->handlerProcessRaw = $processRaw;
|
|
$this->handlerTick = $tick;
|
|
|
|
$this->userClass = '\\PandoraFMS\\Websockets\\WebSocketUser';
|
|
parent::__construct($listen_addr, $listen_port, $bufferLength);
|
|
}
|
|
|
|
|
|
/**
|
|
* Call a target handler function.
|
|
*
|
|
* @param User $user User.
|
|
* @param array $handler Internal handler.
|
|
* @param array $arguments Arguments for handler function.
|
|
*
|
|
* @return mixed handler return or null.
|
|
*/
|
|
public function callHandler($user, $handler, $arguments)
|
|
{
|
|
if (isset($user->headers['sec-websocket-protocol'])) {
|
|
$proto = $user->headers['sec-websocket-protocol'];
|
|
if (isset($handler[$proto])
|
|
&& function_exists($handler[$proto])
|
|
) {
|
|
// Launch configured handler.
|
|
$this->stderr('Calling '.$handler[$proto]);
|
|
return call_user_func_array(
|
|
$handler[$proto],
|
|
array_values(($arguments ?? []))
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Read from user's socket.
|
|
*
|
|
* @param object $user Target user connection.
|
|
* @param integer $flags Socket receive flags:
|
|
* Flag Description
|
|
* MSG_OOB Process out-of-band data.
|
|
* MSG_PEEK Receive data from the beginning of the receive
|
|
* queue without removing it from the queue.
|
|
* MSG_WAITALL Block until at least len are received. However,
|
|
* if a signal is caught or the remote host
|
|
* disconnects, the function may return less data.
|
|
* MSG_DONTWAIT With this flag set, the function returns even
|
|
* if it would normally have blocked.
|
|
*
|
|
* @return string Buffer.
|
|
*/
|
|
public function readSocket($user, $flags=0)
|
|
{
|
|
$buffer = '';
|
|
|
|
$numBytes = socket_recv(
|
|
$user->socket,
|
|
$buffer,
|
|
$this->maxBufferSize,
|
|
$flags
|
|
);
|
|
if ($numBytes === false) {
|
|
// Failed. Disconnect.
|
|
$this->handleSocketError($user->socket);
|
|
return false;
|
|
} else if ($numBytes == 0) {
|
|
$this->disconnect($user->socket);
|
|
$this->stderr(
|
|
'Client disconnected. TCP connection lost: '.$user->id
|
|
);
|
|
return false;
|
|
}
|
|
|
|
$user->lastRawPacket = $buffer;
|
|
return $buffer;
|
|
}
|
|
|
|
|
|
/**
|
|
* Write to socket.
|
|
*
|
|
* @param object $user Target user connection.
|
|
* @param string $message Target message to be sent.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function writeSocket($user, $message)
|
|
{
|
|
if (is_resource($user->socket) === true
|
|
|| ($user->socket instanceof \Socket) === true
|
|
) {
|
|
if (socket_write($user->socket, $message) === false) {
|
|
$this->disconnect($user->socket);
|
|
}
|
|
} else {
|
|
// Failed. Disconnect all.
|
|
if (isset($user) === true) {
|
|
$this->disconnect($user->socket);
|
|
}
|
|
|
|
if (isset($user->redirect) === true) {
|
|
$this->disconnect($user->redirect->socket);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* User already connected.
|
|
*
|
|
* @param object $user User.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function connected($user)
|
|
{
|
|
global $config;
|
|
|
|
$match = [];
|
|
$php_session_id = '';
|
|
\preg_match(
|
|
'/PHPSESSID=(.*)/',
|
|
$user->headers['cookie'],
|
|
$match
|
|
);
|
|
|
|
if (is_array($match) === true) {
|
|
$php_session_id = $match[1];
|
|
}
|
|
|
|
$php_session_id = \preg_replace('/;.*$/', '', $php_session_id);
|
|
|
|
// If being redirected from proxy.
|
|
if (isset($user->headers['x-forwarded-for']) === true) {
|
|
$user->address = $user->headers['x-forwarded-for'];
|
|
}
|
|
|
|
$user->account = User::auth(['phpsessionid' => $php_session_id]);
|
|
$_SERVER['REMOTE_ADDR'] = $user->address;
|
|
|
|
// Ensure user is allowed to connect.
|
|
if (\check_login(false) === false) {
|
|
$this->disconnect($user->socket);
|
|
\db_pandora_audit(
|
|
AUDIT_LOG_WEB_SOCKETS,
|
|
'Trying to access websockets engine without a valid session',
|
|
'N/A'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// User exists, and session is valid.
|
|
\db_pandora_audit(
|
|
AUDIT_LOG_WEB_SOCKETS,
|
|
'WebSocket connection started',
|
|
'N/A'
|
|
);
|
|
$this->stderr('ONLINE '.$user->address.'('.$user->account->idUser.')');
|
|
|
|
if ($this->socketPerSession === true) {
|
|
// Disconnect previous sessions.
|
|
$this->cleanupSocketByCookie($user);
|
|
}
|
|
|
|
// Launch registered handler.
|
|
$this->callHandler(
|
|
$user,
|
|
$this->handlerConnected,
|
|
[
|
|
$this,
|
|
$user,
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Protocol.
|
|
*
|
|
* @param string $protocol Protocol.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function processProtocol($protocol): string
|
|
{
|
|
return 'Sec-Websocket-Protocol: '.$protocol."\r\n";
|
|
}
|
|
|
|
|
|
/**
|
|
* Process programattic function
|
|
*
|
|
* @return void
|
|
*/
|
|
public function tick()
|
|
{
|
|
foreach ($this->users as $user) {
|
|
// Launch registered handler.
|
|
$this->callHandler(
|
|
$user,
|
|
$this->handlerTick,
|
|
[
|
|
$this,
|
|
$user,
|
|
]
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Process undecoded user message.
|
|
*
|
|
* @param object $user User.
|
|
* @param string $buffer Message.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function processRaw($user, $buffer)
|
|
{
|
|
// Launch registered handler.
|
|
return $this->callHandler(
|
|
$user,
|
|
$this->handlerProcessRaw,
|
|
[
|
|
$this,
|
|
$user,
|
|
$buffer,
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Process user message. Implement.
|
|
*
|
|
* @param object $user User.
|
|
* @param string $message Message.
|
|
* @param boolean $str_message String message or not.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function process($user, $message, $str_message)
|
|
{
|
|
if ($str_message === true) {
|
|
$remmitent = $user->address.'('.$user->account->idUser.')';
|
|
$this->stderr($remmitent.': '.$message);
|
|
}
|
|
|
|
// Launch registered handler.
|
|
$this->callHandler(
|
|
$user,
|
|
$this->handlerProcess,
|
|
[
|
|
$this,
|
|
$user,
|
|
$message,
|
|
$str_message,
|
|
]
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Also close internal socket.
|
|
*
|
|
* @param object $user User.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function closed($user)
|
|
{
|
|
if ($user->account) {
|
|
$_SERVER['REMOTE_ADDR'] = $user->address;
|
|
\db_pandora_audit(
|
|
AUDIT_LOG_WEB_SOCKETS,
|
|
'WebSocket connection finished',
|
|
'N/A'
|
|
);
|
|
|
|
$this->stderr('OFFLINE '.$user->address.'('.$user->account->idUser.')');
|
|
}
|
|
|
|
// Ensure both sockets are disconnected.
|
|
$this->disconnect($user->socket);
|
|
if ($user->redirect) {
|
|
$this->disconnect($user->redirect->socket);
|
|
}
|
|
}
|
|
|
|
|
|
}
|