From 3891fe56385b323b435392aadb3f55848ee30857 Mon Sep 17 00:00:00 2001 From: Aldo Cortesi Date: Sun, 19 Apr 2015 18:04:27 +1200 Subject: [PATCH] Refactor pathoc We're getting ready for websockets. All the output specifiers are now on the Pathoc object itself - we can't assume that all input and output happens in response to a method call any more. This has the upside that we can unify the request/print_request methods. --- libpathod/cmdline.py | 1 - libpathod/pathoc.py | 235 +++++++++++++++++++++++-------------------- test/test_pathoc.py | 41 +++++--- 3 files changed, 153 insertions(+), 124 deletions(-) diff --git a/libpathod/cmdline.py b/libpathod/cmdline.py index a3b926495..3323716c8 100644 --- a/libpathod/cmdline.py +++ b/libpathod/cmdline.py @@ -379,7 +379,6 @@ def args_pathod(argv, stdout=sys.stdout, stderr=sys.stderr): if os.path.isfile(spec): data = open(spec).read() spec = data - try: req = language.parse_response(spec) except language.ParseException, v: diff --git a/libpathod/pathoc.py b/libpathod/pathoc.py index b81cb3590..08efdb3d5 100644 --- a/libpathod/pathoc.py +++ b/libpathod/pathoc.py @@ -21,6 +21,40 @@ class SSLInfo: def __init__(self, certchain, cipher): self.certchain, self.cipher = certchain, cipher + def __str__(self): + parts = [ + "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(): + parts.append("\t\t%s=%s"%cn) + parts.append("\tIssuer: ") + for cn in i.get_issuer().get_components(): + parts.append("\t\t%s=%s"%cn) + parts.extend( + [ + "\tVersion: %s"%i.get_version(), + "\tValidity: %s - %s"%( + i.get_notBefore(), i.get_notAfter() + ), + "\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") + parts.append("\tPubkey: %s bit %s"%(pk.bits(), t)) + s = certutils.SSLCert(i) + if s.altnames: + parts.append("\tSANs: %s"%" ".join(s.altnames)) + return "\n".join(parts) + class Response: def __init__( @@ -45,11 +79,34 @@ class Pathoc(tcp.TCPClient): def __init__( self, address, + + # SSL ssl=None, sni=None, sslversion=4, clientcert=None, - ciphers=None): + ciphers=None, + + # Output control + showreq = False, + showresp = False, + explain = False, + hexdump = False, + ignorecodes = False, + ignoretimeout = False, + showsummary = False, + fp = sys.stderr + ): + """ + 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) self.settings = dict( staticdir = os.getcwd(), @@ -60,6 +117,15 @@ class Pathoc(tcp.TCPClient): self.sslversion = utils.SSLVERSIONS[sslversion] self.ciphers = ciphers + self.showreq = showreq + self.showresp = showresp + self.explain = explain + self.hexdump = hexdump + self.ignorecodes = ignorecodes + self.ignoretimeout = ignoretimeout + self.showsummary = showsummary + self.fp = fp + def http_connect(self, connect_to): self.wfile.write( 'CONNECT %s:%s HTTP/1.1\r\n'%tuple(connect_to) + @@ -98,25 +164,11 @@ class Pathoc(tcp.TCPClient): self.get_current_cipher() ) if showssl: - self.print_sslinfo(self.sslinfo, fp) + print >> fp, str(self.sslinfo) - def request(self, spec): - """ - Return a Response object. - - May raise language.ParseException, netlib.http.HttpError or - language.FileAccessDenied. - """ - r = language.parse_requests(spec)[0] - language.serve(r, self.wfile, self.settings, self.address.host) - self.wfile.flush() - ret = list(http.read_response(self.rfile, r.method.string(), None)) - ret.append(self.sslinfo) - return Response(*ret) - - def _show_summary(self, fp, httpversion, code, msg, headers, content): + def _show_summary(self, fp, resp): print >> fp, "<< %s %s: %s bytes"%( - code, utils.xrepr(msg), len(content) + resp.status_code, utils.xrepr(resp.msg), len(resp.content) ) def _show(self, fp, header, data, hexdump): @@ -128,65 +180,20 @@ class Pathoc(tcp.TCPClient): print >> fp, "%s (unprintables escaped):"%header print >> fp, netlib.utils.cleanBin(data) - def print_sslinfo(self, sslinfo, fp): - print >> fp, "Cipher: %s, %s bit, %s"%self.sslinfo.cipher - print >> fp, "SSL certificate chain:\n" - for i in self.sslinfo.certchain: - print >> fp, "\tSubject: ", - for cn in i.get_subject().get_components(): - print >> fp, "%s=%s"%cn, - print >> fp - print >> fp, "\tIssuer: ", - for cn in i.get_issuer().get_components(): - print >> fp, "%s=%s"%cn, - print >> fp - print >> fp, "\tVersion: %s"%i.get_version() - print >> fp, "\tValidity: %s - %s"%( - i.get_notBefore(), i.get_notAfter() - ) - print >> fp, "\tSerial: %s"%i.get_serial_number() - print >> fp, "\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") - print >> fp, "\tPubkey: %s bit %s"%(pk.bits(), t) - s = certutils.SSLCert(i) - if s.altnames: - print >> fp, "\tSANs:", " ".join(s.altnames) - print >> fp - - def print_request( - self, - r, - showreq, - showresp, - explain, - hexdump, - ignorecodes, - ignoretimeout, - fp=sys.stdout - ): + def request(self, r): """ - Performs a series of requests, and prints results to the specified - file descriptor. + Performs a single request. - 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 - ignorecodes: Sequence of return codes to ignore + r: A language.Request object, or a string representing one request. Returns True if we have a non-ignored response. """ + if isinstance(r, basestring): + r = language.parse_requests(r)[0] resp, req = None, None - if showreq: + if self.showreq: self.wfile.start_log() - if showresp: + if self.showresp: self.rfile.start_log() try: req = language.serve( @@ -196,32 +203,44 @@ class Pathoc(tcp.TCPClient): self.address.host ) self.wfile.flush() - resp = http.read_response(self.rfile, r.method.string(), None) + + resp = list(http.read_response(self.rfile, r.method.string(), None)) + resp.append(self.sslinfo) + resp = Response(*resp) except http.HttpError, v: - print >> fp, "<< HTTP Error:", v.message + if self.showsummary: + print >> self.fp, "<< HTTP Error:", v.message + raise except tcp.NetLibTimeout: - if ignoretimeout: - return - print >> fp, "<<", "Timeout" + if self.ignoretimeout: + return None + if self.showsummary: + print >> self.fp, "<<", "Timeout" + raise except tcp.NetLibDisconnect: # pragma: nocover - print >> fp, "<<", "Disconnect" + if self.showsummary: + print >> self.fp, "<<", "Disconnect" + raise + finally: + if req: + if self.ignorecodes and resp and resp.status_code in self.ignorecodes: + return None - if req: - if ignorecodes and resp and resp[1] in ignorecodes: - return + if self.explain: + print >> self.fp, ">> Spec:", r.spec() - if explain: - print >> fp, ">> Spec:", r.spec() + if self.showreq: + self._show( + self.fp, ">> Request", self.wfile.get_log(), self.hexdump + ) - if showreq: - self._show(fp, ">> Request", self.wfile.get_log(), hexdump) - - if showresp: - self._show(fp, "<< Response", self.rfile.get_log(), hexdump) - else: - if resp: - self._show_summary(fp, *resp) - return True + if self.showsummary and resp: + self._show_summary(self.fp, resp) + if self.showresp: + self._show( + self.fp, "<< Response", self.rfile.get_log(), self.hexdump + ) + return resp def main(args): # pragma: nocover @@ -245,11 +264,18 @@ def main(args): # pragma: nocover playlist = args.requests p = Pathoc( (args.host, args.port), - ssl=args.ssl, - sni=args.sni, - sslversion=args.sslversion, - clientcert=args.clientcert, - ciphers=args.ciphers + ssl = args.ssl, + sni = args.sni, + sslversion = args.sslversion, + clientcert = args.clientcert, + ciphers = args.ciphers, + showreq = args.showreq, + showresp = args.showresp, + explain = args.explain, + hexdump = args.hexdump, + ignorecodes = args.ignorecodes, + ignoretimeout = args.ignoretimeout, + showsummary = True ) if args.explain or args.memo: playlist = [ @@ -279,17 +305,12 @@ def main(args): # pragma: nocover if args.timeout: p.settimeout(args.timeout) for spec in playlist: - ret = p.print_request( - spec, - showreq=args.showreq, - showresp=args.showresp, - explain=args.explain, - hexdump=args.hexdump, - ignorecodes=args.ignorecodes, - ignoretimeout=args.ignoretimeout - ) - sys.stdout.flush() - if ret and args.oneshot: - sys.exit(0) + try: + ret = p.request(spec) + sys.stdout.flush() + if ret and args.oneshot: + return + except (http.HttpError, tcp.NetlibError), v: + pass except KeyboardInterrupt: pass diff --git a/test/test_pathoc.py b/test/test_pathoc.py index 1a60ae45a..598b2c81c 100644 --- a/test/test_pathoc.py +++ b/test/test_pathoc.py @@ -2,6 +2,7 @@ import json import cStringIO import re +from netlib import tcp, http from libpathod import pathoc, test, version, pathod, language import tutils @@ -38,8 +39,8 @@ class _TestDaemon: ssl = self.ssl ) c.connect() - r = c.request("get:/api/info") - assert tuple(json.loads(r.content)["version"]) == version.IVERSION + resp = c.request("get:/api/info") + assert tuple(json.loads(resp.content)["version"]) == version.IVERSION def tval( self, @@ -51,10 +52,22 @@ class _TestDaemon: hexdump=False, timeout=None, ignorecodes=None, - ignoretimeout=None + ignoretimeout=None, + showsummary=True ): s = cStringIO.StringIO() - c = pathoc.Pathoc(("127.0.0.1", self.d.port), ssl=self.ssl) + c = pathoc.Pathoc( + ("127.0.0.1", self.d.port), + ssl = self.ssl, + showreq = showreq, + showresp = showresp, + explain = explain, + hexdump = hexdump, + ignorecodes = ignorecodes, + ignoretimeout = ignoretimeout, + showsummary = showsummary, + fp = s + ) c.connect(showssl=showssl, fp=s) if timeout: c.settimeout(timeout) @@ -62,16 +75,10 @@ class _TestDaemon: r = language.parse_requests(i)[0] if explain: r = r.freeze({}) - c.print_request( - r, - showreq = showreq, - showresp = showresp, - explain = explain, - hexdump = hexdump, - ignorecodes = ignorecodes, - ignoretimeout = ignoretimeout, - fp = s - ) + try: + c.request(r) + except (http.HttpError, tcp.NetLibError), v: + pass return s.getvalue() @@ -121,6 +128,8 @@ class TestDaemon(_TestDaemon): assert not "certificate chain" in self.tval(["get:/p/200"], showssl=True) def test_ignorecodes(self): + assert "200" in self.tval(["get:'/p/200:b@1'"]) + assert "200" in self.tval(["get:'/p/200:b@1'"]) assert "200" in self.tval(["get:'/p/200:b@1'"]) assert "200" not in self.tval(["get:'/p/200:b@1'"], ignorecodes=[200]) assert "200" not in self.tval(["get:'/p/200:b@1'"], ignorecodes=[200, 201]) @@ -129,7 +138,7 @@ class TestDaemon(_TestDaemon): def test_timeout(self): assert "Timeout" in self.tval(["get:'/p/200:p0,10'"], timeout=0.01) assert "HTTP" in self.tval(["get:'/p/200:p5,10'"], showresp=True, timeout=0.01) - assert not "HTTP" in self.tval(["get:'/p/200:p5,10'"], showresp=True, timeout=0.01, ignoretimeout=True) + assert not "HTTP" in self.tval(["get:'/p/200:p3,10'"], showresp=True, timeout=0.01, ignoretimeout=True) def test_showresp(self): reqs = ["get:/api/info:p0,0", "get:/api/info:p0,0"] @@ -138,7 +147,7 @@ class TestDaemon(_TestDaemon): assert self.tval(reqs, showresp=True, hexdump=True).count("hex dump") == 2 def test_showresp_httperr(self): - v = self.tval(["get:'/p/200:d20'"], showresp=True) + v = self.tval(["get:'/p/200:d20'"], showresp=True, showsummary=True) assert "Invalid headers" in v assert "HTTP/" in v