mitmproxy/pathod/pathoc.py

582 lines
19 KiB
Python
Raw Normal View History

import contextlib
2014-10-24 01:01:34 +00:00
import sys
import os
import itertools
import hashlib
2016-10-17 02:43:38 +00:00
import queue
import random
2015-04-29 20:03:26 +00:00
import select
import time
import OpenSSL.crypto
import logging
2016-10-19 22:02:52 +00:00
from mitmproxy import certs
from mitmproxy import exceptions
from mitmproxy.net import tcp, tls
from mitmproxy.net import websockets
from mitmproxy.net import socks
from mitmproxy.net import http as net_http
from mitmproxy.types import basethread
from mitmproxy.utils import strutils
2016-10-19 22:02:52 +00:00
from pathod import log
from pathod import language
from pathod.protocols import http2
2015-08-28 15:35:22 +00:00
2015-06-08 08:45:17 +00:00
logging.getLogger("hpack").setLevel(logging.WARNING)
def xrepr(s):
return repr(s)[1:-1]
class PathocError(Exception):
pass
2016-10-17 04:29:45 +00:00
class SSLInfo:
2015-06-18 16:12:11 +00:00
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):
parts = [
"Application Layer Protocol: %s" % strutils.always_str(self.alp, "utf8"),
2015-05-30 00:03:13 +00:00
"Cipher: %s, %s bit, %s" % self.cipher,
"SSL certificate chain:"
]
2016-05-28 20:25:54 +00:00
for n, i in enumerate(self.certchain):
parts.append(" Certificate [%s]" % n)
parts.append("\tSubject: ")
for cn in i.get_subject().get_components():
2016-06-15 05:03:56 +00:00
parts.append("\t\t%s=%s" % (
strutils.always_str(cn[0], "utf8"),
strutils.always_str(cn[1], "utf8"))
2016-06-15 05:03:56 +00:00
)
parts.append("\tIssuer: ")
for cn in i.get_issuer().get_components():
2016-06-15 05:03:56 +00:00
parts.append("\t\t%s=%s" % (
strutils.always_str(cn[0], "utf8"),
strutils.always_str(cn[1], "utf8"))
2016-06-15 05:03:56 +00:00
)
parts.extend(
[
2015-05-30 00:03:13 +00:00
"\tVersion: %s" % i.get_version(),
"\tValidity: %s - %s" % (
strutils.always_str(i.get_notBefore(), "utf8"),
strutils.always_str(i.get_notAfter(), "utf8")
),
2015-05-30 00:03:13 +00:00
"\tSerial: %s" % i.get_serial_number(),
"\tAlgorithm: %s" % strutils.always_str(i.get_signature_algorithm(), "utf8")
]
)
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))
2016-10-19 22:02:52 +00:00
s = certs.SSLCert(i)
if s.altnames:
parts.append("\tSANs: %s" % " ".join(strutils.always_str(n, "utf8") for n in s.altnames))
return "\n".join(parts)
class WebsocketFrameReader(basethread.BaseThread):
2015-06-18 16:12:11 +00:00
2015-05-30 00:03:13 +00:00
def __init__(
self,
rfile,
logfp,
showresp,
hexdump,
ws_read_limit,
timeout
):
basethread.BaseThread.__init__(self, "WebsocketFrameReader")
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
2016-03-20 21:50:03 +00:00
self.terminate = queue.Queue()
self.frames_queue = queue.Queue()
self.logger = log.ConnectionLogger(
self.logfp,
self.hexdump,
False,
rfile if 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
try:
r, _, _ = select.select([self.rfile], [], [], 0.05)
2017-02-10 14:28:02 +00:00
except OSError: # pragma: no cover
return # this is not reliably triggered due to its nature, so we exclude it from coverage.
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
2016-03-20 21:50:03 +00:00
except queue.Empty:
pass
for rfile in r:
with self.logger.ctx() as log:
try:
frm = websockets.Frame.from_file(self.rfile)
except exceptions.TcpDisconnect:
return
self.frames_queue.put(frm)
2016-08-16 16:31:50 +00:00
log("<< %s" % repr(frm.header))
if self.ws_read_limit is not None:
self.ws_read_limit -= 1
starttime = time.time()
class Pathoc(tcp.TCPClient):
2015-06-18 16:12:11 +00:00
def __init__(
self,
address,
# SSL
ssl=None,
sni=None,
ssl_version=tls.DEFAULT_METHOD,
ssl_options=tls.DEFAULT_OPTIONS,
clientcert=None,
ciphers=None,
2015-06-01 16:14:21 +00:00
# HTTP/2
use_http2=False,
http2_skip_connection_preface=False,
2015-06-18 16:12:11 +00:00
http2_framedump=False,
2015-06-01 16:14:21 +00:00
2015-04-29 20:03:26 +00:00
# Websockets
2015-06-18 16:12:11 +00:00
ws_read_limit=None,
2015-04-29 20:03:26 +00:00
# Network
2015-06-18 16:12:11 +00:00
timeout=None,
# Output control
2015-06-18 16:12:11 +00:00
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
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
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-06-11 14:13:22 +00:00
self.http2_framedump = http2_framedump
2015-06-01 16:14:21 +00:00
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.HTTP2StateProtocol(self, dump_frames=self.http2_framedump)
2015-06-08 08:45:17 +00:00
else:
self.protocol = net_http.http1
2015-06-08 08:45:17 +00:00
self.settings = language.Settings(
2015-06-18 16:12:11 +00:00
is_client=True,
staticdir=os.getcwd(),
unconstrained_file_access=True,
request_host=self.address[0],
2015-06-18 16:12:11 +00:00
protocol=self.protocol,
2015-06-08 08:45:17 +00:00
)
def http_connect(self, connect_to):
req = net_http.Request(
first_line_format='authority',
method='CONNECT',
scheme=None,
host=connect_to[0].encode("idna"),
port=connect_to[1],
path=None,
http_version='HTTP/1.1',
content=b'',
)
self.wfile.write(net_http.http1.assemble_request(req))
self.wfile.flush()
2015-09-16 16:44:34 +00:00
try:
resp = self.protocol.read_response(self.rfile, req)
2015-09-16 16:44:34 +00:00
if resp.status_code != 200:
raise exceptions.HttpException("Unexpected status code: %s" % resp.status_code)
except exceptions.HttpException as e:
2016-10-17 02:43:38 +00:00
raise PathocError(
2015-09-16 16:44:34 +00:00
"Proxy CONNECT failed: %s" % repr(e)
2016-10-17 02:43:38 +00:00
)
2015-07-03 00:48:35 +00:00
def socks_connect(self, connect_to):
try:
client_greet = socks.ClientGreeting(
socks.VERSION.SOCKS5,
[socks.METHOD.NO_AUTHENTICATION_REQUIRED]
)
2015-07-03 00:48:35 +00:00
client_greet.to_file(self.wfile)
self.wfile.flush()
server_greet = socks.ServerGreeting.from_file(self.rfile)
server_greet.assert_socks5()
if server_greet.method != socks.METHOD.NO_AUTHENTICATION_REQUIRED:
raise socks.SocksError(
socks.METHOD.NO_ACCEPTABLE_METHODS,
"pathoc only supports SOCKS without authentication"
)
connect_request = socks.Message(
socks.VERSION.SOCKS5,
socks.CMD.CONNECT,
socks.ATYP.DOMAINNAME,
connect_to,
2015-07-03 00:48:35 +00:00
)
connect_request.to_file(self.wfile)
self.wfile.flush()
connect_reply = socks.Message.from_file(self.rfile)
connect_reply.assert_socks5()
if connect_reply.msg != socks.REP.SUCCEEDED:
raise socks.SocksError(
connect_reply.msg,
"SOCKS server error"
)
except (socks.SocksError, exceptions.TcpDisconnect) as e:
2015-07-03 00:48:35 +00:00
raise PathocError(str(e))
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 NotImplementedError("HTTP2 without SSL is not supported.")
2015-06-01 16:45:17 +00:00
with tcp.TCPClient.connect(self) as closer:
if connect_to:
self.http_connect(connect_to)
2015-06-01 16:45:17 +00:00
self.sslinfo = None
if self.ssl:
try:
alpn_protos = [b'http/1.1']
if self.use_http2:
alpn_protos.append(b'h2')
self.convert_to_ssl(
sni=self.sni,
cert=self.clientcert,
method=self.ssl_version,
options=self.ssl_options,
cipher_list=self.ciphers,
alpn_protos=alpn_protos
)
except exceptions.TlsException as v:
raise PathocError(str(v))
self.sslinfo = SSLInfo(
self.connection.get_peer_cert_chain(),
self.get_current_cipher(),
self.get_alpn_proto_negotiated()
)
if showssl:
print(str(self.sslinfo), file=fp)
2015-06-01 16:45:17 +00:00
if self.use_http2:
self.protocol.check_alpn()
if not self.http2_skip_connection_preface:
self.protocol.perform_client_connection_preface()
if self.timeout:
self.settimeout(self.timeout)
return closer.pop()
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(
2015-06-18 16:12:11 +00:00
timeout=timeout,
block=True if timeout != 0 else False
)
2016-03-20 21:50:03 +00:00
except queue.Empty:
if finish:
continue
else:
return
if frm is None:
2015-04-29 20:03:26 +00:00
self.ws_framereader.join()
self.ws_framereader = None
2015-04-29 20:03:26 +00:00
return
yield frm
2015-04-29 20:03:26 +00:00
def websocket_send_frame(self, r):
"""
Sends a single websocket frame.
"""
logger = log.ConnectionLogger(
self.fp,
self.hexdump,
False,
None,
self.wfile if self.showreq else None,
)
with logger.ctx() as lg:
lg(">> %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 a exceptions.NetlibException
"""
logger = log.ConnectionLogger(
self.fp,
self.hexdump,
False,
self.rfile if self.showresp else None,
self.wfile if self.showreq else None,
)
with logger.ctx() as lg:
lg(">> %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
# build a dummy request to read the reponse
# ideally this would be returned directly from language.serve
dummy_req = net_http.Request(
first_line_format="relative",
method=req["method"],
scheme=b"http",
host=b"localhost",
port=80,
path=b"/",
http_version=b"HTTP/1.1",
content=b'',
)
resp = self.protocol.read_response(self.rfile, dummy_req)
resp.sslinfo = self.sslinfo
except exceptions.HttpException as v:
lg("Invalid server response: %s" % v)
2015-06-08 02:01:04 +00:00
raise
except exceptions.TcpTimeout:
if self.ignoretimeout:
lg("Timeout (ignored)")
return None
lg("Timeout")
raise
finally:
if resp:
2015-07-16 20:57:45 +00:00
lg("<< %s %s: %s bytes" % (
resp.status_code, strutils.escape_control_characters(resp.reason) if resp.reason else "", len(resp.content)
2015-07-16 20:57:45 +00:00
))
if resp.status_code in self.ignorecodes:
lg.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 a exceptions.NetlibException
"""
if isinstance(r, str):
2016-06-15 05:03:56 +00:00
r = next(language.parse_pathoc(r, self.use_http2))
2015-06-08 08:45:17 +00:00
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
def main(args): # pragma: no cover
2016-09-25 05:12:02 +00:00
memo = set()
2015-04-29 20:03:26 +00:00
p = None
2016-09-25 05:12:02 +00:00
if args.repeat == 1:
requests = args.requests
else:
# If we are replaying more than once, we must convert the request generators to lists
# or they will be exhausted after the first run.
# This is bad for the edge-case where get:/:x10000000 (see 0da3e51) is combined with -n 2,
# but does not matter otherwise.
requests = [list(x) for x in args.requests]
2014-10-24 04:12:54 +00:00
try:
2016-09-25 05:12:02 +00:00
requests_done = 0
2015-05-30 00:03:13 +00:00
while True:
2016-09-25 05:12:02 +00:00
if requests_done == args.repeat:
break
2016-09-25 05:12:02 +00:00
if args.wait and requests_done > 0:
time.sleep(args.wait)
2016-09-25 05:12:02 +00:00
requests_done += 1
if args.random:
2016-09-25 05:12:02 +00:00
playlist = random.choice(requests)
else:
playlist = itertools.chain.from_iterable(requests)
2014-10-24 04:12:54 +00:00
p = Pathoc(
(args.host, args.port),
2015-06-18 16:12:11 +00:00
ssl=args.ssl,
sni=args.sni,
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
clientcert=args.clientcert,
ciphers=args.ciphers,
use_http2=args.use_http2,
http2_skip_connection_preface=args.http2_skip_connection_preface,
http2_framedump=args.http2_framedump,
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:
with p.connect(args.connect_to, args.showssl):
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("Memo limit exceeded...", file=sys.stderr)
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.
2016-09-24 23:34:20 +00:00
for _ in p.wait(timeout=0, finish=False):
pass
except exceptions.NetlibException:
break
2016-09-24 23:34:20 +00:00
for _ in p.wait(timeout=0.01, finish=True):
pass
except exceptions.TcpException as v:
2016-04-14 05:34:28 +00:00
print(str(v), file=sys.stderr)
continue
2015-05-30 00:03:13 +00:00
except PathocError as v:
2016-04-14 05:34:28 +00:00
print(str(v), file=sys.stderr)
2014-10-24 04:12:54 +00:00
sys.exit(1)
2014-10-24 04:12:54 +00:00
except KeyboardInterrupt:
pass
2015-04-29 20:03:26 +00:00
if p:
p.stop()