aboutsummaryrefslogtreecommitdiffstats
path: root/libmproxy/protocol2/http.py
diff options
context:
space:
mode:
Diffstat (limited to 'libmproxy/protocol2/http.py')
-rw-r--r--libmproxy/protocol2/http.py174
1 files changed, 150 insertions, 24 deletions
diff --git a/libmproxy/protocol2/http.py b/libmproxy/protocol2/http.py
index 54cc9dbc..44ebf6a8 100644
--- a/libmproxy/protocol2/http.py
+++ b/libmproxy/protocol2/http.py
@@ -1,17 +1,22 @@
from __future__ import (absolute_import, print_function, division)
+from .. import version
+from ..exceptions import InvalidCredentials, HttpException, ProtocolException
from .layer import Layer, ServerConnectionMixin
-from libmproxy import version
-from libmproxy.exceptions import InvalidCredentials
+from .messages import ChangeServer, Connect, Reconnect
+from .http_proxy import HttpProxy, HttpUpstreamProxy
+from libmproxy.protocol import KILL
+
from libmproxy.protocol.http import HTTPFlow
-from libmproxy.protocol.http_wrappers import HTTPResponse
+from libmproxy.protocol.http_wrappers import HTTPResponse, HTTPRequest
from libmproxy.protocol2.http_protocol_mock import HTTP1
+from libmproxy.protocol2.tls import TlsLayer
from netlib import tcp
from netlib.http import status_codes
from netlib import odict
-def send_http_error_response(status_code, message, headers=odict.ODictCaseless()):
+def make_error_response(status_code, message, headers=None):
response = status_codes.RESPONSES.get(status_code, "Unknown")
body = """
<html>
@@ -22,21 +27,40 @@ def send_http_error_response(status_code, message, headers=odict.ODictCaseless()
</html>
""".strip() % (status_code, response, message)
+ if not headers:
+ headers = odict.ODictCaseless()
headers["Server"] = [version.NAMEVERSION]
headers["Connection"] = ["close"]
headers["Content-Length"] = [len(body)]
headers["Content-Type"] = ["text/html"]
- resp = HTTPResponse(
- (1, 1), # if HTTP/2 is used, this value is ignored anyway
+ return HTTPResponse(
+ (1, 1), # FIXME: Should be a string.
status_code,
response,
headers,
body,
)
- protocol = self.c.client_conn.protocol or http1.HTTP1Protocol(self.c.client_conn)
- self.c.client_conn.send(protocol.assemble(resp))
+def make_connect_request(address):
+ return HTTPRequest(
+ "authority", "CONNECT", None, address.host, address.port, None, (1,1),
+ odict.ODictCaseless(), ""
+ )
+
+def make_connect_response(httpversion):
+ headers = odict.ODictCaseless([
+ ["Content-Length", "0"],
+ ["Proxy-Agent", version.NAMEVERSION]
+ ])
+ return HTTPResponse(
+ httpversion,
+ 200,
+ "Connection established",
+ headers,
+ "",
+ )
+
class HttpLayer(Layer, ServerConnectionMixin):
"""
@@ -45,11 +69,16 @@ class HttpLayer(Layer, ServerConnectionMixin):
def __init__(self, ctx):
super(HttpLayer, self).__init__(ctx)
- self.skip_authentication = False
+ if any(isinstance(l, HttpProxy) for l in self.layers):
+ self.mode = "regular"
+ elif any(isinstance(l, HttpUpstreamProxy) for l in self.layers):
+ self.mode = "upstream"
+ else:
+ # also includes socks or reverse mode, which are handled similarly on this layer.
+ self.mode = "transparent"
def __call__(self):
while True:
- flow = HTTPFlow(self.client_conn, self.server_conn)
try:
request = HTTP1.read_request(
self.client_conn,
@@ -62,29 +91,126 @@ class HttpLayer(Layer, ServerConnectionMixin):
self.c.log("request", "debug", [repr(request)])
- self.check_authentication(request)
+ # Handle Proxy Authentication
+ self.authenticate(request)
+ # Regular Proxy Mode: Handle CONNECT
if self.mode == "regular" and request.form_in == "authority":
- raise NotImplementedError
-
+ self.server_address = (request.host, request.port)
+ self.send_to_client(make_connect_response(request.httpversion))
+ layer = self.ctx.next_layer(self)
+ for message in layer():
+ if not self._handle_server_message(message):
+ yield message
+ return
+ # Make sure that the incoming request matches our expectations
+ self.validate_request(request)
- ret = self.process_request(flow, request)
- if ret is True:
- continue
- if ret is False:
+ flow = HTTPFlow(self.client_conn, self.server_conn)
+ flow.request = request
+ if not self.process_request_hook(flow):
+ self.log("Connection killed", "info")
return
- def check_authentication(self, request):
+ if not flow.response:
+ self.establish_server_connection(flow)
+
+ def process_request_hook(self, flow):
+ # Determine .scheme, .host and .port attributes for inline scripts.
+ # For absolute-form requests, they are directly given in the request.
+ # For authority-form requests, we only need to determine the request scheme.
+ # For relative-form requests, we need to determine host and port as
+ # well.
+ if self.mode == "regular":
+ pass # only absolute-form at this point, nothing to do here.
+ elif self.mode == "upstream":
+ if flow.request.form_in == "authority":
+ flow.request.scheme = "http" # pseudo value
+ else:
+ flow.request.host = self.ctx.server_address.host
+ flow.request.port = self.ctx.server_address.port
+ flow.request.scheme = self.server_conn.tls_established
+
+ # TODO: Expose ChangeServer functionality to inline scripts somehow? (yield_from_callback?)
+ request_reply = self.c.channel.ask("request", flow)
+ if request_reply is None or request_reply == KILL:
+ return False
+ if isinstance(request_reply, HTTPResponse):
+ flow.response = request_reply
+ return
+
+ def establish_server_connection(self, flow):
+
+ address = tcp.Address((flow.request.host, flow.request.port))
+ tls = (flow.request.scheme == "https")
+ if self.mode == "regular" or self.mode == "transparent":
+ # If there's an existing connection that doesn't match our expectations, kill it.
+ if self.server_address != address or tls != self.server_address.ssl_established:
+ yield ChangeServer(address, tls, address.host)
+ # Establish connection is neccessary.
+ if not self.server_conn:
+ yield Connect()
+
+ # ChangeServer is not guaranteed to work with TLS:
+ # If there's not TlsLayer below which could catch the exception,
+ # TLS will not be established.
+ if tls and not self.server_conn.tls_established:
+ raise ProtocolException("Cannot upgrade to SSL, no TLS layer on the protocol stack.")
+
+ else:
+ if tls:
+ raise HttpException("Cannot change scheme in upstream proxy mode.")
+ """
+ # This is a very ugly (untested) workaround to solve a very ugly problem.
+ # FIXME: Check if connected first.
+ if self.server_conn.tls_established and not ssl:
+ yield Reconnect()
+ elif ssl and not hasattr(self, "connected_to") or self.connected_to != address:
+ if self.server_conn.tls_established:
+ yield Reconnect()
+
+ self.send_to_server(make_connect_request(address))
+ tls_layer = TlsLayer(self, False, True)
+ tls_layer._establish_tls_with_server()
+ """
+
+ def validate_request(self, request):
+ if request.form_in == "absolute" and request.scheme != "http":
+ self.send_resplonse(make_error_response(400, "Invalid request scheme: %s" % request.scheme))
+ raise HttpException("Invalid request scheme: %s" % request.scheme)
+
+ expected_request_forms = {
+ "regular": ("absolute",), # an authority request would already be handled.
+ "upstream": ("authority", "absolute"),
+ "transparent": ("regular",)
+ }
+
+ allowed_request_forms = expected_request_forms[self.mode]
+ if request.form_in not in allowed_request_forms:
+ err_message = "Invalid HTTP request form (expected: %s, got: %s)" % (
+ " or ".join(allowed_request_forms), request.form_in
+ )
+ self.send_to_client(make_error_response(400, err_message))
+ raise HttpException(err_message)
+
+ def authenticate(self, request):
if self.config.authenticator:
if self.config.authenticator.authenticate(request.headers):
self.config.authenticator.clean(request.headers)
else:
- self.send_error()
+ self.send_to_client(make_error_response(
+ 407,
+ "Proxy Authentication Required",
+ self.config.authenticator.auth_challenge_headers()
+ ))
raise InvalidCredentials("Proxy Authentication Required")
- raise http.HttpAuthenticationError(
- self.c.config.authenticator.auth_challenge_headers())
- return request.headers
- def send_error(self, code, message, headers):
- pass \ No newline at end of file
+ def send_to_server(self, message):
+ self.server_conn.wfile.wrie(message)
+
+ def send_to_client(self, message):
+ # FIXME
+ # - possibly do some http2 stuff here
+ # - fix message assembly.
+ self.client_conn.wfile.write(message)