Complete upstream authentication module

- Handles upstream CONNECT and regular requests, plus HTTP Basic for reverse
proxy
- Add some tests to make sure we can rely on the .via attribute on server
connections.
This commit is contained in:
Aldo Cortesi 2016-11-13 11:43:27 +13:00
parent a9b4560187
commit 3b00bc339d
5 changed files with 62 additions and 11 deletions

View File

@ -15,16 +15,39 @@ def parse_upstream_auth(auth):
class UpstreamProxyAuth(): class UpstreamProxyAuth():
"""
This addon handles authentication to systems upstream from us for the
upstream proxy and reverse proxy mode. There are 3 cases:
- Upstream proxy CONNECT requests should have authentication added, and
subsequent already connected requests should not.
- Upstream proxy regular requests
- Reverse proxy regular requests (CONNECT is invalid in this mode)
"""
def __init__(self): def __init__(self):
self.auth = None self.auth = None
self.root_mode = None
def configure(self, options, updated): def configure(self, options, updated):
# FIXME: We're doing this because our proxy core is terminally confused
# at the moment. Ideally, we should be able to check if we're in
# reverse proxy mode at the HTTP layer, so that scripts can put the
# proxy in reverse proxy mode for specific reuests.
if "mode" in updated:
self.root_mode = options.mode
if "upstream_auth" in updated: if "upstream_auth" in updated:
if options.upstream_auth is None: if options.upstream_auth is None:
self.auth = None self.auth = None
else: else:
self.auth = parse_upstream_auth(options.upstream_auth) self.auth = parse_upstream_auth(options.upstream_auth)
def requestheaders(self, f): def http_connect(self, f):
if self.auth and f.mode == "upstream": if self.auth and f.mode == "upstream":
f.request.headers["Proxy-Authorization"] = self.auth f.request.headers["Proxy-Authorization"] = self.auth
def requestheaders(self, f):
if self.auth:
if f.mode == "upstream" and not f.server_conn.via:
f.request.headers["Proxy-Authorization"] = self.auth
elif self.root_mode == "reverse":
f.request.headers["Proxy-Authorization"] = self.auth

View File

@ -135,7 +135,9 @@ MODE_REQUEST_FORMS = {
def validate_request_form(mode, request): def validate_request_form(mode, request):
if request.first_line_format == "absolute" and request.scheme != "http": if request.first_line_format == "absolute" and request.scheme != "http":
raise exceptions.HttpException("Invalid request scheme: %s" % request.scheme) raise exceptions.HttpException(
"Invalid request scheme: %s" % request.scheme
)
allowed_request_forms = MODE_REQUEST_FORMS[mode] allowed_request_forms = MODE_REQUEST_FORMS[mode]
if request.first_line_format not in allowed_request_forms: if request.first_line_format not in allowed_request_forms:
err_message = "Invalid HTTP request form (expected: %s, got: %s)" % ( err_message = "Invalid HTTP request form (expected: %s, got: %s)" % (
@ -275,8 +277,6 @@ class HttpLayer(base.Layer):
if not self.connect_request and not self.authenticate(request): if not self.connect_request and not self.authenticate(request):
return False return False
f.request = request
# update host header in reverse proxy mode # update host header in reverse proxy mode
if self.config.options.mode == "reverse": if self.config.options.mode == "reverse":
f.request.headers["Host"] = self.config.upstream_server.address.host f.request.headers["Host"] = self.config.upstream_server.address.host
@ -389,10 +389,8 @@ class HttpLayer(base.Layer):
# Handle 101 Switching Protocols # Handle 101 Switching Protocols
if f.response.status_code == 101: if f.response.status_code == 101:
""" # Handle a successful HTTP 101 Switching Protocols Response,
Handle a successful HTTP 101 Switching Protocols Response, received after # received after e.g. a WebSocket upgrade request.
e.g. a WebSocket upgrade request.
"""
# Check for WebSockets handshake # Check for WebSockets handshake
is_websockets = ( is_websockets = (
websockets.check_handshake(f.request.headers) and websockets.check_handshake(f.request.headers) and
@ -467,13 +465,17 @@ class HttpLayer(base.Layer):
self.send_response(http.make_error_response( self.send_response(http.make_error_response(
401, 401,
"Authentication Required", "Authentication Required",
mitmproxy.net.http.Headers(**self.config.authenticator.auth_challenge_headers()) mitmproxy.net.http.Headers(
**self.config.authenticator.auth_challenge_headers()
)
)) ))
else: else:
self.send_response(http.make_error_response( self.send_response(http.make_error_response(
407, 407,
"Proxy Authentication Required", "Proxy Authentication Required",
mitmproxy.net.http.Headers(**self.config.authenticator.auth_challenge_headers()) mitmproxy.net.http.Headers(
**self.config.authenticator.auth_challenge_headers()
)
)) ))
return False return False
return True return True

View File

@ -64,7 +64,12 @@ class RootContext:
# An inline script may upgrade from http to https, # An inline script may upgrade from http to https,
# in which case we need some form of TLS layer. # in which case we need some form of TLS layer.
if isinstance(top_layer, modes.ReverseProxy): if isinstance(top_layer, modes.ReverseProxy):
return protocol.TlsLayer(top_layer, client_tls, top_layer.server_tls, top_layer.server_conn.address.host) return protocol.TlsLayer(
top_layer,
client_tls,
top_layer.server_tls,
top_layer.server_conn.address.host
)
if isinstance(top_layer, protocol.ServerConnectionMixin) or isinstance(top_layer, protocol.UpstreamConnectLayer): if isinstance(top_layer, protocol.ServerConnectionMixin) or isinstance(top_layer, protocol.UpstreamConnectLayer):
return protocol.TlsLayer(top_layer, client_tls, client_tls) return protocol.TlsLayer(top_layer, client_tls, client_tls)

View File

@ -52,3 +52,14 @@ def test_simple():
f = tflow.tflow() f = tflow.tflow()
up.requestheaders(f) up.requestheaders(f)
assert "proxy-authorization" not in f.request.headers assert "proxy-authorization" not in f.request.headers
tctx.configure(up, mode="reverse")
f = tflow.tflow()
f.mode = "transparent"
up.requestheaders(f)
assert "proxy-authorization" in f.request.headers
f = tflow.tflow()
f.mode = "upstream"
up.http_connect(f)
assert "proxy-authorization" in f.request.headers

View File

@ -669,6 +669,13 @@ class TestProxySSL(tservers.HTTPProxyTest):
first_flow = self.master.state.flows[0] first_flow = self.master.state.flows[0]
assert first_flow.server_conn.timestamp_ssl_setup assert first_flow.server_conn.timestamp_ssl_setup
def test_via(self):
# tests that the ssl timestamp is present when ssl is used
f = self.pathod("200:b@10")
assert f.status_code == 200
first_flow = self.master.state.flows[0]
assert not first_flow.server_conn.via
class MasterRedirectRequest(tservers.TestMaster): class MasterRedirectRequest(tservers.TestMaster):
redirect_port = None # Set by TestRedirectRequest redirect_port = None # Set by TestRedirectRequest
@ -950,11 +957,14 @@ class TestUpstreamProxySSL(
# CONNECT from pathoc to chain[0], # CONNECT from pathoc to chain[0],
assert self.proxy.tmaster.state.flow_count() == 1 assert self.proxy.tmaster.state.flow_count() == 1
assert self.proxy.tmaster.state.flows[0].server_conn.via
# request from pathoc to chain[0] # request from pathoc to chain[0]
# CONNECT from proxy to chain[1], # CONNECT from proxy to chain[1],
assert self.chain[0].tmaster.state.flow_count() == 1 assert self.chain[0].tmaster.state.flow_count() == 1
assert self.chain[0].tmaster.state.flows[0].server_conn.via
# request from proxy to chain[1] # request from proxy to chain[1]
# request from chain[0] (regular proxy doesn't store CONNECTs) # request from chain[0] (regular proxy doesn't store CONNECTs)
assert not self.chain[1].tmaster.state.flows[0].server_conn.via
assert self.chain[1].tmaster.state.flow_count() == 1 assert self.chain[1].tmaster.state.flow_count() == 1