228 lines
6.8 KiB
Python
Executable File
228 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
#
|
|
# @RDE_HEADER@
|
|
# $Id$
|
|
# $HeadURL$
|
|
# $Author$
|
|
# Created by Christian Fröbel, SLSE, 2016-07-07
|
|
#
|
|
|
|
import socket
|
|
from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler
|
|
import threading
|
|
from threading import Thread, Lock
|
|
from argparse import ArgumentParser
|
|
import time
|
|
|
|
# normale Farben
|
|
BLACK = '\x1b[30m'
|
|
RED = '\x1b[31m'
|
|
GREEN = '\x1b[32m'
|
|
ORANGE = '\x1b[33m'
|
|
BLUE = '\x1b[34m'
|
|
PURPLE = '\x1b[35m'
|
|
CYAN = '\x1b[36m'
|
|
LIGHT_GREY = '\x1b[37m'
|
|
|
|
# intensive Farben
|
|
DARK_GREY = '\x1b[30;1m'
|
|
LIGHT_RED = '\x1b[31;1m'
|
|
LIGHT_GREEN = '\x1b[32;1m'
|
|
YELLOW = '\x1b[33;1m'
|
|
LIGHT_BLUE = '\x1b[34;1m'
|
|
LIGHT_PURPLE = '\x1b[35;1m'
|
|
LIGHT_CYAN = '\x1b[36;1m'
|
|
WHITE = '\x1b[37;1m'
|
|
|
|
RESET = '\x1b[0m'
|
|
|
|
IN_MARKER = '{}<=={}'.format(LIGHT_PURPLE, RESET)
|
|
OUT_MARKER = '{}==>{}'.format(LIGHT_GREEN, RESET)
|
|
|
|
class _ThreadedTCPServer(ThreadingMixIn, TCPServer):
|
|
allow_reuse_address = True
|
|
|
|
|
|
def _parseCmdLine():
|
|
parser = ArgumentParser()
|
|
parser.add_argument('--listen', '-l', type=int, required=True, help='Listening Port für den Client')
|
|
parser.add_argument('--host', default='localhost', help='Server Host, default: localhost')
|
|
parser.add_argument('--port', '-p', type=int, required=True, help='Server Port')
|
|
parser.add_argument('--delay', '-d', type=int, default=0, help='Verzögerung von Requests und Responses in ms')
|
|
return parser.parse_args()
|
|
|
|
|
|
class _Acceptor(object):
|
|
def __init__(self):
|
|
self._doRun = True
|
|
self._handlers = []
|
|
self._mutex = Lock()
|
|
|
|
def run(self, connectionHandlerFactory):
|
|
acceptor = _ThreadedTCPServer(('', options.listen), connectionHandlerFactory)
|
|
acceptor.allow_reuse_address = True # entspricht socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) für den listening-Socket des TCPServers
|
|
acceptor.timeout = 1
|
|
|
|
while self._doRun:
|
|
acceptor.handle_request() # mit timeout (siehe __init__)
|
|
self._joinFinishedHandlers()
|
|
|
|
self._joinAllHandlers()
|
|
|
|
def initShutdown(self):
|
|
self._doRun = False
|
|
|
|
#
|
|
# @param handler: Eine _ConnectionHandler-Instanz
|
|
#
|
|
def registerHandler(self, handler):
|
|
with self._mutex:
|
|
self._handlers.append(handler)
|
|
|
|
def _joinFinishedHandlers(self):
|
|
with self._mutex:
|
|
for i in range(len(self._handlers)-1, -1, -1): # rückwärts durchlaufen, weil wir ggf. auch gleich das Element aus der Liste löschen wollen
|
|
handler = self._handlers[i]
|
|
handler.join(0) # ohne Timeout
|
|
if not handler.isAlive():
|
|
del self._handlers[i]
|
|
|
|
def _joinAllHandlers(self):
|
|
# Ohne Mutex, weil zu diesem Zeitpunkt keine weiteren Handler mehr gestartet werden können.
|
|
for handler in self._handlers:
|
|
handler.initShutdown()
|
|
for handler in self._handlers:
|
|
handler.join()
|
|
|
|
|
|
#
|
|
# Die Factory brauche ich, damit ich dem _MyHandler-Konstruktor
|
|
# meine Parameter mitgeben kann.
|
|
#
|
|
class _ConnectionHandlerFactory(object):
|
|
|
|
def __init__(self, acceptor, options):
|
|
self._acceptor = acceptor
|
|
self._options = options
|
|
|
|
def __call__(self, *args):
|
|
return _MyHandler(self._acceptor, self._options, *args)
|
|
|
|
|
|
#
|
|
# Wird für jede neue Verbindung instantiiert.
|
|
#
|
|
class _MyHandler(StreamRequestHandler):
|
|
def __init__(self, acceptor, options, *args):
|
|
self._options = options
|
|
self._delay = self._options.delay
|
|
self._acceptor = acceptor
|
|
self._thread = None
|
|
StreamRequestHandler.__init__(self, *args)
|
|
|
|
def handle(self):
|
|
print('new connection: {}:{}'.format(self.client_address[0], self.client_address[1]))
|
|
|
|
self._thread = threading.currentThread()
|
|
self._acceptor.registerHandler(self)
|
|
|
|
client = Client(self._options.host, self._options.port, self.wfile, self._options.delay)
|
|
client.start()
|
|
try:
|
|
self._readAndForward(client)
|
|
finally:
|
|
client.initShutdown()
|
|
client.join()
|
|
print('connection closed: {}:{}'.format(self.client_address[0], self.client_address[1]))
|
|
|
|
# Schließen des Sockets (der Leseseite), damit der Service aus dem blockierenden read() rauskommt.
|
|
def initShutdown(self):
|
|
_shutdownAndCloseSocket(self.request)
|
|
|
|
def join(self, timeout = None):
|
|
assert self._thread is not None, 'Darf erst gerufen werden, nachdem der Thread gestartet wurde'
|
|
self._thread.join(timeout)
|
|
|
|
def isAlive(self):
|
|
return self._thread.isAlive()
|
|
|
|
#
|
|
# Das Forwarding vom Client zum Server
|
|
#
|
|
def _readAndForward(self, client):
|
|
while True:
|
|
line = self.rfile.readline()
|
|
if len(line) == 0:
|
|
break # EOF
|
|
print('{} {}'.format(OUT_MARKER, line))
|
|
if self._delay > 0:
|
|
time.sleep(self._delay/1000)
|
|
client.send(line)
|
|
|
|
|
|
class Client(object):
|
|
#
|
|
# @param outFile: File-like, um an den Client des Proxys senden zu können.
|
|
#
|
|
def __init__(self, host, port, outFile, delay):
|
|
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
self._sock.connect((host, port))
|
|
self._outFile = outFile
|
|
self._delay = delay
|
|
self._thread = Thread(target=self._readAndForward)
|
|
|
|
def start(self):
|
|
self._thread.start()
|
|
|
|
def send(self, line):
|
|
self._sock.sendall(line)
|
|
|
|
def initShutdown(self):
|
|
_shutdownAndCloseSocket(self._sock)
|
|
|
|
def join(self):
|
|
self._thread.join()
|
|
|
|
#
|
|
# Das Forwarding vom Server zum Client
|
|
# Diese Methode wird in einem separaten Thread ausgeführt.
|
|
# Sie liest die Zeilen vom Server und leitet sie den Client weiter.
|
|
#
|
|
def _readAndForward(self):
|
|
inFile = self._sock.makefile('rb')
|
|
while True:
|
|
line = inFile.readline()
|
|
if len(line) == 0:
|
|
break # EOF
|
|
print('{} {}'.format(IN_MARKER, line))
|
|
if self._delay > 0:
|
|
time.sleep(self._delay/1000)
|
|
self._outFile.write(line)
|
|
|
|
|
|
def _shutdownAndCloseSocket(sock):
|
|
try:
|
|
sock.shutdown(socket.SHUT_RD) # wichtig, close() alleine reicht nicht
|
|
except socket.error:
|
|
print('socket error')
|
|
# kann passieren, wenn der Socket schon geschlossen wurde
|
|
pass
|
|
sock.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
options = _parseCmdLine()
|
|
acceptor = _Acceptor()
|
|
connectionHandlerFactory = _ConnectionHandlerFactory(acceptor, options)
|
|
t = Thread(target=acceptor.run, args=[connectionHandlerFactory])
|
|
t.start()
|
|
try:
|
|
while True:
|
|
t.join(0.5)
|
|
if not t.is_alive():
|
|
break
|
|
except KeyboardInterrupt:
|
|
acceptor.initShutdown()
|
|
|