mitmproxy/pathod/pathod.py

497 lines
15 KiB
Python
Raw Normal View History

import copy
2014-10-24 01:01:34 +00:00
import logging
import os
import sys
import threading
2016-10-17 02:43:38 +00:00
import urllib
import typing # noqa
2012-06-19 04:57:57 +00:00
2020-11-02 20:08:54 +00:00
from mitmproxy import certs as mcerts, exceptions, version
from mitmproxy.net import tcp, tls, websocket
from pathod import language, utils, log, protocols
2016-06-08 16:04:56 +00:00
DEFAULT_CERT_DOMAIN = b"pathod.net"
CONFDIR = "~/.mitmproxy"
CERTSTORE_BASENAME = "mitmproxy"
CA_CERT_NAME = "mitmproxy-ca.pem"
DEFAULT_CRAFT_ANCHOR = "/p/"
KEY_SIZE = 2048
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
2016-10-17 04:29:45 +00:00
class SSLOptions:
def __init__(
self,
confdir=CONFDIR,
cn=None,
sans=(),
not_after_connect=None,
request_client_cert=False,
ssl_version=tls.DEFAULT_METHOD,
ssl_options=tls.DEFAULT_OPTIONS,
ciphers=None,
2015-06-08 15:12:51 +00:00
certs=None,
2015-09-26 15:40:22 +00:00
alpn_select=b'h2',
):
self.confdir = confdir
self.cn = cn
2015-06-08 15:12:51 +00:00
self.sans = sans
self.not_after_connect = not_after_connect
self.request_client_cert = request_client_cert
2015-06-22 18:38:53 +00:00
self.ssl_version = ssl_version
2015-08-29 10:30:54 +00:00
self.ssl_options = ssl_options
2015-06-08 15:12:51 +00:00
self.ciphers = ciphers
self.alpn_select = alpn_select
2016-10-19 22:02:52 +00:00
self.certstore = mcerts.CertStore.from_store(
os.path.expanduser(confdir),
CERTSTORE_BASENAME,
KEY_SIZE
)
for i in certs or []:
self.certstore.add_cert_file(*i)
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: typing.Union[str, None, bool] = None
2014-07-21 12:08:09 +00:00
def __init__(
self,
connection,
address,
server,
logfp,
settings,
http2_framedump=False
):
tcp.BaseHandler.__init__(self, connection, address, server)
2015-06-11 14:13:22 +00:00
self.logfp = logfp
self.settings = copy.copy(settings)
2015-06-11 14:13:22 +00:00
self.protocol = None
self.use_http2 = False
self.http2_framedump = http2_framedump
2015-07-22 11:04:14 +00:00
def handle_sni(self, connection):
2016-07-07 04:03:17 +00:00
sni = connection.get_servername()
if sni:
sni = sni.decode("idna")
self.sni = sni
def http_serve_crafted(self, crafted, logctx):
2015-04-22 20:24:26 +00:00
error, crafted = self.server.check_policy(
crafted, self.settings
2015-04-22 20:24:26 +00:00
)
if error:
2015-06-11 14:13:22 +00:00
err = self.make_http_error_response(error)
language.serve(err, self.wfile, self.settings)
return None, dict(
2014-07-21 12:08:09 +00:00
type="error",
2015-06-18 16:12:11 +00:00
msg=error
)
2015-06-11 14:13:22 +00:00
if self.server.explain and not hasattr(crafted, 'is_error_response'):
crafted = crafted.freeze(self.settings)
logctx(">> Spec: %s" % crafted.spec())
2015-06-11 14:13:22 +00:00
response_log = language.serve(
crafted,
self.wfile,
self.settings
)
if response_log["disconnect"]:
return None, response_log
return self.handle_http_request, response_log
def handle_http_request(self, logger):
2012-07-21 08:50:41 +00:00
"""
Returns a (handler, log) tuple.
handler: Handler for the next request, or None to disconnect
log: A dictionary, or None
2012-07-21 08:50:41 +00:00
"""
with logger.ctx() as lg:
2015-07-19 18:10:07 +00:00
try:
2015-09-16 16:44:34 +00:00
req = self.protocol.read_request(self.rfile)
2016-10-19 22:02:52 +00:00
except exceptions.HttpReadDisconnect:
2015-09-16 18:12:53 +00:00
return None, None
2016-10-19 22:02:52 +00:00
except exceptions.HttpException as s:
2015-07-19 18:10:07 +00:00
s = str(s)
lg(s)
return None, dict(type="error", msg=s)
2015-07-22 11:04:14 +00:00
if req.method == 'CONNECT':
return self.protocol.handle_http_connect([req.host, req.port, req.http_version], lg)
2015-07-19 18:10:07 +00:00
method = req.method
path = req.path
http_version = req.http_version
2015-07-20 14:33:12 +00:00
headers = req.headers
first_line_format = req.first_line_format
clientcert = None
if self.clientcert:
clientcert = dict(
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(
type="crafted",
2015-06-05 00:04:40 +00:00
protocol="http",
request=dict(
path=path,
method=method,
2015-09-05 16:16:08 +00:00
headers=headers.fields,
http_version=http_version,
sni=self.sni,
remote_address=self.address,
clientcert=clientcert,
first_line_format=first_line_format
),
cipher=None,
2014-07-21 12:08:09 +00:00
)
if self.tls_established:
retlog["cipher"] = self.get_current_cipher()
m = utils.MemBool()
2016-08-16 16:31:50 +00:00
2020-11-02 20:08:54 +00:00
valid_websocket_handshake = websocket.check_handshake(headers)
self.settings.websocket_key = websocket.get_client_key(headers)
# If this is a websocket initiation, we respond with a proper
# server response, unless over-ridden.
if valid_websocket_handshake:
anchor_gen = language.parse_pathod("ws")
else:
anchor_gen = None
2015-06-11 14:13:22 +00:00
for regex, spec in self.server.anchors:
if regex.match(path):
anchor_gen = language.parse_pathod(spec, self.use_http2)
break
else:
if m(path.startswith(self.server.craftanchor)):
2016-06-08 16:04:56 +00:00
spec = urllib.parse.unquote(path)[len(self.server.craftanchor):]
if spec:
try:
2015-06-11 14:13:22 +00:00
anchor_gen = language.parse_pathod(spec, self.use_http2)
except language.ParseException as v:
lg("Parse error: %s" % v.msg)
2015-06-11 14:13:22 +00:00
anchor_gen = iter([self.make_http_error_response(
"Parse Error",
"Error parsing response spec: %s\n" % (
v.msg + v.marked()
)
2015-06-07 04:11:32 +00:00
)])
2015-06-11 14:13:22 +00:00
else:
if self.use_http2:
2015-06-16 09:33:10 +00:00
anchor_gen = iter([self.make_http_error_response(
"Spec Error",
"HTTP/2 only supports request/response with the craft anchor point: %s" %
2015-06-18 16:05:09 +00:00
self.server.craftanchor
2015-06-16 09:33:10 +00:00
)])
2015-06-11 14:13:22 +00:00
if not anchor_gen:
anchor_gen = iter([self.make_http_error_response(
"Not found",
"No valid craft request found"
)])
2015-06-11 14:13:22 +00:00
2016-06-08 16:04:56 +00:00
spec = next(anchor_gen)
2015-06-11 14:13:22 +00:00
if self.use_http2 and isinstance(spec, language.http2.Response):
spec.stream_id = req.stream_id
lg("crafting spec: %s" % spec)
nexthandler, retlog["response"] = self.http_serve_crafted(
spec,
lg
)
if nexthandler and valid_websocket_handshake:
self.protocol = protocols.websockets.WebsocketsProtocol(self)
return self.protocol.handle_websocket, retlog
else:
return nexthandler, retlog
2015-06-11 14:13:22 +00:00
def make_http_error_response(self, reason, body=None):
resp = self.protocol.make_error_response(reason, body)
2015-06-11 14:13:22 +00:00
resp.is_error_response = True
return resp
def handle(self):
2015-06-11 14:13:22 +00:00
self.settimeout(self.server.timeout)
if self.server.ssl:
try:
2015-04-19 20:56:47 +00:00
cert, key, _ = self.server.ssloptions.get_cert(None)
self.convert_to_tls(
2015-05-30 00:03:13 +00:00
cert,
key,
2015-07-22 11:04:14 +00:00
handle_sni=self.handle_sni,
2014-07-21 12:08:09 +00:00
request_client_cert=self.server.ssloptions.request_client_cert,
cipher_list=self.server.ssloptions.ciphers,
2015-06-22 18:38:53 +00:00
method=self.server.ssloptions.ssl_version,
2015-08-29 10:30:54 +00:00
options=self.server.ssloptions.ssl_options,
2015-06-08 15:12:51 +00:00
alpn_select=self.server.ssloptions.alpn_select,
)
2016-10-19 22:02:52 +00:00
except exceptions.TlsException as v:
s = str(v)
self.server.add_log(
dict(
2014-07-21 12:08:09 +00:00
type="error",
msg=s
)
)
log.write_raw(self.logfp, s)
2012-07-21 08:50:41 +00:00
return
2015-06-11 14:13:22 +00:00
alp = self.get_alpn_proto_negotiated()
2015-09-26 15:40:22 +00:00
if alp == b'h2':
self.protocol = protocols.http2.HTTP2Protocol(self)
2015-06-11 14:13:22 +00:00
self.use_http2 = True
if not self.protocol:
self.protocol = protocols.http.HTTPProtocol(self)
lr = self.rfile if self.server.logreq else None
lw = self.wfile if self.server.logresp else None
logger = log.ConnectionLogger(self.logfp, self.server.hexdump, True, lr, lw)
2015-06-11 14:13:22 +00:00
self.settings.protocol = self.protocol
handler = self.handle_http_request
2015-06-11 14:13:22 +00:00
2012-06-24 23:34:29 +00:00
while not self.finished:
handler, l = handler(logger)
if l:
self.addlog(l)
if not handler:
2012-07-21 08:50:41 +00:00
return
2012-06-19 04:57:57 +00:00
2015-06-11 14:13:22 +00:00
def addlog(self, log):
if self.server.logreq:
2016-06-08 16:04:56 +00:00
log["request_bytes"] = self.rfile.get_log()
2015-06-11 14:13:22 +00:00
if self.server.logresp:
2016-06-08 16:04:56 +00:00
log["response_bytes"] = self.wfile.get_log()
2015-06-11 14:13:22 +00:00
self.server.add_log(log)
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=DEFAULT_CRAFT_ANCHOR,
staticdir=None,
anchors=(),
sizelimit=None,
nocraft=False,
nohang=False,
timeout=None,
logreq=False,
logresp=False,
explain=False,
hexdump=False,
2015-06-11 14:13:22 +00:00
http2_framedump=False,
webdebug=False,
logfp=sys.stdout,
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: URL prefix 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.
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.nocraft = nocraft
self.nohang = nohang
self.timeout, self.logreq = timeout, logreq
self.logresp, self.hexdump = logresp, hexdump
2015-06-11 14:13:22 +00:00
self.http2_framedump = http2_framedump
self.explain = explain
self.logfp = logfp
self.log = []
self.logid = 0
self.anchors = anchors
self.settings = language.Settings(
2015-06-18 16:12:11 +00:00
staticdir=self.staticdir
)
self.loglock = threading.Lock()
def check_policy(self, req, settings):
"""
A policy check that verifies the request size is within limits.
"""
if self.nocraft:
return "Crafting disabled.", None
try:
req = req.resolve(settings)
l = req.maximum_length(settings)
except language.FileAccessDenied:
return "File access denied.", None
if self.sizelimit and l > self.sizelimit:
return "Response too large.", None
pauses = [isinstance(i, language.actions.PauseAt) for i in req.actions]
if self.nohang and any(pauses):
return "Pauses have been disabled.", None
return None, req
2014-01-09 17:04:04 +00:00
def handle_client_connection(self, request, client_address):
h = PathodHandler(
request,
client_address,
self,
self.logfp,
2015-06-11 14:13:22 +00:00
self.settings,
self.http2_framedump,
)
2012-07-29 23:58:29 +00:00
try:
h.handle()
h.finish()
2016-10-19 22:02:52 +00:00
except exceptions.TcpDisconnect: # pragma: no cover
log.write_raw(self.logfp, "Disconnect")
2012-07-29 23:58:29 +00:00
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
2016-10-19 22:02:52 +00:00
except exceptions.TcpTimeout:
log.write_raw(self.logfp, "Timeout")
self.add_log(
dict(
2014-07-21 12:08:09 +00:00
type="timeout",
)
)
return
def add_log(self, d):
with self.loglock:
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):
with self.loglock:
self.log = []
2015-06-18 16:05:09 +00:00
def log_by_id(self, identifier):
with self.loglock:
for i in self.log:
if i["id"] == identifier:
return i
def get_log(self):
with self.loglock:
return self.log
2014-10-24 04:12:54 +00:00
def main(args): # pragma: no cover
2014-10-24 04:12:54 +00:00
ssloptions = SSLOptions(
2015-06-18 16:12:11 +00:00
cn=args.cn,
confdir=args.confdir,
not_after_connect=args.ssl_not_after_connect,
ciphers=args.ciphers,
2015-06-22 18:38:53 +00:00
ssl_version=args.ssl_version,
2015-08-29 10:30:54 +00:00
ssl_options=args.ssl_options,
2015-06-18 16:12:11 +00:00
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),
2015-06-18 16:12:11 +00:00
craftanchor=args.craftanchor,
ssl=args.ssl,
ssloptions=ssloptions,
staticdir=args.staticdir,
anchors=args.anchors,
sizelimit=args.sizelimit,
nocraft=args.nocraft,
nohang=args.nohang,
timeout=args.timeout,
logreq=args.logreq,
logresp=args.logresp,
hexdump=args.hexdump,
http2_framedump=args.http2_framedump,
explain=args.explain,
webdebug=args.webdebug
2014-10-24 04:12:54 +00:00
)
2015-05-30 00:03:13 +00:00
except PathodError as v:
2016-04-14 05:34:28 +00:00
print("Error: %s" % v, file=sys.stderr)
sys.exit(1)
2015-05-30 00:03:13 +00:00
except language.FileAccessDenied as v:
2016-04-14 05:34:28 +00:00
print("Error: %s" % v, file=sys.stderr)
if args.daemonize:
utils.daemonize()
2014-10-24 04:12:54 +00:00
try:
print("{} listening on {}".format(
version.PATHOD,
2016-04-14 05:34:28 +00:00
repr(pd.address)
))
2014-10-24 04:12:54 +00:00
pd.serve_forever()
except KeyboardInterrupt:
pass