diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..e892ee722357afacacda556c7fdb951d057d636b --- /dev/null +++ b/setup.py @@ -0,0 +1,90 @@ +"""A setuptools based setup module. + +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/pypa/sampleproject +""" + +# Always prefer setuptools over distutils +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) + +long_description = 'Transparent WebSocket Proxy' + +setup( + name='strudelv2_tws', + + version='0.0.1', + + description=long_description, + long_description=long_description, + + # The project's main homepage. + url='backend', + + # Author details + author='Chris Hines', + author_email='help@massive.org.au', + + # Choose your license + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + + # Indicate who your project is intended for + 'Intended Audience :: Office workers', + 'Topic :: Software Development :: Build Tools', + + # Pick your license as you wish (should match "license" above) + 'License :: OSI Approved :: MIT License', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + + # What does your project relate to? + keywords='', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + packages=find_packages(exclude=['contrib', 'docs', 'tests']), + + # Alternatively, if you want to distribute just a my_module.py, uncomment + # this: + # py_modules=["my_module"], + + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=[ + ], + + + data_files = [('',[])], + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. + entry_points={ + 'console_scripts': [ 'twsproxy=twsproxy:main'], + 'gui_scripts': [ ] + }, +) diff --git a/twsproxy/__init__.py b/twsproxy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..26181f58de7eac67a281dbee37d4feefa5f1933b --- /dev/null +++ b/twsproxy/__init__.py @@ -0,0 +1,253 @@ +import threading +import socket, array +import select +import logging +TES = 'http://localhost:8080/' +failthresh = 10 +class TWSProxy(threading.Thread): + + TIMEOUT = 10 + AUTHTIMEOUT = 60 + MAXBUFF = 8192 + MAXHEADERS = 8192 + + def __init__(self,socket): + super().__init__() + self.csock = socket + self.ssock = None + self.authtok = None + self._timeout = 10 + + + + def inittws(self,closed): + """ Read from the client socket until one of + 1) We can verify the request based on headers + 2) We hit a timeout (i.e. no more headers will be sent and the client is waiting for a response) + If we can verify + open a new socket to the server and return this + """ + + logger = logging.getLogger() + bytessofar = 0 + header=bytearray(TWSProxy.MAXHEADERS) + keepreading = True + initcount=0 + while keepreading: + initcount=initcount+1 + r,w,e = select.select([self.csock],[],[],5) + if len(r) > 0: + partial = self.csock.recv(TWSProxy.MAXBUFF) + header[bytessofar:bytessofar+len(partial)] = partial + bytessofar = bytessofar + len(partial) + logger.debug('inittws, checking headers') + port = TWSProxy.verifyauth(header[0:bytessofar]) + if port is not None: + logger.debug('inittws, found auth token and got port {}'.format(port)) + keepreading = False + if port is None: + logger.debug('inittws, no authtok found in the first {} bytes'.format(bytessofar)) + else: + logger.debug('inittws, select returned with no more info, verifying headers for the last time') + port = TWSProxy.verifyauth(header[0:bytessofar]) + keepreading = False + if initcount > failthresh: + logger.debug('inittws, checked headers enough times, got {} bytes with no success'.format(bytessofar)) + keepreading = False + + if port is not None: + self.ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ssock.setblocking(True) + try: + self.ssock.connect(('127.0.0.1',port)) + if bytessofar > 0: + logger.debug('inittws: returning the initial {} bytes'.format(bytessofar)) + return (header,bytessofar) + #TWSProxy.reliablesend(self.ssock,header[0:bytessofar],bytessofar) + except ConnectionRefusedError as e: + logger.error('inittws, got my tokens, got my port attempted to send data and it all went pear shaped') + self.ssock.close() + self.csock.close() + closed.set() + else: + logger.debug('inittws, unable to determine correct port, closing connection') + self.csock.close() + closed.set() + + + + def run(self): + import logging + logger = logging.getLogger() + logger.debug('starting new thread listening on {}'.format(self.csock)) + initshutdown = threading.Event() + initshutdown.clear() + (header, bytessofar) = self.inittws(initshutdown) + logger.debug('connecting {} to {}'.format(self.csock,self.ssock)) + if initshutdown.isSet(): + logger.debug('NOT connecting {} inittws did not connect us'.format(self.csock)) + + t1 = None + t2 = None + logger.debug('testing initshutdown') + if not initshutdown.isSet(): + logger.debug('creating threads') +# TWSProxy.twosocks(self.csock,self.ssock,initshutdown) + t1 = threading.Thread(target=TWSProxy.sockcopy, args=(self.ssock, self.csock, initshutdown),name='s2c') + t2 = threading.Thread(target=TWSProxy.sockcopy, args=(self.csock, self.ssock, initshutdown),name='c2s') + t1.start() + t2.start() + logger.debug('connections made, threads listening, passing along the initial headers') + TWSProxy.reliablesend(self.ssock,header[0:bytessofar],bytessofar) + t1.join() + t2.join() + else: + logger.debug('not creating threads, shutdown is set') + if self.ssock is not None: + self.ssock.close() + if self.csock is not None: + self.csock.close() + + @staticmethod + def verifyauth(header): + import re + import requests + logger = logging.getLogger() + token = b'twsproxyauth=(?P<authtok>\w+)[\W|$]' + m = re.search(token,header) + if m: + authtok = m.groupdict()['authtok'] + logger.debug('authtok found {}'.format(authtok)) + s = requests.Session() + url = TES+'tunnelstat/'+authtok.decode() + try: + logger.debug('verify auth querying url {}'.format(url)) + r = s.get(url) + port = r.json() + if port is None: + logger.debug('authtok found but no tunnel to connect to {}'.format(authtok)) + else: + logger.debug('authtok found port found {}'.format(port)) + except: + logger.error('authtok found port found {}'.format(port)) + raise Exception('unable to get a port number for the authtok {}'.format(r.text)) + return port + return None +# if m: +# print('match verify!',m.group(0)) +# return 8888 +# else: +# return None + + + @staticmethod + def reliablesend(socket,buff,msglength): + totalsent = 0 + while totalsent < msglength: + sent = socket.send(buff[totalsent:]) + if sent == 0: + raise RuntimeError("socket connection broken") + totalsent = totalsent + sent + + @staticmethod + def twosocks(client,server,initshutdown): + import threading + logger=logging.getLogger() + closed = False + clientopen = True + serveropen = True + shuttype = socket.SHUT_RD + while serveropen or clientopen: + r,w,e = select.select([client,server],[],[],TWSProxy.TIMEOUT) + if client in r: + try: + buff = client.recv(TWSProxy.MAXBUFF) + msglength = len(buff) + if msglength > 0: + TWSProxy.reliablesend(server,buff,msglength) + else: + clientopen = False + server.shutdown(shuttype) + except: + clientopen = False + if server in r: + try: + buff = server.recv(TWSProxy.MAXBUFF) + msglength = len(buff) + if msglength > 0: + TWSProxy.reliablesend(client,buff,msglength) + else: + client.shutdown(shuttype) +# print("server closed socket for reading") + serveropen = False + except: + serveropen = False + # If the client has finished sending, and we've finished transmitting to the server + # The server may still have some data to transmit to the client + closed = False + while not closed: + r,w,e = select.select([server],[],[],TWSProxy.TIMEOUT) + buff = server.recv(TWSProxy.MAXBUFF) + msglength = len(buff) + if msglength > 0: + try: + TWSProxy.reliablesend(client,buff,msglength) + except BrokenPipeError as e: + pass + else: + closed = True + + + + @staticmethod + def sockcopy(src,dest,initshutdown): + shuttype = socket.SHUT_RD + import threading + logger = logging.getLogger() + logger.debug('sock copy started') + closed = False + name = threading.current_thread().name + failcount=0 + failthresh = 10 + while not closed and failcount < failthresh: + r,w,e = select.select([src],[],[],TWSProxy.TIMEOUT) + if len(r) > 0: + failcount=0 + buff = None + msglength = -1 + try: + buff = src.recv(TWSProxy.MAXBUFF) + if buff is None: + continue + except ConnectionResetError as e: + close = True + continue + except Exception as e: + import traceback + logger.error(traceback.format_exc()) +# closed = True + continue + msglength = len(buff) + if msglength > 0: + TWSProxy.reliablesend(dest,buff,msglength) + if msglength == 0: + dest.shutdown(shuttype) + initshutdown.set() + closed = True + else: + failcount=failcount+1 + + if failcount > failthresh: + dest.shutdown(shuttype) + initshutdown.set() + +def main(): + from . import server + import logging + logging.basicConfig(filename="/var/log/tws.log",format="%(asctime)s %(levelname)s:%(process)s: %(message)s") + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.debug("starting TWS proxy") + server = server.TWSServer() + logger.debug("initialised server object") + server.run() diff --git a/twsproxy/__init__.pyc b/twsproxy/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e090b12163034609d876d5519901ac3a9a07174b Binary files /dev/null and b/twsproxy/__init__.pyc differ diff --git a/twsproxy/__main__.py b/twsproxy/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..aee234e7afd81450fa13ea10d0706a7cc332edf7 --- /dev/null +++ b/twsproxy/__main__.py @@ -0,0 +1,10 @@ +from . import server +import logging +logging.basicConfig(filename="/var/log/tws.log",format="%(asctime)s %(levelname)s:%(process)s: %(message)s") +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) + +logger.debug("starting TWS proxy") +print("starting TWS proxy") +server = server.TWSServer() +server.run() diff --git a/twsproxy/server/__init__.py b/twsproxy/server/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..99ca5fb78823a33dfc1ba0fab5897c69ba420a32 --- /dev/null +++ b/twsproxy/server/__init__.py @@ -0,0 +1,41 @@ +import socket +from .. import TWSProxy +import logging +class TWSServer: + import socket + + LISTENPORT = 4000 + MAXCONN = 5 + + def run(self): + logger = logging.getLogger() + logger.debug("starting up server") + serversocket = socket.socket( + socket.AF_INET, socket.SOCK_STREAM) + #bind the socket to a public host, + # and a well-known port + for port in range(self.LISTENPORT,self.LISTENPORT+1): + try: + serversocket.bind(('127.0.0.1', port)) + #become a server socket + serversocket.listen(self.MAXCONN) + logger.debug("Server listening on port {}".format(port)) + break + except Exception as e: + print(e) + pass + openconnections = [] + logger.debug("waiting for a connection") + while 1: + (clientsocket, address) = serversocket.accept() + logger.debug("accepted connection on {}".format(clientsocket)) + clientsocket.setblocking(True) + tunnel = TWSProxy(clientsocket) + tunnel.daemon = True + tunnel.start() + openconnections.append(tunnel) + for c in openconnections: + if not c.is_alive(): + c.join() + openconnections.remove(c) + diff --git a/twsproxy/server/__init__.pyc b/twsproxy/server/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45968f93cdd9c9776ea3fbd704daed6312114aa6 Binary files /dev/null and b/twsproxy/server/__init__.pyc differ