diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/.gitignore | 1 | ||||
-rw-r--r-- | test/.pry | 6 | ||||
-rw-r--r-- | test/data/htpasswd | 1 | ||||
-rw-r--r-- | test/data/htpasswd.invalid | 1 | ||||
-rwxr-xr-x | test/fuzzing/go_proxy | 22 | ||||
-rw-r--r-- | test/test_authentication.py | 58 | ||||
-rw-r--r-- | test/test_console_common.py | 10 | ||||
-rw-r--r-- | test/test_controller.py | 12 | ||||
-rw-r--r-- | test/test_dump.py | 24 | ||||
-rw-r--r-- | test/test_flow.py | 64 | ||||
-rw-r--r-- | test/test_fuzzing.py | 39 | ||||
-rw-r--r-- | test/test_proxy.py | 102 | ||||
-rw-r--r-- | test/test_server.py | 259 | ||||
-rw-r--r-- | test/tservers.py | 251 | ||||
-rw-r--r-- | test/tutils.py | 176 |
15 files changed, 740 insertions, 286 deletions
diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index 6350e986..00000000 --- a/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.coverage diff --git a/test/.pry b/test/.pry deleted file mode 100644 index f6f18e7b..00000000 --- a/test/.pry +++ /dev/null @@ -1,6 +0,0 @@ -base = .. -coverage = ../libmproxy -exclude = . - ../libmproxy/contrib - ../libmproxy/tnetstring.py - diff --git a/test/data/htpasswd b/test/data/htpasswd new file mode 100644 index 00000000..54c95b8c --- /dev/null +++ b/test/data/htpasswd @@ -0,0 +1 @@ +test:$apr1$/LkYxy3x$WI4.YbiJlu537jLGEW2eu1 diff --git a/test/data/htpasswd.invalid b/test/data/htpasswd.invalid new file mode 100644 index 00000000..257cc564 --- /dev/null +++ b/test/data/htpasswd.invalid @@ -0,0 +1 @@ +foo diff --git a/test/fuzzing/go_proxy b/test/fuzzing/go_proxy new file mode 100755 index 00000000..c9b6aef6 --- /dev/null +++ b/test/fuzzing/go_proxy @@ -0,0 +1,22 @@ +#!/bin/sh +# Assuming: +# mitmproxy/mitmdump is running on port 8080 in straight proxy mode. +# pathod is running on port 9999 + +BASE_HTTP="/Users/aldo/git/public/pathod/pathoc -Tt 1 -eo -I 200,400,405,502 -p 8080 localhost " +#$BASE_HTTP -n 10000 "get:'http://localhost:9999':ir,@1" +#$BASE_HTTP -n 100 "get:'http://localhost:9999':dr" +#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@300.0 + + +# Assuming: +# mitmproxy/mitmdump is running on port 8080 in straight proxy mode. +# pathod with SSL enabled is running on port 9999 + +BASE_HTTPS="/Users/aldo/git/public/pathod/pathoc -sc localhost:9999 -Tt 1 -eo -I 200,400,404,405,502,800 -p 8080 localhost " +$BASE_HTTPS -en 10000 "get:'/p/200:b@10:ir,@1'" +#$BASE_HTTPS -en 10000 "get:'/p/200:ir,@1'" + +#$BASE_HTTPS -n 100 "get:'/p/200:dr'" +#$BASE_HTTPS -n 10000 "get:'/p/200:ir,@3000'" +#$BASE_HTTPS -n 10000 "get:'/p/200:ir,\"\\n\"'" diff --git a/test/test_authentication.py b/test/test_authentication.py deleted file mode 100644 index f7a5ecd3..00000000 --- a/test/test_authentication.py +++ /dev/null @@ -1,58 +0,0 @@ -import binascii -from libmproxy import authentication -from netlib import odict -import tutils - - -class TestNullProxyAuth: - def test_simple(self): - na = authentication.NullProxyAuth(authentication.PermissivePasswordManager()) - assert not na.auth_challenge_headers() - assert na.authenticate("foo") - na.clean({}) - - -class TestBasicProxyAuth: - def test_simple(self): - ba = authentication.BasicProxyAuth(authentication.PermissivePasswordManager(), "test") - h = odict.ODictCaseless() - assert ba.auth_challenge_headers() - assert not ba.authenticate(h) - - def test_parse_auth_value(self): - ba = authentication.BasicProxyAuth(authentication.PermissivePasswordManager(), "test") - vals = ("basic", "foo", "bar") - assert ba.parse_auth_value(ba.unparse_auth_value(*vals)) == vals - tutils.raises(ValueError, ba.parse_auth_value, "") - tutils.raises(ValueError, ba.parse_auth_value, "foo bar") - - v = "basic " + binascii.b2a_base64("foo") - tutils.raises(ValueError, ba.parse_auth_value, v) - - def test_authenticate_clean(self): - ba = authentication.BasicProxyAuth(authentication.PermissivePasswordManager(), "test") - - hdrs = odict.ODictCaseless() - vals = ("basic", "foo", "bar") - hdrs[ba.AUTH_HEADER] = [ba.unparse_auth_value(*vals)] - assert ba.authenticate(hdrs) - - ba.clean(hdrs) - assert not ba.AUTH_HEADER in hdrs - - - hdrs[ba.AUTH_HEADER] = [""] - assert not ba.authenticate(hdrs) - - hdrs[ba.AUTH_HEADER] = ["foo"] - assert not ba.authenticate(hdrs) - - vals = ("foo", "foo", "bar") - hdrs[ba.AUTH_HEADER] = [ba.unparse_auth_value(*vals)] - assert not ba.authenticate(hdrs) - - ba = authentication.BasicProxyAuth(authentication.PasswordManager(), "test") - vals = ("basic", "foo", "bar") - hdrs[ba.AUTH_HEADER] = [ba.unparse_auth_value(*vals)] - assert not ba.authenticate(hdrs) - diff --git a/test/test_console_common.py b/test/test_console_common.py new file mode 100644 index 00000000..29bf7b84 --- /dev/null +++ b/test/test_console_common.py @@ -0,0 +1,10 @@ +import libmproxy.console.common as common +from libmproxy import utils, flow, encoding +import tutils + + +def test_format_flow(): + f = tutils.tflow_full() + assert common.format_flow(f, True) + assert common.format_flow(f, True, hostheader=True) + assert common.format_flow(f, True, extended=True) diff --git a/test/test_controller.py b/test/test_controller.py new file mode 100644 index 00000000..f6d6b5eb --- /dev/null +++ b/test/test_controller.py @@ -0,0 +1,12 @@ +import mock +from libmproxy import controller + + +class TestMaster: + def test_default_handler(self): + m = controller.Master(None) + msg = mock.MagicMock() + m.handle(msg) + assert msg.reply.call_count == 1 + + diff --git a/test/test_dump.py b/test/test_dump.py index e1241e29..94d0b195 100644 --- a/test/test_dump.py +++ b/test/test_dump.py @@ -1,8 +1,8 @@ import os from cStringIO import StringIO -import libpry from libmproxy import dump, flow, proxy import tutils +import mock def test_strfuncs(): t = tutils.tresp() @@ -12,8 +12,10 @@ def test_strfuncs(): t = tutils.treq() t.client_conn = None t.stickycookie = True - assert "stickycookie" in dump.str_request(t) - assert "replay" in dump.str_request(t) + assert "stickycookie" in dump.str_request(t, False) + assert "stickycookie" in dump.str_request(t, True) + assert "replay" in dump.str_request(t, False) + assert "replay" in dump.str_request(t, True) class TestDumpMaster: @@ -21,6 +23,7 @@ class TestDumpMaster: req = tutils.treq() req.content = content l = proxy.Log("connect") + l.reply = mock.MagicMock() m.handle_log(l) cc = req.client_conn cc.connection_error = "error" @@ -29,7 +32,9 @@ class TestDumpMaster: m.handle_clientconnect(cc) m.handle_request(req) f = m.handle_response(resp) - m.handle_clientdisconnect(flow.ClientDisconnect(cc)) + cd = flow.ClientDisconnect(cc) + cd.reply = mock.MagicMock() + m.handle_clientdisconnect(cd) return f def _dummy_cycle(self, n, filt, content, **options): @@ -61,7 +66,7 @@ class TestDumpMaster: cs = StringIO() o = dump.Options(server_replay="nonexistent", kill=True) - libpry.raises(dump.DumpError, dump.DumpMaster, None, o, None, outfile=cs) + tutils.raises(dump.DumpError, dump.DumpMaster, None, o, None, outfile=cs) with tutils.tmpdir() as t: p = os.path.join(t, "rep") @@ -86,7 +91,7 @@ class TestDumpMaster: self._flowfile(p) assert "GET" in self._dummy_cycle(0, None, "", verbosity=1, rfile=p) - libpry.raises( + tutils.raises( dump.DumpError, self._dummy_cycle, 0, None, "", verbosity=1, rfile="/nonexistent" ) @@ -97,7 +102,6 @@ class TestDumpMaster: def test_options(self): o = dump.Options(verbosity = 2) assert o.verbosity == 2 - libpry.raises(AttributeError, dump.Options, nonexistent = 2) def test_filter(self): assert not "GET" in self._dummy_cycle(1, "~u foo", "", verbosity=1) @@ -127,7 +131,7 @@ class TestDumpMaster: assert len(list(flow.FlowReader(open(p)).stream())) == 1 def test_write_err(self): - libpry.raises( + tutils.raises( dump.DumpError, self._dummy_cycle, 1, @@ -145,11 +149,11 @@ class TestDumpMaster: assert "XREQUEST" in ret assert "XRESPONSE" in ret assert "XCLIENTDISCONNECT" in ret - libpry.raises( + tutils.raises( dump.DumpError, self._dummy_cycle, 1, None, "", script="nonexistent" ) - libpry.raises( + tutils.raises( dump.DumpError, self._dummy_cycle, 1, None, "", script="starterr.py" ) diff --git a/test/test_flow.py b/test/test_flow.py index 0c713c03..4ad692bc 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -223,16 +223,16 @@ class TestFlow: f = tutils.tflow() f.request = tutils.treq() f.intercept() - assert not f.request.acked + assert not f.request.reply.acked f.kill(fm) - assert f.request.acked + assert f.request.reply.acked f.intercept() f.response = tutils.tresp() f.request = f.response.request - f.request._ack() - assert not f.response.acked + f.request.reply() + assert not f.response.reply.acked f.kill(fm) - assert f.response.acked + assert f.response.reply.acked def test_killall(self): s = flow.State() @@ -245,25 +245,25 @@ class TestFlow: fm.handle_request(r) for i in s.view: - assert not i.request.acked + assert not i.request.reply.acked s.killall(fm) for i in s.view: - assert i.request.acked + assert i.request.reply.acked def test_accept_intercept(self): f = tutils.tflow() f.request = tutils.treq() f.intercept() - assert not f.request.acked + assert not f.request.reply.acked f.accept_intercept() - assert f.request.acked + assert f.request.reply.acked f.response = tutils.tresp() f.request = f.response.request f.intercept() - f.request._ack() - assert not f.response.acked + f.request.reply() + assert not f.response.reply.acked f.accept_intercept() - assert f.response.acked + assert f.response.reply.acked def test_serialization(self): f = flow.Flow(None) @@ -498,6 +498,23 @@ class TestSerialize: fm.load_flows(r) assert len(s._flow_list) == 6 + def test_filter(self): + sio = StringIO() + fl = filt.parse("~c 200") + w = flow.FilteredFlowWriter(sio, fl) + + f = tutils.tflow_full() + f.response.code = 200 + w.add(f) + + f = tutils.tflow_full() + f.response.code = 201 + w.add(f) + + sio.seek(0) + r = flow.FlowReader(sio) + assert len(list(r.stream())) + def test_error(self): sio = StringIO() @@ -562,9 +579,11 @@ class TestFlowMaster: fm.handle_response(resp) assert fm.script.ns["log"][-1] == "response" dc = flow.ClientDisconnect(req.client_conn) + dc.reply = controller.DummyReply() fm.handle_clientdisconnect(dc) assert fm.script.ns["log"][-1] == "clientdisconnect" err = flow.Error(f.request, "msg") + err.reply = controller.DummyReply() fm.handle_error(err) assert fm.script.ns["log"][-1] == "error" @@ -598,10 +617,12 @@ class TestFlowMaster: assert not fm.handle_response(rx) dc = flow.ClientDisconnect(req.client_conn) + dc.reply = controller.DummyReply() req.client_conn.requestcount = 1 fm.handle_clientdisconnect(dc) err = flow.Error(f.request, "msg") + err.reply = controller.DummyReply() fm.handle_error(err) fm.load_script(tutils.test_data.path("scripts/a.py")) @@ -621,7 +642,9 @@ class TestFlowMaster: fm.tick(q) assert fm.state.flow_count() - fm.handle_error(flow.Error(f.request, "error")) + err = flow.Error(f.request, "error") + err.reply = controller.DummyReply() + fm.handle_error(err) def test_server_playback(self): controller.should_exit = False @@ -717,7 +740,7 @@ class TestFlowMaster: fm = flow.FlowMaster(None, s) tf = tutils.tflow_full() - fm.start_stream(file(p, "ab")) + fm.start_stream(file(p, "ab"), None) fm.handle_request(tf.request) fm.handle_response(tf.response) fm.stop_stream() @@ -725,7 +748,7 @@ class TestFlowMaster: assert r()[0].response tf = tutils.tflow_full() - fm.start_stream(file(p, "ab")) + fm.start_stream(file(p, "ab"), None) fm.handle_request(tf.request) fm.shutdown() @@ -760,6 +783,17 @@ class TestRequest: r.content = flow.CONTENT_MISSING assert not r._assemble() + def test_get_url(self): + h = flow.ODictCaseless() + h["test"] = ["test"] + c = flow.ClientConnect(("addr", 2222)) + r = flow.Request(c, (1, 1), "host", 22, "https", "GET", "/", h, "content") + assert r.get_url() == "https://host:22/" + assert r.get_url(hostheader=True) == "https://host:22/" + r.headers["Host"] = ["foo.com"] + assert r.get_url() == "https://host:22/" + assert r.get_url(hostheader=True) == "https://foo.com:22/" + def test_path_components(self): h = flow.ODictCaseless() c = flow.ClientConnect(("addr", 2222)) diff --git a/test/test_fuzzing.py b/test/test_fuzzing.py new file mode 100644 index 00000000..ba7b751c --- /dev/null +++ b/test/test_fuzzing.py @@ -0,0 +1,39 @@ +import tservers + +""" + A collection of errors turned up by fuzzing. Errors are integrated here + after being fixed to check for regressions. +""" + +class TestFuzzy(tservers.HTTPProxTest): + def test_idna_err(self): + req = r'get:"http://localhost:%s":i10,"\xc6"' + p = self.pathoc() + assert p.request(req%self.server.port).status_code == 400 + + def test_nullbytes(self): + req = r'get:"http://localhost:%s":i19,"\x00"' + p = self.pathoc() + assert p.request(req%self.server.port).status_code == 400 + + def test_invalid_ports(self): + req = 'get:"http://localhost:999999"' + p = self.pathoc() + assert p.request(req).status_code == 400 + + def test_invalid_ipv6_url(self): + req = 'get:"http://localhost:%s":i13,"["' + p = self.pathoc() + assert p.request(req%self.server.port).status_code == 400 + + def test_invalid_upstream(self): + req = r"get:'http://localhost:%s/p/200:i10,\'+\''" + p = self.pathoc() + assert p.request(req%self.server.port).status_code == 502 + + def test_upstream_disconnect(self): + req = r'200:d0:h"Date"="Sun, 03 Mar 2013 04:00:00 GMT"' + p = self.pathod(req) + assert p.status_code == 400 + + diff --git a/test/test_proxy.py b/test/test_proxy.py index c73f61d8..5828d077 100644 --- a/test/test_proxy.py +++ b/test/test_proxy.py @@ -1,7 +1,8 @@ -from libmproxy import proxy, flow +import argparse +from libmproxy import proxy, flow, cmdline import tutils from libpathod import test -from netlib import http +from netlib import http, tcp import mock @@ -22,7 +23,6 @@ def test_app_registry(): r.port = 81 assert not ar.get(r) - r = tutils.treq() r.host = "domain2" r.port = 80 @@ -39,8 +39,8 @@ class TestServerConnection: self.d.shutdown() def test_simple(self): - sc = proxy.ServerConnection(proxy.ProxyConfig(), self.d.IFACE, self.d.port) - sc.connect("http") + sc = proxy.ServerConnection(proxy.ProxyConfig(), "http", self.d.IFACE, self.d.port, "host.com") + sc.connect() r = tutils.treq() r.path = "/p/200:da" sc.send(r) @@ -53,8 +53,96 @@ class TestServerConnection: sc.terminate() def test_terminate_error(self): - sc = proxy.ServerConnection(proxy.ProxyConfig(), self.d.IFACE, self.d.port) - sc.connect("http") + sc = proxy.ServerConnection(proxy.ProxyConfig(), "http", self.d.IFACE, self.d.port, "host.com") + sc.connect() sc.connection = mock.Mock() sc.connection.close = mock.Mock(side_effect=IOError) sc.terminate() + + +class MockParser: + def __init__(self): + self.err = None + + def error(self, e): + self.err = e + + def __repr__(self): + return "ParseError(%s)"%self.err + + +class TestProcessProxyOptions: + def p(self, *args): + parser = argparse.ArgumentParser() + cmdline.common_options(parser) + opts = parser.parse_args(args=args) + m = MockParser() + return m, proxy.process_proxy_options(m, opts) + + def assert_err(self, err, *args): + m, p = self.p(*args) + assert err.lower() in m.err.lower() + + def assert_noerr(self, *args): + m, p = self.p(*args) + assert p + return p + + def test_simple(self): + assert self.p() + + def test_cert(self): + self.assert_noerr("--cert", tutils.test_data.path("data/testkey.pem")) + self.assert_err("does not exist", "--cert", "nonexistent") + + def test_confdir(self): + with tutils.tmpdir() as confdir: + self.assert_noerr("--confdir", confdir) + + @mock.patch("libmproxy.platform.resolver", None) + def test_no_transparent(self): + self.assert_err("transparent mode not supported", "-T") + + @mock.patch("libmproxy.platform.resolver") + def test_transparent_reverse(self, o): + self.assert_err("can't set both", "-P", "reverse", "-T") + self.assert_noerr("-T") + assert o.call_count == 1 + self.assert_err("invalid reverse proxy", "-P", "reverse") + self.assert_noerr("-P", "http://localhost") + + def test_certs(self): + with tutils.tmpdir() as confdir: + self.assert_noerr("--client-certs", confdir) + self.assert_err("directory does not exist", "--client-certs", "nonexistent") + + self.assert_noerr("--dummy-certs", confdir) + self.assert_err("directory does not exist", "--dummy-certs", "nonexistent") + + def test_auth(self): + p = self.assert_noerr("--nonanonymous") + assert p.authenticator + + p = self.assert_noerr("--htpasswd", tutils.test_data.path("data/htpasswd")) + assert p.authenticator + self.assert_err("invalid htpasswd file", "--htpasswd", tutils.test_data.path("data/htpasswd.invalid")) + + p = self.assert_noerr("--singleuser", "test:test") + assert p.authenticator + self.assert_err("invalid single-user specification", "--singleuser", "test") + + +class TestProxyServer: + def test_err(self): + parser = argparse.ArgumentParser() + cmdline.common_options(parser) + opts = parser.parse_args(args=[]) + tutils.raises("error starting proxy server", proxy.ProxyServer, opts, 1) + + +class TestDummyServer: + def test_simple(self): + d = proxy.DummyServer(None) + d.start_slave() + d.shutdown() + diff --git a/test/test_server.py b/test/test_server.py index 0a2f142e..c20b0cea 100644 --- a/test/test_server.py +++ b/test/test_server.py @@ -1,7 +1,9 @@ import socket, time -from netlib import tcp +import mock +from netlib import tcp, http_auth, http from libpathod import pathoc -import tutils +import tutils, tservers +from libmproxy import flow, proxy """ Note that the choice of response code in these tests matters more than you @@ -11,11 +13,7 @@ import tutils for a 200 response. """ -class SanityMixin: - def test_http(self): - assert self.pathod("304").status_code == 304 - assert self.master.state.view - +class CommonMixin: def test_large(self): assert len(self.pathod("200:b@50k").content) == 1024*50 @@ -38,15 +36,39 @@ class SanityMixin: self.master.replay_request(l, block=True) assert l.error + def test_http(self): + f = self.pathod("304") + assert f.status_code == 304 + + l = self.master.state.view[0] + assert l.request.client_conn.address + assert "host" in l.request.headers + assert l.response.code == 304 -class TestHTTP(tutils.HTTPProxTest, SanityMixin): def test_invalid_http(self): t = tcp.TCPClient("127.0.0.1", self.proxy.port) t.connect() - t.wfile.write("invalid\n\n") + t.wfile.write("invalid\r\n\r\n") t.wfile.flush() assert "Bad Request" in t.rfile.readline() + + +class AppMixin: + def test_app(self): + ret = self.app("/") + assert ret.status_code == 200 + assert "mitmproxy" in ret.content + + + +class TestHTTP(tservers.HTTPProxTest, CommonMixin, AppMixin): + def test_app_err(self): + p = self.pathoc() + ret = p.request("get:'http://errapp/'") + assert ret.status_code == 500 + assert "ValueError" in ret.content + def test_invalid_connect(self): t = tcp.TCPClient("127.0.0.1", self.proxy.port) t.connect() @@ -57,35 +79,157 @@ class TestHTTP(tutils.HTTPProxTest, SanityMixin): def test_upstream_ssl_error(self): p = self.pathoc() ret = p.request("get:'https://localhost:%s/'"%self.server.port) - assert ret[1] == 400 + assert ret.status_code == 400 - def test_http(self): - f = self.pathod("304") - assert f.status_code == 304 + def test_connection_close(self): + # Add a body, so we have a content-length header, which combined with + # HTTP1.1 means the connection is kept alive. + response = '%s/p/200:b@1'%self.server.urlbase - l = self.master.state.view[0] - assert l.request.client_conn.address - assert "host" in l.request.headers - assert l.response.code == 304 + # Lets sanity check that the connection does indeed stay open by + # issuing two requests over the same connection + p = self.pathoc() + assert p.request("get:'%s'"%response) + assert p.request("get:'%s'"%response) + + # Now check that the connection is closed as the client specifies + p = self.pathoc() + assert p.request("get:'%s':h'Connection'='close'"%response) + tutils.raises("disconnect", p.request, "get:'%s'"%response) + + def test_reconnect(self): + req = "get:'%s/p/200:b@1:da'"%self.server.urlbase + p = self.pathoc() + assert p.request(req) + # Server has disconnected. Mitmproxy should detect this, and reconnect. + assert p.request(req) + assert p.request(req) + + # However, if the server disconnects on our first try, it's an error. + req = "get:'%s/p/200:b@1:d0'"%self.server.urlbase + p = self.pathoc() + tutils.raises("server disconnect", p.request, req) + + def test_proxy_ioerror(self): + # Tests a difficult-to-trigger condition, where an IOError is raised + # within our read loop. + with mock.patch("libmproxy.proxy.ProxyHandler.read_request") as m: + m.side_effect = IOError("error!") + tutils.raises("server disconnect", self.pathod, "304") + + def test_get_connection_switching(self): + def switched(l): + for i in l: + if "switching" in i: + return True + req = "get:'%s/p/200:b@1'" + p = self.pathoc() + assert p.request(req%self.server.urlbase) + assert p.request(req%self.server2.urlbase) + assert switched(self.proxy.log) + + def test_get_connection_err(self): + p = self.pathoc() + ret = p.request("get:'http://localhost:0'") + assert ret.status_code == 502 + + def test_blank_leading_line(self): + p = self.pathoc() + req = "get:'%s/p/201':i0,'\r\n'" + assert p.request(req%self.server.urlbase).status_code == 201 + + def test_invalid_headers(self): + p = self.pathoc() + req = p.request("get:'http://foo':h':foo'='bar'") + assert req.status_code == 400 + + + +class TestHTTPAuth(tservers.HTTPProxTest): + authenticator = http_auth.BasicProxyAuth(http_auth.PassManSingleUser("test", "test"), "realm") + def test_auth(self): + assert self.pathod("202").status_code == 407 + p = self.pathoc() + ret = p.request(""" + get + 'http://localhost:%s/p/202' + h'%s'='%s' + """%( + self.server.port, + http_auth.BasicProxyAuth.AUTH_HEADER, + http.assemble_http_basic_auth("basic", "test", "test") + )) + assert ret.status_code == 202 -class TestHTTPS(tutils.HTTPProxTest, SanityMixin): +class TestHTTPConnectSSLError(tservers.HTTPProxTest): + certfile = True + def test_go(self): + p = self.pathoc() + req = "connect:'localhost:%s'"%self.proxy.port + assert p.request(req).status_code == 200 + assert p.request(req).status_code == 400 + + +class TestHTTPS(tservers.HTTPProxTest, CommonMixin): ssl = True clientcerts = True def test_clientcert(self): f = self.pathod("304") - assert self.last_log()["request"]["clientcert"]["keyinfo"] + assert f.status_code == 304 + assert self.server.last_log()["request"]["clientcert"]["keyinfo"] + + def test_sni(self): + f = self.pathod("304", sni="testserver.com") + assert f.status_code == 304 + l = self.server.last_log() + assert self.server.last_log()["request"]["sni"] == "testserver.com" + + def test_error_post_connect(self): + p = self.pathoc() + assert p.request("get:/:i0,'invalid\r\n\r\n'").status_code == 400 + + +class TestHTTPSNoUpstream(tservers.HTTPProxTest, CommonMixin): + ssl = True + no_upstream_cert = True + def test_cert_gen_error(self): + f = self.pathoc_raw() + f.connect((u"foo..bar".encode("utf8"), 0)) + f.request("get:/") + assert "dummy cert" in "".join(self.proxy.log) -class TestReverse(tutils.ReverseProxTest, SanityMixin): +class TestHTTPSCertfile(tservers.HTTPProxTest, CommonMixin): + ssl = True + certfile = True + def test_certfile(self): + assert self.pathod("304") + + +class TestReverse(tservers.ReverseProxTest, CommonMixin): reverse = True -class TestTransparent(tutils.TransparentProxTest, SanityMixin): - transparent = True +class TestTransparent(tservers.TransparentProxTest, CommonMixin): + ssl = False + + +class TestTransparentSSL(tservers.TransparentProxTest, CommonMixin): + ssl = True + def test_sni(self): + f = self.pathod("304", sni="testserver.com") + assert f.status_code == 304 + l = self.server.last_log() + assert self.server.last_log()["request"]["sni"] == "testserver.com" + def test_sslerr(self): + p = pathoc.Pathoc("localhost", self.proxy.port) + p.connect() + assert p.request("get:/").status_code == 400 -class TestProxy(tutils.HTTPProxTest): + +class TestProxy(tservers.HTTPProxTest): def test_http(self): f = self.pathod("304") assert f.status_code == 304 @@ -132,3 +276,72 @@ class TestProxy(tutils.HTTPProxTest): request = self.master.state.view[1].request assert request.timestamp_end - request.timestamp_start <= 0.1 + + + +class MasterFakeResponse(tservers.TestMaster): + def handle_request(self, m): + resp = tutils.tresp() + m.reply(resp) + + +class TestFakeResponse(tservers.HTTPProxTest): + masterclass = MasterFakeResponse + def test_fake(self): + f = self.pathod("200") + assert "header_response" in f.headers.keys() + + + +class MasterKillRequest(tservers.TestMaster): + def handle_request(self, m): + m.reply(proxy.KILL) + + +class TestKillRequest(tservers.HTTPProxTest): + masterclass = MasterKillRequest + def test_kill(self): + tutils.raises("server disconnect", self.pathod, "200") + # Nothing should have hit the server + assert not self.server.last_log() + + +class MasterKillResponse(tservers.TestMaster): + def handle_response(self, m): + m.reply(proxy.KILL) + + +class TestKillResponse(tservers.HTTPProxTest): + masterclass = MasterKillResponse + def test_kill(self): + tutils.raises("server disconnect", self.pathod, "200") + # The server should have seen a request + assert self.server.last_log() + + +class EResolver(tservers.TResolver): + def original_addr(self, sock): + return None + + +class TestTransparentResolveError(tservers.TransparentProxTest): + resolver = EResolver + def test_resolve_error(self): + assert self.pathod("304").status_code == 502 + + + +class MasterIncomplete(tservers.TestMaster): + def handle_request(self, m): + resp = tutils.tresp() + resp.content = flow.CONTENT_MISSING + m.reply(resp) + + +class TestIncompleteResponse(tservers.HTTPProxTest): + masterclass = MasterIncomplete + def test_incomplete(self): + assert self.pathod("200").status_code == 502 + + + diff --git a/test/tservers.py b/test/tservers.py new file mode 100644 index 00000000..91ce4dc0 --- /dev/null +++ b/test/tservers.py @@ -0,0 +1,251 @@ +import threading, Queue +import flask +import libpathod.test, libpathod.pathoc +from libmproxy import proxy, flow, controller +import tutils + +testapp = flask.Flask(__name__) + +@testapp.route("/") +def hello(): + return "testapp" + +@testapp.route("/error") +def error(): + raise ValueError("An exception...") + + +def errapp(environ, start_response): + raise ValueError("errapp") + + +class TestMaster(flow.FlowMaster): + def __init__(self, testq, config): + s = proxy.ProxyServer(config, 0) + s.apps.add(testapp, "testapp", 80) + s.apps.add(errapp, "errapp", 80) + state = flow.State() + flow.FlowMaster.__init__(self, s, state) + self.testq = testq + self.clear_log() + + def handle_request(self, m): + flow.FlowMaster.handle_request(self, m) + m.reply() + + def handle_response(self, m): + flow.FlowMaster.handle_response(self, m) + m.reply() + + def clear_log(self): + self.log = [] + + def handle_log(self, l): + self.log.append(l.msg) + l.reply() + + +class ProxyThread(threading.Thread): + def __init__(self, tmaster): + threading.Thread.__init__(self) + self.tmaster = tmaster + controller.should_exit = False + + @property + def port(self): + return self.tmaster.server.port + + @property + def log(self): + return self.tmaster.log + + def run(self): + self.tmaster.run() + + def shutdown(self): + self.tmaster.shutdown() + + +class ProxTestBase: + # Test Configuration + ssl = None + clientcerts = False + certfile = None + no_upstream_cert = False + authenticator = None + masterclass = TestMaster + @classmethod + def setupAll(cls): + cls.tqueue = Queue.Queue() + cls.server = libpathod.test.Daemon(ssl=cls.ssl) + cls.server2 = libpathod.test.Daemon(ssl=cls.ssl) + pconf = cls.get_proxy_config() + config = proxy.ProxyConfig( + no_upstream_cert = cls.no_upstream_cert, + cacert = tutils.test_data.path("data/serverkey.pem"), + authenticator = cls.authenticator, + app = True, + **pconf + ) + tmaster = cls.masterclass(cls.tqueue, config) + cls.proxy = ProxyThread(tmaster) + cls.proxy.start() + + @property + def master(cls): + return cls.proxy.tmaster + + @classmethod + def teardownAll(cls): + cls.proxy.shutdown() + cls.server.shutdown() + cls.server2.shutdown() + + def setUp(self): + self.master.clear_log() + self.master.state.clear() + self.server.clear_log() + self.server2.clear_log() + + @property + def scheme(self): + return "https" if self.ssl else "http" + + @property + def proxies(self): + """ + The URL base for the server instance. + """ + return ( + (self.scheme, ("127.0.0.1", self.proxy.port)) + ) + + @classmethod + def get_proxy_config(cls): + d = dict() + if cls.clientcerts: + d["clientcerts"] = tutils.test_data.path("data/clientcert") + if cls.certfile: + d["certfile"] =tutils.test_data.path("data/testkey.pem") + return d + + +class HTTPProxTest(ProxTestBase): + def pathoc_raw(self): + return libpathod.pathoc.Pathoc("127.0.0.1", self.proxy.port) + + def pathoc(self, sni=None): + """ + Returns a connected Pathoc instance. + """ + p = libpathod.pathoc.Pathoc("localhost", self.proxy.port, ssl=self.ssl, sni=sni) + if self.ssl: + p.connect(("127.0.0.1", self.server.port)) + else: + p.connect() + return p + + def pathod(self, spec, sni=None): + """ + Constructs a pathod GET request, with the appropriate base and proxy. + """ + p = self.pathoc(sni=sni) + spec = spec.encode("string_escape") + if self.ssl: + q = "get:'/p/%s'"%spec + else: + q = "get:'%s/p/%s'"%(self.server.urlbase, spec) + return p.request(q) + + def app(self, page): + if self.ssl: + p = libpathod.pathoc.Pathoc("127.0.0.1", self.proxy.port, True) + print "PRE" + p.connect((proxy.APP_IP, 80)) + print "POST" + return p.request("get:'/%s'"%page) + else: + p = self.pathoc() + return p.request("get:'http://%s/%s'"%(proxy.APP_DOMAIN, page)) + + +class TResolver: + def __init__(self, port): + self.port = port + + def original_addr(self, sock): + return ("127.0.0.1", self.port) + + +class TransparentProxTest(ProxTestBase): + ssl = None + resolver = TResolver + @classmethod + def get_proxy_config(cls): + d = ProxTestBase.get_proxy_config() + if cls.ssl: + ports = [cls.server.port, cls.server2.port] + else: + ports = [] + d["transparent_proxy"] = dict( + resolver = cls.resolver(cls.server.port), + sslports = ports + ) + return d + + def pathod(self, spec, sni=None): + """ + Constructs a pathod GET request, with the appropriate base and proxy. + """ + if self.ssl: + p = self.pathoc(sni=sni) + q = "get:'/p/%s'"%spec + else: + p = self.pathoc() + q = "get:'/p/%s'"%spec + return p.request(q) + + def pathoc(self, sni=None): + """ + Returns a connected Pathoc instance. + """ + p = libpathod.pathoc.Pathoc("localhost", self.proxy.port, ssl=self.ssl, sni=sni) + p.connect() + return p + + +class ReverseProxTest(ProxTestBase): + ssl = None + @classmethod + def get_proxy_config(cls): + d = ProxTestBase.get_proxy_config() + d["reverse_proxy"] = ( + "https" if cls.ssl else "http", + "127.0.0.1", + cls.server.port + ) + return d + + def pathoc(self, sni=None): + """ + Returns a connected Pathoc instance. + """ + p = libpathod.pathoc.Pathoc("localhost", self.proxy.port, ssl=self.ssl, sni=sni) + p.connect() + return p + + def pathod(self, spec, sni=None): + """ + Constructs a pathod GET request, with the appropriate base and proxy. + """ + if self.ssl: + p = self.pathoc(sni=sni) + q = "get:'/p/%s'"%spec + else: + p = self.pathoc() + q = "get:'/p/%s'"%spec + return p.request(q) + + + + diff --git a/test/tutils.py b/test/tutils.py index 9868c778..1a1c8724 100644 --- a/test/tutils.py +++ b/test/tutils.py @@ -1,17 +1,18 @@ -import threading, Queue import os, shutil, tempfile from contextlib import contextmanager -from libmproxy import proxy, flow, controller, utils +from libmproxy import flow, utils, controller from netlib import certutils -import human_curl as hurl -import libpathod.test, libpathod.pathoc +import mock def treq(conn=None): if not conn: conn = flow.ClientConnect(("address", 22)) + conn.reply = controller.DummyReply() headers = flow.ODictCaseless() headers["header"] = ["qvalue"] - return flow.Request(conn, (1, 1), "host", 80, "http", "GET", "/path", headers, "content") + r = flow.Request(conn, (1, 1), "host", 80, "http", "GET", "/path", headers, "content") + r.reply = controller.DummyReply() + return r def tresp(req=None): @@ -20,7 +21,9 @@ def tresp(req=None): headers = flow.ODictCaseless() headers["header_response"] = ["svalue"] cert = certutils.SSLCert.from_der(file(test_data.path("data/dercert")).read()) - return flow.Response(req, (1, 1), 200, "message", headers, "content_response", cert) + resp = flow.Response(req, (1, 1), 200, "message", headers, "content_response", cert) + resp.reply = controller.DummyReply() + return resp def tflow(): @@ -39,168 +42,10 @@ def tflow_err(): r = treq() f = flow.Flow(r) f.error = flow.Error(r, "error") + f.error.reply = controller.DummyReply() return f -class TestMaster(flow.FlowMaster): - def __init__(self, testq, config): - s = proxy.ProxyServer(config, 0) - state = flow.State() - flow.FlowMaster.__init__(self, s, state) - self.testq = testq - - def handle(self, m): - flow.FlowMaster.handle(self, m) - m._ack() - - -class ProxyThread(threading.Thread): - def __init__(self, testq, config): - self.tmaster = TestMaster(testq, config) - controller.should_exit = False - threading.Thread.__init__(self) - - @property - def port(self): - return self.tmaster.server.port - - def run(self): - self.tmaster.run() - - def shutdown(self): - self.tmaster.shutdown() - - -class ProxTestBase: - @classmethod - def setupAll(cls): - cls.tqueue = Queue.Queue() - cls.server = libpathod.test.Daemon(ssl=cls.ssl) - pconf = cls.get_proxy_config() - config = proxy.ProxyConfig( - certfile=test_data.path("data/testkey.pem"), - **pconf - ) - cls.proxy = ProxyThread(cls.tqueue, config) - cls.proxy.start() - - @property - def master(cls): - return cls.proxy.tmaster - - @classmethod - def teardownAll(cls): - cls.proxy.shutdown() - cls.server.shutdown() - - def setUp(self): - self.master.state.clear() - - @property - def scheme(self): - return "https" if self.ssl else "http" - - @property - def proxies(self): - """ - The URL base for the server instance. - """ - return ( - (self.scheme, ("127.0.0.1", self.proxy.port)) - ) - - @property - def urlbase(self): - """ - The URL base for the server instance. - """ - return self.server.urlbase - - def last_log(self): - return self.server.last_log() - - -class HTTPProxTest(ProxTestBase): - ssl = None - clientcerts = False - @classmethod - def get_proxy_config(cls): - d = dict() - if cls.clientcerts: - d["clientcerts"] = test_data.path("data/clientcert") - return d - - def pathoc(self, connect_to = None): - p = libpathod.pathoc.Pathoc("localhost", self.proxy.port) - p.connect(connect_to) - return p - - def pathod(self, spec): - """ - Constructs a pathod request, with the appropriate base and proxy. - """ - return hurl.get( - self.urlbase + "/p/" + spec, - proxy=self.proxies, - validate_cert=False, - #debug=hurl.utils.stdout_debug - ) - - -class TResolver: - def __init__(self, port): - self.port = port - - def original_addr(self, sock): - return ("127.0.0.1", self.port) - - -class TransparentProxTest(ProxTestBase): - ssl = None - @classmethod - def get_proxy_config(cls): - return dict( - transparent_proxy = dict( - resolver = TResolver(cls.server.port), - sslports = [] - ) - ) - - def pathod(self, spec): - """ - Constructs a pathod request, with the appropriate base and proxy. - """ - r = hurl.get( - "http://127.0.0.1:%s"%self.proxy.port + "/p/" + spec, - validate_cert=False, - #debug=hurl.utils.stdout_debug - ) - return r - - -class ReverseProxTest(ProxTestBase): - ssl = None - @classmethod - def get_proxy_config(cls): - return dict( - reverse_proxy = ( - "https" if cls.ssl else "http", - "127.0.0.1", - cls.server.port - ) - ) - - def pathod(self, spec): - """ - Constructs a pathod request, with the appropriate base and proxy. - """ - r = hurl.get( - "http://127.0.0.1:%s"%self.proxy.port + "/p/" + spec, - validate_cert=False, - #debug=hurl.utils.stdout_debug - ) - return r - @contextmanager def tmpdir(*args, **kwargs): @@ -252,5 +97,4 @@ def raises(exc, obj, *args, **kwargs): ) raise AssertionError("No exception raised.") - test_data = utils.Data(__name__) |