wip: gotty redirection experiment p2

This commit is contained in:
fbsanchez 2019-10-14 19:33:00 +02:00
parent 86170b3ad8
commit 3a9e5fa477
8 changed files with 545 additions and 1527 deletions

View File

@ -0,0 +1,277 @@
<?php
require_once __DIR__.'/WebSocketServer.php';
require_once __DIR__.'/WSProxyUser.php';
use \PandoraFMS\WebSocketServer;
/**
* Redirects ws communication between two endpoints.
*/
class WSProxy extends WebSocketServer
{
/**
* Target host.
*
* @var string
*/
private $intHost = '127.0.0.1';
/**
* Target port
*
* @var integer
*/
private $intPort = 8080;
/**
* Internal URL.
*
* @var string
*/
private $intUrl = '/ws';
/**
* 1MB... overkill for an echo server, but potentially plausible for other
* applications.
*
* @var integer
*/
protected $maxBufferSize = 1048576;
/**
* Interactive mode.
*
* @var boolean
*/
protected $interative = false;
/**
* Builder.
*
* @param string $listen_addr Target address (external).
* @param integer $listen_port Target port (external).
* @param string $to_addr Target address (internal).
* @param integer $to_port Target port (internal).
* @param integer $to_url Target url (internal).
* @param integer $bufferLength Max buffer size.
* @param boolean $debug Enable traces.
*/
public function __construct(
$listen_addr,
$listen_port,
$to_addr,
$to_port,
$to_url='/ws',
$bufferLength=1048576,
$debug=false
) {
$this->intHost = $to_addr;
$this->intPort = $to_port;
$this->intUrl = $to_url;
$this->maxBufferSize = $bufferLength;
$this->interactive = $debug;
$this->userClass = 'WSProxyUser';
parent::__construct($listen_addr, $listen_port, $bufferLength);
}
/**
* Read from socket
*
* @param socket $socket Target connection.
*
* @return string Buffer.
*/
protected function readSocket($user)
{
$buffer;
$numBytes = socket_recv(
$user->socket,
$buffer,
$this->maxBufferSize,
0
);
if ($numBytes === false) {
// Failed. Disconnect.
$this->disconnect($user->socket);
return false;
}
return $this->splitPacket($numBytes, $buffer, $user);
}
/**
* Write to socket.
*
* @param socket $socket Target socket.
* @param string $message Target message to be sent.
*
* @return void
*/
protected function writeSocket($user, $message)
{
if ($user->socket) {
socket_write(
$user->socket,
$message
);
} else {
// Failed. Disconnect.
$this->disconnect($user->socket);
}
}
/**
* Connects to internal socket.
*
* @param array $headers Communication headers.
*
* @return socket Active socket or null.
*/
protected function connectInt($headers)
{
$intSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$connect = socket_connect(
$intSocket,
$this->intHost,
$this->intPort
);
if (!$connect) {
return null;
}
$c_str = 'GET '.$this->intUrl." HTTP/1.1\r\n";
$c_str .= 'Host: '.$this->intHost."\r\n";
$c_str .= "Upgrade: websocket\r\n";
$c_str .= "Connection: Upgrade\r\n";
$c_str .= 'Origin: http://'.$this->intHost."\r\n";
$c_str .= 'Sec-WebSocket-Key: '.$headers['Sec-WebSocket-Key']."\r\n";
$c_str .= 'Sec-WebSocket-Version: '.$headers['Sec-WebSocket-Version']."\r\n";
if (isset($headers['Sec-WebSocket-Protocol'])) {
$c_str .= 'Sec-WebSocket-Protocol: '.$headers['Sec-WebSocket-Protocol']."\r\n";
}
$c_str .= "\r\n";
// Send.
// Register user - internal.
$intUser = new $this->userClass('INTERNAL-'.uniqid('u'), $intSocket);
$intUser->headers = [
'get' => $this->intUrl.' HTTP/1.1',
'host' => $this->intHost,
'origin' => $this->intHost,
];
$this->writeSocket($intUser, $c_str);
return $intUser;
}
/**
* User already connected.
*
* @param object $user User.
*
* @return void
*/
protected function connected($user)
{
echo '** CONNECTED'."\n";
/*
* $user->intSocket is connected to internal.
* $user->socket is connected to external.
*/
// Create a new socket connection (internal).
$intUser = $this->connectInt($this->rawHeaders);
// Map user.
$user->intUser = $intUser;
// And socket.
$user->intSocket = $intUser->socket;
$response = $this->readSocket($intUser);
}
/**
* Protocol.
*
* @param string $protocol Protocol.
*
* @return string
*/
protected function processProtocol($protocol): string
{
return 'Sec-Websocket-Protocol: '.$protocol."\r\n";
}
/**
* From client to socket (internal);
*
* @param object $user Caller.
* @param string $message Message.
*
* @return void
*/
protected function process($user, $message)
{
echo '>> ['.$user->id.'] => ['.$user->intUser->id."]\n";
echo $this->dump($this->rawPacket);
$this->writeSocket(
$user->intUser,
$this->rawPacket
);
echo '<< ['.$user->intUser->id.'] => ['.$user->id."]\n";
$response = $this->readSocket($user->intUser);
$this->send($user, $response);
echo $this->dump($this->rawPacket);
print "\n********************************************\n";
print "\n\n\n";
print "\n\n\n";
print "\n\n\n";
}
/**
* Also close internal socket.
*
* @param object $user User.
*
* @return void
*/
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);
}
}

View File

@ -0,0 +1,44 @@
<?php
require_once __DIR__.'/WebSocketServer.php';
require_once __DIR__.'/WebSocketUser.php';
use \PandoraFMS\WebSocketServer;
use \PandoraFMS\WebSocketUser;
/**
* WebSocket proxy user.
*/
class WSProxyUser extends WebSocketUser
{
/**
* Redirection socket.
*
* @var socket
*/
public $intSocket;
/**
* User identifier.
*
* @var string
*/
public $myId;
/**
* Builder.
*
* @param string $id Identifier.
* @param socket $socket Socket (origin).
* @param socket $intSocket Socket (internal).
*/
public function __construct($id, $socket)
{
parent::__construct($id, $socket);
$this->myId = $id;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -132,12 +132,19 @@ abstract class WebSocketServer
protected $headerSecWebSocketExtensionsRequired = false;
/**
* Stored raw headers for rediretion.
* Stored raw headers for redirection.
*
* @var array
*/
public $rawHeaders = [];
/**
* Raw packet for redirection.
*
* @var string
*/
public $rawPacket;
/**
* Builder.
@ -148,7 +155,7 @@ abstract class WebSocketServer
* @param integer $maxConnections Max concurrent connections.
*/
public function __construct(
string $addr,
$addr,
int $port,
int $bufferLength=2048,
int $maxConnections=20
@ -190,7 +197,7 @@ abstract class WebSocketServer
*
* @return void
*/
abstract protected function process($user, string $message);
abstract protected function process($user, $message);
/**
@ -236,7 +243,7 @@ abstract class WebSocketServer
*
* @return void
*/
protected function send($user, string $message)
protected function send($user, $message)
{
if ($user->handshake) {
$message = $this->frame($message, $user);
@ -394,6 +401,8 @@ abstract class WebSocketServer
);
} else {
$user = $this->getUserBySocket($socket);
$pair = $this->getIntUserBySocket($socket);
if (!$user->handshake) {
$tmp = str_replace("\r", '', $buffer);
if (strpos($tmp, "\n\n") === false) {
@ -405,8 +414,13 @@ abstract class WebSocketServer
$this->doHandshake($user, $buffer);
} else {
// Split packet into frame and send it to deframe.
$this->splitPacket($numBytes, $buffer, $user);
if (is_array($pair) && $pair['int']) {
echo '~~ RESEND!'."\n";
socket_write($pair['ext']->socket, $buffer);
} else {
// Split packet into frame and send it to deframe.
$this->splitPacket($numBytes, $buffer, $user);
}
}
}
}
@ -484,7 +498,7 @@ abstract class WebSocketServer
*
* @return void
*/
protected function doHandshake($user, string $buffer)
protected function doHandshake($user, $buffer)
{
$magicGUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$headers = [];
@ -688,7 +702,7 @@ abstract class WebSocketServer
*
* @return string
*/
protected function processProtocol(string $protocol): string
protected function processProtocol($protocol): string
{
return '';
}
@ -703,7 +717,7 @@ abstract class WebSocketServer
*
* @return string
*/
protected function processExtensions(string $extensions): string
protected function processExtensions($extensions): string
{
return '';
}
@ -728,6 +742,28 @@ abstract class WebSocketServer
}
/**
* Return INT user associated to target socket.
*
* @param Socket $socket Socket.
*
* @return object
*/
protected function getIntUserBySocket($socket)
{
foreach ($this->users as $user) {
if ($user->intSocket == $socket) {
return [
'ext' => $user,
'int' => $user->intUser,
];
}
}
return null;
}
/**
* Dump to stdout.
*
@ -750,7 +786,7 @@ abstract class WebSocketServer
*
* @return void
*/
public function stderr(string $message=null)
public function stderr($message=null)
{
if ($this->interactive) {
echo $message."\n";
@ -769,9 +805,9 @@ abstract class WebSocketServer
* @return string Framed message.
*/
protected function frame(
string $message,
$message,
$user,
string $messageType='text',
$messageType='text',
bool $messageContinues=false
) {
switch ($messageType) {
@ -858,7 +894,8 @@ abstract class WebSocketServer
}
}
return chr($b1).chr($b2).$lengthField.$message;
$out = chr($b1).chr($b2).$lengthField.$message;
return $out;
}
@ -874,7 +911,7 @@ abstract class WebSocketServer
*/
protected function splitPacket(
int $length,
string $packet,
$packet,
$user
) {
// Add PartialPacket and calculate the new $length.
@ -884,6 +921,7 @@ abstract class WebSocketServer
$length = strlen($packet);
}
$this->rawPacket = $packet;
$fullpacket = $packet;
$frame_pos = 0;
$frame_id = 1;
@ -958,7 +996,7 @@ abstract class WebSocketServer
* @return boolean Process ok or not.
*/
protected function deframe(
string $message,
$message,
&$user
) {
/*
@ -1052,7 +1090,7 @@ abstract class WebSocketServer
*
* @return array Headers.
*/
protected function extractHeaders(string $message): array
protected function extractHeaders($message): array
{
$header = [
'fin' => ($message[0] & chr(128)),
@ -1115,7 +1153,7 @@ abstract class WebSocketServer
* @return string
*/
protected function extractPayload(
string $message,
$message,
array $headers
) {
$offset = 2;
@ -1143,7 +1181,7 @@ abstract class WebSocketServer
*/
protected function applyMask(
array $headers,
string $payload
$payload
) {
$effectiveMask = '';
if ($headers['hasmask']) {
@ -1211,7 +1249,7 @@ abstract class WebSocketServer
* @return string HEX string.
*/
protected function strtohex(
string $str=''
$str=''
): string {
$strout = '';
$len = strlen($str);
@ -1266,4 +1304,58 @@ abstract class WebSocketServer
}
/**
* View any string as a hexdump.
*
* @param string $data The string to be dumped.
*
* @return string
*/
public function dump($data)
{
// Init.
$hexi = '';
$ascii = '';
$dump = '';
$offset = 0;
$len = strlen($data);
// Iterate string.
for ($i = 0, $j = 0; $i < $len; $i++) {
// Convert to hexidecimal.
$hexi .= sprintf('%02x ', ord($data[$i]));
// Replace non-viewable bytes with '.'.
if (ord($data[$i]) >= 32) {
$ascii .= $data[$i];
} else {
$ascii .= '.';
}
// Add extra column spacing.
if ($j === 7 && $i !== ($len - 1)) {
$hexi .= ' ';
$ascii .= ' ';
}
// Add row.
if (++$j === 16 || $i === ($len - 1)) {
// Join the hexi / ascii output.
$dump .= sprintf('%04x %-49s %s', $offset, $hexi, $ascii);
// Reset vars.
$hexi = $ascii = '';
$offset += 16;
$j = 0;
// Add newline.
if ($i !== ($len - 1)) {
$dump .= "\n";
}
}
}
// Finish dump.
$dump .= "\n";
return $dump;
}
}

View File

@ -1,68 +0,0 @@
<?php
/**
* PHP WebocketServer from:
*
* Copyright (c) 2012, Adam Alexander
* All rights reserved.
*
* Adapted to PandoraFMS by Fco de Borja Sanchez <fborja.sanchez@artica.es>
* Compatible with PHP >= 7.0
*
* @category External library
* @package Pandora FMS
* @subpackage WebSocketServer
* @version 1.0.0
* @license See below
* @filesource https://github.com/ghedipunk/PHP-Websockets
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of PHP WebSockets nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
// Begin.
namespace \PandoraFMS\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;
}
}

View File

@ -0,0 +1,97 @@
<html>
<head>
<title>WebSocket</title>
<style type="text/css">
html,
body {
font: normal 0.9em arial, helvetica;
}
#log {
width: 600px;
height: 300px;
border: 1px solid #7f9db9;
overflow: auto;
}
#msg {
width: 400px;
}
</style>
<script type="text/javascript">
var socket;
function init() {
var host = "ws://172.16.0.2:8081/echobot"; // SET THIS TO YOUR SERVER
try {
socket = new WebSocket(host);
log("WebSocket - status " + socket.readyState);
socket.onopen = function(msg) {
log("Welcome - status " + this.readyState);
};
socket.onmessage = function(msg) {
log("Received: " + msg.data);
};
socket.onclose = function(msg) {
log("Disconnected - status " + this.readyState);
};
} catch (ex) {
log(ex);
}
$("msg").focus();
}
function send() {
var txt, msg;
txt = $("msg");
msg = txt.value;
if (!msg) {
alert("Message can not be empty");
return;
}
txt.value = "";
txt.focus();
try {
socket.send(msg);
log("Sent: " + msg);
} catch (ex) {
log(ex);
}
}
function quit() {
if (socket != null) {
log("Goodbye!");
socket.close();
socket = null;
}
}
function reconnect() {
quit();
init();
}
// Utilities
function $(id) {
return document.getElementById(id);
}
function log(msg) {
$("log").innerHTML += "<br>" + msg;
}
function onkey(event) {
if (event.keyCode == 13) {
send();
}
}
</script>
</head>
<body onload="init()">
<h3>WebSocket v2.00</h3>
<div id="log"></div>
<input id="msg" type="textbox" onkeypress="onkey(event)" />
<button onclick="send()">Send</button>
<button onclick="quit()">Quit</button>
<button onclick="reconnect()">Reconnect</button>
</body>
</html>

View File

@ -4,7 +4,7 @@ $host = '127.0.0.1';
$port = 8080;
$r = file_get_contents('http://'.$host.':'.$port.'/js/hterm.js');
$r .= file_get_contents('http://'.$host.':'.$port.'/hterms.js');
$r .= file_get_contents('http://'.$host.':'.$port.'/auth_token.js');
$gotty = file_get_contents('http://'.$host.':'.$port.'/js/gotty.js');
@ -18,11 +18,19 @@ $gotty = str_replace($url, $new, $gotty);
<!doctype html>
<html>
<head>
<title>GoTTY</title>
<style>body, #terminal {position: absolute; height: 100%; width: 100%; margin: 0px;}</style>
<title>Quick Shell experiment</title>
<style>#terminal {
height: 590px;
width: 100%;
margin: 0px;
padding: 0;
border: 1px solid red;
}
</style>
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<h1>Prueba</h1>
<div id="terminal"></div>
<script type="text/javascript">
<?php echo $r; ?>

View File

@ -4,190 +4,13 @@
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once __DIR__.'/WebSocketServer.php';
require_once __DIR__.'/WebSocketUser.php';
require_once __DIR__.'/WSProxy.php';
use \PandoraFMS\WebSocketServer;
use \PandoraFMS\WebSocketUser;
class MyUser extends WebSocketUser
{
public $myId;
function __construct($id, $socket)
{
parent::__construct($id, $socket);
$this->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');
$wsproxy = new WSProxy('0.0.0.0', '8081', '127.0.0.1', '8080');
try {
$echo->run();
echo "Server running \n";
$wsproxy->run();
} catch (Exception $e) {
$echo->stdout($e->getMessage());
$wsproxy->stdout($e->getMessage());
}