Refactor reverse proxying

- Retain the specification from the Host header as a Request's description.
- Expand upstream proxy specifications to include the scheme. We now say https://hostname:port
- Move the "R" revert keybinding to "v" to make room for a reverse proxy
binding that matches the command-line flag.
This commit is contained in:
Aldo Cortesi 2012-02-18 14:45:22 +13:00
parent acdc2d00b4
commit a7df6e1503
7 changed files with 57 additions and 43 deletions

View File

@ -66,7 +66,7 @@ def common_options(parser):
)
parser.add_option(
"-R",
action="store", dest="reverse_upstream", default=None,
action="store", dest="reverse_proxy", default=None,
help="Reverse proxy: upstream server host:port"
)
parser.add_option(

View File

@ -8,11 +8,11 @@ def _mkhelp():
("a", "accept this intercepted connection"),
("C", "clear connection list or eventlog"),
("d", "delete flow"),
("e", "toggle eventlog"),
("l", "set limit filter pattern"),
("L", "load saved flows"),
("r", "replay request"),
("R", "revert changes to request"),
("v", "toggle eventlog"),
("w", "save all flows matching current limit"),
("W", "save this flow"),
("X", "kill and delete connection, even if it's mid-intercept"),
@ -133,8 +133,6 @@ class ConnectionItem(common.WWrap):
)
elif key == "X":
self.flow.kill(self.master)
elif key == "v":
self.master.toggle_eventlog()
elif key == "enter":
if self.flow.request:
self.master.view_flow(self.flow)
@ -190,7 +188,7 @@ class ConnectionListBox(urwid.ListBox):
elif key == "C":
self.master.clear_connections()
key = None
elif key == "v":
elif key == "e":
self.master.toggle_eventlog()
key = None
return urwid.ListBox.keypress(self, size, key)

View File

@ -26,7 +26,7 @@ def _mkhelp():
),
("p", "previous flow"),
("r", "replay request"),
("R", "revert changes to request"),
("V", "revert changes to request"),
("v", "view body in external viewer"),
("w", "save all flows matching current limit"),
("W", "save this flow"),
@ -513,7 +513,7 @@ class ConnectionView(common.WWrap):
if r:
self.master.statusbar.message(r)
self.master.refresh_connection(self.flow)
elif key == "R":
elif key == "V":
self.state.revert(self.flow)
self.master.refresh_connection(self.flow)
elif key == "W":

View File

@ -1292,9 +1292,9 @@ class FlowMaster(controller.Master):
f.error = None
self.process_new_request(f)
rt = proxy.RequestReplayThread(
self.server.config,
f,
self.masterq,
self.server.config.body_size_limit
)
rt.start()
#end nocover

View File

@ -2,8 +2,6 @@
A simple proxy server implementation, which always reads all of a server
response into memory, performs some transformation, and then writes it back
to the client.
Development started from Neil Schemenauer's munchy.py
"""
import sys, os, string, socket, time
import shutil, tempfile, threading
@ -22,14 +20,14 @@ class ProxyError(Exception):
class ProxyConfig:
def __init__(self, certfile = None, ciphers = None, cacert = None, cert_wait_time=0, body_size_limit = None, reverse_upstream=None):
def __init__(self, certfile = None, ciphers = None, cacert = None, cert_wait_time=0, body_size_limit = None, reverse_proxy=None):
self.certfile = certfile
self.ciphers = ciphers
self.cacert = cacert
self.certdir = None
self.cert_wait_time = cert_wait_time
self.body_size_limit = body_size_limit
self.reverse_upstream = reverse_upstream
self.reverse_proxy = reverse_proxy
def read_chunked(fp, limit):
@ -162,14 +160,14 @@ class FileLike:
#begin nocover
class RequestReplayThread(threading.Thread):
def __init__(self, flow, masterq, body_size_limit):
self.flow, self.masterq, self.body_size_limit = flow, masterq, body_size_limit
def __init__(self, config, flow, masterq):
self.config, self.flow, self.masterq = config, flow, masterq
threading.Thread.__init__(self)
def run(self):
try:
server = ServerConnection(self.flow.request, self.body_size_limit)
server.send_request(self.flow.request)
server = ServerConnection(self.config, self.flow.request)
server.send()
response = server.read_response()
response._send(self.masterq)
except ProxyError, v:
@ -178,11 +176,14 @@ class RequestReplayThread(threading.Thread):
class ServerConnection:
def __init__(self, request, body_size_limit):
self.body_size_limit = body_size_limit
self.host = request.host
self.port = request.port
self.scheme = request.scheme
def __init__(self, config, request):
self.config, self.request = config, request
if config.reverse_proxy:
self.scheme, self.host, self.port = config.reverse_proxy
else:
self.host = request.host
self.port = request.port
self.scheme = request.scheme
self.close = False
self.server, self.rfile, self.wfile = None, None, None
self.connect()
@ -199,14 +200,13 @@ class ServerConnection:
self.server = server
self.rfile, self.wfile = server.makefile('rb'), server.makefile('wb')
def send_request(self, request):
self.request = request
request.close = self.close
def send(self):
self.request.close = self.close
try:
self.wfile.write(request._assemble())
self.wfile.write(self.request._assemble())
self.wfile.flush()
except socket.error, err:
raise ProxyError(502, 'Error sending data to "%s": %s' % (request.host, err))
raise ProxyError(502, 'Error sending data to "%s": %s' % (self.request.host, err))
def read_response(self):
line = self.rfile.readline()
@ -231,7 +231,7 @@ class ServerConnection:
if self.request.method == "HEAD" or code == 204 or code == 304:
content = ""
else:
content = read_http_body(self.rfile, self, headers, True, self.body_size_limit)
content = read_http_body(self.rfile, self, headers, True, self.config.body_size_limit)
return flow.Response(self.request, code, msg, headers, content)
def terminate(self):
@ -279,8 +279,8 @@ class ProxyHandler(SocketServer.StreamRequestHandler):
request = False
response = response._send(self.mqueue)
else:
server = ServerConnection(request, self.config.body_size_limit)
server.send_request(request)
server = ServerConnection(self.config, request)
server.send()
try:
response = server.read_response()
except IOError, v:
@ -348,15 +348,6 @@ class ProxyHandler(SocketServer.StreamRequestHandler):
self.rfile = FileLike(self.connection)
self.wfile = FileLike(self.connection)
method, scheme, host, port, path, httpminor = parse_request_line(self.rfile.readline())
# If we're in reverse proxy mode, we only get the path and
# version in the request and need to fill up host and port
# from the configuration. This still assumes that the client will
# provide the correct Host: header and we do not need to tamper
# with that (or will tamper using other means).
if self.config.reverse_upstream:
scheme = 'http'
host, port = self.config.reverse_upstream.split(':')
port = int(port)
if scheme is None:
scheme = "https"
headers = flow.Headers()
@ -374,9 +365,12 @@ class ProxyHandler(SocketServer.StreamRequestHandler):
port = 80
port = int(port)
if host is None:
# FIXME: We only specify the first part of the invalid request in this error.
# We should gather up everything read from the socket, and specify it all.
raise ProxyError(400, 'Invalid request: %s'%line)
if self.config.reverse_proxy:
scheme, host, port = self.config.reverse_proxy
else:
# FIXME: We only specify the first part of the invalid request in this error.
# We should gather up everything read from the socket, and specify it all.
raise ProxyError(400, 'Invalid request: %s'%line)
if "expect" in headers:
expect = ",".join(headers['expect'])
if expect == "100-continue" and httpminor >= 1:
@ -493,13 +487,21 @@ def process_proxy_options(parser, options):
if getattr(options, "cache", None) is not None:
options.cache = os.path.expanduser(options.cache)
body_size_limit = utils.parse_size(options.body_size_limit)
if options.reverse_proxy:
rp = utils.parse_proxy_spec(options.reverse_proxy)
if not rp:
parser.error("Invalid reverse proxy specification: %s"%options.reverse_proxy)
else:
rp = None
return ProxyConfig(
certfile = options.cert,
cacert = cacert,
ciphers = options.ciphers,
cert_wait_time = options.cert_wait_time,
body_size_limit = body_size_limit,
reverse_upstream = options.reverse_upstream
reverse_proxy = rp
)

View File

@ -161,7 +161,6 @@ def del_all(dict, keys):
del dict[key]
def pretty_size(size):
suffixes = [
("B", 2**10),
@ -421,6 +420,13 @@ def parse_url(url):
return scheme, host, port, path
def parse_proxy_spec(url):
p = parse_url(url)
if not p:
return None
return p[:3]
def clean_hanging_newline(t):
"""
Many editors will silently add a newline to the final line of a

View File

@ -200,6 +200,13 @@ class uLRUCache(libpry.AutoTree):
assert len(f._cachelist_one) == 2
class u_parse_proxy_spec(libpry.AutoTree):
def test_simple(self):
assert not utils.parse_proxy_spec("")
assert utils.parse_proxy_spec("http://foo.com:88") == ("http", "foo.com", 88)
assert utils.parse_proxy_spec("http://foo.com") == ("http", "foo.com", 80)
assert not utils.parse_proxy_spec("foo.com")
class u_parse_url(libpry.AutoTree):
def test_simple(self):
@ -254,6 +261,7 @@ tests = [
udummy_cert(),
uLRUCache(),
u_parse_url(),
u_parse_proxy_spec(),
u_parse_size(),
uclean_hanging_newline()
]