first initial proof-of-concept

This commit is contained in:
Maximilian Hils 2015-07-24 13:31:55 +02:00
parent a9fcef868b
commit 863113f989
3 changed files with 282 additions and 8 deletions

181
libmproxy/proxy/layer.py Normal file
View File

@ -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")

View File

@ -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

View File

@ -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: