From 863113f989ee2a089c86b06a88a22e92d840348b Mon Sep 17 00:00:00 2001 From: Maximilian Hils Date: Fri, 24 Jul 2015 13:31:55 +0200 Subject: first initial proof-of-concept --- libmproxy/proxy/layer.py | 181 +++++++++++++++++++++++++++++++++++++++++++++ libmproxy/proxy/message.py | 40 ++++++++++ libmproxy/proxy/server.py | 69 +++++++++++++++-- 3 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 libmproxy/proxy/layer.py create mode 100644 libmproxy/proxy/message.py (limited to 'libmproxy/proxy') diff --git a/libmproxy/proxy/layer.py b/libmproxy/proxy/layer.py new file mode 100644 index 00000000..500fb6ba --- /dev/null +++ b/libmproxy/proxy/layer.py @@ -0,0 +1,181 @@ +from __future__ import (absolute_import, print_function, division, unicode_literals) +from libmproxy.protocol.tcp import TCPHandler +from libmproxy.proxy.connection import ServerConnection +from netlib import tcp +from .primitives import Socks5ProxyMode, ProxyError, Log +from .message import Connect, Reconnect, ChangeServer + + +""" +mitmproxy protocol architecture + +In mitmproxy, protocols are implemented as a set of layers, which are composed on top each other. +For example, the following scenarios depict possible scenarios (lowest layer first): + +Transparent HTTP proxy, no SSL: + TransparentModeLayer + HttpLayer + +Regular proxy, CONNECT request with WebSockets over SSL: + RegularModeLayer + HttpLayer + SslLayer + WebsocketLayer (or TcpLayer) + +Automated protocol detection by peeking into the buffer: + TransparentModeLayer + AutoLayer + SslLayer + AutoLayer + Http2Layer + +Communication between layers is done as follows: + - lower layers provide context information to higher layers + - higher layers can "yield" commands to lower layers, + which are propagated until they reach a suitable layer. + +Further goals: + - Connections should always be peekable to make automatic protocol detection work. + - Upstream connections should be established as late as possible; + inline scripts shall have a chance to handle everything locally. +""" + + +class ProxyError2(Exception): + def __init__(self, message, cause=None): + super(ProxyError2, self).__init__(message) + self.cause = cause + + +class RootContext(object): + """ + The outmost context provided to the root layer. + As a consequence, every layer has .client_conn, .channel and .config. + """ + + def __init__(self, client_conn, config, channel): + self.client_conn = client_conn # Client Connection + self.channel = channel # provides .ask() method to communicate with FlowMaster + self.config = config # Proxy Configuration + + def __getattr__(self, name): + """ + Accessing a nonexisting attribute does not throw an error but returns None instead. + """ + return None + + +class LayerCodeCompletion(object): + """ + Dummy class that provides type hinting in PyCharm, which simplifies development a lot. + """ + def __init__(self): + if True: + return + self.config = None + """@type: libmproxy.proxy.config.ProxyConfig""" + self.client_conn = None + """@type: libmproxy.proxy.connection.ClientConnection""" + self.channel = None + """@type: libmproxy.controller.Channel""" + + +class Layer(LayerCodeCompletion): + def __init__(self, ctx): + """ + Args: + ctx: The (read-only) higher layer. + """ + super(Layer, self).__init__() + self.ctx = ctx + + def __call__(self): + """ + Logic of the layer. + Raises: + ProxyError2 in case of protocol exceptions. + """ + raise NotImplementedError + + def __getattr__(self, name): + """ + Attributes not present on the current layer may exist on a higher layer. + """ + return getattr(self.ctx, name) + + def log(self, msg, level, subs=()): + full_msg = [ + "%s:%s: %s" % + (self.client_conn.address.host, + self.client_conn.address.port, + msg)] + for i in subs: + full_msg.append(" -> " + i) + full_msg = "\n".join(full_msg) + self.channel.tell("log", Log(full_msg, level)) + + +class _ServerConnectionMixin(object): + def __init__(self): + self._server_address = None + self.server_conn = None + + def _handle_message(self, message): + if message == Reconnect: + self._disconnect() + self._connect() + return True + elif message == Connect: + self._connect() + return True + elif message == ChangeServer: + raise NotImplementedError + return False + + def _disconnect(self): + """ + Deletes (and closes) an existing server connection. + """ + self.log("serverdisconnect", "debug", [repr(self.server_conn.address)]) + self.server_conn.finish() + self.server_conn.close() + # self.channel.tell("serverdisconnect", self) + self.server_conn = None + + def _connect(self): + self.log("serverconnect", "debug", [repr(self.server_conn.address)]) + try: + self.server_conn.connect() + except tcp.NetLibError as e: + raise ProxyError2("Server connection to '%s' failed: %s" % (self._server_address, repr(e)), e) + + def _set_address(self, address): + a = tcp.Address.wrap(address) + self.log("Set new server address: " + repr(a), "debug") + self.server_conn = ServerConnection(a) + + +class Socks5IncomingLayer(Layer, _ServerConnectionMixin): + def __call__(self): + try: + s5mode = Socks5ProxyMode(self.config.ssl_ports) + address = s5mode.get_upstream_server(self.client_conn)[2:] + except ProxyError as e: + raise ProxyError2(str(e), e) + + self._set_address(address) + + layer = TcpLayer(self) + for message in layer(): + if not self._handle_message(message): + yield message + + +class TcpLayer(Layer): + def __call__(self): + yield Connect() + tcp_handler = TCPHandler(self) + tcp_handler.handle_messages() + + def establish_server_connection(self): + print("establish server conn called") \ No newline at end of file diff --git a/libmproxy/proxy/message.py b/libmproxy/proxy/message.py new file mode 100644 index 00000000..a667123c --- /dev/null +++ b/libmproxy/proxy/message.py @@ -0,0 +1,40 @@ +""" +This module contains all valid messages layers can send to the underlying layers. +""" + + +class _Message(object): + def __eq__(self, other): + # Allow message == Connect checks. + # FIXME: make Connect == message work. + if isinstance(self, other): + return True + return self is other + + +class Connect(_Message): + """ + Connect to the server + """ + + +class Reconnect(_Message): + """ + Re-establish the server connection + """ + + +class ChangeServer(_Message): + """ + Change the upstream server. + """ + + def __init__(self, address, server_ssl, sni, depth=1): + self.address = address + self.server_ssl = server_ssl + self.sni = sni + + # upstream proxy scenario: you may want to change either the final target or the upstream proxy. + # We can express this neatly as the "nth-server-providing-layer" + # ServerConnection could get a `via` attribute. + self.depth = depth diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py index 2f6ee061..a45276d4 100644 --- a/libmproxy/proxy/server.py +++ b/libmproxy/proxy/server.py @@ -1,13 +1,14 @@ -from __future__ import absolute_import +from __future__ import absolute_import, print_function +import traceback +import sys import socket -from OpenSSL import SSL from netlib import tcp +from . import layer from .primitives import ProxyServerError, Log, ProxyError from .connection import ClientConnection, ServerConnection from ..protocol.handle import protocol_handler -from .. import version class DummyServer: @@ -46,7 +47,7 @@ class ProxyServer(tcp.TCPServer): self.channel = channel def handle_client_connection(self, conn, client_address): - h = ConnectionHandler( + h = ConnectionHandler2( self.config, conn, client_address, @@ -56,6 +57,57 @@ class ProxyServer(tcp.TCPServer): h.finish() +class ConnectionHandler2: + # FIXME: parameter ordering + # FIXME: remove server attribute + def __init__(self, config, client_conn, client_address, server, channel): + self.config = config + """@type: libmproxy.proxy.config.ProxyConfig""" + self.client_conn = ClientConnection( + client_conn, + client_address, + server) + """@type: libmproxy.proxy.connection.ClientConnection""" + self.channel = channel + """@type: libmproxy.controller.Channel""" + + def handle(self): + self.log("clientconnect", "info") + + root_context = layer.RootContext( + self.client_conn, + self.config, + self.channel + ) + root_layer = layer.Socks5IncomingLayer(root_context) + + try: + for message in root_layer(): + print("Root layer receveived: %s" % message) + except layer.ProxyError2 as e: + self.log(e, "info") + except Exception: + self.log(traceback.format_exc(), "error") + print(traceback.format_exc(), file=sys.stderr) + print("mitmproxy has crashed!", file=sys.stderr) + print("Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy", file=sys.stderr) + + def finish(self): + self.client_conn.finish() + + def log(self, msg, level, subs=()): + # FIXME: Duplicate code + full_msg = [ + "%s:%s: %s" % + (self.client_conn.address.host, + self.client_conn.address.port, + msg)] + for i in subs: + full_msg.append(" -> " + i) + full_msg = "\n".join(full_msg) + self.channel.tell("log", Log(full_msg, level)) + + class ConnectionHandler: def __init__( self, @@ -74,6 +126,7 @@ class ConnectionHandler: self.server_conn = None """@type: libmproxy.proxy.connection.ServerConnection""" self.channel = channel + """@type: libmproxy.controller.Channel""" self.conntype = "http" @@ -144,9 +197,9 @@ class ConnectionHandler: import sys self.log(traceback.format_exc(), "error") - print >> sys.stderr, traceback.format_exc() - print >> sys.stderr, "mitmproxy has crashed!" - print >> sys.stderr, "Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy" + print(traceback.format_exc(), file=sys.stderr) + print("mitmproxy has crashed!", file=sys.stderr) + print("Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy", file=sys.stderr) finally: # Make sure that we close the server connection in any case. # The client connection is closed by the ProxyServer and does not @@ -201,7 +254,7 @@ class ConnectionHandler: "serverconnect", "debug", [ "%s:%s" % self.server_conn.address()[ - :2]]) + :2]]) if ask: self.channel.ask("serverconnect", self) try: -- cgit v1.2.3