diff options
Diffstat (limited to 'libmproxy/proxy.py')
-rw-r--r-- | libmproxy/proxy.py | 135 |
1 files changed, 55 insertions, 80 deletions
diff --git a/libmproxy/proxy.py b/libmproxy/proxy.py index 283072ab..1fc289ed 100644 --- a/libmproxy/proxy.py +++ b/libmproxy/proxy.py @@ -1,27 +1,11 @@ -# Copyright (C) 2012 Aldo Cortesi -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. import sys, os, string, socket, time import shutil, tempfile, threading import SocketServer from OpenSSL import SSL from netlib import odict, tcp, http, wsgi, certutils, http_status, http_auth -import utils, flow, version, platform, controller, app +import utils, flow, version, platform, controller -APP_DOMAIN = "mitm" -APP_IP = "1.1.1.1" KILL = 0 @@ -39,17 +23,17 @@ class Log: class ProxyConfig: - def __init__(self, app=False, certfile = None, cacert = None, clientcerts = None, no_upstream_cert=False, body_size_limit = None, reverse_proxy=None, transparent_proxy=None, certdir = None, authenticator=None): - self.app = app + def __init__(self, certfile = None, cacert = None, clientcerts = None, no_upstream_cert=False, body_size_limit = None, reverse_proxy=None, forward_proxy=None, transparent_proxy=None, authenticator=None): self.certfile = certfile self.cacert = cacert self.clientcerts = clientcerts self.no_upstream_cert = no_upstream_cert self.body_size_limit = body_size_limit self.reverse_proxy = reverse_proxy + self.forward_proxy = forward_proxy self.transparent_proxy = transparent_proxy self.authenticator = authenticator - self.certstore = certutils.CertStore(certdir) + self.certstore = certutils.CertStore() class ServerConnection(tcp.TCPClient): @@ -61,7 +45,6 @@ class ServerConnection(tcp.TCPClient): self.tcp_setup_timestamp = None self.ssl_setup_timestamp = None - def connect(self): tcp.TCPClient.connect(self) self.tcp_setup_timestamp = time.time() @@ -86,11 +69,12 @@ class ServerConnection(tcp.TCPClient): self.wfile.flush() def terminate(self): - try: - self.wfile.flush() + if self.connection: + try: + self.wfile.flush() + except tcp.NetLibDisconnect: # pragma: no cover + pass self.connection.close() - except IOError: - pass @@ -105,11 +89,13 @@ class RequestReplayThread(threading.Thread): server = ServerConnection(self.config, r.scheme, r.host, r.port, r.host) server.connect() server.send(r) + tsstart = utils.timestamp() httpversion, code, msg, headers, content = http.read_response( server.rfile, r.method, self.config.body_size_limit ) response = flow.Response( - self.flow.request, httpversion, code, msg, headers, content, server.cert + self.flow.request, httpversion, code, msg, headers, content, server.cert, + server.rfile.first_byte_timestamp ) self.channel.ask(response) except (ProxyError, http.HttpError, tcp.NetLibError), v: @@ -129,7 +115,7 @@ class HandleSNI: self.handler.get_server_connection(self.client_conn, "https", self.host, self.port, sn) new_context = SSL.Context(SSL.TLSv1_METHOD) new_context.use_privatekey_file(self.key) - new_context.use_certificate_file(self.cert) + new_context.use_certificate(self.cert.x509) connection.set_context(new_context) self.handler.sni = sn.decode("utf8").encode("idna") # An unhandled exception in this method will core dump PyOpenSSL, so @@ -173,12 +159,15 @@ class ProxyHandler(tcp.BaseHandler): if not self.server_conn: try: self.server_conn = ServerConnection(self.config, scheme, host, port, sni) + self.channel.ask(self.server_conn) self.server_conn.connect() except tcp.NetLibError, v: raise ProxyError(502, v) return self.server_conn def del_server_connection(self): + if self.server_conn: + self.server_conn.terminate() self.server_conn = None def handle(self): @@ -188,6 +177,7 @@ class ProxyHandler(tcp.BaseHandler): while self.handle_request(cc) and not cc.close: pass cc.close = True + self.del_server_connection() cd = flow.ClientDisconnect(cc) self.log( @@ -213,7 +203,7 @@ class ProxyHandler(tcp.BaseHandler): return else: request_reply = self.channel.ask(request) - if request_reply == KILL: + if request_reply is None or request_reply == KILL: return elif isinstance(request_reply, flow.Response): request = False @@ -231,13 +221,19 @@ class ProxyHandler(tcp.BaseHandler): # the case, we want to reconnect without sending an error # to the client. while 1: - sc = self.get_server_connection(cc, scheme, host, port, self.sni) + if self.config.forward_proxy: + forward_scheme, forward_host, forward_port = self.config.forward_proxy + sc = self.get_server_connection(cc, forward_scheme, forward_host, forward_port, self.sni) + else: + sc = self.get_server_connection(cc, scheme, host, port, self.sni) + sc.send(request) if sc.requestcount == 1: # add timestamps only for first request (others are not directly affected) request.tcp_setup_timestamp = sc.tcp_setup_timestamp request.ssl_setup_timestamp = sc.ssl_setup_timestamp sc.rfile.reset_timestamps() try: + tsstart = utils.timestamp() httpversion, code, msg, headers, content = http.read_response( sc.rfile, request.method, @@ -256,7 +252,7 @@ class ProxyHandler(tcp.BaseHandler): response = flow.Response( request, httpversion, code, msg, headers, content, sc.cert, - sc.rfile.first_byte_timestamp, utils.timestamp() + sc.rfile.first_byte_timestamp ) response_reply = self.channel.ask(response) # Not replying to the server invalidates the server @@ -269,15 +265,15 @@ class ProxyHandler(tcp.BaseHandler): else: response = response_reply self.send_response(response) - if request and http.request_connection_close(request.httpversion, request.headers): + if request and http.connection_close(request.httpversion, request.headers): return # We could keep the client connection when the server # connection needs to go away. However, we want to mimic # behaviour as closely as possible to the client, so we # disconnect. - if http.response_connection_close(response.httpversion, response.headers): + if http.connection_close(response.httpversion, response.headers): return - except (IOError, ProxyError, http.HttpError, tcp.NetLibDisconnect), e: + except (IOError, ProxyError, http.HttpError, tcp.NetLibError), e: if hasattr(e, "code"): cc.error = "%s: %s"%(e.code, e.msg) else: @@ -309,7 +305,7 @@ class ProxyHandler(tcp.BaseHandler): def find_cert(self, cc, host, port, sni): if self.config.certfile: - return self.config.certfile + return certutils.SSLCert.from_pem(file(self.config.certfile, "r").read()) else: sans = [] if not self.config.no_upstream_cert: @@ -321,6 +317,17 @@ class ProxyHandler(tcp.BaseHandler): raise ProxyError(502, "Unable to generate dummy cert.") return ret + def establish_ssl(self, client_conn, host, port): + dummycert = self.find_cert(client_conn, host, port, host) + sni = HandleSNI( + self, client_conn, host, port, + dummycert, self.config.certfile or self.config.cacert + ) + try: + self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) + except tcp.NetLibError, v: + raise ProxyError(400, str(v)) + def get_line(self, fp): """ Get a line, possibly preceded by a blank. @@ -340,15 +347,7 @@ class ProxyHandler(tcp.BaseHandler): if port in self.config.transparent_proxy["sslports"]: scheme = "https" if not self.ssl_established: - dummycert = self.find_cert(client_conn, host, port, host) - sni = HandleSNI( - self, client_conn, host, port, - dummycert, self.config.certfile or self.config.cacert - ) - try: - self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) - except tcp.NetLibError, v: - raise ProxyError(400, str(v)) + self.establish_ssl(client_conn, host, port) else: scheme = "http" line = self.get_line(self.rfile) @@ -383,15 +382,7 @@ class ProxyHandler(tcp.BaseHandler): '\r\n' ) self.wfile.flush() - dummycert = self.find_cert(client_conn, host, port, host) - sni = HandleSNI( - self, client_conn, host, port, - dummycert, self.config.certfile or self.config.cacert - ) - try: - self.convert_to_ssl(dummycert, self.config.certfile or self.config.cacert, handle_sni=sni) - except tcp.NetLibError, v: - raise ProxyError(400, str(v)) + self.establish_ssl(client_conn, host, port) self.proxy_connect_state = (host, port, httpversion) line = self.rfile.readline(line) @@ -425,10 +416,12 @@ class ProxyHandler(tcp.BaseHandler): ) def read_request_reverse(self, client_conn): + scheme, host, port = self.config.reverse_proxy + if scheme.lower() == "https" and not self.ssl_established: + self.establish_ssl(client_conn, host, port) line = self.get_line(self.rfile) if line == "": return None - scheme, host, port = self.config.reverse_proxy r = http.parse_init_http(line) if not r: raise ProxyError(400, "Bad HTTP request line: %s"%repr(line)) @@ -438,7 +431,7 @@ class ProxyHandler(tcp.BaseHandler): self.rfile, self.wfile, headers, httpversion, self.config.body_size_limit ) return flow.Request( - client_conn, httpversion, host, port, "http", method, path, headers, content, + client_conn, httpversion, host, port, scheme, method, path, headers, content, self.rfile.first_byte_timestamp, utils.timestamp() ) @@ -509,17 +502,6 @@ class ProxyServer(tcp.TCPServer): raise ProxyServerError('Error starting proxy server: ' + v.strerror) self.channel = None self.apps = AppRegistry() - if config.app: - self.apps.add( - app.mapp, - APP_DOMAIN, - 80 - ) - self.apps.add( - app.mapp, - APP_IP, - 80 - ) def start_slave(self, klass, channel): slave = klass(channel, self) @@ -533,9 +515,6 @@ class ProxyServer(tcp.TCPServer): h.handle() h.finish() - def handle_shutdown(self): - self.config.certstore.cleanup() - class AppRegistry: def __init__(self): @@ -584,11 +563,6 @@ def certificate_option_group(parser): type = str, dest = "clientcerts", default=None, help = "Client certificate directory." ) - group.add_argument( - "--dummy-certs", action="store", - type = str, dest = "certdir", default=None, - help = "Generated dummy certs directory." - ) TRANSPARENT_SSL_PORTS = [443, 8443] @@ -624,16 +598,18 @@ def process_proxy_options(parser, options): else: rp = None + if options.forward_proxy: + fp = utils.parse_proxy_spec(options.forward_proxy) + if not fp: + return parser.error("Invalid forward proxy specification: %s"%options.forward_proxy) + else: + fp = None + if options.clientcerts: options.clientcerts = os.path.expanduser(options.clientcerts) if not os.path.exists(options.clientcerts) or not os.path.isdir(options.clientcerts): return parser.error("Client certificate directory does not exist or is not a directory: %s"%options.clientcerts) - if options.certdir: - options.certdir = os.path.expanduser(options.certdir) - if not os.path.exists(options.certdir) or not os.path.isdir(options.certdir): - return parser.error("Dummy cert directory does not exist or is not a directory: %s"%options.certdir) - if (options.auth_nonanonymous or options.auth_singleuser or options.auth_htpasswd): if options.auth_singleuser: if len(options.auth_singleuser.split(':')) != 2: @@ -652,14 +628,13 @@ def process_proxy_options(parser, options): authenticator = http_auth.NullProxyAuth(None) return ProxyConfig( - app = options.app, certfile = options.cert, cacert = cacert, clientcerts = options.clientcerts, body_size_limit = body_size_limit, no_upstream_cert = options.no_upstream_cert, reverse_proxy = rp, + forward_proxy = fp, transparent_proxy = trans, - certdir = options.certdir, authenticator = authenticator ) |