proxy.protocol.http: cleanups, extract request validation

This commit is contained in:
Aldo Cortesi 2016-11-05 09:37:54 +13:00
parent 4eea265925
commit 53b77fc475
1 changed files with 35 additions and 28 deletions

View File

@ -112,12 +112,34 @@ class UpstreamConnectLayer(base.Layer):
self.server_conn.address = address self.server_conn.address = address
FIRSTLINES = set(["absolute", "relative", "authority"])
# At this point, we see only a subset of the proxy modes
MODES = set(["regular", "transparent", "upstream"])
MODE_REQUEST_FORMS = {
"regular": ("authority", "absolute"),
"transparent": ("relative"),
"upstream": ("authority", "absolute"),
}
def validate_request_form(mode, request):
if request.first_line_format == "absolute" and request.scheme != "http":
raise exceptions.HttpException("Invalid request scheme: %s" % request.scheme)
allowed_request_forms = MODE_REQUEST_FORMS[mode]
if request.first_line_format not in allowed_request_forms:
err_message = "Invalid HTTP request form (expected: %s, got: %s)" % (
" or ".join(allowed_request_forms), request.first_line_format
)
raise exceptions.HttpException(err_message)
class HttpLayer(base.Layer): class HttpLayer(base.Layer):
def __init__(self, ctx, mode): def __init__(self, ctx, mode):
super().__init__(ctx) super().__init__(ctx)
if mode not in MODES:
raise exceptions.ServerException("Invalid mode: %s"%mode)
self.mode = mode self.mode = mode
self.flow = None # type: http.HTTPFlow
self.__initial_server_conn = None self.__initial_server_conn = None
"Contains the original destination in transparent mode, which needs to be restored" "Contains the original destination in transparent mode, which needs to be restored"
"if an inline script modified the target server for a single http request" "if an inline script modified the target server for a single http request"
@ -125,23 +147,21 @@ class HttpLayer(base.Layer):
# see https://github.com/mitmproxy/mitmproxy/issues/925 # see https://github.com/mitmproxy/mitmproxy/issues/925
self.__initial_server_tls = None self.__initial_server_tls = None
# Requests happening after CONNECT do not need Proxy-Authorization headers. # Requests happening after CONNECT do not need Proxy-Authorization headers.
self.http_authenticated = False self.connect_request = False
def __call__(self): def __call__(self):
if self.mode == "transparent": if self.mode == "transparent":
self.__initial_server_tls = self.server_tls self.__initial_server_tls = self.server_tls
self.__initial_server_conn = self.server_conn self.__initial_server_conn = self.server_conn
while True: while True:
self.flow = http.HTTPFlow(self.client_conn, self.server_conn, live=self) flow = http.HTTPFlow(self.client_conn, self.server_conn, live=self)
if not self._process_flow(self.flow): if not self._process_flow(flow):
return return
def _process_flow(self, f): def _process_flow(self, f):
try: try:
request = self.read_request_headers(f) request = self.read_request_headers(f)
request.data.content = b"".join( request.data.content = b"".join(self.read_request_body(request))
self.read_request_body(request)
)
request.timestamp_end = time.time() request.timestamp_end = time.time()
f.request = request f.request = request
self.channel.ask("requestheaders", f) self.channel.ask("requestheaders", f)
@ -152,25 +172,11 @@ class HttpLayer(base.Layer):
request.content = b"".join(self.read_request_body(request)) request.content = b"".join(self.read_request_body(request))
request.timestamp_end = time.time() request.timestamp_end = time.time()
# Make sure that the incoming request matches our expectations validate_request_form(self.mode, request)
if request.first_line_format == "absolute" and request.scheme != "http":
raise exceptions.HttpException("Invalid request scheme: %s" % request.scheme)
expected_request_forms = {
"regular": ("authority", "absolute",),
"upstream": ("authority", "absolute"),
"transparent": ("relative",)
}
allowed_request_forms = expected_request_forms[self.mode]
if request.first_line_format not in allowed_request_forms:
err_message = "Invalid HTTP request form (expected: %s, got: %s)" % (
" or ".join(allowed_request_forms), request.first_line_format
)
raise exceptions.HttpException(err_message)
if self.mode == "regular" and request.first_line_format == "absolute": if self.mode == "regular" and request.first_line_format == "absolute":
request.first_line_format = "relative" request.first_line_format = "relative"
except exceptions.HttpReadDisconnect: except exceptions.HttpReadDisconnect:
# don't throw an error for disconnects that happen before/between requests. # don't throw an error for disconnects that happen before/between requests.
return False return False
@ -188,7 +194,7 @@ class HttpLayer(base.Layer):
# Proxy Authentication conceptually does not work in transparent mode. # Proxy Authentication conceptually does not work in transparent mode.
# We catch this misconfiguration on startup. Here, we sort out requests # We catch this misconfiguration on startup. Here, we sort out requests
# after a successful CONNECT request (which do not need to be validated anymore) # after a successful CONNECT request (which do not need to be validated anymore)
if not (self.http_authenticated or self.authenticate(request)): if not self.connect_request and not self.authenticate(request):
return False return False
f.request = request f.request = request
@ -196,7 +202,7 @@ class HttpLayer(base.Layer):
try: try:
# Regular Proxy Mode: Handle CONNECT # Regular Proxy Mode: Handle CONNECT
if self.mode == "regular" and request.first_line_format == "authority": if self.mode == "regular" and request.first_line_format == "authority":
self.http_authenticated = True self.connect_request = True
self.set_server((request.host, request.port)) self.set_server((request.host, request.port))
self.send_response(http.make_connect_response(request.data.http_version)) self.send_response(http.make_connect_response(request.data.http_version))
layer = self.ctx.next_layer(self) layer = self.ctx.next_layer(self)
@ -275,7 +281,9 @@ class HttpLayer(base.Layer):
if isinstance(e, exceptions.Http2ProtocolException): if isinstance(e, exceptions.Http2ProtocolException):
# do not try to reconnect for HTTP2 # do not try to reconnect for HTTP2
raise exceptions.ProtocolException("First and only attempt to get response via HTTP2 failed.") raise exceptions.ProtocolException(
"First and only attempt to get response via HTTP2 failed."
)
self.disconnect() self.disconnect()
self.connect() self.connect()
@ -334,13 +342,12 @@ class HttpLayer(base.Layer):
""" """
# Check for WebSockets handshake # Check for WebSockets handshake
is_websockets = ( is_websockets = (
f and
websockets.check_handshake(f.request.headers) and websockets.check_handshake(f.request.headers) and
websockets.check_handshake(f.response.headers) websockets.check_handshake(f.response.headers)
) )
if is_websockets and not self.config.options.websockets: if is_websockets and not self.config.options.websockets:
self.log( self.log(
"Client requested WebSocket connection, but the protocol is currently disabled in mitmproxy.", "Client requested WebSocket connection, but the protocol is disabled.",
"info" "info"
) )