mitmproxy/libpathod/pathod.py

326 lines
12 KiB
Python
Raw Normal View History

import urllib, threading, re, logging, socket, sys, base64
from netlib import tcp, http, odict, wsgi
import netlib.utils
import version, app, language, utils
2012-06-19 04:57:57 +00:00
logger = logging.getLogger('pathod')
2012-06-24 04:38:32 +00:00
class PathodError(Exception): pass
class SSLOptions:
def __init__(self, certfile=None, keyfile=None, not_after_connect=None):
self.keyfile = keyfile or utils.data.path("resources/server.key")
self.certfile = certfile or utils.data.path("resources/server.crt")
self.not_after_connect = not_after_connect
2012-06-19 01:23:07 +00:00
class PathodHandler(tcp.BaseHandler):
wbufsize = 0
sni = None
def info(self, s):
logger.info("%s:%s: %s"%(self.client_address[0], self.client_address[1], str(s)))
def handle_sni(self, connection):
self.sni = connection.get_servername()
def serve_crafted(self, crafted, request_log):
c = self.server.check_policy(crafted, self.server.request_settings)
if c:
err = language.PathodErrorResponse(c)
language.serve(err, self.wfile, self.server.request_settings)
log = dict(
type = "error",
msg = c
)
return False, log
if self.server.explain:
crafted = crafted.freeze(self.server.request_settings, None)
response_log = language.serve(crafted, self.wfile, self.server.request_settings, None)
log = dict(
type = "crafted",
request=request_log,
response=response_log
)
if response_log["disconnect"]:
return False, log
return True, 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()
if line == "\r\n" or line == "\n": # Possible leftover from previous message
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(
'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:
self.convert_to_ssl(
self.server.ssloptions.certfile,
self.server.ssloptions.keyfile,
2013-02-26 20:05:05 +00:00
handle_sni = self.handle_sni
)
except tcp.NetLibError, v:
s = str(v)
self.info(s)
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:
s = "Invalid first line: %s"%repr(line)
2012-07-21 08:50:41 +00:00
self.info(s)
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)
return False, dict(type = "error", msg = s)
2012-07-30 00:53:41 +00:00
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,
)
request_log = dict(
path = path,
method = method,
headers = headers.lst,
httpversion = httpversion,
sni = self.sni,
remote_address = self.client_address,
clientcert = clientcert
)
2012-07-22 00:30:10 +00:00
try:
content = http.read_http_body_request(
self.rfile, self.wfile, headers, httpversion, None
)
except http.HttpError, s:
s = str(s)
self.info(s)
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):
self.info("crafting anchor: %s"%path)
2012-10-04 21:30:32 +00:00
aresp = language.parse_response(self.server.request_settings, i[1])
2012-07-30 00:53:41 +00:00
return self.serve_crafted(aresp, request_log)
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):]
self.info("crafting spec: %s"%spec)
2012-07-21 08:50:41 +00:00
try:
2012-10-04 21:30:32 +00:00
crafted = language.parse_response(self.server.request_settings, spec)
except language.ParseException, v:
self.info("Parse error: %s"%v.msg)
2012-10-04 21:30:32 +00:00
crafted = language.PathodErrorResponse(
"Parse Error",
"Error parsing response spec: %s\n"%v.msg + v.marked()
)
return self.serve_crafted(crafted, request_log)
elif self.server.noweb:
2012-10-04 21:30:32 +00:00
crafted = language.PathodErrorResponse("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:
self.info("app: %s %s"%(method, path))
2012-07-21 08:50:41 +00:00
cc = wsgi.ClientConn(self.client_address)
req = wsgi.Request(cc, "http", method, path, headers, content)
sn = self.connection.getsockname()
app = wsgi.WSGIAdaptor(
self.server.app,
sn[0],
self.server.port,
version.NAMEVERSION
)
app.serve(req, self.wfile)
return True, None
def _log_bytes(self, header, data, hexdump):
s = []
if hexdump:
s.append("%s (hex dump):"%header)
for line in netlib.utils.hexdump(data):
s.append("\t%s %s %s"%line)
else:
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:
self.convert_to_ssl(
self.server.ssloptions.certfile,
self.server.ssloptions.keyfile,
2013-02-26 20:05:05 +00:00
handle_sni = self.handle_sni
)
except tcp.NetLibError, v:
s = str(v)
self.server.add_log(
dict(
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
def __init__( self,
addr, ssl=False, ssloptions=None, craftanchor="/p/", staticdir=None, anchors=None,
sizelimit=None, noweb=False, nocraft=False, noapi=False, nohang=False,
timeout=None, logreq=False, logresp=False, explain=False, hexdump=False
):
2012-06-24 04:20:50 +00:00
"""
addr: (address, port) tuple. If port is 0, a free port will be
automatically chosen.
ssloptions: a dictionary containing certfile and keyfile specifications.
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: A list of (regex, spec) 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, self.noapi, self.nohang = noweb, nocraft, noapi, nohang
self.timeout, self.logreq, self.logresp, self.hexdump = timeout, logreq, 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
2012-06-24 04:20:50 +00:00
self.anchors = []
if anchors:
for i in anchors:
2012-06-24 04:38:32 +00:00
try:
arex = re.compile(i[0])
except re.error:
raise PathodError("Invalid regex in anchor: %s"%i[0])
try:
2012-10-04 21:30:32 +00:00
aresp = language.parse_response(self.request_settings, i[1])
except language.ParseException, v:
2012-06-24 04:38:32 +00:00
raise PathodError("Invalid page spec in anchor: '%s', %s"%(i[1], str(v)))
2012-07-30 00:53:41 +00:00
self.anchors.append((arex, i[1]))
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, v:
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(
staticdir = self.staticdir
)
def handle_connection(self, request, client_address):
h = PathodHandler(request, client_address, self)
2012-07-29 23:58:29 +00:00
try:
h.handle()
h.finish()
except tcp.NetLibDisconnect: # pragma: no cover
h.info("Disconnect")
self.add_log(
dict(
type = "error",
msg = "Disconnect"
)
)
return
2012-10-30 23:32:13 +00:00
except tcp.NetLibTimeout:
h.info("Timeout")
self.add_log(
dict(
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