mitmproxy/libpathod/pathoc.py

513 lines
16 KiB
Python
Raw Normal View History

import contextlib
2014-10-24 01:01:34 +00:00
import sys
import os
import itertools
import hashlib
2015-04-29 20:03:26 +00:00
import Queue
import random
2015-04-29 20:03:26 +00:00
import select
import time
import threading
import OpenSSL.crypto
2015-06-01 16:45:17 +00:00
from netlib import tcp, http, http2, certutils, websockets
import language.http
import language.websockets
from . import utils, log
2015-06-08 08:45:17 +00:00
import logging
logging.getLogger("hpack").setLevel(logging.WARNING)
class PathocError(Exception):
pass
class SSLInfo:
2015-06-01 16:45:17 +00:00
def __init__(self, certchain, cipher, alp):
self.certchain, self.cipher, self.alp = certchain, cipher, alp
def __str__(self):
2015-06-01 16:45:17 +00:00
if self.alp:
alp = self.alp
else:
alp = '<no protocol negotiated>'
parts = [
2015-06-01 16:45:17 +00:00
"Application Layer Protocol: %s" % alp,
2015-05-30 00:03:13 +00:00
"Cipher: %s, %s bit, %s" % self.cipher,
"SSL certificate chain:"
]
for i in self.certchain:
parts.append("\tSubject: ")
for cn in i.get_subject().get_components():
2015-05-30 00:03:13 +00:00
parts.append("\t\t%s=%s" % cn)
parts.append("\tIssuer: ")
for cn in i.get_issuer().get_components():
2015-05-30 00:03:13 +00:00
parts.append("\t\t%s=%s" % cn)
parts.extend(
[
2015-05-30 00:03:13 +00:00
"\tVersion: %s" % i.get_version(),
"\tValidity: %s - %s" % (
i.get_notBefore(), i.get_notAfter()
),
2015-05-30 00:03:13 +00:00
"\tSerial: %s" % i.get_serial_number(),
"\tAlgorithm: %s" % i.get_signature_algorithm()
]
)
pk = i.get_pubkey()
types = {
OpenSSL.crypto.TYPE_RSA: "RSA",
OpenSSL.crypto.TYPE_DSA: "DSA"
}
t = types.get(pk.type(), "Uknown")
2015-05-30 00:03:13 +00:00
parts.append("\tPubkey: %s bit %s" % (pk.bits(), t))
s = certutils.SSLCert(i)
if s.altnames:
2015-05-30 00:03:13 +00:00
parts.append("\tSANs: %s" % " ".join(s.altnames))
return "\n".join(parts)
class Response:
def __init__(
self,
httpversion,
status_code,
msg,
headers,
content,
sslinfo
):
self.httpversion, self.status_code = httpversion, status_code
self.msg = msg
self.headers, self.content = headers, content
self.sslinfo = sslinfo
def __repr__(self):
2015-05-30 00:03:13 +00:00
return "Response(%s - %s)" % (self.status_code, self.msg)
class WebsocketFrameReader(threading.Thread):
2015-05-30 00:03:13 +00:00
def __init__(
self,
rfile,
logfp,
showresp,
hexdump,
ws_read_limit,
timeout
):
threading.Thread.__init__(self)
self.timeout = timeout
2015-04-29 20:03:26 +00:00
self.ws_read_limit = ws_read_limit
self.logfp = logfp
self.showresp = showresp
self.hexdump = hexdump
self.rfile = rfile
2015-04-29 20:03:26 +00:00
self.terminate = Queue.Queue()
self.frames_queue = Queue.Queue()
def log(self, rfile):
return log.Log(
self.logfp,
self.hexdump,
rfile if self.showresp else None,
None
)
@contextlib.contextmanager
def terminator(self):
yield
self.frames_queue.put(None)
def run(self):
starttime = time.time()
with self.terminator():
while True:
if self.ws_read_limit == 0:
return
r, _, x = select.select([self.rfile], [], [], 0.05)
2015-06-08 03:57:29 +00:00
delta = time.time() - starttime
if not r and self.timeout and delta > self.timeout:
return
try:
self.terminate.get_nowait()
return
except Queue.Empty:
pass
for rfile in r:
with self.log(rfile) as log:
frm = websockets.Frame.from_file(self.rfile)
self.frames_queue.put(frm)
log("<< %s" % frm.header.human_readable())
if self.ws_read_limit is not None:
self.ws_read_limit -= 1
starttime = time.time()
class Pathoc(tcp.TCPClient):
def __init__(
self,
address,
# SSL
ssl=None,
sni=None,
sslversion=4,
clientcert=None,
ciphers=None,
2015-06-01 16:14:21 +00:00
# HTTP/2
use_http2=False,
http2_skip_connection_preface=False,
2015-04-29 20:03:26 +00:00
# Websockets
ws_read_limit = None,
# Network
timeout = None,
# Output control
showreq = False,
showresp = False,
explain = False,
hexdump = False,
ignorecodes = (),
ignoretimeout = False,
showsummary = False,
fp = sys.stdout
):
"""
spec: A request specification
showreq: Print requests
showresp: Print responses
explain: Print request explanation
showssl: Print info on SSL connection
hexdump: When printing requests or responses, use hex dump output
showsummary: Show a summary of requests
ignorecodes: Sequence of return codes to ignore
"""
tcp.TCPClient.__init__(self, address)
2015-06-08 08:45:17 +00:00
2013-01-03 21:37:26 +00:00
self.ssl, self.sni = ssl, sni
self.clientcert = clientcert
self.sslversion = utils.SSLVERSIONS[sslversion]
self.ciphers = ciphers
2015-04-19 20:56:47 +00:00
self.sslinfo = None
2013-01-03 21:37:26 +00:00
2015-06-01 16:14:21 +00:00
self.use_http2 = use_http2
self.http2_skip_connection_preface = http2_skip_connection_preface
2015-04-29 20:03:26 +00:00
self.ws_read_limit = ws_read_limit
self.timeout = timeout
self.showreq = showreq
self.showresp = showresp
self.explain = explain
self.hexdump = hexdump
self.ignorecodes = ignorecodes
self.ignoretimeout = ignoretimeout
self.showsummary = showsummary
self.fp = fp
2015-04-29 20:03:26 +00:00
self.ws_framereader = None
2015-06-08 08:45:17 +00:00
if self.use_http2:
self.protocol = http2.HTTP2Protocol(self)
else:
# TODO: create HTTP or Websockets protocol
self.protocol = None
self.settings = language.Settings(
is_client = True,
staticdir = os.getcwd(),
unconstrained_file_access = True,
request_host = self.address.host,
protocol = self.protocol,
)
def log(self):
return log.Log(
self.fp,
self.hexdump,
self.rfile if self.showresp else None,
self.wfile if self.showreq else None,
)
def http_connect(self, connect_to):
self.wfile.write(
2015-05-30 00:03:13 +00:00
'CONNECT %s:%s HTTP/1.1\r\n' % tuple(connect_to) +
'\r\n'
)
self.wfile.flush()
l = self.rfile.readline()
if not l:
raise PathocError("Proxy CONNECT failed")
parsed = http.parse_response_line(l)
if not parsed[1] == 200:
raise PathocError(
2015-05-30 00:03:13 +00:00
"Proxy CONNECT failed: %s - %s" % (parsed[1], parsed[2])
)
http.read_headers(self.rfile)
def connect(self, connect_to=None, showssl=False, fp=sys.stdout):
"""
connect_to: A (host, port) tuple, which will be connected to with
an HTTP CONNECT request.
"""
2015-06-01 16:45:17 +00:00
if self.use_http2 and not self.ssl:
raise ValueError("HTTP2 without SSL is not supported.")
2013-01-03 21:37:26 +00:00
tcp.TCPClient.connect(self)
2015-06-01 16:45:17 +00:00
if connect_to:
self.http_connect(connect_to)
2015-06-01 16:45:17 +00:00
self.sslinfo = None
2013-01-03 21:37:26 +00:00
if self.ssl:
try:
2015-06-08 08:45:17 +00:00
alpn_protos = [b'http1.1'] # TODO: move to a new HTTP1 protocol
2015-06-01 16:45:17 +00:00
if self.use_http2:
2015-06-08 08:45:17 +00:00
alpn_protos.append(http2.HTTP2Protocol.ALPN_PROTO_H2)
2015-06-01 16:45:17 +00:00
self.convert_to_ssl(
sni=self.sni,
cert=self.clientcert,
method=self.sslversion,
2015-06-01 16:45:17 +00:00
cipher_list=self.ciphers,
alpn_protos=alpn_protos
)
2015-05-30 00:03:13 +00:00
except tcp.NetLibError as v:
2013-01-03 21:37:26 +00:00
raise PathocError(str(v))
2015-06-01 16:45:17 +00:00
self.sslinfo = SSLInfo(
self.connection.get_peer_cert_chain(),
2015-06-01 16:45:17 +00:00
self.get_current_cipher(),
self.get_alpn_proto_negotiated()
)
if showssl:
print >> fp, str(self.sslinfo)
2015-06-01 16:45:17 +00:00
if self.use_http2:
2015-06-08 08:45:17 +00:00
self.protocol.check_alpn()
2015-06-01 16:45:17 +00:00
if not self.http2_skip_connection_preface:
2015-06-08 08:45:17 +00:00
self.protocol.perform_connection_preface()
2015-06-01 16:45:17 +00:00
2015-06-08 03:57:29 +00:00
if self.timeout:
self.settimeout(self.timeout)
def _resp_summary(self, resp):
2015-05-30 00:03:13 +00:00
return "<< %s %s: %s bytes" % (
resp.status_code, utils.xrepr(resp.msg), len(resp.content)
)
2015-04-29 20:03:26 +00:00
def stop(self):
if self.ws_framereader:
self.ws_framereader.terminate.put(None)
2015-04-29 20:03:26 +00:00
def wait(self, timeout=0.01, finish=True):
"""
A generator that yields frames until Pathoc terminates.
timeout: If specified None may be yielded instead if timeout is
reached. If timeout is None, wait forever. If timeout is 0, return
immedately if nothing is on the queue.
finish: If true, consume messages until the reader shuts down.
Otherwise, return None on timeout.
"""
2015-04-29 20:03:26 +00:00
if self.ws_framereader:
2015-05-30 00:03:13 +00:00
while True:
2015-04-29 20:03:26 +00:00
try:
frm = self.ws_framereader.frames_queue.get(
timeout = timeout,
block = True if timeout != 0 else False
)
except Queue.Empty:
if finish:
continue
else:
return
if frm is None:
2015-04-29 20:03:26 +00:00
self.ws_framereader.join()
return
yield frm
2015-04-29 20:03:26 +00:00
def websocket_send_frame(self, r):
"""
Sends a single websocket frame.
"""
with self.log() as log:
2015-05-30 00:03:13 +00:00
log(">> %s" % r)
2015-06-08 04:25:33 +00:00
language.serve(r, self.wfile, self.settings)
self.wfile.flush()
2015-06-08 04:25:33 +00:00
def websocket_start(self, r):
"""
Performs an HTTP request, and attempts to drop into websocket
connection.
"""
resp = self.http(r)
if resp.status_code == 101:
self.ws_framereader = WebsocketFrameReader(
self.rfile,
self.fp,
self.showresp,
self.hexdump,
self.ws_read_limit,
self.timeout
)
self.ws_framereader.start()
return resp
def http(self, r):
"""
Performs a single request.
r: A language.http.Request object, or a string representing one
request.
Returns Response if we have a non-ignored response.
2015-04-19 20:56:47 +00:00
May raise http.HTTPError, tcp.NetLibError
"""
with self.log() as log:
2015-05-30 00:03:13 +00:00
log(">> %s" % r)
resp, req = None, None
try:
req = language.serve(r, self.wfile, self.settings)
self.wfile.flush()
2015-06-08 08:45:17 +00:00
if self.use_http2:
status_code, headers, body = self.protocol.read_response()
resp = Response("HTTP/2", status_code, "", headers, body, self.sslinfo)
else:
resp = list(
http.read_response(
self.rfile,
req["method"],
None
)
)
2015-06-08 08:45:17 +00:00
resp.append(self.sslinfo)
resp = Response(*resp)
2015-06-08 02:01:04 +00:00
except http.HttpError, v:
log("Invalid server response: %s" % v)
raise
except tcp.NetLibTimeout:
if self.ignoretimeout:
log("Timeout (ignored)")
return None
2015-06-08 02:01:04 +00:00
log("Timeout")
raise
finally:
if resp:
log(self._resp_summary(resp))
if resp.status_code in self.ignorecodes:
log.suppress()
return resp
def request(self, r):
"""
Performs a single request.
2015-06-05 00:04:40 +00:00
r: A language.message.Messsage object, or a string representing
one.
Returns Response if we have a non-ignored response.
May raise http.HTTPError, tcp.NetLibError
"""
if isinstance(r, basestring):
2015-06-08 08:45:17 +00:00
r = language.parse_pathoc(r, self.use_http2).next()
if isinstance(r, language.http.Request):
if r.ws:
2015-06-08 04:25:33 +00:00
return self.websocket_start(r)
else:
return self.http(r)
elif isinstance(r, language.websockets.WebsocketFrame):
self.websocket_send_frame(r)
2015-06-08 08:45:17 +00:00
elif isinstance(r, language.http2.Request):
return self.http(r)
# elif isinstance(r, language.http2.Frame):
# TODO: do something
2015-05-30 00:03:13 +00:00
def main(args): # pragma: nocover
memo = set([])
trycount = 0
2015-04-29 20:03:26 +00:00
p = None
2014-10-24 04:12:54 +00:00
try:
2014-10-25 04:58:59 +00:00
cnt = 0
2015-05-30 00:03:13 +00:00
while True:
if cnt == args.repeat and args.repeat != 0:
break
if args.wait and cnt != 0:
time.sleep(args.wait)
2014-10-25 04:58:59 +00:00
cnt += 1
playlist = itertools.chain(*args.requests)
if args.random:
playlist = random.choice(args.requests)
2014-10-24 04:12:54 +00:00
p = Pathoc(
(args.host, args.port),
ssl = args.ssl,
sni = args.sni,
sslversion = args.sslversion,
clientcert = args.clientcert,
ciphers = args.ciphers,
2015-06-01 16:14:21 +00:00
use_http2 = args.use_http2,
http2_skip_connection_preface = args.http2_skip_connection_preface,
showreq = args.showreq,
showresp = args.showresp,
explain = args.explain,
hexdump = args.hexdump,
ignorecodes = args.ignorecodes,
timeout = args.timeout,
ignoretimeout = args.ignoretimeout,
showsummary = True
2014-10-24 04:12:54 +00:00
)
trycount = 0
2014-10-24 04:12:54 +00:00
try:
p.connect(args.connect_to, args.showssl)
2015-05-30 00:03:13 +00:00
except tcp.NetLibError as v:
print >> sys.stderr, str(v)
continue
2015-05-30 00:03:13 +00:00
except PathocError as v:
2014-10-24 04:12:54 +00:00
print >> sys.stderr, str(v)
sys.exit(1)
for spec in playlist:
if args.explain or args.memo:
spec = spec.freeze(p.settings)
if args.memo:
h = hashlib.sha256(spec.spec()).digest()
if h not in memo:
trycount = 0
memo.add(h)
else:
trycount += 1
if trycount > args.memolimit:
print >> sys.stderr, "Memo limit exceeded..."
return
else:
continue
try:
ret = p.request(spec)
if ret and args.oneshot:
return
# We consume the queue when we can, so it doesn't build up.
for i in p.wait(timeout=0, finish=False):
pass
2015-05-30 00:03:13 +00:00
except (http.HttpError, tcp.NetLibError) as v:
break
for i in p.wait(timeout=0.01, finish=True):
pass
2014-10-24 04:12:54 +00:00
except KeyboardInterrupt:
pass
2015-04-29 20:03:26 +00:00
if p:
p.stop()