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.
This commit is contained in:
Aldo Cortesi 2015-04-19 18:04:27 +12:00
parent f8469a283b
commit 3891fe5638
3 changed files with 153 additions and 124 deletions

View File

@ -379,7 +379,6 @@ def args_pathod(argv, stdout=sys.stdout, stderr=sys.stderr):
if os.path.isfile(spec): if os.path.isfile(spec):
data = open(spec).read() data = open(spec).read()
spec = data spec = data
try: try:
req = language.parse_response(spec) req = language.parse_response(spec)
except language.ParseException, v: except language.ParseException, v:

View File

@ -21,6 +21,40 @@ class SSLInfo:
def __init__(self, certchain, cipher): def __init__(self, certchain, cipher):
self.certchain, self.cipher = 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: class Response:
def __init__( def __init__(
@ -45,11 +79,34 @@ class Pathoc(tcp.TCPClient):
def __init__( def __init__(
self, self,
address, address,
# SSL
ssl=None, ssl=None,
sni=None, sni=None,
sslversion=4, sslversion=4,
clientcert=None, 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) tcp.TCPClient.__init__(self, address)
self.settings = dict( self.settings = dict(
staticdir = os.getcwd(), staticdir = os.getcwd(),
@ -60,6 +117,15 @@ class Pathoc(tcp.TCPClient):
self.sslversion = utils.SSLVERSIONS[sslversion] self.sslversion = utils.SSLVERSIONS[sslversion]
self.ciphers = ciphers 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): def http_connect(self, connect_to):
self.wfile.write( self.wfile.write(
'CONNECT %s:%s HTTP/1.1\r\n'%tuple(connect_to) + 'CONNECT %s:%s HTTP/1.1\r\n'%tuple(connect_to) +
@ -98,25 +164,11 @@ class Pathoc(tcp.TCPClient):
self.get_current_cipher() self.get_current_cipher()
) )
if showssl: if showssl:
self.print_sslinfo(self.sslinfo, fp) print >> fp, str(self.sslinfo)
def request(self, spec): def _show_summary(self, fp, resp):
"""
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):
print >> fp, "<< %s %s: %s bytes"%( 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): def _show(self, fp, header, data, hexdump):
@ -128,65 +180,20 @@ class Pathoc(tcp.TCPClient):
print >> fp, "%s (unprintables escaped):"%header print >> fp, "%s (unprintables escaped):"%header
print >> fp, netlib.utils.cleanBin(data) print >> fp, netlib.utils.cleanBin(data)
def print_sslinfo(self, sslinfo, fp): def request(self, r):
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
):
""" """
Performs a series of requests, and prints results to the specified Performs a single request.
file descriptor.
spec: A request specification r: A language.Request object, or a string representing one request.
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
Returns True if we have a non-ignored response. Returns True if we have a non-ignored response.
""" """
if isinstance(r, basestring):
r = language.parse_requests(r)[0]
resp, req = None, None resp, req = None, None
if showreq: if self.showreq:
self.wfile.start_log() self.wfile.start_log()
if showresp: if self.showresp:
self.rfile.start_log() self.rfile.start_log()
try: try:
req = language.serve( req = language.serve(
@ -196,32 +203,44 @@ class Pathoc(tcp.TCPClient):
self.address.host self.address.host
) )
self.wfile.flush() 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: except http.HttpError, v:
print >> fp, "<< HTTP Error:", v.message if self.showsummary:
print >> self.fp, "<< HTTP Error:", v.message
raise
except tcp.NetLibTimeout: except tcp.NetLibTimeout:
if ignoretimeout: if self.ignoretimeout:
return return None
print >> fp, "<<", "Timeout" if self.showsummary:
print >> self.fp, "<<", "Timeout"
raise
except tcp.NetLibDisconnect: # pragma: nocover 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 self.explain:
if ignorecodes and resp and resp[1] in ignorecodes: print >> self.fp, ">> Spec:", r.spec()
return
if explain: if self.showreq:
print >> fp, ">> Spec:", r.spec() self._show(
self.fp, ">> Request", self.wfile.get_log(), self.hexdump
)
if showreq: if self.showsummary and resp:
self._show(fp, ">> Request", self.wfile.get_log(), hexdump) self._show_summary(self.fp, resp)
if self.showresp:
if showresp: self._show(
self._show(fp, "<< Response", self.rfile.get_log(), hexdump) self.fp, "<< Response", self.rfile.get_log(), self.hexdump
else: )
if resp: return resp
self._show_summary(fp, *resp)
return True
def main(args): # pragma: nocover def main(args): # pragma: nocover
@ -245,11 +264,18 @@ def main(args): # pragma: nocover
playlist = args.requests playlist = args.requests
p = Pathoc( p = Pathoc(
(args.host, args.port), (args.host, args.port),
ssl=args.ssl, ssl = args.ssl,
sni=args.sni, sni = args.sni,
sslversion=args.sslversion, sslversion = args.sslversion,
clientcert=args.clientcert, clientcert = args.clientcert,
ciphers=args.ciphers 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: if args.explain or args.memo:
playlist = [ playlist = [
@ -279,17 +305,12 @@ def main(args): # pragma: nocover
if args.timeout: if args.timeout:
p.settimeout(args.timeout) p.settimeout(args.timeout)
for spec in playlist: for spec in playlist:
ret = p.print_request( try:
spec, ret = p.request(spec)
showreq=args.showreq, sys.stdout.flush()
showresp=args.showresp, if ret and args.oneshot:
explain=args.explain, return
hexdump=args.hexdump, except (http.HttpError, tcp.NetlibError), v:
ignorecodes=args.ignorecodes, pass
ignoretimeout=args.ignoretimeout
)
sys.stdout.flush()
if ret and args.oneshot:
sys.exit(0)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View File

@ -2,6 +2,7 @@ import json
import cStringIO import cStringIO
import re import re
from netlib import tcp, http
from libpathod import pathoc, test, version, pathod, language from libpathod import pathoc, test, version, pathod, language
import tutils import tutils
@ -38,8 +39,8 @@ class _TestDaemon:
ssl = self.ssl ssl = self.ssl
) )
c.connect() c.connect()
r = c.request("get:/api/info") resp = c.request("get:/api/info")
assert tuple(json.loads(r.content)["version"]) == version.IVERSION assert tuple(json.loads(resp.content)["version"]) == version.IVERSION
def tval( def tval(
self, self,
@ -51,10 +52,22 @@ class _TestDaemon:
hexdump=False, hexdump=False,
timeout=None, timeout=None,
ignorecodes=None, ignorecodes=None,
ignoretimeout=None ignoretimeout=None,
showsummary=True
): ):
s = cStringIO.StringIO() 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) c.connect(showssl=showssl, fp=s)
if timeout: if timeout:
c.settimeout(timeout) c.settimeout(timeout)
@ -62,16 +75,10 @@ class _TestDaemon:
r = language.parse_requests(i)[0] r = language.parse_requests(i)[0]
if explain: if explain:
r = r.freeze({}) r = r.freeze({})
c.print_request( try:
r, c.request(r)
showreq = showreq, except (http.HttpError, tcp.NetLibError), v:
showresp = showresp, pass
explain = explain,
hexdump = hexdump,
ignorecodes = ignorecodes,
ignoretimeout = ignoretimeout,
fp = s
)
return s.getvalue() return s.getvalue()
@ -121,6 +128,8 @@ class TestDaemon(_TestDaemon):
assert not "certificate chain" in self.tval(["get:/p/200"], showssl=True) assert not "certificate chain" in self.tval(["get:/p/200"], showssl=True)
def test_ignorecodes(self): 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" 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])
assert "200" not in self.tval(["get:'/p/200:b@1'"], ignorecodes=[200, 201]) 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): def test_timeout(self):
assert "Timeout" in self.tval(["get:'/p/200:p0,10'"], timeout=0.01) 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 "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): def test_showresp(self):
reqs = ["get:/api/info:p0,0", "get:/api/info:p0,0"] 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 assert self.tval(reqs, showresp=True, hexdump=True).count("hex dump") == 2
def test_showresp_httperr(self): 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 "Invalid headers" in v
assert "HTTP/" in v assert "HTTP/" in v