diff --git a/pandora_console/extensions/quick_shell.php b/pandora_console/extensions/quick_shell.php new file mode 100644 index 0000000000..a89abe5dea --- /dev/null +++ b/pandora_console/extensions/quick_shell.php @@ -0,0 +1,76 @@ + false, + 'text' => ''.html_print_image('images/setup.png', true, ['title' => __('Wetty settings')]).'', + ]; + + ui_print_page_header(__('Wetty'), 'images/extensions.png', false, '', true, $buttons); + + $table->width = '100%'; + $table->class = 'databox data'; + $table->data = []; + $table->head = []; + $table->align = []; + // $table->align[3] = 'left'; + $table->style = []; + $table->size = []; + // $table->size[3] = '10%'; + $table->style[0] = 'font-weight: bold'; + + $table->head[0] = __('Wetty'); + + // $data[0] = ''; + $data[0] = ''; + + // $data[0] .= '
'; + array_push($table->data, $data); + + html_print_table($table); + +} + + +extensions_add_godmode_menu_option(__('Wetty'), 'AW', 'gextensions', null, 'v1'); +extensions_add_godmode_function('mainWetty'); diff --git a/pandora_console/extensions/quick_shell/Gotty.class.php b/pandora_console/extensions/quick_shell/Gotty.class.php new file mode 100644 index 0000000000..929a47e09c --- /dev/null +++ b/pandora_console/extensions/quick_shell/Gotty.class.php @@ -0,0 +1,74 @@ + + * 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\WebSocketServer; + + +/** + * Abstract class to be implemented. + */ +abstract class WebSocketServer +{ + + /** + * Bae class to be created. + * + * @var string + */ + protected $userClass = 'WebSocketUser'; + + /** + * Redefine this if you want a custom user class. The custom user class + * should inherit from WebSocketUser. + * + * @var integer + */ + protected $maxBufferSize; + + /** + * Max. concurrent connections. + * + * @var integer + */ + protected $maxConnections = 20; + + /** + * Undocumented variable + * + * @var [type] + */ + protected $master; + + /** + * Undocumented variable + * + * @var array + */ + protected $sockets = []; + + /** + * Undocumented variable + * + * @var array + */ + protected $users = []; + + /** + * Undocumented variable + * + * @var array + */ + protected $heldMessages = []; + + /** + * Undocumented variable + * + * @var array + */ + protected $interactive = true; + + /** + * Undocumented variable + * + * @var array + */ + protected $headerOriginRequired = false; + + /** + * Undocumented variable + * + * @var array + */ + protected $headerSecWebSocketProtocolRequired = false; + + /** + * Undocumented variable + * + * @var array + */ + protected $headerSecWebSocketExtensionsRequired = false; + + + /** + * Builder. + * + * @param string $addr Address where websocketserver will listen. + * @param integer $port Port where listen. + * @param integer $bufferLength Max buffer length. + * @param integer $maxConnections Max concurrent connections. + */ + public function __construct( + string $addr, + int $port, + int $bufferLength=2048, + int $maxConnections=20 + ) { + $this->maxBufferSize = $bufferLength; + + if (is_numeric($maxConnections) && $maxConnections > 0) { + $this->maxConnections = $maxConnections; + } + + $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->master || die('Failed: socket_create()'); + + $__tmp = socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1); + $__tmp || die('Failed: socket_option()'); + + $__tmp = socket_bind($this->master, $addr, $port); + $__tmp || die('Failed: socket_bind()'); + + $__tmp = socket_listen($this->master, $this->maxConnections); + $__tmp || die('Failed: socket_listen()'); + + $this->sockets['m'] = $this->master; + $this->stdout("Server started\nListening on: ".$addr.':'.$port."\n"); + $this->stdout('Master socket: '.$this->master."\n"); + + } + + + /** + * Process user message. Implement. + * + * @param object $user User. + * @param string $message Message. + * + * @return void + */ + abstract protected function process($user, string $message); + + + /** + * Called immediately when the data is recieved. + * + * @param object $user User. + * + * @return void + */ + abstract protected function connected($user); + + + /** + * Called after the handshake response is sent to the client. + * + * @param object $user User. + * + * @return void + */ + abstract protected function closed($user); + + + /** + * Called after the connection is closed. + * Override to handle a connecting user, after the instance of the User + * is created, but before the handshake has completed. + * + * @param object $user User. + * + * @return void + */ + protected function connecting($user) + { + // Optional implementation. + } + + + /** + * Send a message to target user. + * + * @param object $user User. + * @param string $message Message. + * + * @return void + */ + protected function send($user, string $message) + { + if ($user->handshake) { + $message = $this->frame($message, $user); + $result = socket_write($user->socket, $message, strlen($message)); + } else { + // User has not yet performed their handshake. + // Store for sending later. + $holdingMessage = [ + 'user' => $user, + 'message' => $message, + ]; + $this->heldMessages[] = $holdingMessage; + } + } + + + /** + * Override this for any process that should happen periodically. + * Will happen at least once per second, but possibly more often. + * + * @return void + */ + protected function tick() + { + // Optional implementation. + } + + + /** + * Internal backend for tick. + * + * @return void + */ + protected function pTick() + { + // Core maintenance processes, such as retrying failed messages. + foreach ($this->heldMessages as $key => $hm) { + $found = false; + foreach ($this->users as $currentUser) { + if ($hm['user']->socket == $currentUser->socket) { + $found = true; + if ($currentUser->handshake) { + unset($this->heldMessages[$key]); + $this->send($currentUser, $hm['message']); + } + } + } + + if (!$found) { + // If they're no longer in the list of connected users, + // drop the message. + unset($this->heldMessages[$key]); + } + } + } + + + /** + * Main processing loop + * + * @return void + */ + public function run() + { + while (true) { + if (empty($this->sockets) === true) { + $this->sockets['m'] = $this->master; + } + + $read = $this->sockets; + $except = null; + $write = null; + $this->pTick(); + $this->tick(); + socket_select($read, $write, $except, 1); + foreach ($read as $socket) { + if ($socket == $this->master) { + $client = socket_accept($socket); + if ($client < 0) { + $this->stderr('Failed: socket_accept()'); + continue; + } else { + $this->connect($client); + $this->stdout('Client connected. '.$client); + } + } else { + $numBytes = socket_recv( + $socket, + $buffer, + $this->maxBufferSize, + 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; + } + } else if ($numBytes == 0) { + $this->disconnect($socket); + $this->stderr( + 'Client disconnected. TCP connection lost: '.$socket + ); + } else { + $user = $this->getUserBySocket($socket); + if (!$user->handshake) { + $tmp = str_replace("\r", '', $buffer); + if (strpos($tmp, "\n\n") === false) { + continue; + // If the client has not finished sending the + // header, then wait before sending our upgrade + // response. + } + + $this->doHandshake($user, $buffer); + } else { + // Split packet into frame and send it to deframe. + $this->splitPacket($numBytes, $buffer, $user); + } + } + } + } + } + } + + + /** + * Register user (and its socket) into master. + * + * @param Socket $socket Socket. + * + * @return void + */ + protected function connect($socket) + { + $user = new $this->userClass(uniqid('u'), $socket); + $this->users[$user->id] = $user; + $this->sockets[$user->id] = $socket; + $this->connecting($user); + } + + + /** + * Disconnect socket from master. + * + * @param Socket $socket Socket. + * @param boolean $triggerClosed Also close. + * @param integer $sockErrNo Clear error. + * + * @return void + */ + protected function disconnect( + $socket, + bool $triggerClosed=true, + $sockErrNo=null + ) { + $disconnectedUser = $this->getUserBySocket($socket); + + if ($disconnectedUser !== null) { + unset($this->users[$disconnectedUser->id]); + + if (array_key_exists($disconnectedUser->id, $this->sockets)) { + unset($this->sockets[$disconnectedUser->id]); + } + + if ($sockErrNo !== null) { + socket_clear_error($socket); + } + + if ($triggerClosed) { + $this->closed($disconnectedUser); + $this->stdout( + 'Client disconnected. '.$disconnectedUser->socket + ); + socket_close($disconnectedUser->socket); + } else { + $message = $this->frame('', $disconnectedUser, 'close'); + socket_write( + $disconnectedUser->socket, + $message, + strlen($message) + ); + } + } + } + + + /** + * Perform a handshake. + * + * @param object $user User. + * @param string $buffer Buffer. + * + * @return void + */ + protected function doHandshake($user, string $buffer) + { + $magicGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; + $headers = []; + $lines = explode("\n", $buffer); + foreach ($lines as $line) { + if (strpos($line, ':') !== false) { + $header = explode(':', $line, 2); + $headers[strtolower(trim($header[0]))] = trim($header[1]); + } else if (stripos($line, 'get ') !== false) { + preg_match('/GET (.*) HTTP/i', $buffer, $reqResource); + $headers['get'] = trim($reqResource[1]); + } + } + + if (isset($headers['get'])) { + $user->requestedResource = $headers['get']; + } else { + // TODO: fail the connection. + $handshakeResponse = "HTTP/1.1 405 Method Not Allowed\r\n\r\n"; + } + + if (!isset($headers['host']) + || !$this->checkHost($headers['host']) + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['upgrade']) + || strtolower($headers['upgrade']) != 'websocket' + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['connection']) + || strpos(strtolower($headers['connection']), 'upgrade') === false + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['sec-websocket-key'])) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['sec-websocket-version']) + || strtolower($headers['sec-websocket-version']) != 13 + ) { + $handshakeResponse = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13"; + } + + if (($this->headerOriginRequired + && !isset($headers['origin']) ) + || ($this->headerOriginRequired + && !$this->checkOrigin($headers['origin'])) + ) { + $handshakeResponse = 'HTTP/1.1 403 Forbidden'; + } + + if (($this->headerSecWebSocketProtocolRequired + && !isset($headers['sec-websocket-protocol'])) + || ($this->headerSecWebSocketProtocolRequired + && !$this->checkWebsocProtocol( + $headers['sec-websocket-protocol'] + )) + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (($this->headerSecWebSocketExtensionsRequired + && !isset($headers['sec-websocket-extensions'])) + || ($this->headerSecWebSocketExtensionsRequired + && !$this->checkWebsocExtensions( + $headers['sec-websocket-extensions'] + )) + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + // Done verifying the _required_ headers and optionally required headers. + if (isset($handshakeResponse)) { + socket_write( + $user->socket, + $handshakeResponse, + strlen($handshakeResponse) + ); + $this->disconnect($user->socket); + return; + } + + $user->headers = $headers; + $user->handshake = $buffer; + + $webSocketKeyHash = sha1($headers['sec-websocket-key'].$magicGUID); + + $rawToken = ''; + for ($i = 0; $i < 20; $i++) { + $rawToken .= chr(hexdec(substr($webSocketKeyHash, ($i * 2), 2))); + } + + $handshakeToken = base64_encode($rawToken)."\r\n"; + + $subProtocol = ''; + if (isset($headers['sec-websocket-protocol'])) { + $subProtocol = $this->processProtocol( + $headers['sec-websocket-protocol'] + ); + } + + $extensions = ''; + if (isset($headers['sec-websocket-extensions'])) { + $extensions = $this->processExtensions( + $headers['sec-websocket-extensions'] + ); + } + + $handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\n"; + $handshakeResponse .= "Upgrade: websocket\r\nConnection: Upgrade\r\n"; + $handshakeResponse .= 'Sec-WebSocket-Accept: '; + $handshakeResponse .= $handshakeToken.$subProtocol.$extensions."\r\n"; + socket_write( + $user->socket, + $handshakeResponse, + strlen($handshakeResponse) + ); + $this->connected($user); + } + + + /** + * Check target host. + * + * @param string $hostName Target hostname to be checked. + * + * @return boolean Ok or not. + */ + protected function checkHost($hostName): bool + { + // Override and return false if host is not one that you would expect. + // Ex: You only want to accept hosts from the my-domain.com domain, + // but you receive a host from malicious-site.com instead. + return true; + } + + + /** + * Check origin. + * + * @param string $origin Origin of connections. + * + * @return boolean Allowed or not. + */ + protected function checkOrigin($origin): bool + { + // Override and return false if origin is not one that you would expect. + return true; + } + + + /** + * Check websocket protocol. + * + * @param string $protocol Protocol received. + * + * @return boolean Expected or not. + */ + protected function checkWebsocProtocol($protocol): bool + { + // Override and return false if a protocol is not found that you + // would expect. + return true; + } + + + /** + * Check websocket extension. + * + * @param string $extensions Extension. + * + * @return boolean Allowed or not. + */ + protected function checkWebsocExtensions($extensions): bool + { + // Override and return false if an extension is not found that you + // would expect. + return true; + } + + + /** + * Return either + * * "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" + * or return an empty string. + * + * The carriage return/newline combo must appear at the end of a non-empty + * string, and must not appear at the beginning of the string nor in an + * otherwise empty string, or it will be considered part of the response + * body, which will trigger an error in the client as it will not be + * formatted correctly. + * + * @param string $protocol Selected protocol. + * + * @return string + */ + protected function processProtocol(string $protocol): string + { + return ''; + } + + + /** + * Return either + * * "Sec-WebSocket-Extensions: SelectedExtensions\r\n" + * or return an empty string. + * + * @param string $extensions Selected extensions. + * + * @return string + */ + protected function processExtensions(string $extensions): string + { + return ''; + } + + + /** + * Return user associated to target socket. + * + * @param Socket $socket Socket. + * + * @return object + */ + protected function getUserBySocket($socket) + { + foreach ($this->users as $user) { + if ($user->socket == $socket) { + return $user; + } + } + + return null; + } + + + /** + * Dump to stdout. + * + * @param string $message Message. + * + * @return void + */ + public function stdout($message=null) + { + if ((bool) $this->interactive === true) { + echo $message."\n"; + } + } + + + /** + * Dump to stderr. + * + * @param string $message Message. + * + * @return void + */ + public function stderr(string $message=null) + { + if ($this->interactive) { + echo $message."\n"; + } + } + + + /** + * Process a frame message. + * + * @param string $message Message. + * @param object $user User. + * @param string $messageType MessageType. + * @param boolean $messageContinues MessageContinues. + * + * @return string Framed message. + */ + protected function frame( + string $message, + $user, + string $messageType='text', + bool $messageContinues=false + ) { + switch ($messageType) { + case 'continuous': + $b1 = 0; + break; + + case 'text': + $b1 = ($user->sendingContinuous) ? 0 : 1; + break; + + case 'binary': + $b1 = ($user->sendingContinuous) ? 0 : 2; + break; + + case 'close': + $b1 = 8; + break; + + case 'ping': + $b1 = 9; + break; + + case 'pong': + $b1 = 10; + break; + + default: + // Ignore. + break; + } + + if ($messageContinues) { + $user->sendingContinuous = true; + } else { + $b1 += 128; + $user->sendingContinuous = false; + } + + $length = strlen($message); + $lengthField = ''; + if ($length < 126) { + $b2 = $length; + } else if ($length < 65536) { + $b2 = 126; + $hexLength = dechex($length); + // $this->stdout("Hex Length: $hexLength"); + if ((strlen($hexLength) % 2) == 1) { + $hexLength = '0'.$hexLength; + } + + $n = (strlen($hexLength) - 2); + + for ($i = $n; $i >= 0; $i = ($i - 2)) { + $lengthField = chr( + hexdec(substr($hexLength, $i, 2)) + ).$lengthField; + } + + $len = strlen($lengthField); + while ($len < 2) { + $lengthField = chr(0).$lengthField; + $len = strlen($lengthField); + } + } else { + $b2 = 127; + $hexLength = dechex($length); + if ((strlen($hexLength) % 2) == 1) { + $hexLength = '0'.$hexLength; + } + + $n = (strlen($hexLength) - 2); + + for ($i = $n; $i >= 0; $i = ($i - 2)) { + $lengthField = chr( + hexdec(substr($hexLength, $i, 2)) + ).$lengthField; + } + + $len = strlen($lengthField); + while ($length < 8) { + $lengthField = chr(0).$lengthField; + $len = strlen($lengthField); + } + } + + return chr($b1).chr($b2).$lengthField.$message; + } + + + /** + * Check packet if he have more than one frame and process each frame + * individually. + * + * @param integer $length Length. + * @param string $packet Packet. + * @param object $user User. + * + * @return void + */ + protected function splitPacket( + int $length, + string $packet, + $user + ) { + // Add PartialPacket and calculate the new $length. + if ($user->handlingPartialPacket) { + $packet = $user->partialBuffer.$packet; + $user->handlingPartialPacket = false; + $length = strlen($packet); + } + + $fullpacket = $packet; + $frame_pos = 0; + $frame_id = 1; + + while ($frame_pos < $length) { + $headers = $this->extractHeaders($packet); + $headers_size = $this->calcOffset($headers); + $framesize = ($headers['length'] + $headers_size); + + // Split frame from packet and process it. + $frame = substr($fullpacket, $frame_pos, $framesize); + + $message = $this->deframe($frame, $user, $headers); + if ($message !== false) { + if ($user->hasSentClose) { + $this->disconnect($user->socket); + } else { + if ((preg_match('//u', $message)) + || ($headers['opcode'] == 2) + ) { + /* + * Debug purposes. + * $this->stdout("Text msg encoded UTF-8 or Binary msg\n".$message); + */ + + $this->process($user, $message); + } else { + $this->stderr("not UTF-8\n"); + } + } + } + + // Get the new position also modify packet data. + $frame_pos += $framesize; + $packet = substr($fullpacket, $frame_pos); + $frame_id++; + } + } + + + /** + * Calculate offset. + * + * @param array $headers Headers received. + * + * @return integer Calculated offset. + */ + protected function calcOffset(array $headers): int + { + $offset = 2; + if ($headers['hasmask']) { + $offset += 4; + } + + if ($headers['length'] > 65535) { + $offset += 8; + } else if ($headers['length'] > 125) { + $offset += 2; + } + + return $offset; + } + + + /** + * Parse frame. + * + * @param string $message Message received. + * @param object $user Origin. + * + * @return boolean Process ok or not. + */ + protected function deframe( + string $message, + &$user + ): bool { + /* + * Debug purposes. + * echo $this->strtohex($message); + */ + + $headers = $this->extractHeaders($message); + $pongReply = false; + $willClose = false; + + /* + * Ignored: + * case 0 + * case 1 + * case 2 + * case 10 + */ + + switch ($headers['opcode']) { + case 8: + // TODO: close the connection. + $user->hasSentClose = true; + return ''; + + case 9: + $pongReply = true; + break; + + default: + /* + * TODO: fail connection. + * $this->disconnect($user); + */ + + $willClose = true; + break; + } + + /* + * Deal by splitPacket() as now deframe() do only one frame at a time. + * if ($user->handlingPartialPacket) { + * $message = $user->partialBuffer . $message; + * $user->handlingPartialPacket = false; + * return $this->deframe($message, $user); + * } + */ + + if ($this->checkRSVBits($headers, $user)) { + return false; + } + + if ($willClose) { + // TODO: fail the connection. + return false; + } + + $payload = $user->partialMessage.$this->extractPayload( + $message, + $headers + ); + + if ($pongReply) { + $reply = $this->frame($payload, $user, 'pong'); + socket_write($user->socket, $reply, strlen($reply)); + return false; + } + + if ($headers['length'] > strlen($this->applyMask($headers, $payload))) { + $user->handlingPartialPacket = true; + $user->partialBuffer = $message; + return false; + } + + $payload = $this->applyMask($headers, $payload); + + if ($headers['fin']) { + $user->partialMessage = ''; + return $payload; + } + + $user->partialMessage = $payload; + return false; + } + + + /** + * Extract headers from message. + * + * @param string $message Message. + * + * @return array Headers. + */ + protected function extractHeaders(string $message): array + { + $header = [ + 'fin' => ($message[0] & chr(128)), + 'rsv1' => ($message[0] & chr(64)), + 'rsv2' => ($message[0] & chr(32)), + 'rsv3' => ($message[0] & chr(16)), + 'opcode' => (ord($message[0]) & 15), + 'hasmask' => ($message[1] & chr(128)), + 'length' => 0, + 'mask' => '', + ]; + + $header['length'] = ord($message[1]); + if (ord($message[1]) >= 128) { + $header['length'] = (ord($message[1]) - 128); + } + + if ($header['length'] == 126) { + if ($header['hasmask']) { + $header['mask'] = $message[4].$message[5]; + $header['mask'] .= $message[6].$message[7]; + } + + $header['length'] = (ord($message[2]) * 256 + ord($message[3])); + } else if ($header['length'] == 127) { + if ($header['hasmask']) { + $header['mask'] = $message[10].$message[11]; + $header['mask'] .= $message[12].$message[13]; + } + + $header['length'] = (ord($message[2]) * 65536 * 65536 * 65536 * 256); + $header['length'] += (ord($message[3]) * 65536 * 65536 * 65536); + $header['length'] += (ord($message[4]) * 65536 * 65536 * 256); + $header['length'] += (ord($message[5]) * 65536 * 65536); + $header['length'] += (ord($message[6]) * 65536 * 256); + $header['length'] += (ord($message[7]) * 65536); + $header['length'] += (ord($message[8]) * 256); + $header['length'] += ord($message[9]); + } else if ($header['hasmask']) { + $header['mask'] = $message[2].$message[3].$message[4].$message[5]; + } + + /* + * Debug purposes. + * echo $this->strtohex($message); + * + * $this->printHeaders($header); + */ + + return $header; + } + + + /** + * Get payload from message using headers. + * + * @param string $message Message. + * @param array $headers Headers. + * + * @return string + */ + protected function extractPayload( + string $message, + array $headers + ): string { + $offset = 2; + if ($headers['hasmask']) { + $offset += 4; + } + + if ($headers['length'] > 65535) { + $offset += 8; + } else if ($headers['length'] > 125) { + $offset += 2; + } + + return substr($message, $offset); + } + + + /** + * Apply mask. + * + * @param array $headers Headers. + * @param string $payload Payload. + * + * @return string Xor. + */ + protected function applyMask( + array $headers, + string $payload + ): string { + $effectiveMask = ''; + if ($headers['hasmask']) { + $mask = $headers['mask']; + } else { + return $payload; + } + + $len_mask = strlen($effectiveMask); + $len_payload = strlen($payload); + + // Enlarge. + while ($len_mask < $len_payload) { + $effectiveMask .= $mask; + $len_mask = strlen($effectiveMask); + $len_payload = strlen($payload); + } + + // Decrease. + while ($len_mask > $len_payload) { + $effectiveMask = substr($effectiveMask, 0, -1); + $len_mask = strlen($effectiveMask); + $len_payload = strlen($payload); + } + + return ($effectiveMask ^ $payload); + } + + + /** + * Check RSV bits. + * Override this method if you are using an extension where RSV bits are + * being used. + * + * @param array $headers Headers. + * @param object $user User. + * + * @return boolean OK or not. + */ + protected function checkRSVBits( + array $headers, + $user + ): bool { + $len = ord($headers['rsv1']); + $len += ord($headers['rsv2']); + $len += ord($headers['rsv3']); + if ($len > 0) { + /* + * TODO: fail connection. + * $this->disconnect($user); + */ + + return true; + } + + return false; + } + + + /** + * Transforms string into HEX string. + * + * @param string $str String. + * + * @return string HEX string. + */ + protected function strtohex( + string $str='' + ): string { + $strout = ''; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + if (ord($str[$i]) < 16) { + $strout .= '0'.dechex(ord($str[$i])); + } else { + $strout .= dechex(ord($str[$i])); + } + + $strout .= ' '; + if (($i % 32) == 7) { + $strout .= ': '; + } + + if (($i % 32) == 15) { + $strout .= ': '; + } + + if (($i % 32) == 23) { + $strout .= ': '; + } + + if (($i % 32) == 31) { + $strout .= "\n"; + } + } + + return $strout."\n"; + } + + + /** + * Debug purposes. Print headers. + * + * @param array $headers Headers. + * + * @return void + */ + protected function printHeaders($headers) + { + echo "Array\n(\n"; + foreach ($headers as $key => $value) { + if ($key == 'length' || $key == 'opcode') { + echo "\t[".$key.'] => '.$value."\n\n"; + } else { + echo "\t[".$key.'] => '.$this->strtohex($value)."\n"; + } + } + + echo ")\n"; + } + + +} diff --git a/pandora_console/extensions/quick_shell/WebSocketServer.php b/pandora_console/extensions/quick_shell/WebSocketServer.php new file mode 100644 index 0000000000..7729345f9c --- /dev/null +++ b/pandora_console/extensions/quick_shell/WebSocketServer.php @@ -0,0 +1,1269 @@ + + * 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; + +use \PandoraFMS\WebSocketUser; + +/** + * Abstract class to be implemented. + */ +abstract class WebSocketServer +{ + + /** + * Bae class to be created. + * + * @var string + */ + protected $userClass = 'WebSocketUser'; + + /** + * Redefine this if you want a custom user class. The custom user class + * should inherit from WebSocketUser. + * + * @var integer + */ + protected $maxBufferSize; + + /** + * Max. concurrent connections. + * + * @var integer + */ + protected $maxConnections = 20; + + /** + * Undocumented variable + * + * @var [type] + */ + protected $master; + + /** + * Undocumented variable + * + * @var array + */ + protected $sockets = []; + + /** + * Undocumented variable + * + * @var array + */ + protected $users = []; + + /** + * Undocumented variable + * + * @var array + */ + protected $heldMessages = []; + + /** + * Undocumented variable + * + * @var array + */ + protected $interactive = true; + + /** + * Undocumented variable + * + * @var array + */ + protected $headerOriginRequired = false; + + /** + * Undocumented variable + * + * @var array + */ + protected $headerSecWebSocketProtocolRequired = false; + + /** + * Undocumented variable + * + * @var array + */ + protected $headerSecWebSocketExtensionsRequired = false; + + /** + * Stored raw headers for rediretion. + * + * @var array + */ + public $rawHeaders = []; + + + /** + * Builder. + * + * @param string $addr Address where websocketserver will listen. + * @param integer $port Port where listen. + * @param integer $bufferLength Max buffer length. + * @param integer $maxConnections Max concurrent connections. + */ + public function __construct( + string $addr, + int $port, + int $bufferLength=2048, + int $maxConnections=20 + ) { + if (isset($this->maxBufferSize) + && $this->maxBufferSize < $bufferLength + ) { + $this->maxBufferSize = $bufferLength; + } + + if (is_numeric($maxConnections) && $maxConnections > 0) { + $this->maxConnections = $maxConnections; + } + + $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $this->master || die('Failed: socket_create()'); + + $__tmp = socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1); + $__tmp || die('Failed: socket_option()'); + + $__tmp = socket_bind($this->master, $addr, $port); + $__tmp || die('Failed: socket_bind()'); + + $__tmp = socket_listen($this->master, $this->maxConnections); + $__tmp || die('Failed: socket_listen()'); + + $this->sockets['m'] = $this->master; + $this->stdout("Server started\nListening on: ".$addr.':'.$port."\n"); + $this->stdout('Master socket: '.$this->master."\n"); + + } + + + /** + * Process user message. Implement. + * + * @param object $user User. + * @param string $message Message. + * + * @return void + */ + abstract protected function process($user, string $message); + + + /** + * Called immediately when the data is recieved. + * + * @param object $user User. + * + * @return void + */ + abstract protected function connected($user); + + + /** + * Called after the handshake response is sent to the client. + * + * @param object $user User. + * + * @return void + */ + abstract protected function closed($user); + + + /** + * Called after the connection is closed. + * Override to handle a connecting user, after the instance of the User + * is created, but before the handshake has completed. + * + * @param object $user User. + * + * @return void + */ + protected function connecting($user) + { + // Optional implementation. + } + + + /** + * Send a message to target user. + * + * @param object $user User. + * @param string $message Message. + * + * @return void + */ + protected function send($user, string $message) + { + if ($user->handshake) { + $message = $this->frame($message, $user); + $result = socket_write($user->socket, $message, strlen($message)); + } else { + // User has not yet performed their handshake. + // Store for sending later. + $holdingMessage = [ + 'user' => $user, + 'message' => $message, + ]; + $this->heldMessages[] = $holdingMessage; + } + } + + + /** + * Override this for any process that should happen periodically. + * Will happen at least once per second, but possibly more often. + * + * @return void + */ + protected function tick() + { + // Optional implementation. + } + + + /** + * Internal backend for tick. + * + * @return void + */ + protected function pTick() + { + // Core maintenance processes, such as retrying failed messages. + foreach ($this->heldMessages as $key => $hm) { + $found = false; + foreach ($this->users as $currentUser) { + if ($hm['user']->socket == $currentUser->socket) { + $found = true; + if ($currentUser->handshake) { + unset($this->heldMessages[$key]); + $this->send($currentUser, $hm['message']); + } + } + } + + if (!$found) { + // If they're no longer in the list of connected users, + // drop the message. + unset($this->heldMessages[$key]); + } + } + } + + + /** + * Main processing loop + * + * @return void + */ + public function run() + { + while (true) { + if (empty($this->sockets) === true) { + $this->sockets['m'] = $this->master; + } + + $read = $this->sockets; + $except = null; + $write = null; + $this->pTick(); + $this->tick(); + socket_select($read, $write, $except, 1); + foreach ($read as $socket) { + if ($socket == $this->master) { + $client = socket_accept($socket); + if ($client < 0) { + $this->stderr('Failed: socket_accept()'); + continue; + } else { + $this->connect($client); + $this->stdout('Client connected. '.$client); + } + } else { + $numBytes = socket_recv( + $socket, + $buffer, + $this->maxBufferSize, + 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; + } + } else if ($numBytes == 0) { + $this->disconnect($socket); + $this->stderr( + 'Client disconnected. TCP connection lost: '.$socket + ); + } else { + $user = $this->getUserBySocket($socket); + if (!$user->handshake) { + $tmp = str_replace("\r", '', $buffer); + if (strpos($tmp, "\n\n") === false) { + continue; + // If the client has not finished sending the + // header, then wait before sending our upgrade + // response. + } + + $this->doHandshake($user, $buffer); + } else { + // Split packet into frame and send it to deframe. + $this->splitPacket($numBytes, $buffer, $user); + } + } + } + } + } + } + + + /** + * Register user (and its socket) into master. + * + * @param Socket $socket Socket. + * + * @return void + */ + protected function connect($socket) + { + $user = new $this->userClass(uniqid('u'), $socket); + $this->users[$user->id] = $user; + $this->sockets[$user->id] = $socket; + $this->connecting($user); + } + + + /** + * Disconnect socket from master. + * + * @param Socket $socket Socket. + * @param boolean $triggerClosed Also close. + * @param integer $sockErrNo Clear error. + * + * @return void + */ + protected function disconnect( + $socket, + bool $triggerClosed=true, + $sockErrNo=null + ) { + $disconnectedUser = $this->getUserBySocket($socket); + + if ($disconnectedUser !== null) { + unset($this->users[$disconnectedUser->id]); + + if (array_key_exists($disconnectedUser->id, $this->sockets)) { + unset($this->sockets[$disconnectedUser->id]); + } + + if ($sockErrNo !== null) { + socket_clear_error($socket); + } + + if ($triggerClosed) { + $this->closed($disconnectedUser); + $this->stdout( + 'Client disconnected. '.$disconnectedUser->socket + ); + socket_close($disconnectedUser->socket); + } else { + $message = $this->frame('', $disconnectedUser, 'close'); + socket_write( + $disconnectedUser->socket, + $message, + strlen($message) + ); + } + } + } + + + /** + * Perform a handshake. + * + * @param object $user User. + * @param string $buffer Buffer. + * + * @return void + */ + protected function doHandshake($user, string $buffer) + { + $magicGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; + $headers = []; + $lines = explode("\n", $buffer); + foreach ($lines as $line) { + if (strpos($line, ':') !== false) { + $header = explode(':', $line, 2); + $headers[strtolower(trim($header[0]))] = trim($header[1]); + $this->rawHeaders[trim($header[0])] = trim($header[1]); + } else if (stripos($line, 'get ') !== false) { + preg_match('/GET (.*) HTTP/i', $buffer, $reqResource); + $headers['get'] = trim($reqResource[1]); + } + } + + if (isset($headers['get'])) { + $user->requestedResource = $headers['get']; + } else { + // TODO: fail the connection. + $handshakeResponse = "HTTP/1.1 405 Method Not Allowed\r\n\r\n"; + } + + if (!isset($headers['host']) + || !$this->checkHost($headers['host']) + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['upgrade']) + || strtolower($headers['upgrade']) != 'websocket' + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['connection']) + || strpos(strtolower($headers['connection']), 'upgrade') === false + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['sec-websocket-key'])) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (!isset($headers['sec-websocket-version']) + || strtolower($headers['sec-websocket-version']) != 13 + ) { + $handshakeResponse = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13"; + } + + if (($this->headerOriginRequired + && !isset($headers['origin']) ) + || ($this->headerOriginRequired + && !$this->checkOrigin($headers['origin'])) + ) { + $handshakeResponse = 'HTTP/1.1 403 Forbidden'; + } + + if (($this->headerSecWebSocketProtocolRequired + && !isset($headers['sec-websocket-protocol'])) + || ($this->headerSecWebSocketProtocolRequired + && !$this->checkWebsocProtocol( + $headers['sec-websocket-protocol'] + )) + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + if (($this->headerSecWebSocketExtensionsRequired + && !isset($headers['sec-websocket-extensions'])) + || ($this->headerSecWebSocketExtensionsRequired + && !$this->checkWebsocExtensions( + $headers['sec-websocket-extensions'] + )) + ) { + $handshakeResponse = 'HTTP/1.1 400 Bad Request'; + } + + // Done verifying the _required_ headers and optionally required headers. + if (isset($handshakeResponse)) { + socket_write( + $user->socket, + $handshakeResponse, + strlen($handshakeResponse) + ); + $this->disconnect($user->socket); + return; + } + + $user->headers = $headers; + $user->handshake = $buffer; + + $webSocketKeyHash = sha1($headers['sec-websocket-key'].$magicGUID); + + $rawToken = ''; + for ($i = 0; $i < 20; $i++) { + $rawToken .= chr(hexdec(substr($webSocketKeyHash, ($i * 2), 2))); + } + + $handshakeToken = base64_encode($rawToken)."\r\n"; + + $subProtocol = ''; + if (isset($headers['sec-websocket-protocol'])) { + $subProtocol = $this->processProtocol( + $headers['sec-websocket-protocol'] + ); + } + + $extensions = ''; + if (isset($headers['sec-websocket-extensions'])) { + $extensions = $this->processExtensions( + $headers['sec-websocket-extensions'] + ); + } + + $handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\n"; + $handshakeResponse .= "Upgrade: websocket\r\nConnection: Upgrade\r\n"; + $handshakeResponse .= 'Sec-WebSocket-Accept: '; + $handshakeResponse .= $handshakeToken.$subProtocol.$extensions."\r\n"; + socket_write( + $user->socket, + $handshakeResponse, + strlen($handshakeResponse) + ); + $this->connected($user); + } + + + /** + * Check target host. + * + * @param string $hostName Target hostname to be checked. + * + * @return boolean Ok or not. + */ + protected function checkHost($hostName): bool + { + // Override and return false if host is not one that you would expect. + // Ex: You only want to accept hosts from the my-domain.com domain, + // but you receive a host from malicious-site.com instead. + return true; + } + + + /** + * Check origin. + * + * @param string $origin Origin of connections. + * + * @return boolean Allowed or not. + */ + protected function checkOrigin($origin): bool + { + // Override and return false if origin is not one that you would expect. + return true; + } + + + /** + * Check websocket protocol. + * + * @param string $protocol Protocol received. + * + * @return boolean Expected or not. + */ + protected function checkWebsocProtocol($protocol): bool + { + // Override and return false if a protocol is not found that you + // would expect. + return true; + } + + + /** + * Check websocket extension. + * + * @param string $extensions Extension. + * + * @return boolean Allowed or not. + */ + protected function checkWebsocExtensions($extensions): bool + { + // Override and return false if an extension is not found that you + // would expect. + return true; + } + + + /** + * Return either + * * "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" + * or return an empty string. + * + * The carriage return/newline combo must appear at the end of a non-empty + * string, and must not appear at the beginning of the string nor in an + * otherwise empty string, or it will be considered part of the response + * body, which will trigger an error in the client as it will not be + * formatted correctly. + * + * @param string $protocol Selected protocol. + * + * @return string + */ + protected function processProtocol(string $protocol): string + { + return ''; + } + + + /** + * Return either + * * "Sec-WebSocket-Extensions: SelectedExtensions\r\n" + * or return an empty string. + * + * @param string $extensions Selected extensions. + * + * @return string + */ + protected function processExtensions(string $extensions): string + { + return ''; + } + + + /** + * Return user associated to target socket. + * + * @param Socket $socket Socket. + * + * @return object + */ + protected function getUserBySocket($socket) + { + foreach ($this->users as $user) { + if ($user->socket == $socket) { + return $user; + } + } + + return null; + } + + + /** + * Dump to stdout. + * + * @param string $message Message. + * + * @return void + */ + public function stdout($message=null) + { + if ((bool) $this->interactive === true) { + echo $message."\n"; + } + } + + + /** + * Dump to stderr. + * + * @param string $message Message. + * + * @return void + */ + public function stderr(string $message=null) + { + if ($this->interactive) { + echo $message."\n"; + } + } + + + /** + * Process a frame message. + * + * @param string $message Message. + * @param object $user User. + * @param string $messageType MessageType. + * @param boolean $messageContinues MessageContinues. + * + * @return string Framed message. + */ + protected function frame( + string $message, + $user, + string $messageType='text', + bool $messageContinues=false + ) { + switch ($messageType) { + case 'continuous': + $b1 = 0; + break; + + case 'text': + $b1 = ($user->sendingContinuous) ? 0 : 1; + break; + + case 'binary': + $b1 = ($user->sendingContinuous) ? 0 : 2; + break; + + case 'close': + $b1 = 8; + break; + + case 'ping': + $b1 = 9; + break; + + case 'pong': + $b1 = 10; + break; + + default: + // Ignore. + break; + } + + if ($messageContinues) { + $user->sendingContinuous = true; + } else { + $b1 += 128; + $user->sendingContinuous = false; + } + + $length = strlen($message); + $lengthField = ''; + if ($length < 126) { + $b2 = $length; + } else if ($length < 65536) { + $b2 = 126; + $hexLength = dechex($length); + // $this->stdout("Hex Length: $hexLength"); + if ((strlen($hexLength) % 2) == 1) { + $hexLength = '0'.$hexLength; + } + + $n = (strlen($hexLength) - 2); + + for ($i = $n; $i >= 0; $i = ($i - 2)) { + $lengthField = chr( + hexdec(substr($hexLength, $i, 2)) + ).$lengthField; + } + + $len = strlen($lengthField); + while ($len < 2) { + $lengthField = chr(0).$lengthField; + $len = strlen($lengthField); + } + } else { + $b2 = 127; + $hexLength = dechex($length); + if ((strlen($hexLength) % 2) == 1) { + $hexLength = '0'.$hexLength; + } + + $n = (strlen($hexLength) - 2); + + for ($i = $n; $i >= 0; $i = ($i - 2)) { + $lengthField = chr( + hexdec(substr($hexLength, $i, 2)) + ).$lengthField; + } + + $len = strlen($lengthField); + while ($length < 8) { + $lengthField = chr(0).$lengthField; + $len = strlen($lengthField); + } + } + + return chr($b1).chr($b2).$lengthField.$message; + } + + + /** + * Check packet if he have more than one frame and process each frame + * individually. + * + * @param integer $length Length. + * @param string $packet Packet. + * @param object $user User. + * + * @return void + */ + protected function splitPacket( + int $length, + string $packet, + $user + ) { + // Add PartialPacket and calculate the new $length. + if ($user->handlingPartialPacket) { + $packet = $user->partialBuffer.$packet; + $user->handlingPartialPacket = false; + $length = strlen($packet); + } + + $fullpacket = $packet; + $frame_pos = 0; + $frame_id = 1; + + while ($frame_pos < $length) { + $headers = $this->extractHeaders($packet); + $headers_size = $this->calcOffset($headers); + $framesize = ($headers['length'] + $headers_size); + + // Split frame from packet and process it. + $frame = substr($fullpacket, $frame_pos, $framesize); + + $message = $this->deframe($frame, $user, $headers); + + if ($message !== false) { + if ($user->hasSentClose) { + $this->disconnect($user->socket); + } else { + if ((preg_match('//u', $message)) + || ($headers['opcode'] == 2) + ) { + /* + * Debug purposes. + * $this->stdout("Text msg encoded UTF-8 or Binary msg\n".$message); + */ + + $this->process($user, $message); + } else { + $this->stderr("not UTF-8\n"); + } + } + } + + // Get the new position also modify packet data. + $frame_pos += $framesize; + $packet = substr($fullpacket, $frame_pos); + $frame_id++; + } + } + + + /** + * Calculate offset. + * + * @param array $headers Headers received. + * + * @return integer Calculated offset. + */ + protected function calcOffset(array $headers): int + { + $offset = 2; + if ($headers['hasmask']) { + $offset += 4; + } + + if ($headers['length'] > 65535) { + $offset += 8; + } else if ($headers['length'] > 125) { + $offset += 2; + } + + return $offset; + } + + + /** + * Parse frame. + * + * @param string $message Message received. + * @param object $user Origin. + * + * @return boolean Process ok or not. + */ + protected function deframe( + string $message, + &$user + ) { + /* + * Debug purposes. + * echo $this->strtohex($message); + */ + + $headers = $this->extractHeaders($message); + $pongReply = false; + $willClose = false; + + switch ($headers['opcode']) { + case 0: + case 1: + case 2: + case 10: + $willClose = false; + break; + + case 8: + // TODO: close the connection. + $user->hasSentClose = true; + return ''; + + case 9: + $pongReply = true; + break; + + default: + /* + * TODO: fail connection. + * $this->disconnect($user); + */ + + $willClose = true; + break; + } + + /* + * Deal by splitPacket() as now deframe() do only one frame at a time. + * if ($user->handlingPartialPacket) { + * $message = $user->partialBuffer . $message; + * $user->handlingPartialPacket = false; + * return $this->deframe($message, $user); + * } + */ + + if ($this->checkRSVBits($headers, $user)) { + return false; + } + + if ($willClose) { + // TODO: fail the connection. + return false; + } + + $payload = $user->partialMessage.$this->extractPayload( + $message, + $headers + ); + + if ($pongReply) { + $reply = $this->frame($payload, $user, 'pong'); + socket_write($user->socket, $reply, strlen($reply)); + return false; + } + + if ($headers['length'] > strlen($this->applyMask($headers, $payload))) { + $user->handlingPartialPacket = true; + $user->partialBuffer = $message; + return false; + } + + $payload = $this->applyMask($headers, $payload); + + if ($headers['fin']) { + $user->partialMessage = ''; + return $payload; + } + + $user->partialMessage = $payload; + + return false; + } + + + /** + * Extract headers from message. + * + * @param string $message Message. + * + * @return array Headers. + */ + protected function extractHeaders(string $message): array + { + $header = [ + 'fin' => ($message[0] & chr(128)), + 'rsv1' => ($message[0] & chr(64)), + 'rsv2' => ($message[0] & chr(32)), + 'rsv3' => ($message[0] & chr(16)), + 'opcode' => (ord($message[0]) & 15), + 'hasmask' => ($message[1] & chr(128)), + 'length' => 0, + 'mask' => '', + ]; + + $header['length'] = ord($message[1]); + if (ord($message[1]) >= 128) { + $header['length'] = (ord($message[1]) - 128); + } + + if ($header['length'] == 126) { + if ($header['hasmask']) { + $header['mask'] = $message[4].$message[5]; + $header['mask'] .= $message[6].$message[7]; + } + + $header['length'] = (ord($message[2]) * 256 + ord($message[3])); + } else if ($header['length'] == 127) { + if ($header['hasmask']) { + $header['mask'] = $message[10].$message[11]; + $header['mask'] .= $message[12].$message[13]; + } + + $header['length'] = (ord($message[2]) * 65536 * 65536 * 65536 * 256); + $header['length'] += (ord($message[3]) * 65536 * 65536 * 65536); + $header['length'] += (ord($message[4]) * 65536 * 65536 * 256); + $header['length'] += (ord($message[5]) * 65536 * 65536); + $header['length'] += (ord($message[6]) * 65536 * 256); + $header['length'] += (ord($message[7]) * 65536); + $header['length'] += (ord($message[8]) * 256); + $header['length'] += ord($message[9]); + } else if ($header['hasmask']) { + $header['mask'] = $message[2].$message[3].$message[4].$message[5]; + } + + /* + * Debug purposes. + * echo $this->strtohex($message); + * + * $this->printHeaders($header); + */ + + return $header; + } + + + /** + * Get payload from message using headers. + * + * @param string $message Message. + * @param array $headers Headers. + * + * @return string + */ + protected function extractPayload( + string $message, + array $headers + ) { + $offset = 2; + if ($headers['hasmask']) { + $offset += 4; + } + + if ($headers['length'] > 65535) { + $offset += 8; + } else if ($headers['length'] > 125) { + $offset += 2; + } + + return substr($message, $offset); + } + + + /** + * Apply mask. + * + * @param array $headers Headers. + * @param string $payload Payload. + * + * @return string Xor. + */ + protected function applyMask( + array $headers, + string $payload + ) { + $effectiveMask = ''; + if ($headers['hasmask']) { + $mask = $headers['mask']; + } else { + return $payload; + } + + $len_mask = strlen($effectiveMask); + $len_payload = strlen($payload); + + // Enlarge. + while ($len_mask < $len_payload) { + $effectiveMask .= $mask; + $len_mask = strlen($effectiveMask); + $len_payload = strlen($payload); + } + + // Decrease. + while ($len_mask > $len_payload) { + $effectiveMask = substr($effectiveMask, 0, -1); + $len_mask = strlen($effectiveMask); + $len_payload = strlen($payload); + } + + return ($effectiveMask ^ $payload); + } + + + /** + * Check RSV bits. + * Override this method if you are using an extension where RSV bits are + * being used. + * + * @param array $headers Headers. + * @param object $user User. + * + * @return boolean OK or not. + */ + protected function checkRSVBits( + array $headers, + $user + ): bool { + $len = ord($headers['rsv1']); + $len += ord($headers['rsv2']); + $len += ord($headers['rsv3']); + if ($len > 0) { + /* + * TODO: fail connection. + * $this->disconnect($user); + */ + + return true; + } + + return false; + } + + + /** + * Transforms string into HEX string. + * + * @param string $str String. + * + * @return string HEX string. + */ + protected function strtohex( + string $str='' + ): string { + $strout = ''; + $len = strlen($str); + for ($i = 0; $i < $len; $i++) { + if (ord($str[$i]) < 16) { + $strout .= '0'.dechex(ord($str[$i])); + } else { + $strout .= dechex(ord($str[$i])); + } + + $strout .= ' '; + if (($i % 32) == 7) { + $strout .= ': '; + } + + if (($i % 32) == 15) { + $strout .= ': '; + } + + if (($i % 32) == 23) { + $strout .= ': '; + } + + if (($i % 32) == 31) { + $strout .= "\n"; + } + } + + return $strout."\n"; + } + + + /** + * Debug purposes. Print headers. + * + * @param array $headers Headers. + * + * @return void + */ + protected function printHeaders($headers) + { + echo "Array\n(\n"; + foreach ($headers as $key => $value) { + if ($key == 'length' || $key == 'opcode') { + echo "\t[".$key.'] => '.$value."\n\n"; + } else { + echo "\t[".$key.'] => '.$this->strtohex($value)."\n"; + } + } + + echo ")\n"; + } + + +} diff --git a/pandora_console/extensions/quick_shell/WebSocketUser b/pandora_console/extensions/quick_shell/WebSocketUser new file mode 100644 index 0000000000..41aa5960f6 --- /dev/null +++ b/pandora_console/extensions/quick_shell/WebSocketUser @@ -0,0 +1,68 @@ + + * 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\WebSocketServer; +class WebSocketUser +{ + + public $socket; + public $id; + public $headers = array(); + public $handshake = false; + + public $handlingPartialPacket = false; + public $partialBuffer = ""; + + public $sendingContinuous = false; + public $partialMessage = ""; + + public $hasSentClose = false; + + function __construct($id, $socket) + { + $this->id = $id; + $this->socket = $socket; + } +} \ No newline at end of file diff --git a/pandora_console/extensions/quick_shell/WebSocketUser.php b/pandora_console/extensions/quick_shell/WebSocketUser.php new file mode 100644 index 0000000000..d8e9d989da --- /dev/null +++ b/pandora_console/extensions/quick_shell/WebSocketUser.php @@ -0,0 +1,162 @@ + + * Compatible with PHP >= 7.0 + * + * @category External library + * @package Pandora FMS + * @subpackage WebSocketUser + * @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; + +/** + * Parent class for WebSocket User. + */ +class WebSocketUser +{ + + /** + * Socket. + * + * @var Socket + */ + public $socket; + + /** + * Id. + * + * @var string + */ + public $id; + + /** + * Headers. + * + * @var array + */ + public $headers = []; + + /** + * Handshake. + * + * @var boolean + */ + public $handshake = false; + + /** + * HandlingPartialPacket. + * + * @var boolean + */ + public $handlingPartialPacket = false; + + /** + * PartialBuffer. + * + * @var string + */ + public $partialBuffer = ''; + + /** + * SendingContinuous. + * + * @var boolean + */ + public $sendingContinuous = false; + + /** + * PartialMessage. + * + * @var string + */ + public $partialMessage = ''; + + /** + * HasSentClose. + * + * @var boolean + */ + public $hasSentClose = false; + + /** + * Received raw packet, to be redirected. + * + * @var string + */ + public $rawPacket; + + + /** + * Initializes a websocket user. + * + * @param string $id Id. + * @param Socket $socket Socket. + */ + public function __construct($id, $socket) + { + $this->id = $id; + $this->socket = $socket; + } + + + /** + * Retun last packet (raw) received + * + * @return string + */ + public function getRawPacket() + { + return $this->rawPacket; + } + + + /** + * Store raw packet. + * + * @param string $packet received. + * + * @return void + */ + public function setRawPacket($packet) + { + $this->rawPacket = $packet; + } + + +} diff --git a/pandora_console/extensions/quick_shell/old_ext.php b/pandora_console/extensions/quick_shell/old_ext.php new file mode 100644 index 0000000000..243a4c4219 --- /dev/null +++ b/pandora_console/extensions/quick_shell/old_ext.php @@ -0,0 +1,100 @@ + false, + 'text' => ''.html_print_image('images/groups_small/application_osx_terminal.png', true, ['title' => __('Wetty')]).'', + ]; + + ui_print_page_header(__('Wetty'), 'images/extensions.png', false, '', true, $buttons); + + $row = 0; + + echo '
'; + + $table->width = '100%'; + $table->class = 'databox data'; + $table->data = []; + $table->head = []; + $table->align = []; + // $table->align[3] = 'left'; + $table->style = []; + $table->size = []; + // $table->size[3] = '10%'; + $table->style[0] = 'font-weight: bold'; + + $table->head[0] = __('Wetty Configuration'); + $table->head[1] = __(''); + + $table->data[$row][0] = __('Wetty ip address connection'); + $table->data[$row][1] = html_print_input_text('wetty_ip', $config['wetty_ip'], '', 25, 25, true); + $row++; + + $table->data[$row][0] = __('Wetty port connection'); + $table->data[$row][1] = html_print_input_text('wetty_port', $config['wetty_port'], '', 25, 25, true); + $row++; + + array_push($table->data, $data); + + html_print_table($table); + + echo '
'; + html_print_submit_button(__('Update'), 'update_button', false, 'class="sub upd"'); + echo '
'; + + echo '
'; + +} + + +extensions_add_godmode_function('confWetty'); + diff --git a/pandora_console/extensions/quick_shell/old_ext1.php b/pandora_console/extensions/quick_shell/old_ext1.php new file mode 100644 index 0000000000..a89abe5dea --- /dev/null +++ b/pandora_console/extensions/quick_shell/old_ext1.php @@ -0,0 +1,76 @@ + false, + 'text' => ''.html_print_image('images/setup.png', true, ['title' => __('Wetty settings')]).'', + ]; + + ui_print_page_header(__('Wetty'), 'images/extensions.png', false, '', true, $buttons); + + $table->width = '100%'; + $table->class = 'databox data'; + $table->data = []; + $table->head = []; + $table->align = []; + // $table->align[3] = 'left'; + $table->style = []; + $table->size = []; + // $table->size[3] = '10%'; + $table->style[0] = 'font-weight: bold'; + + $table->head[0] = __('Wetty'); + + // $data[0] = ''; + $data[0] = ''; + + // $data[0] .= '
'; + array_push($table->data, $data); + + html_print_table($table); + +} + + +extensions_add_godmode_menu_option(__('Wetty'), 'AW', 'gextensions', null, 'v1'); +extensions_add_godmode_function('mainWetty'); diff --git a/pandora_console/extensions/quick_shell/test.php b/pandora_console/extensions/quick_shell/test.php new file mode 100644 index 0000000000..c6024e4213 --- /dev/null +++ b/pandora_console/extensions/quick_shell/test.php @@ -0,0 +1,77 @@ + + + + + GoTTY + + + + +
+ + + + +disconnect($user->socket); + } else { + echo ">> me he conectado al otro lado \n"; + echo "Enviando petición HTTP HEAD ...\n"; + + $out .= 'GET ws://'.$host.':'.$port.'/ws HTTP/1.1'."\r\n"; + $out .= 'Host: '.$host.':'.$port."\r\n"; + $out .= 'Connection: Upgrade'."\r\n"; + $out .= 'Upgrade: websocket'."\r\n"; + //$out .= 'Sec-WebSocket-Key: tqIu95AAeKFFlrLTsixBAA=='."\r\n"; + $out .= 'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits'."\r\n"; + $out .= 'Sec-WebSocket-Protocol: gotty'."\r\n"; + + $out .= '{"Arguments":"","AuthToken":""}'; + + socket_write($socket, $out); + echo "Leyendo respuesta:\n\n"; + while ($out = socket_read($socket, 2048)) { + echo '['.$out.']'; + } + } + + + + // Disconnect. + $out = "GET / HTTP/1.1\r\n"; + $out .= 'Host: '.$host."\r\n"; + $out .= "Connection: Close\r\n\r\n"; + + socket_write($socket, $out); + echo ">> Recibiendo respuesta de peticion de cierre:\n"; + while ($out = socket_read($socket, 2048)) { + echo '['.$out.']'; + } + + socket_close($socket); +*/ diff --git a/pandora_console/extensions/quick_shell/test_websocket.php b/pandora_console/extensions/quick_shell/test_websocket.php new file mode 100644 index 0000000000..aa971ad0cb --- /dev/null +++ b/pandora_console/extensions/quick_shell/test_websocket.php @@ -0,0 +1,193 @@ +#!/usr/bin/env php +myId = $id; + } + + +} + +class echoServer extends WebSocketServer +{ + + private $redirectedSocket; + + private $redirectedHost = '127.0.0.1'; + + + function __construct($addr, $port, $bufferLength=2048) + { + parent::__construct($addr, $port, $bufferLength); + $this->userClass = 'MyUser'; + } + + + /** + * 1MB... overkill for an echo server, but potentially plausible for other + * applications. + * + * @var integer + */ + protected $maxBufferSize = 1048576; + + + protected function readSocket() + { + $buffer; + + $numBytes = socket_recv( + $this->redirectedSocket, + $buffer, + $this->maxBufferSize, + 0 + ); + if ($numBytes === false) { + return false; + } + + return $buffer; + } + + + protected function parseGottyHeaders($response) + { + $headers = []; + $lines = explode('\n', $response); + foreach ($lines as $l) { + $c = explode(':', $l); + + $headers[trim($c[0])] = trim($c[1]); + } + + return $headers; + } + + + protected function translateGottyHeaders($headers) + { + // Redirect. + $h = $headers; + /* + $h['Sec-Websocket-Key'] = base64_encode($headers['sec-websocket-key']); + $h['Sec-Websocket-Extensions'] = $headers['sec-websocket-extensions']; + $h['Sec-Websocket-Protocol'] = $headers['sec-websocket-protocol']; + $h['Sec-Websocket-Version'] = $headers['sec-websocket-version']; + + $h['Connection'] = $headers['connection']; + if ($headers['upgrade']) { + $h['Upgrade'] = $headers['upgrade']; + } + */ + + $h['Host'] = '127.0.0.1:8081'; + $h['Origin'] = 'http://127.0.0.1:8081'; + + // Cleanup. + unset($h['get']); + unset($h['user-agent']); + unset($h['cache-control']); + unset($h['pragma']); + + // Build. + $out = "GET /ws HTTP/1.1\r\n"; + foreach ($h as $key => $value) { + $out .= ucfirst($key).': '.$value."\r\n"; + } + + $out .= "\r\n"; + return $out; + } + + + protected function process($user, $message) + { + // What to do with received message. + echo 'Received from client> ['.$message."]\n"; + + socket_write($this->redirectedSocket, $message); + $out = $this->readSocket(); + + $this->send($user, $out); + } + + + protected function processProtocol(string $protocol): string + { + return 'Sec-Websocket-Protocol: '.$protocol; + } + + + protected function connected($user) + { + $this->redirectedSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $connect = socket_connect($this->redirectedSocket, $this->redirectedHost, 8080); + if (!$connect) { + echo 'failed to open target socket redirection'; + $this->disconnect($user->socket); + } else { + var_dump($this->rawHeaders); + $out = $this->translateGottyHeaders($this->rawHeaders); + echo '????'; + socket_write($this->redirectedSocket, $out); + + $response = ''; + $response = $this->readSocket(); + + // Upgrade $user headers. + $new_headers = $this->parseGottyHeaders($response); + $user->headers += $new_headers; + + print ">> Reenviando [gotty] [$response]\n"; + + // $this->send($user, $response); + } + + // Do nothing: This is just an echo server, there's no need to track the user. + // However, if we did care about the users, we would probably have a cookie to + // parse at this step, would be looking them up in permanent storage, etc. + } + + + protected function closed($user) + { + $out = "GET /ws HTTP/1.1\r\n"; + $out .= 'Host: '.$this->redirectedHost."\r\n"; + $out .= "Connection: Close\r\n\r\n"; + socket_write($this->redirectedSocket, $out); + echo ">> Recibiendo respuesta de peticion de cierre:\n"; + $out = $this->readSocket(); + + socket_close($this->redirectedSocket); + // Do nothing: This is where cleanup would go, in case the user had any sort of + // open files or other objects associated with them. This runs after the socket + // has been closed, so there is no need to clean up the socket itself here. + } + + +} + +$echo = new echoServer('0.0.0.0', '8081'); + +try { + $echo->run(); +} catch (Exception $e) { + $echo->stdout($e->getMessage()); +}