mitmproxy/libpathod/pathod.py

462 lines
15 KiB
Python
Raw Normal View History

2014-10-24 01:01:34 +00:00
import urllib
import threading
import logging
import os
import sys
from netlib import tcp, http, wsgi, certutils
import netlib.utils
2014-10-24 01:01:34 +00:00
from . import version, app, language, utils
2012-06-19 04:57:57 +00:00
DEFAULT_CERT_DOMAIN = "pathod.net"
CONFDIR = "~/.mitmproxy"
CERTSTORE_BASENAME = "mitmproxy"
CA_CERT_NAME = "mitmproxy-ca.pem"
logger = logging.getLogger('pathod')
2014-07-21 12:08:09 +00:00
2014-10-24 01:01:34 +00:00
class PathodError(Exception):
pass
2012-06-24 04:38:32 +00:00
class SSLOptions:
def __init__(self,
confdir=CONFDIR,
cn=None,
sans=(),
not_after_connect=None,
request_client_cert=False,
sslversion=tcp.SSLv23_METHOD,
ciphers=None,
certs=None):
self.confdir = confdir
self.cn = cn
self.certstore = certutils.CertStore.from_store(
os.path.expanduser(confdir),
CERTSTORE_BASENAME
)
for i in certs or []:
self.certstore.add_cert_file(*i)
self.not_after_connect = not_after_connect
self.request_client_cert = request_client_cert
self.ciphers = ciphers
self.sslversion = sslversion
self.sans = sans
2014-03-02 02:13:56 +00:00
def get_cert(self, name):
if self.cn:
name = self.cn
elif not name:
name = DEFAULT_CERT_DOMAIN
return self.certstore.get_cert(name, self.sans)
2014-03-02 02:13:56 +00:00
2012-06-19 01:23:07 +00:00
class PathodHandler(tcp.BaseHandler):
wbufsize = 0
sni = None
2014-07-21 12:08:09 +00:00
def info(self, s):
logger.info(
"%s:%s: %s" % (self.address.host, self.address.port, str(s))
)
def handle_sni(self, connection):
self.sni = connection.get_servername()
def serve_crafted(self, crafted):
c = self.server.check_policy(crafted, self.server.request_settings)
if c:
err = language.make_error_response(c)
language.serve(err, self.wfile, self.server.request_settings)
log = dict(
2014-07-21 12:08:09 +00:00
type="error",
msg=c
)
return False, log
if self.server.explain and not isinstance(crafted, language.PathodErrorResponse):
crafted = crafted.freeze(self.server.request_settings)
2014-07-21 12:08:09 +00:00
self.info(">> Spec: %s" % crafted.spec())
response_log = language.serve(
crafted,
self.wfile,
self.server.request_settings
)
if response_log["disconnect"]:
return False, response_log
return True, response_log
2012-07-21 08:50:41 +00:00
def handle_request(self):
"""
2012-10-04 21:30:32 +00:00
Returns a (again, log) tuple.
again: True if request handling should continue.
log: A dictionary, or None
2012-07-21 08:50:41 +00:00
"""
line = self.rfile.readline()
2014-07-21 12:08:09 +00:00
if line == "\r\n" or line == "\n": # Possible leftover from previous message
2012-07-21 08:50:41 +00:00
line = self.rfile.readline()
if line == "":
# Normal termination
return False, None
2012-07-21 08:50:41 +00:00
m = utils.MemBool()
if m(http.parse_init_connect(line)):
headers = http.read_headers(self.rfile)
self.wfile.write(
2014-07-21 12:08:09 +00:00
'HTTP/1.1 200 Connection established\r\n' +
('Proxy-agent: %s\r\n' % version.NAMEVERSION) +
'\r\n'
)
self.wfile.flush()
if not self.server.ssloptions.not_after_connect:
try:
2014-10-08 18:48:16 +00:00
cert, key, chain_file = self.server.ssloptions.get_cert(m.v[0])
self.convert_to_ssl(
cert, key,
2014-07-21 12:08:09 +00:00
handle_sni=self.handle_sni,
request_client_cert=self.server.ssloptions.request_client_cert,
cipher_list=self.server.ssloptions.ciphers,
method=self.server.ssloptions.sslversion,
)
except tcp.NetLibError, v:
s = str(v)
self.info(s)
2014-07-21 12:08:09 +00:00
return False, dict(type="error", msg=s)
return True, None
elif m(http.parse_init_proxy(line)):
method, _, _, _, path, httpversion = m.v
elif m(http.parse_init_http(line)):
method, path, httpversion = m.v
else:
2014-07-21 12:08:09 +00:00
s = "Invalid first line: %s" % repr(line)
2012-07-21 08:50:41 +00:00
self.info(s)
2014-07-21 12:08:09 +00:00
return False, dict(type="error", msg=s)
2012-07-21 08:50:41 +00:00
headers = http.read_headers(self.rfile)
2012-07-30 00:53:41 +00:00
if headers is None:
s = "Invalid headers"
self.info(s)
2014-07-21 12:08:09 +00:00
return False, dict(type="error", msg=s)
2012-07-30 00:53:41 +00:00
clientcert = None
if self.clientcert:
clientcert = dict(
2014-07-21 12:08:09 +00:00
cn=self.clientcert.cn,
subject=self.clientcert.subject,
serial=self.clientcert.serial,
notbefore=self.clientcert.notbefore.isoformat(),
notafter=self.clientcert.notafter.isoformat(),
keyinfo=self.clientcert.keyinfo,
)
retlog = dict(
2014-07-21 12:08:09 +00:00
type="crafted",
request=dict(
path=path,
method=method,
headers=headers.lst,
httpversion=httpversion,
sni=self.sni,
remote_address=self.address(),
clientcert=clientcert,
),
2014-07-21 12:08:09 +00:00
cipher=None,
)
if self.ssl_established:
retlog["cipher"] = self.get_current_cipher()
2012-07-22 00:30:10 +00:00
try:
content = http.read_http_body(
2014-07-21 12:08:09 +00:00
self.rfile, headers, None,
method, None, True
)
2012-07-22 00:30:10 +00:00
except http.HttpError, s:
s = str(s)
self.info(s)
2014-07-21 12:08:09 +00:00
return False, dict(type="error", msg=s)
2012-07-21 08:50:41 +00:00
for i in self.server.anchors:
if i[0].match(path):
2014-07-21 12:08:09 +00:00
self.info("crafting anchor: %s" % path)
again, retlog["response"] = self.serve_crafted(i[1])
return again, retlog
2012-07-21 08:50:41 +00:00
if not self.server.nocraft and path.startswith(self.server.craftanchor):
spec = urllib.unquote(path)[len(self.server.craftanchor):]
2014-07-21 12:08:09 +00:00
self.info("crafting spec: %s" % spec)
2012-07-21 08:50:41 +00:00
try:
crafted = language.parse_response(spec)
2012-10-04 21:30:32 +00:00
except language.ParseException, v:
2014-07-21 12:08:09 +00:00
self.info("Parse error: %s" % v.msg)
crafted = language.make_error_response(
2014-07-21 12:08:09 +00:00
"Parse Error",
"Error parsing response spec: %s\n" % v.msg + v.marked()
)
again, retlog["response"] = self.serve_crafted(crafted)
return again, retlog
elif self.server.noweb:
crafted = language.make_error_response("Access Denied")
language.serve(crafted, self.wfile, self.server.request_settings)
return False, dict(
type="error",
msg="Access denied: web interface disabled"
)
2012-07-21 08:50:41 +00:00
else:
2014-07-21 12:08:09 +00:00
self.info("app: %s %s" % (method, path))
2014-09-03 15:14:18 +00:00
req = wsgi.Request("http", method, path, headers, content)
flow = wsgi.Flow(self.address, req)
2012-07-21 08:50:41 +00:00
sn = self.connection.getsockname()
a = wsgi.WSGIAdaptor(
2012-07-21 08:50:41 +00:00
self.server.app,
sn[0],
self.server.address.port,
2012-07-21 08:50:41 +00:00
version.NAMEVERSION
)
2014-09-03 15:14:18 +00:00
a.serve(flow, self.wfile)
return True, None
def _log_bytes(self, header, data, hexdump):
s = []
if hexdump:
2014-07-21 12:08:09 +00:00
s.append("%s (hex dump):" % header)
for line in netlib.utils.hexdump(data):
2014-07-21 12:08:09 +00:00
s.append("\t%s %s %s" % line)
else:
2014-07-21 12:08:09 +00:00
s.append("%s (unprintables escaped):" % header)
s.append(netlib.utils.cleanBin(data))
self.info("\n".join(s))
2012-07-21 08:50:41 +00:00
def handle(self):
if self.server.ssl:
try:
2015-04-19 20:56:47 +00:00
cert, key, _ = self.server.ssloptions.get_cert(None)
self.convert_to_ssl(
cert, key,
2014-07-21 12:08:09 +00:00
handle_sni=self.handle_sni,
request_client_cert=self.server.ssloptions.request_client_cert,
cipher_list=self.server.ssloptions.ciphers,
method=self.server.ssloptions.sslversion,
)
except tcp.NetLibError, v:
s = str(v)
self.server.add_log(
dict(
2014-07-21 12:08:09 +00:00
type="error",
msg=s
)
)
self.info(s)
2012-07-21 08:50:41 +00:00
return
self.settimeout(self.server.timeout)
2012-06-24 23:34:29 +00:00
while not self.finished:
if self.server.logreq:
self.rfile.start_log()
if self.server.logresp:
self.wfile.start_log()
again, log = self.handle_request()
if log:
if self.server.logreq:
log["request_bytes"] = self.rfile.get_log().encode("string_escape")
self._log_bytes("Request", log["request_bytes"], self.server.hexdump)
if self.server.logresp:
log["response_bytes"] = self.wfile.get_log().encode("string_escape")
self._log_bytes("Response", log["response_bytes"], self.server.hexdump)
self.server.add_log(log)
if not again:
2012-07-21 08:50:41 +00:00
return
2012-06-19 04:57:57 +00:00
2012-06-19 01:23:07 +00:00
class Pathod(tcp.TCPServer):
LOGBUF = 500
2014-07-21 12:08:09 +00:00
def __init__(
self,
addr,
ssl=False,
ssloptions=None,
craftanchor="/p/",
staticdir=None,
anchors=(),
sizelimit=None,
noweb=False,
nocraft=False,
noapi=False,
nohang=False,
timeout=None,
logreq=False,
logresp=False,
explain=False,
hexdump=False
2014-07-21 12:08:09 +00:00
):
2012-06-24 04:20:50 +00:00
"""
addr: (address, port) tuple. If port is 0, a free port will be
automatically chosen.
ssloptions: an SSLOptions object.
craftanchor: string specifying the path under which to anchor
response generation.
2012-06-24 04:20:50 +00:00
staticdir: path to a directory of static resources, or None.
anchors: List of (regex object, language.Request object) tuples, or
None.
sizelimit: Limit size of served data.
nocraft: Disable response crafting.
noapi: Disable the API.
nohang: Disable pauses.
2012-06-24 04:20:50 +00:00
"""
2012-06-19 01:23:07 +00:00
tcp.TCPServer.__init__(self, addr)
self.ssl = ssl
self.ssloptions = ssloptions or SSLOptions()
self.staticdir = staticdir
self.craftanchor = craftanchor
self.sizelimit = sizelimit
self.noweb, self.nocraft = noweb, nocraft
self.noapi, self.nohang = noapi, nohang
self.timeout, self.logreq = timeout, logreq
self.logresp, self.hexdump = logresp, hexdump
self.explain = explain
self.app = app.make_app(noapi)
2012-06-19 04:57:57 +00:00
self.app.config["pathod"] = self
self.log = []
self.logid = 0
self.anchors = anchors
def check_policy(self, req, settings):
"""
A policy check that verifies the request size is withing limits.
"""
try:
l = req.maximum_length(settings)
except language.FileAccessDenied:
return "File access denied."
if self.sizelimit and l > self.sizelimit:
return "Response too large."
if self.nohang and any([isinstance(i, language.PauseAt) for i in req.actions]):
return "Pauses have been disabled."
return False
@property
def request_settings(self):
return dict(
2014-07-21 12:08:09 +00:00
staticdir=self.staticdir
)
2014-01-09 17:04:04 +00:00
def handle_client_connection(self, request, client_address):
h = PathodHandler(request, client_address, self)
2012-07-29 23:58:29 +00:00
try:
h.handle()
h.finish()
2014-07-21 12:08:09 +00:00
except tcp.NetLibDisconnect: # pragma: no cover
2012-07-29 23:58:29 +00:00
h.info("Disconnect")
self.add_log(
dict(
2014-07-21 12:08:09 +00:00
type="error",
msg="Disconnect"
2012-07-29 23:58:29 +00:00
)
)
return
2012-10-30 23:32:13 +00:00
except tcp.NetLibTimeout:
h.info("Timeout")
self.add_log(
dict(
2014-07-21 12:08:09 +00:00
type="timeout",
)
)
return
def add_log(self, d):
if not self.noapi:
lock = threading.Lock()
with lock:
d["id"] = self.logid
self.log.insert(0, d)
if len(self.log) > self.LOGBUF:
self.log.pop()
self.logid += 1
return d["id"]
def clear_log(self):
lock = threading.Lock()
with lock:
self.log = []
def log_by_id(self, id):
for i in self.log:
if i["id"] == id:
return i
def get_log(self):
return self.log
2014-10-24 04:12:54 +00:00
def main(args): # pragma: nocover
2014-10-24 04:12:54 +00:00
ssloptions = SSLOptions(
cn = args.cn,
confdir = args.confdir,
not_after_connect = args.ssl_not_after_connect,
ciphers = args.ciphers,
sslversion = utils.SSLVERSIONS[args.sslversion],
certs = args.ssl_certs,
sans = args.sans
2014-10-24 04:12:54 +00:00
)
root = logging.getLogger()
if root.handlers:
for handler in root.handlers:
root.removeHandler(handler)
log = logging.getLogger('pathod')
log.setLevel(logging.DEBUG)
fmt = logging.Formatter(
'%(asctime)s: %(message)s',
datefmt='%d-%m-%y %H:%M:%S',
)
if args.logfile:
fh = logging.handlers.WatchedFileHandler(args.logfile)
fh.setFormatter(fmt)
log.addHandler(fh)
if not args.daemonize:
sh = logging.StreamHandler()
sh.setFormatter(fmt)
log.addHandler(sh)
try:
pd = Pathod(
(args.address, args.port),
craftanchor = args.craftanchor,
ssl = args.ssl,
ssloptions = ssloptions,
staticdir = args.staticdir,
anchors = args.anchors,
sizelimit = args.sizelimit,
2014-10-24 04:12:54 +00:00
noweb = args.noweb,
nocraft = args.nocraft,
noapi = args.noapi,
nohang = args.nohang,
timeout = args.timeout,
logreq = args.logreq,
logresp = args.logresp,
hexdump = args.hexdump,
explain = args.explain,
)
except PathodError, v:
print >> sys.stderr, "Error: %s"%v
sys.exit(1)
2014-10-24 04:12:54 +00:00
except language.FileAccessDenied, v:
print >> sys.stderr, "Error: %s"%v
if args.daemonize:
utils.daemonize()
2014-10-24 04:12:54 +00:00
try:
print "%s listening on %s:%s"%(
version.NAMEVERSION,
pd.address.host,
pd.address.port
)
pd.serve_forever()
except KeyboardInterrupt:
pass