Skip to content

Commit

Permalink
Added proxy support.
Browse files Browse the repository at this point in the history
A very simple/crude reverse proxy has been created. It does not scale, but works for smaller stuff.

See issue #11 for more information on some of the limitations.
  • Loading branch information
Torxed committed Jul 16, 2020
1 parent 638ae52 commit 6848798
Showing 1 changed file with 62 additions and 3 deletions.
65 changes: 62 additions & 3 deletions slimHTTP.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ class Events():
CLIENT_UPGRADE_ISSUE = 0b01000100
CLIENT_URL_ROUTED = 0b01000101
CLIENT_DATA_FRAGMENTED = 0b01000110
CLIENT_RESPONSE_PROXY_DATA = 0b01000111

WS_CLIENT_DATA = 0b11000000
WS_CLIENT_REQUEST = 0b11000001
Expand All @@ -320,6 +321,8 @@ class Events():

NOT_YET_IMPLEMENTED = 0b00000000

DATA_EVENTS = (CLIENT_RESPONSE_DATA, CLIENT_URL_ROUTED, CLIENT_RESPONSE_PROXY_DATA)

def convert(_int):
def_map = {v: k for k, v in Events.__dict__.items() if not k.startswith('__') and k != 'convert'}
return def_map[_int] if _int in def_map else None
Expand Down Expand Up @@ -503,11 +506,19 @@ def log(self, *args, **kwargs):
"""
print('[LOG] '.join([str(x) for x in args]))

# TODO: Dump raw requests/logs to a .pcap: (Optional, if scapy is precent)
#
# from scapy.all import wrpcap, Ether, IP, UDP
# packet = Ether() / IP(dst="1.2.3.4") / UDP(dport=123)
# wrpcap('foo.pcap', [packet])

def check_config(self, conf):
"""
Makes sure that the given configuration *(either upon startup via `**kwargs` or
during annotation override of configuration (`@http.configuration`))* is correct.
#TODO: Verify that 'proxy' mode endpoints aren't ourself, because that **will** hand slimHTTP. (https://github.com/Torxed/slimHTTP/issues/11)
:param conf: Dictionary representing a valid configuration. #TODO: Add a doc on documentation :P
:type conf: dict
"""
Expand Down Expand Up @@ -725,7 +736,7 @@ def route_handler(request):
.. note:: The above example will handle both GET and POST (any user-defined method actually)
.. warning:: If routes end with a `/`, they will be appended by `/[index]` and treated as a normal folder.
This means that static routes ending on `/` should be defined as `@app.route('/example/index.html')` rather than `@app.route('/example/')`.
This means that static routes ending on `/` should be defined as `@app.route('/example/index.html')` rather than `@app.route('/example/')`.
:param timeout: is in seconds
:type timeout: integer
Expand Down Expand Up @@ -822,7 +833,7 @@ def do_the_dance(self, fileno):
for response_event, *client_response_data in client_parsed_data[0].parse():
yield (response_event, client_response_data)

if response_event in (Events.CLIENT_RESPONSE_DATA, Events.CLIENT_URL_ROUTED) and client_response_data:
if response_event in Events.DATA_EVENTS and client_response_data:
if fileno in self.sockets:
if type(client_response_data[0]) is bytes:
self.sockets[fileno].send(client_response_data[0])
Expand Down Expand Up @@ -989,6 +1000,52 @@ def has_data(self):
def __repr__(self):
return f'<slimhttpd.HTTP_CLIENT_IDENTITY @ {self.address}:{self.source_port}>'

class HTTP_PROXY_REQUEST():
"""
Turns a HTTP Request into a Reverse Proxy request,
based on :class:`~slimHTTP.HTTP_REQUEST` identifying the requested host
to be a vhost with the appropriate `vhost` configuration for a reverse proxy.
"""
def __init__(self, CLIENT_IDENTITY, ORIGINAL_REQUEST):
self.CLIENT_IDENTITY = CLIENT_IDENTITY
self.ORIGINAL_REQUEST = ORIGINAL_REQUEST
self.config = self.CLIENT_IDENTITY.server.config
self.vhost = self.ORIGINAL_REQUEST.vhost

def __repr__(self, *args, **kwargs):
return f"<HTTP_PROXY_REQUEST client={self.CLIENT_IDENTITY} vhost={self.vhost}, proxy={self.config['vhosts'][self.vhost]['proxy']}>"

def parse(self):
poller = epoll()
sock = socket()
sock.settimeout(0.2)
proxy, proxy_port = self.config['vhosts'][self.vhost]['proxy'].split(':',1)
try:
sock.connect((proxy, int(proxy_port)))
except:
# We timed out, or the proxy was to slow to respond.
self.CLIENT_IDENTITY.server.log(f'{self} was to slow to connect/respond. Aborting proxy and sending back empty response to requester.')
return None
sock.settimeout(None)
if 'ssl' in self.config['vhosts'][self.vhost] and self.config['vhosts'][self.vhost]['ssl']:
context = ssl.create_default_context()
sock = context.wrap_socket(sock, server_hostname=proxy)
poller.register(sock.fileno(), EPOLLIN)
sock.send(self.CLIENT_IDENTITY.buffer)
self.CLIENT_IDENTITY.server.log(f'Request sent for: {self}')

data_buffer = b''
# TODO: this will lock the entire application,
# some how we'll have to improve this.
# But for small scale stuff this will do, at least for testing.
while poller.poll(0.2):
tmp = sock.recv(8192)
if len(tmp) <= 0: break
data_buffer += tmp
poller.unregister(sock.fileno())
sock.close()
return data_buffer

class HTTP_REQUEST():
"""
General request formatter passed as an object throughout the event stack.
Expand Down Expand Up @@ -1068,6 +1125,7 @@ def parse(self):
"""
if b'\r\n\r\n' in self.CLIENT_IDENTITY.buffer:
header, remainder = self.CLIENT_IDENTITY.buffer.split(b'\r\n\r\n', 1) # Copy and split the data so we're not working on live data.
self.CLIENT_IDENTITY.server.log(f'Request being parsed: {header[:2048]} ({remainder[:2048]})')
self.payload = b''

self.build_request_headers(header)
Expand Down Expand Up @@ -1108,7 +1166,8 @@ def parse(self):
# Check vhost specifics:
if self.vhost:
if 'proxy' in _config['vhosts'][self.vhost]:
pass
proxy_object = HTTP_PROXY_REQUEST(self.CLIENT_IDENTITY, self)
yield (Events.CLIENT_RESPONSE_PROXY_DATA, proxy_object.parse())
elif 'module' in _config['vhosts'][self.vhost]:
absolute_path = os.path.abspath(_config['vhosts'][self.vhost]['module'])

Expand Down

0 comments on commit 6848798

Please sign in to comment.