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