A fork of pappy proxy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

655 lines
25 KiB

9 years ago
import collections
import copy
9 years ago
import datetime
import os
import random
9 years ago
9 years ago
from OpenSSL import SSL
9 years ago
from OpenSSL import crypto
from pappyproxy import context
from pappyproxy import http
9 years ago
from pappyproxy import macros
9 years ago
from pappyproxy.util import PappyException, printable_data
from twisted.internet import defer
9 years ago
from twisted.internet import reactor, ssl
from twisted.internet.protocol import ClientFactory, ServerFactory
9 years ago
from twisted.protocols.basic import LineReceiver
next_connection_id = 1
cached_certs = {}
def get_next_connection_id():
global next_connection_id
ret_id = next_connection_id
next_connection_id += 1
return ret_id
def add_intercepting_macro(key, macro, int_macro_dict):
if key in int_macro_dict:
raise PappyException('Macro with key %s already exists' % key)
int_macro_dict[key] = macro
def remove_intercepting_macro(key, int_macro_dict):
if not key in int_macro_dict:
9 years ago
raise PappyException('Macro with key %s not currently running' % key)
del int_macro_dict[key]
9 years ago
def log(message, id=None, symbol='*', verbosity_level=1):
9 years ago
from pappyproxy.pappy import session
if session.config.debug_to_file or session.config.debug_verbosity > 0:
if session.config.debug_to_file and not os.path.exists(session.config.debug_dir):
os.makedirs(session.config.debug_dir)
9 years ago
if id:
debug_str = '[%s](%d) %s' % (symbol, id, message)
9 years ago
if session.config.debug_to_file:
with open(session.config.debug_dir+'/connection_%d.log' % id, 'a') as f:
9 years ago
f.write(debug_str+'\n')
else:
debug_str = '[%s] %s' % (symbol, message)
9 years ago
if session.config.debug_to_file:
with open(session.config.debug_dir+'/debug.log', 'a') as f:
9 years ago
f.write(debug_str+'\n')
9 years ago
if session.config.debug_verbosity >= verbosity_level:
9 years ago
print debug_str
9 years ago
def log_request(request, id=None, symbol='*', verbosity_level=3):
9 years ago
from pappyproxy.pappy import session
if session.config.debug_to_file or session.config.debug_verbosity > 0:
9 years ago
r_split = request.split('\r\n')
for l in r_split:
log(l, id, symbol, verbosity_level)
9 years ago
9 years ago
def get_endpoint(target_host, target_port, target_ssl, socks_config=None, use_http_proxy=False, debugid=None):
9 years ago
# Imports go here to allow mocking for tests
from twisted.internet.endpoints import SSL4ClientEndpoint, TCP4ClientEndpoint
from txsocksx.client import SOCKS5ClientEndpoint
from txsocksx.tls import TLSWrapClientEndpoint
9 years ago
from pappyproxy.pappy import session
log("Getting endpoint for host '%s' on port %d ssl=%s, socks_config=%s, use_http_proxy=%s" % (target_host, target_port, target_ssl, str(socks_config), use_http_proxy), id=debugid, verbosity_level=3)
if session.config.http_proxy and use_http_proxy:
target_host = session.config.http_proxy['host']
target_port = session.config.http_proxy['port']
target_ssl = False # We turn on ssl after CONNECT request if needed
log("Connecting to http proxy at %s:%d" % (target_host, target_port), id=debugid, verbosity_level=3)
9 years ago
if socks_config is not None:
sock_host = socks_config['host']
sock_port = int(socks_config['port'])
methods = {'anonymous': ()}
if 'username' in socks_config and 'password' in socks_config:
methods['login'] = (socks_config['username'], socks_config['password'])
tcp_endpoint = TCP4ClientEndpoint(reactor, sock_host, sock_port)
socks_endpoint = SOCKS5ClientEndpoint(target_host, target_port, tcp_endpoint, methods=methods)
if target_ssl:
endpoint = TLSWrapClientEndpoint(ClientTLSContext(), socks_endpoint)
else:
endpoint = socks_endpoint
else:
if target_ssl:
endpoint = SSL4ClientEndpoint(reactor, target_host, target_port,
ClientTLSContext())
else:
endpoint = TCP4ClientEndpoint(reactor, target_host, target_port)
return endpoint
9 years ago
class ClientTLSContext(ssl.ClientContextFactory):
isClient = 1
def getContext(self):
return SSL.Context(SSL.TLSv1_METHOD)
class ProxyClient(LineReceiver):
def __init__(self, request):
self.factory = None
self._response_sent = False
self._sent = False
self.request = request
self.data_defer = defer.Deferred()
9 years ago
self.completed = False
9 years ago
self.stream_response = True # used so child classes can temporarily turn off response streaming
9 years ago
self._response_obj = http.Response()
def log(self, message, symbol='*', verbosity_level=1):
log(message, id=self.factory.connection_id, symbol=symbol, verbosity_level=verbosity_level)
def lineReceived(self, *args, **kwargs):
line = args[0]
if line is None:
line = ''
self.log(line, symbol='r<', verbosity_level=3)
9 years ago
self._response_obj.add_line(line)
9 years ago
if self._response_obj.headers_complete:
self.setRawMode()
def rawDataReceived(self, *args, **kwargs):
9 years ago
from pappyproxy.pappy import session
9 years ago
data = args[0]
9 years ago
self.log('Returning data back through stream')
9 years ago
if not self._response_obj.complete:
if data:
9 years ago
if session.config.debug_to_file or session.config.debug_verbosity > 0:
9 years ago
s = printable_data(data)
dlines = s.split('\n')
for l in dlines:
self.log(l, symbol='<rd', verbosity_level=3)
9 years ago
self._response_obj.add_data(data)
9 years ago
def dataReceived(self, data):
9 years ago
if self.factory.stream_response and self.stream_response:
9 years ago
self.factory.return_transport.write(data)
LineReceiver.dataReceived(self, data)
if not self.completed:
9 years ago
if self._response_obj.complete:
9 years ago
self.completed = True
9 years ago
self.handle_response_end()
9 years ago
9 years ago
def connectionMade(self):
9 years ago
self.log("Connection made, sending request", verbosity_level=3)
9 years ago
lines = self.request.full_request.splitlines()
for l in lines:
self.log(l, symbol='>r', verbosity_level=3)
9 years ago
self.transport.write(self.request.full_request)
9 years ago
def handle_response_end(self, *args, **kwargs):
self.log("Remote response finished, returning data to original stream")
self.request.response = self._response_obj
9 years ago
self.log('Response ended, losing connection')
9 years ago
self.transport.loseConnection()
assert self._response_obj.full_response
9 years ago
self.data_defer.callback(self.request)
def clientConnectionFailed(self, connector, reason):
self.log("Connection with remote server failed: %s" % reason)
def clientConnectionLost(self, connector, reason):
self.log("Connection with remote server lost: %s" % reason)
9 years ago
9 years ago
class UpstreamHTTPProxyClient(ProxyClient):
def __init__(self, request):
ProxyClient.__init__(self, request)
self.connect_response = False
self.proxy_connected = False
self.stream_response = False
self.creds = None
def write_proxied_request(self, request):
"""
Takes an unencrypted request and sends it to the proxy server to be
forwarded.
"""
sendreq = request.copy()
sendreq.path_type = http.PATH_ABSOLUTE
if self.creds is not None:
sendreq.proxy_creds = self.creds
lines = sendreq.full_request.splitlines()
for l in lines:
9 years ago
self.log(l, symbol='>rp', verbosity_level=3)
9 years ago
self.transport.write(sendreq.full_message)
def connectionMade(self):
self.log("Connection made to http proxy", verbosity_level=3)
if not self.proxy_connected:
if self.request.is_ssl:
connreq = self.request.connect_request
self.connect_response = True
if self.creds is not None:
connreq.proxy_creds = self.creds
9 years ago
lines = connreq.full_message.splitlines()
for l in lines:
self.log(l, symbol='>p', verbosity_level=3)
9 years ago
self.transport.write(connreq.full_message)
else:
self.proxy_connected = True
self.stream_response = True
9 years ago
lines = self.request.full_message.splitlines()
for l in lines:
self.log(l, symbol='>p', verbosity_level=3)
9 years ago
self.write_proxied_request(self.request)
def handle_response_end(self, *args, **kwargs):
if self._response_obj.response_code == 407:
print "Incorrect credentials for HTTP proxy. Please check your username and password."
self.transport.loseConnection()
return
if self.proxy_connected:
self.log("Received request while connected, forwarding to http proxy", verbosity_level=3)
self.request.response = self._response_obj
self.transport.loseConnection()
assert self._response_obj.full_response
self.data_defer.callback(self.request)
9 years ago
elif self._response_obj.response_code != 200:
print "Error establishing connection to proxy"
self.transport.loseConnection()
return
9 years ago
elif self.connect_response:
self.log("Response to CONNECT request recieved from http proxy", verbosity_level=3)
self.proxy_connected = True
self.stream_response = True
self._response_obj = http.Response()
self.setLineMode()
self.completed = False
self._sent = False
9 years ago
self.log("Starting TLS", verbosity_level=3)
9 years ago
self.transport.startTLS(ClientTLSContext())
9 years ago
self.log("TLS started", verbosity_level=3)
9 years ago
lines = self.request.full_message.splitlines()
for l in lines:
9 years ago
self.log(l, symbol='>rpr', verbosity_level=3)
9 years ago
self.transport.write(self.request.full_message)
9 years ago
class ProxyClientFactory(ClientFactory):
9 years ago
def __init__(self, request, save_all=False, stream_response=False,
return_transport=None):
9 years ago
self.request = request
self.connection_id = -1
self.data_defer = defer.Deferred()
9 years ago
self.start_time = datetime.datetime.utcnow()
9 years ago
self.end_time = None
self.save_all = save_all
9 years ago
self.stream_response = stream_response
self.return_transport = return_transport
self.intercepting_macros = {}
9 years ago
self.use_as_proxy = False
9 years ago
self.sendback_function = None
9 years ago
def log(self, message, symbol='*', verbosity_level=1):
log(message, id=self.connection_id, symbol=symbol, verbosity_level=verbosity_level)
9 years ago
def buildProtocol(self, addr, _do_callback=True):
9 years ago
from pappyproxy.pappy import session
9 years ago
# _do_callback is intended to help with testing and should not be modified
9 years ago
if self.use_as_proxy and context.in_scope(self.request):
p = UpstreamHTTPProxyClient(self.request)
if 'username' in session.config.http_proxy and 'password' in session.config.http_proxy:
username = session.config.http_proxy['username']
password = session.config.http_proxy['password']
p.creds = (username, password)
else:
p = ProxyClient(self.request)
9 years ago
p.factory = self
9 years ago
self.log("Building protocol", verbosity_level=3)
if _do_callback:
p.data_defer.addCallback(self.return_request_pair)
9 years ago
return p
def clientConnectionFailed(self, connector, reason):
self.log("Connection failed with remote server: %s" % reason.getErrorMessage())
def clientConnectionLost(self, connector, reason):
self.log("Connection lost with remote server: %s" % reason.getErrorMessage())
9 years ago
@defer.inlineCallbacks
def prepare_request(self):
"""
Prepares request for submitting
Saves the associated request with a temporary start time, mangles it, then
9 years ago
saves the mangled version with an update start time. Also updates flags
and values needed for submitting the request.
9 years ago
"""
9 years ago
from pappyproxy.pappy import session
9 years ago
sendreq = self.request
if context.in_scope(sendreq):
mangle_macros = copy.copy(self.intercepting_macros)
self.request.time_start = datetime.datetime.utcnow()
if self.save_all:
if self.stream_response and not mangle_macros:
self.request.async_deep_save()
else:
yield self.request.async_deep_save()
(sendreq, mangled) = yield macros.mangle_request(sendreq, mangle_macros)
if sendreq and mangled and self.save_all:
self.start_time = datetime.datetime.utcnow()
sendreq.time_start = self.start_time
yield sendreq.async_deep_save()
9 years ago
if session.config.http_proxy:
self.use_as_proxy = True
9 years ago
if (not self.stream_response) and self.sendback_function:
self.data_defer.addCallback(self.sendback_function)
9 years ago
else:
self.log("Request out of scope, passing along unmangled")
self.request = sendreq
defer.returnValue(self.request)
9 years ago
@defer.inlineCallbacks
def return_request_pair(self, request):
9 years ago
"""
If the request is in scope, it saves the completed request,
sets the start/end time, mangles the response, saves the
9 years ago
mangled version, then calls back data_defer with the mangled
request
9 years ago
"""
9 years ago
from pappyproxy.pappy import session
9 years ago
self.end_time = datetime.datetime.utcnow()
9 years ago
if session.config.debug_to_file or session.config.debug_verbosity > 0:
9 years ago
log_request(printable_data(request.response.full_response), id=self.connection_id, symbol='<m', verbosity_level=3)
request.time_start = self.start_time
request.time_end = self.end_time
if context.in_scope(request):
9 years ago
mangle_macros = copy.copy(self.intercepting_macros)
if self.save_all:
9 years ago
if self.stream_response and not mangle_macros:
9 years ago
request.async_deep_save()
else:
yield request.async_deep_save()
9 years ago
mangled = yield macros.mangle_response(request, mangle_macros)
if mangled and self.save_all:
yield request.async_deep_save()
9 years ago
if request.response and (session.config.debug_to_file or session.config.debug_verbosity > 0):
9 years ago
log_request(printable_data(request.response.full_response),
id=self.connection_id, symbol='<', verbosity_level=3)
else:
self.log("Response out of scope, passing along unmangled")
self.data_defer.callback(request)
defer.returnValue(None)
9 years ago
@defer.inlineCallbacks
def connect(self):
from pappyproxy.pappy import session
yield self.prepare_request()
if context.in_scope(self.request):
# Get connection using config
endpoint = get_endpoint(self.request.host,
self.request.port,
self.request.is_ssl,
socks_config=session.config.socks_proxy,
use_http_proxy=True)
else:
# Just forward it normally
endpoint = get_endpoint(self.request.host,
self.request.port,
self.request.is_ssl)
# Connect via the endpoint
self.log("Accessing using endpoint")
yield endpoint.connect(self)
self.log("Connected")
class ProxyServerFactory(ServerFactory):
def __init__(self, save_all=False):
9 years ago
from pappyproxy.site import PappyWebServer
9 years ago
self.intercepting_macros = collections.OrderedDict()
self.save_all = save_all
9 years ago
self.force_ssl = False
9 years ago
self.web_server = PappyWebServer()
9 years ago
self.forward_host = None
9 years ago
def buildProtocol(self, addr):
prot = ProxyServer()
prot.factory = self
return prot
9 years ago
class ProxyServer(LineReceiver):
def log(self, message, symbol='*', verbosity_level=1):
log(message, id=self.connection_id, symbol=symbol, verbosity_level=verbosity_level)
def __init__(self, *args, **kwargs):
global next_connection_id
self.connection_id = get_next_connection_id()
self._request_obj = http.Request()
self._connect_response = False
self._forward = True
self._connect_uri = None
9 years ago
self._connect_host = None
self._connect_ssl = None
self._connect_port = None
self._client_factory = None
9 years ago
def lineReceived(self, *args, **kwargs):
line = args[0]
self.log(line, symbol='>', verbosity_level=3)
self._request_obj.add_line(line)
if self._request_obj.headers_complete:
self.setRawMode()
def rawDataReceived(self, *args, **kwargs):
data = args[0]
self._request_obj.add_data(data)
self.log(data, symbol='d>', verbosity_level=3)
9 years ago
def dataReceived(self, *args, **kwargs):
# receives the data then checks if the request is complete.
# if it is, it calls full_Request_received
LineReceiver.dataReceived(self, *args, **kwargs)
9 years ago
if self._request_obj.complete:
9 years ago
self.full_request_received()
9 years ago
9 years ago
def _start_tls(self, cert_host=None):
9 years ago
from pappyproxy.pappy import session
9 years ago
# Generate a cert for the hostname and start tls
if cert_host is None:
host = self._request_obj.host
else:
host = cert_host
if not host in cached_certs:
log("Generating cert for '%s'" % host,
verbosity_level=3)
(pkey, cert) = generate_cert(host,
9 years ago
session.config.cert_dir)
9 years ago
cached_certs[host] = (pkey, cert)
else:
log("Using cached cert for %s" % host, verbosity_level=3)
(pkey, cert) = cached_certs[host]
ctx = ServerTLSContext(
private_key=pkey,
certificate=cert,
)
self.transport.startTLS(ctx, self.factory)
def _connect_okay(self):
self.log('Responding to browser CONNECT request', verbosity_level=3)
okay_str = 'HTTP/1.1 200 Connection established\r\n\r\n'
self.transport.write(okay_str)
9 years ago
@defer.inlineCallbacks
9 years ago
def full_request_received(self):
9 years ago
from pappyproxy.http import Request
9 years ago
global cached_certs
self.log('End of request', verbosity_level=3)
9 years ago
forward = True
if self._request_obj.verb.upper() == 'CONNECT':
self._connect_okay()
self._start_tls()
self._connect_uri = self._request_obj.url
self._connect_host = self._request_obj.host
self._connect_ssl = True # do we just assume connect means ssl?
self._connect_port = self._request_obj.port
self.log('uri=%s, ssl=%s, connect_port=%s' % (self._connect_uri, self._connect_ssl, self._connect_port), verbosity_level=3)
forward = False
9 years ago
if self._request_obj.host == 'pappy':
yield self.factory.web_server.handle_request(self._request_obj)
self.transport.write(self._request_obj.response.full_message)
forward = False
9 years ago
# if _request_obj.host is a listener, forward = False
9 years ago
if self.factory.intercepting_macros:
return_transport = None
else:
return_transport = self.transport
9 years ago
if forward:
9 years ago
d = Request.submit_request(self._request_obj,
save_request=True,
intercepting_macros=self.factory.intercepting_macros,
stream_transport=return_transport)
if return_transport is None:
d.addCallback(self.send_response_back)
9 years ago
self._reset()
def _reset(self):
# Reset per-request variables and have the request default to using
# some parameters from the connect request
9 years ago
self.log("Resetting per-request data", verbosity_level=3)
self._connect_response = False
self._request_obj = http.Request()
if self._connect_uri:
self._request_obj.url = self._connect_uri
9 years ago
if self._connect_host:
self._request_obj._host = self._connect_host
if self._connect_ssl:
self._request_obj.is_ssl = self._connect_ssl
if self._connect_port:
self._request_obj.port = self._connect_port
9 years ago
self.setLineMode()
9 years ago
def send_response_back(self, request):
if request.response is not None:
self.transport.write(request.response.full_response)
9 years ago
self.log("Response sent back, losing connection")
9 years ago
self.transport.loseConnection()
9 years ago
def connectionMade(self):
if self.factory.force_ssl:
self._start_tls(self.factory.forward_host)
9 years ago
def connectionLost(self, reason):
self.log('Connection lost with browser: %s' % reason.getErrorMessage())
class ServerTLSContext(ssl.ContextFactory):
def __init__(self, private_key, certificate):
self.private_key = private_key
self.certificate = certificate
self.sslmethod = SSL.TLSv1_METHOD
self.cacheContext()
def cacheContext(self):
ctx = SSL.Context(self.sslmethod)
ctx.use_certificate(self.certificate)
ctx.use_privatekey(self.private_key)
self._context = ctx
def __getstate__(self):
d = self.__dict__.copy()
del d['_context']
return d
def __setstate__(self, state):
self.__dict__ = state
self.cacheContext()
def getContext(self):
"""Create an SSL context.
"""
return self._context
def generate_cert_serial():
# Generates a random serial to be used for the cert
return random.getrandbits(8*20)
def load_certs_from_dir(cert_dir):
9 years ago
from pappyproxy.pappy import session
try:
9 years ago
with open(cert_dir+'/'+session.config.ssl_ca_file, 'rt') as f:
ca_raw = f.read()
except IOError:
9 years ago
raise PappyException("Could not load CA cert! Generate certs using the `gencerts` command then add the .crt file to your browser.")
try:
9 years ago
with open(cert_dir+'/'+session.config.ssl_pkey_file, 'rt') as f:
ca_key_raw = f.read()
except IOError:
raise PappyException("Could not load CA private key!")
return (ca_raw, ca_key_raw)
9 years ago
def generate_cert(hostname, cert_dir):
(ca_raw, ca_key_raw) = load_certs_from_dir(cert_dir)
9 years ago
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_raw)
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_raw)
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
cert = crypto.X509()
cert.get_subject().CN = hostname
cert.set_serial_number(generate_cert_serial())
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(10*365*24*60*60)
cert.set_issuer(ca_cert.get_subject())
cert.set_pubkey(key)
cert.sign(ca_key, "sha256")
return (key, cert)
def generate_ca_certs(cert_dir):
9 years ago
from pappyproxy.pappy import session
9 years ago
# Make directory if necessary
if not os.path.exists(cert_dir):
os.makedirs(cert_dir)
# Private key
print "Generating private key... ",
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)
9 years ago
with os.fdopen(os.open(cert_dir+'/'+session.config.ssl_pkey_file, os.O_WRONLY | os.O_CREAT, 0o0600), 'w') as f:
9 years ago
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
print "Done!"
# Hostname doesn't matter since it's a client cert
print "Generating client cert... ",
cert = crypto.X509()
cert.get_subject().C = 'US' # Country name
cert.get_subject().ST = 'Michigan' # State or province name
cert.get_subject().L = 'Ann Arbor' # Locality name
cert.get_subject().O = 'Pappy Proxy' # Organization name
#cert.get_subject().OU = '' # Organizational unit name
cert.get_subject().CN = 'Pappy Proxy' # Common name
cert.set_serial_number(generate_cert_serial())
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(10*365*24*60*60)
cert.set_issuer(cert.get_subject())
cert.add_extensions([
crypto.X509Extension("basicConstraints", True,
"CA:TRUE, pathlen:0"),
crypto.X509Extension("keyUsage", True,
"keyCertSign, cRLSign"),
crypto.X509Extension("subjectKeyIdentifier", False, "hash",
subject=cert),
])
cert.set_pubkey(key)
cert.sign(key, 'sha256')
9 years ago
with os.fdopen(os.open(cert_dir+'/'+session.config.ssl_ca_file, os.O_WRONLY | os.O_CREAT, 0o0600), 'w') as f:
9 years ago
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
print "Done!"