From a2bf19125a92a4fa372708cf2cda7887ab62ac76 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer Date: Sat, 11 Jul 2015 23:47:43 +0200 Subject: [PATCH] refactor protocol-related pathod methods --- libpathod/pathod.py | 185 ++---------------------------- libpathod/protocols/__init__.py | 1 + libpathod/protocols/http.py | 110 ++++++++++++++++++ libpathod/protocols/http2.py | 20 ++++ libpathod/protocols/websockets.py | 54 +++++++++ 5 files changed, 195 insertions(+), 175 deletions(-) create mode 100644 libpathod/protocols/__init__.py create mode 100644 libpathod/protocols/http.py create mode 100644 libpathod/protocols/http2.py create mode 100644 libpathod/protocols/websockets.py diff --git a/libpathod/pathod.py b/libpathod/pathod.py index daad64be3..31d821c2a 100644 --- a/libpathod/pathod.py +++ b/libpathod/pathod.py @@ -8,7 +8,7 @@ import time from netlib import tcp, http, http2, wsgi, certutils, websockets, odict -from . import version, app, language, utils, log +from . import version, app, language, utils, log, protocols import language.http import language.actions import language.exceptions @@ -88,10 +88,6 @@ class PathodHandler(tcp.BaseHandler): self.sni = connection.get_servername() def http_serve_crafted(self, crafted, logctx): - """ - This method is HTTP/1 and HTTP/2 capable. - """ - error, crafted = self.server.check_policy( crafted, self.settings ) @@ -116,114 +112,9 @@ class PathodHandler(tcp.BaseHandler): return None, response_log return self.handle_http_request, response_log - def handle_websocket(self, logger): - while True: - with logger.ctx() as lg: - started = time.time() - try: - frm = websockets.Frame.from_file(self.rfile) - except tcp.NetLibIncomplete as e: - lg("Error reading websocket frame: %s" % e) - break - ended = time.time() - lg(frm.human_readable()) - retlog = dict( - type="inbound", - protocol="websockets", - started=started, - duration=ended - started, - frame=dict( - ), - cipher=None, - ) - if self.ssl_established: - retlog["cipher"] = self.get_current_cipher() - self.addlog(retlog) - ld = language.websockets.NESTED_LEADER - if frm.payload.startswith(ld): - nest = frm.payload[len(ld):] - try: - wf_gen = language.parse_websocket_frame(nest) - except language.exceptions.ParseException as v: - logger.write( - "Parse error in reflected frame specifcation:" - " %s" % v.msg - ) - return None, None - for frm in wf_gen: - with logger.ctx() as lg: - frame_log = language.serve( - frm, - self.wfile, - self.settings - ) - lg("crafting websocket spec: %s" % frame_log["spec"]) - self.addlog(frame_log) - return self.handle_websocket, None - - def handle_http_connect(self, connect, lg): - """ - This method is HTTP/1 only. - - Handle a CONNECT request. - """ - http.read_headers(self.rfile) - self.wfile.write( - 'HTTP/1.1 200 Connection established\r\n' + - ('Proxy-agent: %s\r\n' % version.NAMEVERSION) + - '\r\n' - ) - self.wfile.flush() - if not self.server.ssloptions.not_after_connect: - try: - cert, key, chain_file_ = self.server.ssloptions.get_cert( - connect[0] - ) - self.convert_to_ssl( - cert, - key, - handle_sni=self._handle_sni, - request_client_cert=self.server.ssloptions.request_client_cert, - cipher_list=self.server.ssloptions.ciphers, - method=self.server.ssloptions.ssl_version, - alpn_select=self.server.ssloptions.alpn_select, - ) - except tcp.NetLibError as v: - s = str(v) - lg(s) - return None, dict(type="error", msg=s) - return self.handle_http_request, None - - def handle_http_app(self, method, path, headers, content, lg): - """ - This method is HTTP/1 only. - - Handle a request to the built-in app. - """ - if self.server.noweb: - crafted = self.make_http_error_response("Access Denied") - language.serve(crafted, self.wfile, self.settings) - return None, dict( - type="error", - msg="Access denied: web interface disabled" - ) - lg("app: %s %s" % (method, path)) - req = wsgi.Request("http", method, path, headers, content) - flow = wsgi.Flow(self.address, req) - sn = self.connection.getsockname() - a = wsgi.WSGIAdaptor( - self.server.app, - sn[0], - self.server.address.port, - version.NAMEVERSION - ) - a.serve(flow, self.wfile) - return self.handle_http_request, None def handle_http_request(self, logger): """ - This method is HTTP/1 and HTTP/2 capable. - Returns a (handler, log) tuple. handler: Handler for the next request, or None to disconnect @@ -231,14 +122,13 @@ class PathodHandler(tcp.BaseHandler): """ with logger.ctx() as lg: if self.use_http2: - self.protocol.perform_server_connection_preface() stream_id, headers, body = self.protocol.read_request() method = headers[':method'] path = headers[':path'] headers = odict.ODict(headers) httpversion = "" else: - req = self.read_http_request(lg) + req = self.protocol.read_request(lg) if 'next_handle' in req: return req['next_handle'] if 'errors' in req: @@ -328,68 +218,15 @@ class PathodHandler(tcp.BaseHandler): lg ) if nexthandler and websocket_key: - return self.handle_websocket, retlog + self.protocol = protocols.websockets.WebsocketsProtocol(self) + return self.protocol.handle_websocket, retlog else: return nexthandler, retlog else: - return self.handle_http_app(method, path, headers, body, lg) - - def read_http_request(self, lg): - """ - This method is HTTP/1 only. - """ - line = http.get_request_line(self.rfile) - if not line: - # Normal termination - return dict() - - m = utils.MemBool() - if m(http.parse_init_connect(line)): - return dict(next_handle=self.handle_http_connect(m.v, lg)) - elif m(http.parse_init_proxy(line)): - method, _, _, _, path, httpversion = m.v - elif m(http.parse_init_http(line)): - method, path, httpversion = m.v - else: - s = "Invalid first line: %s" % repr(line) - lg(s) - return dict(errors=dict(type="error", msg=s)) - - headers = http.read_headers(self.rfile) - if headers is None: - s = "Invalid headers" - lg(s) - return dict(errors=dict(type="error", msg=s)) - - try: - body = http.read_http_body( - self.rfile, - headers, - None, - method, - None, - True, - ) - except http.HttpError as s: - s = str(s) - lg(s) - return dict(errors=dict(type="error", msg=s)) - - return dict( - method=method, - path=path, - headers=headers, - body=body, - httpversion=httpversion) + return self.protocol.handle_http_app(method, path, headers, body, lg) def make_http_error_response(self, reason, body=None): - """ - This method is HTTP/1 and HTTP/2 capable. - """ - if self.use_http2: - resp = language.http2.make_error_response(reason, body) - else: - resp = language.http.make_error_response(reason, body) + resp = self.protocol.make_error_response(reason, body) resp.is_error_response = True return resp @@ -421,14 +258,12 @@ class PathodHandler(tcp.BaseHandler): alp = self.get_alpn_proto_negotiated() if alp == http2.HTTP2Protocol.ALPN_PROTO_H2: - self.protocol = http2.HTTP2Protocol( - self, is_server=True, dump_frames=self.http2_framedump - ) + self.protocol = protocols.http2.HTTP2Protocol(self) self.use_http2 = True - # if not self.protocol: - # # TODO: create HTTP or Websockets protocol - # self.protocol = None + if not self.protocol: + self.protocol = protocols.http.HTTPProtocol(self) + lr = self.rfile if self.server.logreq else None lw = self.wfile if self.server.logresp else None logger = log.ConnectionLogger(self.logfp, self.server.hexdump, lr, lw) diff --git a/libpathod/protocols/__init__.py b/libpathod/protocols/__init__.py new file mode 100644 index 000000000..1a8c7dabb --- /dev/null +++ b/libpathod/protocols/__init__.py @@ -0,0 +1 @@ +from . import http, http2, websockets diff --git a/libpathod/protocols/http.py b/libpathod/protocols/http.py new file mode 100644 index 000000000..bccdc786a --- /dev/null +++ b/libpathod/protocols/http.py @@ -0,0 +1,110 @@ +from netlib import tcp, http, http2, wsgi, certutils, websockets, odict +from .. import version, app, language, utils, log + +class HTTPProtocol: + + def __init__(self, pathod_handler): + self.pathod_handler = pathod_handler + + def make_error_response(self, reason, body): + return language.http.make_error_response(reason, body) + + def handle_http_app(self, method, path, headers, content, lg): + """ + Handle a request to the built-in app. + """ + if self.pathod_handler.server.noweb: + crafted = self.pathod_handler.make_http_error_response("Access Denied") + language.serve(crafted, self.pathod_handler.wfile, self.pathod_handler.settings) + return None, dict( + type="error", + msg="Access denied: web interface disabled" + ) + lg("app: %s %s" % (method, path)) + req = wsgi.Request("http", method, path, headers, content) + flow = wsgi.Flow(self.pathod_handler.address, req) + sn = self.pathod_handler.connection.getsockname() + a = wsgi.WSGIAdaptor( + self.pathod_handler.server.app, + sn[0], + self.pathod_handler.server.address.port, + version.NAMEVERSION + ) + a.serve(flow, self.pathod_handler.wfile) + return self.pathod_handler.handle_http_request, None + + def handle_http_connect(self, connect, lg): + """ + Handle a CONNECT request. + """ + http.read_headers(self.pathod_handler.rfile) + self.pathod_handler.wfile.write( + 'HTTP/1.1 200 Connection established\r\n' + + ('Proxy-agent: %s\r\n' % version.NAMEVERSION) + + '\r\n' + ) + self.pathod_handler.wfile.flush() + if not self.pathod_handler.server.ssloptions.not_after_connect: + try: + cert, key, chain_file_ = self.pathod_handler.server.ssloptions.get_cert( + connect[0] + ) + self.pathod_handler.convert_to_ssl( + cert, + key, + handle_sni=self.pathod_handler._handle_sni, + request_client_cert=self.pathod_handler.server.ssloptions.request_client_cert, + cipher_list=self.pathod_handler.server.ssloptions.ciphers, + method=self.pathod_handler.server.ssloptions.ssl_version, + alpn_select=self.pathod_handler.server.ssloptions.alpn_select, + ) + except tcp.NetLibError as v: + s = str(v) + lg(s) + return None, dict(type="error", msg=s) + return self.pathod_handler.handle_http_request, None + + def read_request(self, lg): + line = http.get_request_line(self.pathod_handler.rfile) + if not line: + # Normal termination + return dict() + + m = utils.MemBool() + if m(http.parse_init_connect(line)): + return dict(next_handle=self.handle_http_connect(m.v, lg)) + elif m(http.parse_init_proxy(line)): + method, _, _, _, path, httpversion = m.v + elif m(http.parse_init_http(line)): + method, path, httpversion = m.v + else: + s = "Invalid first line: %s" % repr(line) + lg(s) + return dict(errors=dict(type="error", msg=s)) + + headers = http.read_headers(self.pathod_handler.rfile) + if headers is None: + s = "Invalid headers" + lg(s) + return dict(errors=dict(type="error", msg=s)) + + try: + body = http.read_http_body( + self.pathod_handler.rfile, + headers, + None, + method, + None, + True, + ) + except http.HttpError as s: + s = str(s) + lg(s) + return dict(errors=dict(type="error", msg=s)) + + return dict( + method=method, + path=path, + headers=headers, + body=body, + httpversion=httpversion) diff --git a/libpathod/protocols/http2.py b/libpathod/protocols/http2.py new file mode 100644 index 000000000..29c4e5560 --- /dev/null +++ b/libpathod/protocols/http2.py @@ -0,0 +1,20 @@ +from netlib import tcp, http, http2, wsgi, certutils, websockets, odict +from .. import version, app, language, utils, log + +class HTTP2Protocol: + + def __init__(self, pathod_handler): + self.pathod_handler = pathod_handler + self.wire_protocol = http2.HTTP2Protocol( + self.pathod_handler, is_server=True, dump_frames=self.pathod_handler.http2_framedump + ) + + def make_error_response(self, reason, body): + return language.http2.make_error_response(reason, body) + + def read_request(self): + self.wire_protocol.perform_server_connection_preface() + return self.wire_protocol.read_request() + + def create_response(self, code, stream_id, headers, body): + return self.wire_protocol.create_response(code, stream_id, headers, body) diff --git a/libpathod/protocols/websockets.py b/libpathod/protocols/websockets.py new file mode 100644 index 000000000..334f9e9c5 --- /dev/null +++ b/libpathod/protocols/websockets.py @@ -0,0 +1,54 @@ +import time + +from netlib import tcp, http, http2, wsgi, certutils, websockets, odict +from .. import version, app, language, utils, log + +class WebsocketsProtocol: + + def __init__(self, pathod_handler): + self.pathod_handler = pathod_handler + + def handle_websocket(self, logger): + while True: + with logger.ctx() as lg: + started = time.time() + try: + frm = websockets.Frame.from_file(self.pathod_handler.rfile) + except tcp.NetLibIncomplete as e: + lg("Error reading websocket frame: %s" % e) + break + ended = time.time() + lg(frm.human_readable()) + retlog = dict( + type="inbound", + protocol="websockets", + started=started, + duration=ended - started, + frame=dict( + ), + cipher=None, + ) + if self.pathod_handler.ssl_established: + retlog["cipher"] = self.pathod_handler.get_current_cipher() + self.pathod_handler.addlog(retlog) + ld = language.websockets.NESTED_LEADER + if frm.payload.startswith(ld): + nest = frm.payload[len(ld):] + try: + wf_gen = language.parse_websocket_frame(nest) + except language.exceptions.ParseException as v: + logger.write( + "Parse error in reflected frame specifcation:" + " %s" % v.msg + ) + return None, None + for frm in wf_gen: + with logger.ctx() as lg: + frame_log = language.serve( + frm, + self.pathod_handler.wfile, + self.pathod_handler.settings + ) + lg("crafting websocket spec: %s" % frame_log["spec"]) + self.pathod_handler.addlog(frame_log) + return self.handle_websocket, None