diff options
| -rw-r--r-- | libmproxy/protocol/http.py | 60 | ||||
| -rw-r--r-- | libmproxy/protocol/primitives.py | 3 | ||||
| -rw-r--r-- | libmproxy/proxy.py | 4 | ||||
| -rw-r--r-- | test/test_protocol_http.py | 103 | ||||
| -rw-r--r-- | test/tservers.py | 8 | 
5 files changed, 137 insertions, 41 deletions
| diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index 8dcc21d7..95de6606 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -883,8 +883,7 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):              self.process_request(flow.request)              request_reply = self.c.channel.ask("request", flow.request) -            flow.server_conn = self.c.server_conn  # no further manipulation of self.c.server_conn beyond this point. -                                       # we can safely set it as the final attribute valueh here. +            flow.server_conn = self.c.server_conn              if request_reply is None or request_reply == KILL:                  return False @@ -894,14 +893,15 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):              else:                  flow.response = self.get_response_from_server(flow.request) +            flow.server_conn = self.c.server_conn  # no further manipulation of self.c.server_conn beyond this point +            # we can safely set it as the final attribute value here. +              self.c.log("response", [flow.response._assemble_first_line()])              response_reply = self.c.channel.ask("response", flow.response)              if response_reply is None or response_reply == KILL:                  return False -            raw = flow.response._assemble() -            self.c.client_conn.wfile.write(raw) -            self.c.client_conn.wfile.flush() +            self.c.client_conn.send(flow.response._assemble())              flow.timestamp_end = utils.timestamp()              if (http.connection_close(flow.request.httpversion, flow.request.headers) or @@ -909,7 +909,7 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):                  return False              if flow.request.form_in == "authority": -                self.ssl_upgrade(flow.request) +                self.ssl_upgrade()              self.restore_server()  # If the user has changed the target server on this connection,                                     # restore the original target server @@ -968,7 +968,27 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):          self.c.client_conn.wfile.write(html_content)          self.c.client_conn.wfile.flush() -    def ssl_upgrade(self, upstream_request=None): +    def hook_reconnect(self, upstream_request): +        self.c.log("Hook reconnect function") +        original_reconnect_func = self.c.server_reconnect + +        def reconnect_http_proxy(): +            self.c.log("Hooked reconnect function") +            self.c.log("Hook: Run original reconnect") +            original_reconnect_func(no_ssl=True) +            self.c.log("Hook: Write CONNECT request to upstream proxy", [upstream_request._assemble_first_line()]) +            self.c.server_conn.send(upstream_request._assemble()) +            self.c.log("Hook: Read answer to CONNECT request from proxy") +            resp = HTTPResponse.from_stream(self.c.server_conn.rfile, upstream_request.method) +            if resp.code != 200: +                raise ProxyError(resp.code, +                                 "Cannot reestablish SSL connection with upstream proxy: \r\n" + str(resp.headers)) +            self.c.log("Hook: Establish SSL with upstream proxy") +            self.c.establish_ssl(server=True) + +        self.c.server_reconnect = reconnect_http_proxy + +    def ssl_upgrade(self):          """          Upgrade the connection to SSL after an authority (CONNECT) request has been made.          If the authority request has been forwarded upstream (because we have another proxy server there), @@ -981,28 +1001,6 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):          self.c.mode = "transparent"          self.c.determine_conntype()          self.c.establish_ssl(server=True, client=True) - -        if upstream_request: -            self.c.log("Hook reconnect function") -            original_reconnect_func = self.c.server_reconnect - -            def reconnect_http_proxy(): -                self.c.log("Hooked reconnect function") -                self.c.log("Hook: Run original reconnect") -                original_reconnect_func(no_ssl=True) -                self.c.log("Hook: Write CONNECT request to upstream proxy", [upstream_request._assemble_first_line()]) -                self.c.server_conn.wfile.write(upstream_request._assemble()) -                self.c.server_conn.wfile.flush() -                self.c.log("Hook: Read answer to CONNECT request from proxy") -                resp = HTTPResponse.from_stream(self.c.server_conn.rfile, upstream_request.method) -                if resp.code != 200: -                    raise ProxyError(resp.code, -                                     "Cannot reestablish SSL connection with upstream proxy: \r\n" + str(resp.headers)) -                self.c.log("Hook: Establish SSL with upstream proxy") -                self.c.establish_ssl(server=True) - -            self.c.server_reconnect = reconnect_http_proxy -          self.c.log("Upgrade to SSL completed.")          raise ConnectionTypeChange @@ -1028,7 +1026,7 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):          if self.c.mode == "regular":              if request.form_in == "authority":  # forward mode -                pass +                self.hook_reconnect(request)              elif request.form_in == "absolute":                  if request.scheme != "http":                      raise http.HttpError(400, "Invalid Request") @@ -1037,7 +1035,7 @@ class HTTPHandler(ProtocolHandler, TemporaryServerChangeMixin):                      self.c.set_server_address((request.host, request.port), AddressPriority.FROM_PROTOCOL)                      request.flow.server_conn = self.c.server_conn  # Update server_conn attribute on the flow              else: -                raise http.HttpError(400, "Invalid Request") +                raise http.HttpError(400, "Invalid request form (absolute-form or authority-form required)")      def authenticate(self, request):          if self.c.config.authenticator: diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py index 673628bc..90191eeb 100644 --- a/libmproxy/protocol/primitives.py +++ b/libmproxy/protocol/primitives.py @@ -50,6 +50,9 @@ class Error(stateobject.SimpleStateObject):          timestamp=float      ) +    def __str__(self): +        return self.msg +      @classmethod      def _from_state(cls, state):          f = cls(None)  # the default implementation assumes an empty constructor. Override accordingly. diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 5526e102..b6480822 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -89,6 +89,10 @@ class ClientConnection(tcp.BaseHandler, stateobject.SimpleStateObject):      def copy(self):          return copy.copy(self) +    def send(self, message): +        self.wfile.write(message) +        self.wfile.flush() +      @classmethod      def _from_state(cls, state):          f = cls(None, tuple(), None) diff --git a/test/test_protocol_http.py b/test/test_protocol_http.py index 76d192de..afda7a3e 100644 --- a/test/test_protocol_http.py +++ b/test/test_protocol_http.py @@ -1,5 +1,6 @@  from libmproxy import proxy  # FIXME: Remove  from libmproxy.protocol.http import * +from libmproxy.protocol import KILL  from cStringIO import StringIO  import tutils, tservers @@ -86,6 +87,23 @@ class TestHTTPResponse:          tutils.raises("Invalid server response: 'content", HTTPResponse.from_stream, s, "GET") +class TestInvalidRequests(tservers.HTTPProxTest): +    ssl = True + +    def test_double_connect(self): +        p = self.pathoc() +        r = p.request("connect:'%s:%s'" % ("127.0.0.1", self.server2.port)) +        assert r.status_code == 502 +        assert "Must not CONNECT on already encrypted connection" in r.content + +    def test_origin_request(self): +        p = self.pathoc_raw() +        p.connect() +        r = p.request("get:/p/200") +        assert r.status_code == 400 +        assert "Invalid request form" in r.content + +  class TestProxyChaining(tservers.HTTPChainProxyTest):      def test_all(self):          self.chain[1].tmaster.replacehooks.add("~q", "foo", "bar") # replace in request @@ -98,16 +116,83 @@ class TestProxyChaining(tservers.HTTPChainProxyTest):          assert req.content == "ORLY"          assert req.status_code == 418 -        self.chain[0].tmaster.replacehooks.clear() -        self.chain[1].tmaster.replacehooks.clear() -        self.proxy.tmaster.replacehooks.clear() - -  class TestProxyChainingSSL(tservers.HTTPChainProxyTest):      ssl = True -    def test_full(self): +    def test_simple(self): + +        p = self.pathoc() +        req = p.request("get:'/p/418:b\"content\"'") +        assert req.content == "content" +        assert req.status_code == 418 + +        assert self.chain[1].tmaster.state.flow_count() == 2  # CONNECT from pathoc to chain[0], +                                                              # request from pathoc to chain[0] +        assert self.chain[0].tmaster.state.flow_count() == 2  # CONNECT from chain[1] to proxy, +                                                              # request from chain[1] to proxy +        assert self.proxy.tmaster.state.flow_count() == 1  # request from chain[0] (regular proxy doesn't store CONNECTs) + +    def test_reconnect(self): +        """ +        Tests proper functionality of ConnectionHandler.server_reconnect mock. +        If we have a disconnect on a secure connection that's transparently proxified to +        an upstream http proxy, we need to send the CONNECT request again. +        """ +        def kill_requests(master, attr, exclude): +            k = [0]  # variable scope workaround: put into array +            _func = getattr(master, attr) +            def handler(r): +                k[0] += 1 +                if not (k[0] in exclude): +                    r.flow.client_conn.finish() +                    r.flow.error = Error("terminated") +                    r.reply(KILL) +                return _func(r) +            setattr(master, attr, handler) + +        kill_requests(self.proxy.tmaster, "handle_request", +                      exclude=[ +                              # fail first request +                          2,  # allow second request +                      ]) + +        kill_requests(self.chain[0].tmaster, "handle_request", +                      exclude=[ +                          1,  # CONNECT +                              # fail first request +                          3,  # reCONNECT +                          4,  # request +                      ]) +          p = self.pathoc() -        req = p.request("get:'/p/418:b@100'") -        assert len(req.content) == 100 -        assert req.status_code == 418
\ No newline at end of file +        req = p.request("get:'/p/418:b\"content\"'") +        assert self.chain[1].tmaster.state.flow_count() == 2  # CONNECT and request +        assert self.chain[0].tmaster.state.flow_count() == 4  # CONNECT, failing request, +                                                              # reCONNECT, request +        assert self.proxy.tmaster.state.flow_count() == 2  # failing request, request +                                                           # (doesn't store (repeated) CONNECTs from chain[0] +                                                           #  as it is a regular proxy) +        assert req.content == "content" +        assert req.status_code == 418 + +        assert not self.proxy.tmaster.state._flow_list[0].response  # killed +        assert self.proxy.tmaster.state._flow_list[1].response + +        assert self.chain[1].tmaster.state._flow_list[0].request.form_in == "authority" +        assert self.chain[1].tmaster.state._flow_list[1].request.form_in == "origin" + +        assert self.chain[0].tmaster.state._flow_list[0].request.form_in == "authority" +        assert self.chain[0].tmaster.state._flow_list[1].request.form_in == "origin" +        assert self.chain[0].tmaster.state._flow_list[2].request.form_in == "authority" +        assert self.chain[0].tmaster.state._flow_list[3].request.form_in == "origin" + +        assert self.proxy.tmaster.state._flow_list[0].request.form_in == "origin" +        assert self.proxy.tmaster.state._flow_list[1].request.form_in == "origin" + +        req = p.request("get:'/p/418:b\"content2\"'") + +        assert req.status_code == 502 +        assert self.chain[1].tmaster.state.flow_count() == 3  # + new request +        assert self.chain[0].tmaster.state.flow_count() == 6  # + new request, repeated CONNECT from chain[1] +                                                              # (both terminated) +        assert self.proxy.tmaster.state.flow_count() == 2  # nothing happened here diff --git a/test/tservers.py b/test/tservers.py index 156796c4..ee84db57 100644 --- a/test/tservers.py +++ b/test/tservers.py @@ -254,7 +254,6 @@ class ChainProxTest(ProxTestBase):      Chain n instances of mitmproxy in a row - because we can.      """      n = 2 -    chain = []      chain_config = [lambda: proxy.ProxyConfig(          cacert = tutils.test_data.path("data/serverkey.pem")      )] * n @@ -262,6 +261,7 @@ class ChainProxTest(ProxTestBase):      @classmethod      def setupAll(cls):          super(ChainProxTest, cls).setupAll() +        cls.chain = []          for i in range(cls.n):              config = cls.chain_config[i]()              config.forward_proxy = ("http", "127.0.0.1", @@ -278,6 +278,12 @@ class ChainProxTest(ProxTestBase):          for p in cls.chain:              p.tmaster.server.shutdown() +    def setUp(self): +        super(ChainProxTest, self).setUp() +        for p in self.chain: +            p.tmaster.clear_log() +            p.tmaster.state.clear() +  class HTTPChainProxyTest(ChainProxTest):      def pathoc(self, sni=None): | 
