Supporting websockets

This commit is contained in:
Dennis Eichhorn 2016-02-07 14:37:26 +01:00
parent e02de0b5a4
commit 3d7ee2e690
8 changed files with 286 additions and 70 deletions

View File

@ -214,7 +214,7 @@ class FileLogger implements LoggerInterface
* Interpolate context
*
* @param string $message
* @param array $context
* @param array $context
* @param string $level
*
* @return string
@ -292,7 +292,7 @@ class FileLogger implements LoggerInterface
* System is unusable.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return null
*
@ -312,7 +312,7 @@ class FileLogger implements LoggerInterface
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -331,7 +331,7 @@ class FileLogger implements LoggerInterface
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -349,7 +349,7 @@ class FileLogger implements LoggerInterface
* be logged and monitored.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -369,7 +369,7 @@ class FileLogger implements LoggerInterface
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -386,7 +386,7 @@ class FileLogger implements LoggerInterface
* Normal but significant events.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -405,7 +405,7 @@ class FileLogger implements LoggerInterface
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -422,7 +422,7 @@ class FileLogger implements LoggerInterface
* Detailed debug information.
*
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -440,7 +440,7 @@ class FileLogger implements LoggerInterface
*
* @param string $level
* @param string $message
* @param array $context
* @param array $context
*
* @return void
*
@ -610,4 +610,13 @@ class FileLogger implements LoggerInterface
return $log;
}
public function console(string $text, bool $verbose)
{
$text = date('[Y-m-d H:i:s] ') . $text . "\r\n";
if ($verbose) {
echo $text;
}
}
}

View File

@ -17,6 +17,7 @@ namespace phpOMS\Module;
use phpOMS\ApplicationAbstract;
use phpOMS\DataStorage\Database\DatabaseType;
use phpOMS\Log\FileLogger;
use phpOMS\Message\Http\Request;
use phpOMS\System\FilePathException;
use phpOMS\Utils\IO\Json\InvalidJsonException;
@ -429,7 +430,15 @@ class ModuleManager
{
if (is_array($module)) {
foreach ($module as $m) {
$this->initModule($m);
try {
$this->initModule($m);
} catch (\InvalidArgumentException $e) {
$this->app->logger->warning(FileLogger::MSG_FULL, [
'message' => 'Trying to initialize ' . $m . ' without controller.',
'line' => $e->getLine(),
'file' => $e->getFile(),
]);
}
}
} elseif (is_string($module) && realpath(self::MODULE_PATH . '/' . $module . '/Controller.php') !== false) {
$this->running[$module] = ModuleFactory::getInstance($module, $this->app);

View File

@ -0,0 +1,82 @@
<?php
/**
* Orange Management
*
* PHP Version 7.0
*
* @category TBD
* @package TBD
* @author OMS Development Team <dev@oms.com>
* @author Dennis Eichhorn <d.eichhorn@oms.com>
* @copyright 2013 Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link http://orange-management.com
*/
namespace phpOMS\Socket\Client;
use phpOMS\Socket\CommandManager;
use phpOMS\Socket\SocketAbstract;
/**
* Client socket class.
*
* @category Framework
* @package phpOMS\Socket\Client
* @author OMS Development Team <dev@oms.com>
* @author Dennis Eichhorn <d.eichhorn@oms.com>
* @license OMS License 1.0
* @link http://orange-management.com
* @since 1.0.0
*/
class ClientConnection
{
private $id = 0;
private $socket = null;
private $handshake = false;
private $pid = null;
private $connected = true;
public function __construct($id, $socket)
{
$this->id = $id;
$this->socket = $socket;
}
public function getId()
{
return $this->id;
}
public function getSocket() {
return $this->socket;
}
public function getHandshake() {
return $this->handshake;
}
public function getPid() {
return $this->pid;
}
public function isConnected() {
return $this->connected;
}
public function setSocket($socket) {
$this->socket = $socket;
}
public function setHandshake($handshake) {
$this->handshake = $handshake;
}
public function setPid($pid) {
$this->pid = $pid;
}
public function setConnected(bool $connected) {
$this->connected = $connected;
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Orange Management
*
* PHP Version 7.0
*
* @category TBD
* @package TBD
* @author OMS Development Team <dev@oms.com>
* @author Dennis Eichhorn <d.eichhorn@oms.com>
* @copyright 2013 Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link http://orange-management.com
*/
namespace phpOMS\Socket\Client;
use phpOMS\Socket\CommandManager;
use phpOMS\Socket\SocketAbstract;
/**
* Client socket class.
*
* @category Framework
* @package phpOMS\Socket\Client
* @author OMS Development Team <dev@oms.com>
* @author Dennis Eichhorn <d.eichhorn@oms.com>
* @license OMS License 1.0
* @link http://orange-management.com
* @since 1.0.0
*/
class NullClientConnection extends ClientConnection
{
}

View File

@ -69,21 +69,14 @@ class PacketManager
* Handle package.
*
* @param string $data Package data
* @param mixed $key Client Id
*
* @return void
*
* @since 1.0.0
* @author Dennis Eichhorn <d.eichhorn@oms.com>
*/
public function handle(string $data, $key)
public function handle(string $data, $client)
{
/*
if (!empty($data)) {
$data = explode(' ', $data);
$this->commandManager->trigger($data[0], $key, $data);
} else {
$this->commandManager->trigger('empty', $key, $data);
}*/
echo $data;
}
}

View File

@ -16,6 +16,40 @@
namespace phpOMS\Socket\Server;
use phpOMS\Socket\Client\ClientConnection;
use phpOMS\Socket\Client\NullClientConnection;
class ClientManager
{
private $clients = [];
public function add(ClientConnection $client)
{
$this->clients[$client->getId()] = $client;
}
public function get($id)
{
return $this->clients[$id] ?? new NullClientConnection(uniqid(), null);
}
public function getBySocket($socket) {
foreach($this->clients as $client) {
if($client->getSocket() === $socket) {
return $client;
}
}
return new NullClientConnection(uniqid(), null);
}
public function remove($id) {
if(isset($this->clients[$id])) {
unset($this->clients[$id]);
return true;
}
return false;
}
}

View File

@ -15,6 +15,7 @@
*/
namespace phpOMS\Socket\Server;
use phpOMS\Socket\Client\ClientConnection;
use phpOMS\Socket\CommandManager;
use phpOMS\Socket\Packets\PacketManager;
@ -58,6 +59,10 @@ class Server extends SocketAbstract
*/
private $packetManager = null;
private $clientManager = null;
private $verbose = true;
/**
* Socket application.
*
@ -72,6 +77,7 @@ class Server extends SocketAbstract
if ($connected) {
fclose($connected);
return true;
} else {
return false;
@ -89,6 +95,7 @@ class Server extends SocketAbstract
public function __construct($app)
{
$this->app = $app;
$this->clientManager = new ClientManager();
$this->packetManager = new PacketManager(new CommandManager(), new ClientManager());
}
@ -97,7 +104,9 @@ class Server extends SocketAbstract
*/
public function create(string $ip, int $port)
{
$this->app->logger->console('Creating socket...', $this->verbose);
parent::create($ip, $port);
$this->app->logger->console('Binding socket...', $this->verbose);
socket_bind($this->sock, $this->ip, $this->port);
}
@ -116,15 +125,57 @@ class Server extends SocketAbstract
$this->limit = $limit;
}
public function handshake($client, $headers)
{
if (preg_match("/Sec-WebSocket-Version: (.*)\r\n/", $headers, $match)) {
$version = $match[1];
} else {
return false;
}
if ($version == 13) {
if (preg_match("/GET (.*) HTTP/", $headers, $match)) {
$root = $match[1];
}
if (preg_match("/Host: (.*)\r\n/", $headers, $match)) {
$host = $match[1];
}
if (preg_match("/Origin: (.*)\r\n/", $headers, $match)) {
$origin = $match[1];
}
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $headers, $match)) {
$key = $match[1];
}
$acceptKey = $key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$acceptKey = base64_encode(sha1($acceptKey, true));
$upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: $acceptKey" .
"\r\n\r\n";
socket_write($client->getSocket(), $upgrade);
$client->setHandshake(true);
return true;
} else {
return false;
}
}
/**
* {@inheritdoc}
*/
public function run()
{
$this->app->logger->console('Start listening...', $this->verbose);
socket_listen($this->sock);
socket_set_nonblock($this->sock);
$this->conn[] = $this->sock;
$this->app->logger->console('Start running...', $this->verbose);
while ($this->run) {
$read = $this->conn;
@ -138,55 +189,54 @@ class Server extends SocketAbstract
// socket_clear_error();
}
if (in_array($this->sock, $read)) {
$this->conn[] = $newc = socket_accept($this->sock);
foreach ($read as $key => $socket) {
if ($this->sock === $socket) {
$newc = socket_accept($this->sock);
$this->connectClient($newc);
} else {
$client = $this->clientManager->getBySocket($socket);
$data = socket_read($socket, 1024);
// TODO: init account
// This is only the initial connection no auth happens here
// Here the server should request an authentication
$welcome = "Welcome to the server.\n";
socket_write($newc, $welcome, strlen($welcome));
socket_getpeername($newc, $ip);
unset($read[0]);
}
foreach ($read as $key => $client) {
$data = socket_read($client, 1024);
if ($this->clientReadError($key)) {
continue;
if (!$client->getHandshake()) {
$this->app->logger->console('Doing handshake...', $this->verbose);
if ($this->handshake($client, $data)) {
$this->app->logger->console('Handshake succeeded.', $this->verbose);
} else {
$this->app->logger->console('Handshake failed.', $this->verbose);
$this->disconnectClient($client);
}
} else {
$this->packetManager->handle($this->unmask($data), $client);
}
}
$this->packetManager->handle(trim($data), $key);
}
}
$this->close();
}
/**
* Handle client read error.
*
* @param mixed $key Client key
*
* @return bool
*
* @since 1.0.0
* @author Dennis Eichhorn <d.eichhorn@oms.com>
*/
private function clientReadError($key) : bool
public function connectClient($socket)
{
if (socket_last_error() === 10054) {
socket_clear_error();
unset($this->conn[$key]);
$this->conn = array_values($this->conn);
$this->app->logger->console('Connecting client...', $this->verbose);
$this->clientManager->add($client = new ClientConnection(uniqid(), $socket));
$this->conn[$client->getId()] = $socket;
$this->app->logger->console('Connected client.', $this->verbose);
}
return true;
public function disconnectClient($client)
{
$this->app->logger->console('Disconnecting client...', $this->verbose);
$client->setConnected(false);
$client->setHandshake(false);
socket_shutdown($client->getSocket(), 2);
socket_close($client->getSocket());
if (isset($this->conn[$client->getId()])) {
unset($this->conn[$client->getId()]);
}
return false;
$this->clientManager->remove($client->getId());
$this->app->logger->console('Disconnected client.', $this->verbose);
}
/**
@ -205,20 +255,24 @@ class Server extends SocketAbstract
parent::__destruct();
}
/**
* Disconnect client.
*
* @param mixed $key Client key
*
* @return void
*
* @since 1.0.0
* @author Dennis Eichhorn <d.eichhorn@oms.com>
*/
private function disconnect($key)
private function unmask($payload)
{
if (isset($this->conn[$key])) {
unset($this->conn[$key]);
$length = ord($payload[1]) & 127;
if ($length == 126) {
$masks = substr($payload, 4, 4);
$data = substr($payload, 8);
} elseif ($length == 127) {
$masks = substr($payload, 10, 4);
$data = substr($payload, 14);
} else {
$masks = substr($payload, 2, 4);
$data = substr($payload, 6);
}
$text = '';
for ($i = 0; $i < strlen($data); ++$i) {
$text .= $data[$i] ^ $masks[$i % 4];
}
return $text;
}
}

View File

@ -89,6 +89,7 @@ abstract class SocketAbstract implements SocketInterface
public function close()
{
if ($this->sock !== null) {
socket_shutdown($this->sock, 2);
socket_close($this->sock);
$this->sock = null;
}