From 4cceb387628945b881647b5bcd0a922d5ea43651 Mon Sep 17 00:00:00 2001 From: fbsanchez Date: Tue, 15 Oct 2019 22:16:54 +0200 Subject: [PATCH] 1st RC WebSocketProxy --- .../extensions/quick_shell/WSProxy.php | 121 +++++--- .../extensions/quick_shell/WSProxyUser.php | 40 +++ .../quick_shell/WebSocketServer.php | 261 ++++++++++++------ .../extensions/quick_shell/test_websocket.php | 2 +- 4 files changed, 300 insertions(+), 124 deletions(-) diff --git a/pandora_console/extensions/quick_shell/WSProxy.php b/pandora_console/extensions/quick_shell/WSProxy.php index f7d4d8d127..f1c0014c4d 100644 --- a/pandora_console/extensions/quick_shell/WSProxy.php +++ b/pandora_console/extensions/quick_shell/WSProxy.php @@ -1,5 +1,45 @@ + * 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. require_once __DIR__.'/WebSocketServer.php'; require_once __DIR__.'/WSProxyUser.php'; @@ -48,6 +88,13 @@ class WSProxy extends WebSocketServer */ protected $interative = false; + /** + * Use a timeout of 0.1 second to search for messages.. + * + * @var integer + */ + public $timeout = 0.1; + /** * Builder. @@ -80,9 +127,9 @@ class WSProxy extends WebSocketServer /** - * Read from socket + * Read from user's socket. * - * @param socket $socket Target connection. + * @param object $user Target user connection. * * @return string Buffer. */ @@ -98,7 +145,13 @@ class WSProxy extends WebSocketServer ); 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->socket + ); return false; } @@ -110,20 +163,21 @@ class WSProxy extends WebSocketServer /** * Write to socket. * - * @param socket $socket Target socket. + * @param object $user Target user connection. * @param string $message Target message to be sent. * * @return void */ protected function writeSocket($user, $message) { - if ($user->socket) { + if (is_resource($user->socket)) { if (!socket_write($user->socket, $message)) { $this->disconnect($user->socket); } } else { - // Failed. Disconnect. + // Failed. Disconnect all. $this->disconnect($user->socket); + $this->disconnect($user->redirect->socket); } } @@ -185,19 +239,20 @@ class WSProxy extends WebSocketServer */ protected function connected($user) { - echo '** CONNECTED'."\n"; // Disconnect previous sessions. - $this->cleanupSocketByCookie($headers['cookie']); + $this->cleanupSocketByCookie($user); /* * $user->intSocket is connected to internal. * $user->socket is connected to external. */ - var_dump($user); - // Create a new socket connection (internal). $intUser = $this->connectInt($this->rawHeaders); + if ($intUser === null) { + $this->disconnect($user); + return; + } // Map user. $user->intUser = $intUser; @@ -206,8 +261,12 @@ class WSProxy extends WebSocketServer $user->redirect = $intUser; $intUser->redirect = $user; - $response = $this->readSocket($intUser); + // Keep an eye on changes. + $this->remoteSockets[$intUser->id] = $intUser->socket; + $this->remoteUsers[$intUser->id] = $intUser; + // Ignore. Cleanup socket. + $response = $this->readSocket($user->intUser); } @@ -234,37 +293,25 @@ class WSProxy extends WebSocketServer */ protected function processRaw($user, $buffer) { - unset($user->intUser->lastRawPacket); - unset($user->lastRawPacket); - - echo "\n****************** REDIRECT *********************\n"; - echo date('D M j G:i:s').' - '.$user->id.' >> '.$user->intUser->id."\n"; - echo $this->dump($buffer); - $this->writeSocket( - $user->intUser, - $buffer - ); - - echo date('D M j G:i:s').' - '.$user->intUser->id.'] << '.$user->id."\n"; - $response = $this->readSocket($user->intUser); - $this->writeSocket($user, $user->intUser->lastRawPacket); - echo $this->dump($user->intUser->lastRawPacket); + $this->stdout(date('D M j G:i:s').' - '.$user->id.' >> '.$user->redirect->id); + $this->stdout($this->dump($buffer)); + $this->writeSocket($user->redirect, $buffer); return true; } /** - * From client to socket (internal); + * Process user message. Implement. * - * @param object $user Caller. + * @param object $user User. * @param string $message Message. * * @return void */ protected function process($user, $message) { - echo 'Procesando..'."\n"; + } @@ -277,22 +324,8 @@ class WSProxy extends WebSocketServer */ protected function closed($user) { - $response = 'GET '.$this->intUrl." HTTP/1.1\r\n"; - $response .= 'Host: '.$this->intHost."\r\n"; - $response .= "Connection: Close\r\n\r\n"; - $this->writeSocket($user->intUser, $response); - $response = $this->readSocket($user->intUser); - - $this->disconnect($user->intSocket); - $this->disconnect($user->socket); - - } - - - public function out($ss, $str) - { - echo $ss."\n"; - echo $this->dump($str); + $this->disconnect($user); + $this->disconnect($user->redirect); } diff --git a/pandora_console/extensions/quick_shell/WSProxyUser.php b/pandora_console/extensions/quick_shell/WSProxyUser.php index d9e30bcabf..437fc7cad3 100644 --- a/pandora_console/extensions/quick_shell/WSProxyUser.php +++ b/pandora_console/extensions/quick_shell/WSProxyUser.php @@ -1,5 +1,45 @@ + * 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. require_once __DIR__.'/WebSocketServer.php'; require_once __DIR__.'/WebSocketUser.php'; diff --git a/pandora_console/extensions/quick_shell/WebSocketServer.php b/pandora_console/extensions/quick_shell/WebSocketServer.php index 80375d2855..9ac7ec41cb 100644 --- a/pandora_console/extensions/quick_shell/WebSocketServer.php +++ b/pandora_console/extensions/quick_shell/WebSocketServer.php @@ -1,6 +1,6 @@ stderr( + 'Unusual disconnect on socket '.$socket + ); + // Disconnect before clearing error, in case + // someone with their own implementation wants + // to check for error conditions on the socket. + $this->disconnect($socket, true, $sockErrNo); + break; + + default: + $this->stderr( + 'Socket error: '.socket_strerror($sockErrNo) + ); + break; + } + } + + /** * Main processing loop * @@ -326,9 +416,10 @@ abstract class WebSocketServer $write = null; $this->pTick(); $this->tick(); - socket_select($read, $write, $except, 1); + socket_select($read, $write, $except, $this->timeout); foreach ($read as $socket) { if ($socket == $this->master) { + // External to master connection. New client. $client = socket_accept($socket); if ($client < 0) { $this->stderr('Failed: socket_accept()'); @@ -338,6 +429,7 @@ abstract class WebSocketServer $this->stdout('Client connected. '.$client); } } else { + // Updates on 'read' socket. $numBytes = socket_recv( $socket, $buffer, @@ -345,63 +437,7 @@ abstract class WebSocketServer 0 ); if ($numBytes === false) { - $sockErrNo = socket_last_error($socket); - switch ($sockErrNo) { - case 102: - // ENETRESET - // Network dropped connection because of reset. - case 103: - // ECONNABORTED - // Software caused connection abort. - case 104: - // ECONNRESET - // Connection reset by peer. - case 108: - // ESHUTDOWN - // Cannot send after transport endpoint shutdown - // Probably more of an error on our side, - // if we're trying to write after the socket is - // closed. Probably not a critical error, - // though. - case 110: - // ETIMEDOUT - // Connection timed out. - case 111: - // ECONNREFUSED - // Connection refused - // We shouldn't see this one, since we're - // listening... Still not a critical error. - case 112: - // EHOSTDOWN - // Host is down. - // Again, we shouldn't see this, and again, - // not critical because it's just one connection - // and we still want to listen to/for others. - case 113: - // EHOSTUNREACH - // No route to host. - case 121: - // EREMOTEIO - // Rempte I/O error - // Their hard drive just blew up. - case 125: - // ECANCELED - // Operation canceled. - $this->stderr( - 'Unusual disconnect on socket '.$socket - ); - // Disconnect before clearing error, in case - // someone with their own implementation wants - // to check for error conditions on the socket. - $this->disconnect($socket, true, $sockErrNo); - break; - - default: - $this->stderr( - 'Socket error: '.socket_strerror($sockErrNo) - ); - break; - } + $this->handleSocketError($socket); } else if ($numBytes == 0) { $this->disconnect($socket); $this->stderr( @@ -422,12 +458,60 @@ abstract class WebSocketServer } else { if (!$this->processRaw($user, $buffer)) { // Split packet into frame and send it to deframe. - $this->splitPacket($numBytes, $buffer, $user); + $this->splitPacket( + $numBytes, + $buffer, + $user + ); } } } } } + + // Remote updates. + $remotes = $this->remoteSockets; + if (count($remotes) > 0) { + socket_select($remotes, $write, $except, $this->timeout); + foreach ($remotes as $socket) { + // Remote updates - internal. We're client of this sockets. + if (!$socket) { + continue; + } + + $numBytes = socket_recv( + $socket, + $buffer, + $this->maxBufferSize, + 0 + ); + if ($numBytes === false) { + $this->handleSocketError($socket); + } else if ($numBytes == 0) { + $this->disconnect($socket); + $this->stderr( + 'Client disconnected. TCP connection lost: '.$socket + ); + } else { + $user = $this->getUserBySocket($socket); + echo '>>>>> EEH, recibo:['.$user->id."]\n"; + echo $this->dump($buffer); + if (!$user) { + $this->disconnect($socket); + $this->stderr( + 'User was not connected: '.$socket + ); + } else if (!$this->processRaw($user, $buffer)) { + // Split packet into frame and send it to deframe. + $this->splitPacket( + $numBytes, + $buffer, + $user + ); + } + } + } + } } } @@ -462,13 +546,22 @@ abstract class WebSocketServer bool $triggerClosed=true, $sockErrNo=null ) { - $disconnectedUser = $this->getUserBySocket($socket); + $user = $this->getUserBySocket($socket); + if ($user !== null) { + if (array_key_exists($user->id, $this->users)) { + unset($this->users[$user->id]); + } - if ($disconnectedUser !== null) { - unset($this->users[$disconnectedUser->id]); + if (array_key_exists($user->id, $this->remoteUsers)) { + unset($this->remoteUsers[$user->id]); + } - if (array_key_exists($disconnectedUser->id, $this->sockets)) { - unset($this->sockets[$disconnectedUser->id]); + if (array_key_exists($user->id, $this->sockets)) { + unset($this->sockets[$user->id]); + } + + if (array_key_exists($user->id, $this->remoteSockets)) { + unset($this->remoteSockets[$user->id]); } if ($sockErrNo !== null) { @@ -476,15 +569,15 @@ abstract class WebSocketServer } if ($triggerClosed) { - $this->closed($disconnectedUser); + $this->closed($user); $this->stdout( - 'Client disconnected. '.$disconnectedUser->socket + 'Client disconnected. '.$user->socket ); - socket_close($disconnectedUser->socket); + socket_close($user->socket); } else { - $message = $this->frame('', $disconnectedUser, 'close'); + $message = $this->frame('', $user, 'close'); socket_write( - $disconnectedUser->socket, + $user->socket, $message, strlen($message) ); @@ -741,22 +834,31 @@ abstract class WebSocketServer } } + foreach ($this->remoteUsers as $user) { + if ($user->socket == $socket) { + return $user; + } + } + return null; } /** - * Disconnects all users matching target cookie but latest one. + * Disconnects all users matching target cookie but given one. * - * @param string $cookie Cookie identifier. + * @param object $user Latest user. * * @return void */ - protected function cleanupSocketByCookie($cookie) + protected function cleanupSocketByCookie($user) { - foreach ($this->users as $user) { - if ($user->headers['cookie'] == $cookie) { - $this->disconnectUser($user->socket); + $cookie = $user->headers['cookie']; + foreach ($this->users as $u) { + if ($u->id != $user->id + && $u->headers['cookie'] == $cookie + ) { + $this->disconnect($u->socket); } } @@ -1379,7 +1481,8 @@ abstract class WebSocketServer // Join the hexi / ascii output. $dump .= sprintf('%04x %-49s %s', $offset, $hexi, $ascii); // Reset vars. - $hexi = $ascii = ''; + $hexi = ''; + $ascii = ''; $offset += 16; $j = 0; // Add newline. diff --git a/pandora_console/extensions/quick_shell/test_websocket.php b/pandora_console/extensions/quick_shell/test_websocket.php index 006f4e3048..c7a788065f 100644 --- a/pandora_console/extensions/quick_shell/test_websocket.php +++ b/pandora_console/extensions/quick_shell/test_websocket.php @@ -6,7 +6,7 @@ error_reporting(E_ALL); require_once __DIR__.'/WSProxy.php'; -$wsproxy = new WSProxy('0.0.0.0', '8081', '127.0.0.1', '8080'); +$wsproxy = new WSProxy('0.0.0.0', '8081', '127.0.0.1', '8080', '/ws', 1048576, true); try { echo "Server running \n";