mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-27 10:26:23 +00:00
Merge remote-tracking branch 'origin/master'
Conflicts: setup.py
This commit is contained in:
commit
8635e00175
@ -1,3 +1,12 @@
|
|||||||
|
7 November 2014: pathod 0.11:
|
||||||
|
|
||||||
|
* Hugely improved SSL support, including dynamic generation of certificates
|
||||||
|
using the mitproxy cacert
|
||||||
|
* pathoc -S dumps information on the remote SSL certificate chain
|
||||||
|
* Big improvements to fuzzing, including random spec selection and memoization to avoid repeating randomly generated patterns
|
||||||
|
* Reflected patterns, allowing you to embed a pathod server response specification in a pathoc request, resolving both on client side. This makes fuzzing proxies and other intermediate systems much better.
|
||||||
|
|
||||||
|
|
||||||
25 August 2013: pathod 0.9.2:
|
25 August 2013: pathod 0.9.2:
|
||||||
|
|
||||||
* Adapt to interface changes in netlib
|
* Adapt to interface changes in netlib
|
||||||
|
@ -4,6 +4,8 @@ import version, language, utils
|
|||||||
from netlib import http_uastrings
|
from netlib import http_uastrings
|
||||||
|
|
||||||
logging.basicConfig(level="DEBUG")
|
logging.basicConfig(level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
def make_app(noapi):
|
def make_app(noapi):
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@ -14,20 +16,17 @@ def make_app(noapi):
|
|||||||
version = version.IVERSION
|
version = version.IVERSION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/log')
|
@app.route('/api/log')
|
||||||
def api_log():
|
def api_log():
|
||||||
return jsonify(
|
return jsonify(
|
||||||
log = app.config["pathod"].get_log()
|
log = app.config["pathod"].get_log()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/clear_log')
|
@app.route('/api/clear_log')
|
||||||
def api_clear_log():
|
def api_clear_log():
|
||||||
app.config["pathod"].clear_log()
|
app.config["pathod"].clear_log()
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
def render(s, cacheable, **kwargs):
|
def render(s, cacheable, **kwargs):
|
||||||
kwargs["noapi"] = app.config["pathod"].noapi
|
kwargs["noapi"] = app.config["pathod"].noapi
|
||||||
kwargs["nocraft"] = app.config["pathod"].nocraft
|
kwargs["nocraft"] = app.config["pathod"].nocraft
|
||||||
@ -37,30 +36,25 @@ def make_app(noapi):
|
|||||||
resp.headers["Cache-control"] = "public, max-age=4320"
|
resp.headers["Cache-control"] = "public, max-age=4320"
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@app.route('/index.html')
|
@app.route('/index.html')
|
||||||
def index():
|
def index():
|
||||||
return render("index.html", True, section="main")
|
return render("index.html", True, section="main")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/download')
|
@app.route('/download')
|
||||||
@app.route('/download.html')
|
@app.route('/download.html')
|
||||||
def download():
|
def download():
|
||||||
return render("download.html", True, section="download", version=version.VERSION)
|
return render("download.html", True, section="download", version=version.VERSION)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/about')
|
@app.route('/about')
|
||||||
@app.route('/about.html')
|
@app.route('/about.html')
|
||||||
def about():
|
def about():
|
||||||
return render("about.html", True, section="about")
|
return render("about.html", True, section="about")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/docs/pathod')
|
@app.route('/docs/pathod')
|
||||||
def docs_pathod():
|
def docs_pathod():
|
||||||
return render("docs_pathod.html", True, section="docs", subsection="pathod")
|
return render("docs_pathod.html", True, section="docs", subsection="pathod")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/docs/language')
|
@app.route('/docs/language')
|
||||||
def docs_language():
|
def docs_language():
|
||||||
return render(
|
return render(
|
||||||
@ -69,29 +63,24 @@ def make_app(noapi):
|
|||||||
subsection="lang"
|
subsection="lang"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/docs/pathoc')
|
@app.route('/docs/pathoc')
|
||||||
def docs_pathoc():
|
def docs_pathoc():
|
||||||
return render("docs_pathoc.html", True, section="docs", subsection="pathoc")
|
return render("docs_pathoc.html", True, section="docs", subsection="pathoc")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/docs/libpathod')
|
@app.route('/docs/libpathod')
|
||||||
def docs_libpathod():
|
def docs_libpathod():
|
||||||
return render("docs_libpathod.html", True, section="docs", subsection="libpathod")
|
return render("docs_libpathod.html", True, section="docs", subsection="libpathod")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/docs/test')
|
@app.route('/docs/test')
|
||||||
def docs_test():
|
def docs_test():
|
||||||
return render("docs_test.html", True, section="docs", subsection="test")
|
return render("docs_test.html", True, section="docs", subsection="test")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log')
|
@app.route('/log')
|
||||||
def log():
|
def log():
|
||||||
if app.config["pathod"].noapi:
|
if app.config["pathod"].noapi:
|
||||||
abort(404)
|
abort(404)
|
||||||
return render("log.html", False, section="log", log=app.config["pathod"].get_log())
|
return render("log.html", False, section="log", log=app.config["pathod"].get_log())
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log/<int:lid>')
|
@app.route('/log/<int:lid>')
|
||||||
def onelog(lid):
|
def onelog(lid):
|
||||||
item = app.config["pathod"].log_by_id(int(lid))
|
item = app.config["pathod"].log_by_id(int(lid))
|
||||||
@ -100,7 +89,6 @@ def make_app(noapi):
|
|||||||
l = pprint.pformat(item)
|
l = pprint.pformat(item)
|
||||||
return render("onelog.html", False, section="log", alog=l, lid=lid)
|
return render("onelog.html", False, section="log", alog=l, lid=lid)
|
||||||
|
|
||||||
|
|
||||||
def _preview(is_request):
|
def _preview(is_request):
|
||||||
if is_request:
|
if is_request:
|
||||||
template = "request_preview.html"
|
template = "request_preview.html"
|
||||||
@ -121,9 +109,9 @@ def make_app(noapi):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if is_request:
|
if is_request:
|
||||||
r = language.parse_request(app.config["pathod"].request_settings, spec)
|
r = language.parse_requests(spec)[0]
|
||||||
else:
|
else:
|
||||||
r = language.parse_response(app.config["pathod"].request_settings, spec)
|
r = language.parse_response(spec)
|
||||||
except language.ParseException, v:
|
except language.ParseException, v:
|
||||||
args["syntaxerror"] = str(v)
|
args["syntaxerror"] = str(v)
|
||||||
args["marked"] = v.marked()
|
args["marked"] = v.marked()
|
||||||
@ -144,14 +132,11 @@ def make_app(noapi):
|
|||||||
args["output"] = utils.escape_unprintables(s.getvalue())
|
args["output"] = utils.escape_unprintables(s.getvalue())
|
||||||
return render(template, False, **args)
|
return render(template, False, **args)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/response_preview')
|
@app.route('/response_preview')
|
||||||
def response_preview():
|
def response_preview():
|
||||||
return _preview(False)
|
return _preview(False)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/request_preview')
|
@app.route('/request_preview')
|
||||||
def request_preview():
|
def request_preview():
|
||||||
return _preview(True)
|
return _preview(True)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import argparse, sys, logging, logging.handlers, os
|
import argparse
|
||||||
from . import pathoc as _pathoc, pathod as _pathod, utils, version, language
|
import os
|
||||||
from netlib import tcp, http_uastrings
|
import os.path
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from . import pathoc, pathod, version, utils, language
|
||||||
|
from netlib import http_uastrings
|
||||||
|
|
||||||
|
|
||||||
def pathoc():
|
def go_pathoc():
|
||||||
preparser = argparse.ArgumentParser(add_help=False)
|
preparser = argparse.ArgumentParser(add_help=False)
|
||||||
preparser.add_argument(
|
preparser.add_argument(
|
||||||
"--show-uas", dest="showua", action="store_true", default=False,
|
"--show-uas", dest="showua", action="store_true", default=False,
|
||||||
@ -17,20 +21,41 @@ def pathoc():
|
|||||||
print " ", i[1], i[0]
|
print " ", i[1], i[0]
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='A perverse HTTP client.', parents=[preparser])
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument('--version', action='version', version="pathoc " + version.VERSION)
|
description='A perverse HTTP client.', parents=[preparser]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--version',
|
||||||
|
action='version',
|
||||||
|
version="pathoc " + version.VERSION
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", dest="connect_to", type=str, default=False,
|
"-c", dest="connect_to", type=str, default=False,
|
||||||
metavar = "HOST:PORT",
|
metavar = "HOST:PORT",
|
||||||
help="Issue an HTTP CONNECT to connect to the specified host."
|
help="Issue an HTTP CONNECT to connect to the specified host."
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-n", dest='repeat', default=1, type=int, metavar="N",
|
"--memo-limit", dest='memolimit', default=5000, type=int, metavar="N",
|
||||||
help='Repeat requests N times'
|
help='Stop if we do not find a valid request after N attempts.'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p", dest="port", type=int, default=None,
|
"-m", dest='memo', action="store_true", default=False,
|
||||||
help="Port. Defaults to 80, or 443 if SSL is active"
|
help="""
|
||||||
|
Remember specs, and never play the same one twice. Note that this
|
||||||
|
means requests have to be rendered in memory, which means that large
|
||||||
|
generated data can cause issues.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-n", dest='repeat', default=1, type=int, metavar="N",
|
||||||
|
help='Repeat N times. If 0 repeat for ever.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-r", dest="random", action="store_true", default=False,
|
||||||
|
help="""
|
||||||
|
Select a random request from those specified. If this is not specified,
|
||||||
|
requests are all played in sequence.
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-t", dest="timeout", type=int, default=None,
|
"-t", dest="timeout", type=int, default=None,
|
||||||
@ -38,13 +63,16 @@ def pathoc():
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'host', type=str,
|
'host', type=str,
|
||||||
help='Host to connect to'
|
metavar = "host[:port]",
|
||||||
|
help='Host and port to connect to'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'request', type=str, nargs="+",
|
'requests', type=str, nargs="+",
|
||||||
help='Request specification'
|
help="""
|
||||||
|
Request specification, or path to a file containing request
|
||||||
|
specifcations
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
group = parser.add_argument_group(
|
group = parser.add_argument_group(
|
||||||
'SSL',
|
'SSL',
|
||||||
)
|
)
|
||||||
@ -67,7 +95,10 @@ def pathoc():
|
|||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--sslversion", dest="sslversion", type=int, default=4,
|
"--sslversion", dest="sslversion", type=int, default=4,
|
||||||
choices=[1, 2, 3, 4],
|
choices=[1, 2, 3, 4],
|
||||||
help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
|
help="""
|
||||||
|
Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default
|
||||||
|
to SSLv23.
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
group = parser.add_argument_group(
|
group = parser.add_argument_group(
|
||||||
@ -98,7 +129,7 @@ def pathoc():
|
|||||||
help="Print full request"
|
help="Print full request"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"-r", dest="showresp", action="store_true", default=False,
|
"-p", dest="showresp", action="store_true", default=False,
|
||||||
help="Print full response"
|
help="Print full response"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
@ -112,13 +143,21 @@ def pathoc():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
args.port = None
|
||||||
|
if ":" in args.host:
|
||||||
|
h, p = args.host.rsplit(":", 1)
|
||||||
|
try:
|
||||||
|
p = int(p)
|
||||||
|
except ValueError:
|
||||||
|
parser.error("Invalid port in host spec: %s" % args.host)
|
||||||
|
args.host = h
|
||||||
|
args.port = p
|
||||||
|
|
||||||
if args.port is None:
|
if args.port is None:
|
||||||
port = 443 if args.ssl else 80
|
args.port = 443 if args.ssl else 80
|
||||||
else:
|
|
||||||
port = args.port
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
codes = [int(i) for i in args.ignorecodes.split(",") if i]
|
args.ignorecodes = [int(i) for i in args.ignorecodes.split(",") if i]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
parser.error("Invalid return code specification: %s"%args.ignorecodes)
|
parser.error("Invalid return code specification: %s"%args.ignorecodes)
|
||||||
|
|
||||||
@ -130,164 +169,59 @@ def pathoc():
|
|||||||
parts[1] = int(parts[1])
|
parts[1] = int(parts[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
parser.error("Invalid CONNECT specification: %s"%args.connect_to)
|
parser.error("Invalid CONNECT specification: %s"%args.connect_to)
|
||||||
connect_to = parts
|
args.connect_to = parts
|
||||||
else:
|
else:
|
||||||
connect_to = None
|
args.connect_to = None
|
||||||
|
|
||||||
try:
|
reqs = []
|
||||||
for i in range(args.repeat):
|
for r in args.requests:
|
||||||
p = _pathoc.Pathoc(
|
if os.path.exists(r):
|
||||||
(args.host, port),
|
data = open(r).read()
|
||||||
ssl=args.ssl,
|
r = data
|
||||||
sni=args.sni,
|
|
||||||
sslversion=args.sslversion,
|
|
||||||
clientcert=args.clientcert,
|
|
||||||
ciphers=args.ciphers
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
p.connect(connect_to)
|
|
||||||
except (tcp.NetLibError, _pathoc.PathocError), v:
|
|
||||||
print >> sys.stderr, str(v)
|
|
||||||
sys.exit(1)
|
|
||||||
if args.timeout:
|
|
||||||
p.settimeout(args.timeout)
|
|
||||||
for spec in args.request:
|
|
||||||
ret = p.print_request(
|
|
||||||
spec,
|
|
||||||
showreq=args.showreq,
|
|
||||||
showresp=args.showresp,
|
|
||||||
explain=args.explain,
|
|
||||||
showssl=args.showssl,
|
|
||||||
hexdump=args.hexdump,
|
|
||||||
ignorecodes=codes,
|
|
||||||
ignoretimeout=args.ignoretimeout
|
|
||||||
)
|
|
||||||
sys.stdout.flush()
|
|
||||||
if ret and args.oneshot:
|
|
||||||
sys.exit(0)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
|
||||||
try:
|
|
||||||
pid = os.fork()
|
|
||||||
if pid > 0:
|
|
||||||
sys.exit(0)
|
|
||||||
except OSError, e:
|
|
||||||
sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
|
|
||||||
sys.exit(1)
|
|
||||||
os.chdir("/")
|
|
||||||
os.umask(0)
|
|
||||||
os.setsid()
|
|
||||||
try:
|
|
||||||
pid = os.fork()
|
|
||||||
if pid > 0:
|
|
||||||
sys.exit(0)
|
|
||||||
except OSError, e:
|
|
||||||
sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
|
|
||||||
sys.exit(1)
|
|
||||||
si = open(stdin, 'rb')
|
|
||||||
so = open(stdout, 'a+b')
|
|
||||||
se = open(stderr, 'a+b', 0)
|
|
||||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
|
||||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
|
||||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
|
||||||
|
|
||||||
|
|
||||||
def pathod_main(parser, args):
|
|
||||||
certs = []
|
|
||||||
for i in args.ssl_certs:
|
|
||||||
parts = i.split("=", 1)
|
|
||||||
if len(parts) == 1:
|
|
||||||
parts = ["*", parts[0]]
|
|
||||||
parts[1] = os.path.expanduser(parts[1])
|
|
||||||
if not os.path.exists(parts[1]):
|
|
||||||
parser.error("Certificate file does not exist: %s"%parts[1])
|
|
||||||
certs.append(parts)
|
|
||||||
|
|
||||||
ssloptions = _pathod.SSLOptions(
|
|
||||||
cn = args.cn,
|
|
||||||
confdir = args.confdir,
|
|
||||||
not_after_connect = args.ssl_not_after_connect,
|
|
||||||
ciphers = args.ciphers,
|
|
||||||
sslversion = utils.SSLVERSIONS[args.sslversion],
|
|
||||||
certs = certs
|
|
||||||
)
|
|
||||||
|
|
||||||
alst = []
|
|
||||||
for i in args.anchors:
|
|
||||||
parts = utils.parse_anchor_spec(i)
|
|
||||||
if not parts:
|
|
||||||
parser.error("Invalid anchor specification: %s"%i)
|
|
||||||
alst.append(parts)
|
|
||||||
|
|
||||||
root = logging.getLogger()
|
|
||||||
if root.handlers:
|
|
||||||
for handler in root.handlers:
|
|
||||||
root.removeHandler(handler)
|
|
||||||
|
|
||||||
log = logging.getLogger('pathod')
|
|
||||||
log.setLevel(logging.DEBUG)
|
|
||||||
fmt = logging.Formatter(
|
|
||||||
'%(asctime)s: %(message)s',
|
|
||||||
datefmt='%d-%m-%y %H:%M:%S',
|
|
||||||
)
|
|
||||||
if args.logfile:
|
|
||||||
fh = logging.handlers.WatchedFileHandler(args.logfile)
|
|
||||||
fh.setFormatter(fmt)
|
|
||||||
log.addHandler(fh)
|
|
||||||
if not args.daemonize:
|
|
||||||
sh = logging.StreamHandler()
|
|
||||||
sh.setFormatter(fmt)
|
|
||||||
log.addHandler(sh)
|
|
||||||
|
|
||||||
sizelimit = None
|
|
||||||
if args.sizelimit:
|
|
||||||
try:
|
try:
|
||||||
sizelimit = utils.parse_size(args.sizelimit)
|
reqs.extend(language.parse_requests(r))
|
||||||
except ValueError, v:
|
except language.ParseException, v:
|
||||||
parser.error(v)
|
print >> sys.stderr, "Error parsing request spec: %s"%v.msg
|
||||||
|
print >> sys.stderr, v.marked()
|
||||||
try:
|
sys.exit(1)
|
||||||
pd = _pathod.Pathod(
|
args.requests = reqs
|
||||||
(args.address, args.port),
|
pathoc.main(args)
|
||||||
craftanchor = args.craftanchor,
|
|
||||||
ssl = args.ssl,
|
|
||||||
ssloptions = ssloptions,
|
|
||||||
staticdir = args.staticdir,
|
|
||||||
anchors = alst,
|
|
||||||
sizelimit = sizelimit,
|
|
||||||
noweb = args.noweb,
|
|
||||||
nocraft = args.nocraft,
|
|
||||||
noapi = args.noapi,
|
|
||||||
nohang = args.nohang,
|
|
||||||
timeout = args.timeout,
|
|
||||||
logreq = args.logreq,
|
|
||||||
logresp = args.logresp,
|
|
||||||
hexdump = args.hexdump,
|
|
||||||
explain = args.explain,
|
|
||||||
)
|
|
||||||
except _pathod.PathodError, v:
|
|
||||||
parser.error(str(v))
|
|
||||||
except language.FileAccessDenied, v:
|
|
||||||
parser.error("%s You probably want to a -d argument."%str(v))
|
|
||||||
|
|
||||||
try:
|
|
||||||
print "%s listening on %s:%s"%(version.NAMEVERSION, pd.address.host, pd.address.port)
|
|
||||||
pd.serve_forever()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def pathod():
|
def go_pathod():
|
||||||
parser = argparse.ArgumentParser(description='A pathological HTTP/S daemon.')
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument('--version', action='version', version="pathod " + version.VERSION)
|
description='A pathological HTTP/S daemon.'
|
||||||
parser.add_argument("-p", dest='port', default=9999, type=int, help='Port. Specify 0 to pick an arbitrary empty port.')
|
)
|
||||||
parser.add_argument("-l", dest='address', default="127.0.0.1", type=str, help='Listening address.')
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-a", dest='anchors', default=[], type=str, action="append", metavar="ANCHOR",
|
'--version',
|
||||||
help='Add an anchor. Specified as a string with the form pattern=pagespec'
|
action='version',
|
||||||
|
version="pathod " + version.VERSION
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
dest='port',
|
||||||
|
default=9999,
|
||||||
|
type=int,
|
||||||
|
help='Port. Specify 0 to pick an arbitrary empty port.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-l",
|
||||||
|
dest='address',
|
||||||
|
default="127.0.0.1",
|
||||||
|
type=str,
|
||||||
|
help='Listening address.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
dest='anchors',
|
||||||
|
default=[],
|
||||||
|
type=str,
|
||||||
|
action="append",
|
||||||
|
metavar="ANCHOR",
|
||||||
|
help="""
|
||||||
|
Add an anchor. Specified as a string with the form pattern=pagespec, or
|
||||||
|
pattern=filepath
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", dest='craftanchor', default="/p/", type=str,
|
"-c", dest='craftanchor', default="/p/", type=str,
|
||||||
@ -340,7 +274,7 @@ def pathod():
|
|||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--cn", dest="cn", type=str, default=None,
|
"--cn", dest="cn", type=str, default=None,
|
||||||
help="CN for generated SSL certs. Default: %s"%_pathod.DEFAULT_CERT_DOMAIN
|
help="CN for generated SSL certs. Default: %s"%pathod.DEFAULT_CERT_DOMAIN
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"-C", dest='ssl_not_after_connect', default=False, action="store_true",
|
"-C", dest='ssl_not_after_connect', default=False, action="store_true",
|
||||||
@ -349,10 +283,13 @@ def pathod():
|
|||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--cert", dest='ssl_certs', default=[], type=str,
|
"--cert", dest='ssl_certs', default=[], type=str,
|
||||||
metavar = "SPEC", action="append",
|
metavar = "SPEC", action="append",
|
||||||
help='Add an SSL certificate. SPEC is of the form "[domain=]path". '\
|
help = """
|
||||||
'The domain may include a wildcard, and is equal to "*" if not specified. '\
|
Add an SSL certificate. SPEC is of the form "[domain=]path". The domain
|
||||||
'The file at path is a certificate in PEM format. If a private key is included in the PEM, '\
|
may include a wildcard, and is equal to "*" if not specified. The file
|
||||||
'it is used, else the default key in the conf dir is used. Can be passed multiple times.'
|
at path is a certificate in PEM format. If a private key is included in
|
||||||
|
the PEM, it is used, else the default key in the conf dir is used. Can
|
||||||
|
be passed multiple times.'
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--ciphers", dest="ciphers", type=str, default=False,
|
"--ciphers", dest="ciphers", type=str, default=False,
|
||||||
@ -361,7 +298,8 @@ def pathod():
|
|||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--sslversion", dest="sslversion", type=int, default=4,
|
"--sslversion", dest="sslversion", type=int, default=4,
|
||||||
choices=[1, 2, 3, 4],
|
choices=[1, 2, 3, 4],
|
||||||
help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
|
help=""""Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default
|
||||||
|
to SSLv23."""
|
||||||
)
|
)
|
||||||
|
|
||||||
group = parser.add_argument_group(
|
group = parser.add_argument_group(
|
||||||
@ -392,10 +330,52 @@ def pathod():
|
|||||||
help="Log request/response in hexdump format"
|
help="Log request/response in hexdump format"
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.daemonize:
|
|
||||||
daemonize()
|
|
||||||
pathod_main(parser, args)
|
|
||||||
|
|
||||||
|
certs = []
|
||||||
|
for i in args.ssl_certs:
|
||||||
|
parts = i.split("=", 1)
|
||||||
|
if len(parts) == 1:
|
||||||
|
parts = ["*", parts[0]]
|
||||||
|
parts[1] = os.path.expanduser(parts[1])
|
||||||
|
if not os.path.exists(parts[1]):
|
||||||
|
parser.error("Certificate file does not exist: %s"%parts[1])
|
||||||
|
certs.append(parts)
|
||||||
|
args.ssl_certs = certs
|
||||||
|
|
||||||
if __name__ == "__main__":
|
alst = []
|
||||||
pathoc()
|
for i in args.anchors:
|
||||||
|
parts = utils.parse_anchor_spec(i)
|
||||||
|
if not parts:
|
||||||
|
parser.error("Invalid anchor specification: %s"%i)
|
||||||
|
alst.append(parts)
|
||||||
|
args.anchors = alst
|
||||||
|
|
||||||
|
sizelimit = None
|
||||||
|
if args.sizelimit:
|
||||||
|
try:
|
||||||
|
sizelimit = utils.parse_size(args.sizelimit)
|
||||||
|
except ValueError, v:
|
||||||
|
parser.error(v)
|
||||||
|
args.sizelimit = sizelimit
|
||||||
|
|
||||||
|
anchors = []
|
||||||
|
for patt, spec in args.anchors:
|
||||||
|
if os.path.exists(spec):
|
||||||
|
data = open(spec).read()
|
||||||
|
spec = data
|
||||||
|
|
||||||
|
try:
|
||||||
|
req = language.parse_response(spec)
|
||||||
|
except language.ParseException, v:
|
||||||
|
print >> sys.stderr, "Error parsing anchor spec: %s"%v.msg
|
||||||
|
print >> sys.stderr, v.marked()
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
arex = re.compile(patt)
|
||||||
|
except re.error:
|
||||||
|
print >> sys.stderr, "Invalid regex in anchor: %s" % patt
|
||||||
|
sys.exit(1)
|
||||||
|
anchors.append((arex, req))
|
||||||
|
args.anchors = anchors
|
||||||
|
|
||||||
|
pathod.main(args)
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,11 @@
|
|||||||
import operator, string, random, mmap, os, time, copy
|
import operator
|
||||||
|
import string
|
||||||
|
import random
|
||||||
|
import mmap
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import copy
|
||||||
import abc
|
import abc
|
||||||
from email.utils import formatdate
|
|
||||||
import contrib.pyparsing as pp
|
import contrib.pyparsing as pp
|
||||||
from netlib import http_status, tcp, http_uastrings
|
from netlib import http_status, tcp, http_uastrings
|
||||||
|
|
||||||
@ -9,7 +14,20 @@ import utils
|
|||||||
BLOCKSIZE = 1024
|
BLOCKSIZE = 1024
|
||||||
TRUNCATE = 1024
|
TRUNCATE = 1024
|
||||||
|
|
||||||
class FileAccessDenied(Exception): pass
|
|
||||||
|
def escape_backslash(s):
|
||||||
|
return s.replace("\\", "\\\\")
|
||||||
|
|
||||||
|
|
||||||
|
def quote(s):
|
||||||
|
quotechar = s[0]
|
||||||
|
s = s[1:-1]
|
||||||
|
s = s.replace(quotechar, "\\" + quotechar)
|
||||||
|
return quotechar + s + quotechar
|
||||||
|
|
||||||
|
|
||||||
|
class FileAccessDenied(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ParseException(Exception):
|
class ParseException(Exception):
|
||||||
@ -20,7 +38,7 @@ class ParseException(Exception):
|
|||||||
self.col = col
|
self.col = col
|
||||||
|
|
||||||
def marked(self):
|
def marked(self):
|
||||||
return "%s\n%s"%(self.s, " "*(self.col-1) + "^")
|
return "%s\n%s"%(self.s, " "*(self.col - 1) + "^")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s at char %s"%(self.msg, self.col)
|
return "%s at char %s"%(self.msg, self.col)
|
||||||
@ -40,7 +58,9 @@ def send_chunk(fp, val, blocksize, start, end):
|
|||||||
def write_values(fp, vals, actions, sofar=0, skip=0, blocksize=BLOCKSIZE):
|
def write_values(fp, vals, actions, sofar=0, skip=0, blocksize=BLOCKSIZE):
|
||||||
"""
|
"""
|
||||||
vals: A list of values, which may be strings or Value objects.
|
vals: A list of values, which may be strings or Value objects.
|
||||||
actions: A list of (offset, action, arg) tuples. Action may be "pause" or "disconnect".
|
|
||||||
|
actions: A list of (offset, action, arg) tuples. Action may be "pause"
|
||||||
|
or "disconnect".
|
||||||
|
|
||||||
Both vals and actions are in reverse order, with the first items last.
|
Both vals and actions are in reverse order, with the first items last.
|
||||||
|
|
||||||
@ -53,7 +73,13 @@ def write_values(fp, vals, actions, sofar=0, skip=0, blocksize=BLOCKSIZE):
|
|||||||
offset = 0
|
offset = 0
|
||||||
while actions and actions[-1][0] < (sofar + len(v)):
|
while actions and actions[-1][0] < (sofar + len(v)):
|
||||||
a = actions.pop()
|
a = actions.pop()
|
||||||
offset += send_chunk(fp, v, blocksize, offset, a[0]-sofar-offset)
|
offset += send_chunk(
|
||||||
|
fp,
|
||||||
|
v,
|
||||||
|
blocksize,
|
||||||
|
offset,
|
||||||
|
a[0]-sofar-offset
|
||||||
|
)
|
||||||
if a[1] == "pause":
|
if a[1] == "pause":
|
||||||
time.sleep(a[2])
|
time.sleep(a[2])
|
||||||
elif a[1] == "disconnect":
|
elif a[1] == "disconnect":
|
||||||
@ -121,15 +147,25 @@ DATATYPES = dict(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
v_integer = pp.Regex(r"\d+")\
|
v_integer = pp.Word(pp.nums)\
|
||||||
.setName("integer")\
|
.setName("integer")\
|
||||||
.setParseAction(lambda toks: int(toks[0]))
|
.setParseAction(lambda toks: int(toks[0]))
|
||||||
|
|
||||||
|
|
||||||
v_literal = pp.MatchFirst(
|
v_literal = pp.MatchFirst(
|
||||||
[
|
[
|
||||||
pp.QuotedString("\"", escChar="\\", unquoteResults=True, multiline=True),
|
pp.QuotedString(
|
||||||
pp.QuotedString("'", escChar="\\", unquoteResults=True, multiline=True),
|
"\"",
|
||||||
|
escChar="\\",
|
||||||
|
unquoteResults=True,
|
||||||
|
multiline=True
|
||||||
|
),
|
||||||
|
pp.QuotedString(
|
||||||
|
"'",
|
||||||
|
escChar="\\",
|
||||||
|
unquoteResults=True,
|
||||||
|
multiline=True
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,7 +191,7 @@ class LiteralGenerator:
|
|||||||
return self.s.__getslice__(a, b)
|
return self.s.__getslice__(a, b)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '"%s"'%self.s
|
return "'%s'"%self.s
|
||||||
|
|
||||||
|
|
||||||
class RandomGenerator:
|
class RandomGenerator:
|
||||||
@ -202,6 +238,7 @@ class _Token(object):
|
|||||||
A specification token. Tokens are immutable.
|
A specification token. Tokens are immutable.
|
||||||
"""
|
"""
|
||||||
__metaclass__ = abc.ABCMeta
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def expr(klass): # pragma: no cover
|
def expr(klass): # pragma: no cover
|
||||||
"""
|
"""
|
||||||
@ -242,10 +279,16 @@ class ValueLiteral(_ValueLiteral):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def expr(klass):
|
def expr(klass):
|
||||||
e = v_literal.copy()
|
e = v_literal.copy()
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
return e.setParseAction(klass.parseAction)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parseAction(klass, x):
|
||||||
|
v = klass(*x)
|
||||||
|
return v
|
||||||
|
|
||||||
def spec(self):
|
def spec(self):
|
||||||
return '"%s"'%self.val.encode("string_escape")
|
ret = "'%s'"%self.val.encode("string_escape")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class ValueNakedLiteral(_ValueLiteral):
|
class ValueNakedLiteral(_ValueLiteral):
|
||||||
@ -278,7 +321,10 @@ class ValueGenerate(_Token):
|
|||||||
def expr(klass):
|
def expr(klass):
|
||||||
e = pp.Literal("@").suppress() + v_integer
|
e = pp.Literal("@").suppress() + v_integer
|
||||||
|
|
||||||
u = reduce(operator.or_, [pp.Literal(i) for i in utils.SIZE_UNITS.keys()])
|
u = reduce(
|
||||||
|
operator.or_,
|
||||||
|
[pp.Literal(i) for i in utils.SIZE_UNITS.keys()]
|
||||||
|
).leaveWhitespace()
|
||||||
e = e + pp.Optional(u, default=None)
|
e = e + pp.Optional(u, default=None)
|
||||||
|
|
||||||
s = pp.Literal(",").suppress()
|
s = pp.Literal(",").suppress()
|
||||||
@ -318,13 +364,15 @@ class ValueFile(_Token):
|
|||||||
s = os.path.expanduser(self.path)
|
s = os.path.expanduser(self.path)
|
||||||
s = os.path.normpath(os.path.abspath(os.path.join(sd, s)))
|
s = os.path.normpath(os.path.abspath(os.path.join(sd, s)))
|
||||||
if not uf and not s.startswith(sd):
|
if not uf and not s.startswith(sd):
|
||||||
raise FileAccessDenied("File access outside of configured directory")
|
raise FileAccessDenied(
|
||||||
|
"File access outside of configured directory"
|
||||||
|
)
|
||||||
if not os.path.isfile(s):
|
if not os.path.isfile(s):
|
||||||
raise FileAccessDenied("File not readable")
|
raise FileAccessDenied("File not readable")
|
||||||
return FileGenerator(s)
|
return FileGenerator(s)
|
||||||
|
|
||||||
def spec(self):
|
def spec(self):
|
||||||
return '<"%s"'%self.path.encode("string_escape")
|
return "<'%s'"%self.path.encode("string_escape")
|
||||||
|
|
||||||
|
|
||||||
Value = pp.MatchFirst(
|
Value = pp.MatchFirst(
|
||||||
@ -347,12 +395,12 @@ NakedValue = pp.MatchFirst(
|
|||||||
|
|
||||||
|
|
||||||
Offset = pp.MatchFirst(
|
Offset = pp.MatchFirst(
|
||||||
[
|
[
|
||||||
v_integer,
|
v_integer,
|
||||||
pp.Literal("r"),
|
pp.Literal("r"),
|
||||||
pp.Literal("a")
|
pp.Literal("a")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Raw(_Token):
|
class Raw(_Token):
|
||||||
@ -392,11 +440,11 @@ class _Header(_Component):
|
|||||||
|
|
||||||
def values(self, settings):
|
def values(self, settings):
|
||||||
return [
|
return [
|
||||||
self.key.get_generator(settings),
|
self.key.get_generator(settings),
|
||||||
": ",
|
": ",
|
||||||
self.value.get_generator(settings),
|
self.value.get_generator(settings),
|
||||||
"\r\n",
|
"\r\n",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Header(_Header):
|
class Header(_Header):
|
||||||
@ -459,7 +507,10 @@ class ShortcutUserAgent(_Header):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def expr(klass):
|
def expr(klass):
|
||||||
e = pp.Literal("u").suppress()
|
e = pp.Literal("u").suppress()
|
||||||
u = reduce(operator.or_, [pp.Literal(i[1]) for i in http_uastrings.UASTRINGS])
|
u = reduce(
|
||||||
|
operator.or_,
|
||||||
|
[pp.Literal(i[1]) for i in http_uastrings.UASTRINGS]
|
||||||
|
)
|
||||||
e += u | Value
|
e += u | Value
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
return e.setParseAction(lambda x: klass(*x))
|
||||||
|
|
||||||
@ -470,7 +521,6 @@ class ShortcutUserAgent(_Header):
|
|||||||
return ShortcutUserAgent(self.value.freeze(settings))
|
return ShortcutUserAgent(self.value.freeze(settings))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Body(_Component):
|
class Body(_Component):
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
self.value = value
|
self.value = value
|
||||||
@ -483,8 +533,8 @@ class Body(_Component):
|
|||||||
|
|
||||||
def values(self, settings):
|
def values(self, settings):
|
||||||
return [
|
return [
|
||||||
self.value.get_generator(settings),
|
self.value.get_generator(settings),
|
||||||
]
|
]
|
||||||
|
|
||||||
def spec(self):
|
def spec(self):
|
||||||
return "b%s"%(self.value.spec())
|
return "b%s"%(self.value.spec())
|
||||||
@ -493,6 +543,38 @@ class Body(_Component):
|
|||||||
return Body(self.value.freeze(settings))
|
return Body(self.value.freeze(settings))
|
||||||
|
|
||||||
|
|
||||||
|
class PathodSpec(_Token):
|
||||||
|
def __init__(self, value):
|
||||||
|
self.value = value
|
||||||
|
try:
|
||||||
|
self.parsed = Response(
|
||||||
|
Response.expr().parseString(
|
||||||
|
value.val,
|
||||||
|
parseAll=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except pp.ParseException, v:
|
||||||
|
raise ParseException(v.msg, v.line, v.col)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def expr(klass):
|
||||||
|
e = pp.Literal("s").suppress()
|
||||||
|
e = e + ValueLiteral.expr()
|
||||||
|
return e.setParseAction(lambda x: klass(*x))
|
||||||
|
|
||||||
|
def values(self, settings):
|
||||||
|
return [
|
||||||
|
self.value.get_generator(settings),
|
||||||
|
]
|
||||||
|
|
||||||
|
def spec(self):
|
||||||
|
return "s%s"%(self.value.spec())
|
||||||
|
|
||||||
|
def freeze(self, settings):
|
||||||
|
f = self.parsed.freeze(settings).spec()
|
||||||
|
return PathodSpec(ValueLiteral(f.encode("string_escape")))
|
||||||
|
|
||||||
|
|
||||||
class Path(_Component):
|
class Path(_Component):
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
if isinstance(value, basestring):
|
if isinstance(value, basestring):
|
||||||
@ -506,8 +588,8 @@ class Path(_Component):
|
|||||||
|
|
||||||
def values(self, settings):
|
def values(self, settings):
|
||||||
return [
|
return [
|
||||||
self.value.get_generator(settings),
|
self.value.get_generator(settings),
|
||||||
]
|
]
|
||||||
|
|
||||||
def spec(self):
|
def spec(self):
|
||||||
return "%s"%(self.value.spec())
|
return "%s"%(self.value.spec())
|
||||||
@ -527,6 +609,7 @@ class Method(_Component):
|
|||||||
"trace",
|
"trace",
|
||||||
"connect",
|
"connect",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
# If it's a string, we were passed one of the methods, so we upper-case
|
# If it's a string, we were passed one of the methods, so we upper-case
|
||||||
# it to be canonical. The user can specify a different case by using a
|
# it to be canonical. The user can specify a different case by using a
|
||||||
@ -645,11 +728,11 @@ class PauseAt(_Action):
|
|||||||
e += Offset
|
e += Offset
|
||||||
e += pp.Literal(",").suppress()
|
e += pp.Literal(",").suppress()
|
||||||
e += pp.MatchFirst(
|
e += pp.MatchFirst(
|
||||||
[
|
[
|
||||||
v_integer,
|
v_integer,
|
||||||
pp.Literal("f")
|
pp.Literal("f")
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return e.setParseAction(lambda x: klass(*x))
|
return e.setParseAction(lambda x: klass(*x))
|
||||||
|
|
||||||
def spec(self):
|
def spec(self):
|
||||||
@ -700,10 +783,10 @@ class InjectAt(_Action):
|
|||||||
|
|
||||||
def intermediate(self, settings):
|
def intermediate(self, settings):
|
||||||
return (
|
return (
|
||||||
self.offset,
|
self.offset,
|
||||||
"inject",
|
"inject",
|
||||||
self.value.get_generator(settings)
|
self.value.get_generator(settings)
|
||||||
)
|
)
|
||||||
|
|
||||||
def freeze(self, settings):
|
def freeze(self, settings):
|
||||||
return InjectAt(self.offset, self.value.freeze(settings))
|
return InjectAt(self.offset, self.value.freeze(settings))
|
||||||
@ -712,6 +795,7 @@ class InjectAt(_Action):
|
|||||||
class _Message(object):
|
class _Message(object):
|
||||||
__metaclass__ = abc.ABCMeta
|
__metaclass__ = abc.ABCMeta
|
||||||
version = "HTTP/1.1"
|
version = "HTTP/1.1"
|
||||||
|
|
||||||
def __init__(self, tokens):
|
def __init__(self, tokens):
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
|
|
||||||
@ -741,7 +825,8 @@ class _Message(object):
|
|||||||
|
|
||||||
def length(self, settings):
|
def length(self, settings):
|
||||||
"""
|
"""
|
||||||
Calculate the length of the base message without any applied actions.
|
Calculate the length of the base message without any applied
|
||||||
|
actions.
|
||||||
"""
|
"""
|
||||||
return sum(len(x) for x in self.values(settings))
|
return sum(len(x) for x in self.values(settings))
|
||||||
|
|
||||||
@ -754,7 +839,8 @@ class _Message(object):
|
|||||||
|
|
||||||
def maximum_length(self, settings):
|
def maximum_length(self, settings):
|
||||||
"""
|
"""
|
||||||
Calculate the maximum length of the base message with all applied actions.
|
Calculate the maximum length of the base message with all applied
|
||||||
|
actions.
|
||||||
"""
|
"""
|
||||||
l = self.length(settings)
|
l = self.length(settings)
|
||||||
for i in self.actions:
|
for i in self.actions:
|
||||||
@ -781,14 +867,6 @@ class _Message(object):
|
|||||||
ValueLiteral(request_host)
|
ValueLiteral(request_host)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
if not utils.get_header("Date", self.headers):
|
|
||||||
tokens.append(
|
|
||||||
Header(
|
|
||||||
ValueLiteral("Date"),
|
|
||||||
ValueLiteral(formatdate(timeval=None, localtime=False, usegmt=True))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
intermediate = self.__class__(tokens)
|
intermediate = self.__class__(tokens)
|
||||||
return self.__class__([i.resolve(intermediate, settings) for i in tokens])
|
return self.__class__([i.resolve(intermediate, settings) for i in tokens])
|
||||||
|
|
||||||
@ -807,7 +885,8 @@ class _Message(object):
|
|||||||
ret = {}
|
ret = {}
|
||||||
for i in self.logattrs:
|
for i in self.logattrs:
|
||||||
v = getattr(self, i)
|
v = getattr(self, i)
|
||||||
# Careful not to log any VALUE specs without sanitizing them first. We truncate at 1k.
|
# Careful not to log any VALUE specs without sanitizing them first.
|
||||||
|
# We truncate at 1k.
|
||||||
if hasattr(v, "values"):
|
if hasattr(v, "values"):
|
||||||
v = [x[:TRUNCATE] for x in v.values(settings)]
|
v = [x[:TRUNCATE] for x in v.values(settings)]
|
||||||
v = "".join(v).encode("string_escape")
|
v = "".join(v).encode("string_escape")
|
||||||
@ -838,6 +917,7 @@ class _Message(object):
|
|||||||
|
|
||||||
Sep = pp.Optional(pp.Literal(":")).suppress()
|
Sep = pp.Optional(pp.Literal(":")).suppress()
|
||||||
|
|
||||||
|
|
||||||
class Response(_Message):
|
class Response(_Message):
|
||||||
comps = (
|
comps = (
|
||||||
Body,
|
Body,
|
||||||
@ -851,6 +931,7 @@ class Response(_Message):
|
|||||||
Reason
|
Reason
|
||||||
)
|
)
|
||||||
logattrs = ["code", "reason", "version", "body"]
|
logattrs = ["code", "reason", "version", "body"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code(self):
|
def code(self):
|
||||||
return self._get_token(Code)
|
return self._get_token(Code)
|
||||||
@ -866,7 +947,14 @@ class Response(_Message):
|
|||||||
if self.reason:
|
if self.reason:
|
||||||
l.extend(self.reason.values(settings))
|
l.extend(self.reason.values(settings))
|
||||||
else:
|
else:
|
||||||
l.append(LiteralGenerator(http_status.RESPONSES.get(int(self.code.code), "Unknown code")))
|
l.append(
|
||||||
|
LiteralGenerator(
|
||||||
|
http_status.RESPONSES.get(
|
||||||
|
int(self.code.code),
|
||||||
|
"Unknown code"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
return l
|
return l
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -894,9 +982,11 @@ class Request(_Message):
|
|||||||
InjectAt,
|
InjectAt,
|
||||||
ShortcutContentType,
|
ShortcutContentType,
|
||||||
ShortcutUserAgent,
|
ShortcutUserAgent,
|
||||||
Raw
|
Raw,
|
||||||
|
PathodSpec,
|
||||||
)
|
)
|
||||||
logattrs = ["method", "path", "body"]
|
logattrs = ["method", "path", "body"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def method(self):
|
def method(self):
|
||||||
return self._get_token(Method)
|
return self._get_token(Method)
|
||||||
@ -905,10 +995,16 @@ class Request(_Message):
|
|||||||
def path(self):
|
def path(self):
|
||||||
return self._get_token(Path)
|
return self._get_token(Path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pathodspec(self):
|
||||||
|
return self._get_token(PathodSpec)
|
||||||
|
|
||||||
def preamble(self, settings):
|
def preamble(self, settings):
|
||||||
v = self.method.values(settings)
|
v = self.method.values(settings)
|
||||||
v.append(" ")
|
v.append(" ")
|
||||||
v.extend(self.path.values(settings))
|
v.extend(self.path.values(settings))
|
||||||
|
if self.pathodspec:
|
||||||
|
v.append(self.pathodspec.parsed.spec())
|
||||||
v.append(" ")
|
v.append(" ")
|
||||||
v.append(self.version)
|
v.append(self.version)
|
||||||
return v
|
return v
|
||||||
@ -944,7 +1040,7 @@ def make_error_response(reason, body=None):
|
|||||||
]
|
]
|
||||||
return PathodErrorResponse(tokens)
|
return PathodErrorResponse(tokens)
|
||||||
|
|
||||||
FILESTART = "+"
|
|
||||||
def read_file(settings, s):
|
def read_file(settings, s):
|
||||||
uf = settings.get("unconstrained_file_access")
|
uf = settings.get("unconstrained_file_access")
|
||||||
sd = settings.get("staticdir")
|
sd = settings.get("staticdir")
|
||||||
@ -961,33 +1057,34 @@ def read_file(settings, s):
|
|||||||
return file(s, "rb").read()
|
return file(s, "rb").read()
|
||||||
|
|
||||||
|
|
||||||
def parse_response(settings, s):
|
def parse_response(s):
|
||||||
"""
|
"""
|
||||||
May raise ParseException or FileAccessDenied
|
May raise ParseException
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
s = s.decode("ascii")
|
s = s.decode("ascii")
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
raise ParseException("Spec must be valid ASCII.", 0, 0)
|
raise ParseException("Spec must be valid ASCII.", 0, 0)
|
||||||
if s.startswith(FILESTART):
|
|
||||||
s = read_file(settings, s)
|
|
||||||
try:
|
try:
|
||||||
return Response(Response.expr().parseString(s, parseAll=True))
|
return Response(Response.expr().parseString(s, parseAll=True))
|
||||||
except pp.ParseException, v:
|
except pp.ParseException, v:
|
||||||
raise ParseException(v.msg, v.line, v.col)
|
raise ParseException(v.msg, v.line, v.col)
|
||||||
|
|
||||||
|
|
||||||
def parse_request(settings, s):
|
def parse_requests(s):
|
||||||
"""
|
"""
|
||||||
May raise ParseException or FileAccessDenied
|
May raise ParseException
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
s = s.decode("ascii")
|
s = s.decode("ascii")
|
||||||
except UnicodeError:
|
except UnicodeError:
|
||||||
raise ParseException("Spec must be valid ASCII.", 0, 0)
|
raise ParseException("Spec must be valid ASCII.", 0, 0)
|
||||||
if s.startswith(FILESTART):
|
|
||||||
s = read_file(settings, s)
|
|
||||||
try:
|
try:
|
||||||
return Request(Request.expr().parseString(s, parseAll=True))
|
parts = pp.OneOrMore(
|
||||||
|
pp.Group(
|
||||||
|
Request.expr()
|
||||||
|
)
|
||||||
|
).parseString(s, parseAll=True)
|
||||||
|
return [Request(i) for i in parts]
|
||||||
except pp.ParseException, v:
|
except pp.ParseException, v:
|
||||||
raise ParseException(v.msg, v.line, v.col)
|
raise ParseException(v.msg, v.line, v.col)
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import sys, os
|
import sys
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import random
|
||||||
from netlib import tcp, http, certutils
|
from netlib import tcp, http, certutils
|
||||||
import netlib.utils
|
import netlib.utils
|
||||||
import language, utils
|
|
||||||
|
import language
|
||||||
|
import utils
|
||||||
import OpenSSL.crypto
|
import OpenSSL.crypto
|
||||||
|
|
||||||
class PathocError(Exception): pass
|
|
||||||
|
class PathocError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SSLInfo:
|
class SSLInfo:
|
||||||
@ -13,8 +20,17 @@ class SSLInfo:
|
|||||||
|
|
||||||
|
|
||||||
class Response:
|
class Response:
|
||||||
def __init__(self, httpversion, status_code, msg, headers, content, sslinfo):
|
def __init__(
|
||||||
self.httpversion, self.status_code, self.msg = httpversion, status_code, msg
|
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.headers, self.content = headers, content
|
||||||
self.sslinfo = sslinfo
|
self.sslinfo = sslinfo
|
||||||
|
|
||||||
@ -23,7 +39,14 @@ class Response:
|
|||||||
|
|
||||||
|
|
||||||
class Pathoc(tcp.TCPClient):
|
class Pathoc(tcp.TCPClient):
|
||||||
def __init__(self, address, ssl=None, sni=None, sslversion=4, clientcert=None, ciphers=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
address,
|
||||||
|
ssl=None,
|
||||||
|
sni=None,
|
||||||
|
sslversion=4,
|
||||||
|
clientcert=None,
|
||||||
|
ciphers=None):
|
||||||
tcp.TCPClient.__init__(self, address)
|
tcp.TCPClient.__init__(self, address)
|
||||||
self.settings = dict(
|
self.settings = dict(
|
||||||
staticdir = os.getcwd(),
|
staticdir = os.getcwd(),
|
||||||
@ -36,9 +59,9 @@ class Pathoc(tcp.TCPClient):
|
|||||||
|
|
||||||
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) +
|
||||||
'\r\n'
|
'\r\n'
|
||||||
)
|
)
|
||||||
self.wfile.flush()
|
self.wfile.flush()
|
||||||
l = self.rfile.readline()
|
l = self.rfile.readline()
|
||||||
if not l:
|
if not l:
|
||||||
@ -60,17 +83,17 @@ class Pathoc(tcp.TCPClient):
|
|||||||
if self.ssl:
|
if self.ssl:
|
||||||
try:
|
try:
|
||||||
self.convert_to_ssl(
|
self.convert_to_ssl(
|
||||||
sni=self.sni,
|
sni=self.sni,
|
||||||
cert=self.clientcert,
|
cert=self.clientcert,
|
||||||
method=self.sslversion,
|
method=self.sslversion,
|
||||||
cipher_list = self.ciphers
|
cipher_list = self.ciphers
|
||||||
)
|
)
|
||||||
except tcp.NetLibError, v:
|
except tcp.NetLibError, v:
|
||||||
raise PathocError(str(v))
|
raise PathocError(str(v))
|
||||||
self.sslinfo = SSLInfo(
|
self.sslinfo = SSLInfo(
|
||||||
self.connection.get_peer_cert_chain(),
|
self.connection.get_peer_cert_chain(),
|
||||||
self.get_current_cipher()
|
self.get_current_cipher()
|
||||||
)
|
)
|
||||||
|
|
||||||
def request(self, spec):
|
def request(self, spec):
|
||||||
"""
|
"""
|
||||||
@ -79,7 +102,7 @@ class Pathoc(tcp.TCPClient):
|
|||||||
May raise language.ParseException, netlib.http.HttpError or
|
May raise language.ParseException, netlib.http.HttpError or
|
||||||
language.FileAccessDenied.
|
language.FileAccessDenied.
|
||||||
"""
|
"""
|
||||||
r = language.parse_request(self.settings, spec)
|
r = language.parse_requests(spec)[0]
|
||||||
language.serve(r, self.wfile, self.settings, self.address.host)
|
language.serve(r, self.wfile, self.settings, self.address.host)
|
||||||
self.wfile.flush()
|
self.wfile.flush()
|
||||||
ret = list(http.read_response(self.rfile, r.method.string(), None))
|
ret = list(http.read_response(self.rfile, r.method.string(), None))
|
||||||
@ -87,7 +110,9 @@ class Pathoc(tcp.TCPClient):
|
|||||||
return Response(*ret)
|
return Response(*ret)
|
||||||
|
|
||||||
def _show_summary(self, fp, httpversion, code, msg, headers, content):
|
def _show_summary(self, fp, httpversion, code, msg, headers, content):
|
||||||
print >> fp, "<< %s %s: %s bytes"%(code, utils.xrepr(msg), len(content))
|
print >> fp, "<< %s %s: %s bytes"%(
|
||||||
|
code, utils.xrepr(msg), len(content)
|
||||||
|
)
|
||||||
|
|
||||||
def _show(self, fp, header, data, hexdump):
|
def _show(self, fp, header, data, hexdump):
|
||||||
if hexdump:
|
if hexdump:
|
||||||
@ -98,7 +123,18 @@ 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_request(self, spec, showreq, showresp, explain, showssl, hexdump, ignorecodes, ignoretimeout, fp=sys.stdout):
|
def print_request(
|
||||||
|
self,
|
||||||
|
r,
|
||||||
|
showreq,
|
||||||
|
showresp,
|
||||||
|
explain,
|
||||||
|
showssl,
|
||||||
|
hexdump,
|
||||||
|
ignorecodes,
|
||||||
|
ignoretimeout,
|
||||||
|
fp=sys.stdout
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Performs a series of requests, and prints results to the specified
|
Performs a series of requests, and prints results to the specified
|
||||||
file descriptor.
|
file descriptor.
|
||||||
@ -113,26 +149,18 @@ class Pathoc(tcp.TCPClient):
|
|||||||
|
|
||||||
Returns True if we have a non-ignored response.
|
Returns True if we have a non-ignored response.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
r = language.parse_request(self.settings, spec)
|
|
||||||
except language.ParseException, v:
|
|
||||||
print >> fp, "Error parsing request spec: %s"%v.msg
|
|
||||||
print >> fp, v.marked()
|
|
||||||
return
|
|
||||||
except language.FileAccessDenied, v:
|
|
||||||
print >> fp, "File access error: %s"%v
|
|
||||||
return
|
|
||||||
|
|
||||||
if explain:
|
|
||||||
r = r.freeze(self.settings, self.address.host)
|
|
||||||
|
|
||||||
resp, req = None, None
|
resp, req = None, None
|
||||||
if showreq:
|
if showreq:
|
||||||
self.wfile.start_log()
|
self.wfile.start_log()
|
||||||
if showresp:
|
if showresp:
|
||||||
self.rfile.start_log()
|
self.rfile.start_log()
|
||||||
try:
|
try:
|
||||||
req = language.serve(r, self.wfile, self.settings, self.address.host)
|
req = language.serve(
|
||||||
|
r,
|
||||||
|
self.wfile,
|
||||||
|
self.settings,
|
||||||
|
self.address.host
|
||||||
|
)
|
||||||
self.wfile.flush()
|
self.wfile.flush()
|
||||||
resp = http.read_response(self.rfile, r.method.string(), None)
|
resp = http.read_response(self.rfile, r.method.string(), None)
|
||||||
except http.HttpError, v:
|
except http.HttpError, v:
|
||||||
@ -160,7 +188,7 @@ class Pathoc(tcp.TCPClient):
|
|||||||
if resp:
|
if resp:
|
||||||
self._show_summary(fp, *resp)
|
self._show_summary(fp, *resp)
|
||||||
|
|
||||||
if self.sslinfo:
|
if showssl and self.sslinfo:
|
||||||
print >> fp, "Cipher: %s, %s bit, %s"%self.sslinfo.cipher
|
print >> fp, "Cipher: %s, %s bit, %s"%self.sslinfo.cipher
|
||||||
print >> fp, "SSL certificate chain:\n"
|
print >> fp, "SSL certificate chain:\n"
|
||||||
for i in self.sslinfo.certchain:
|
for i in self.sslinfo.certchain:
|
||||||
@ -173,13 +201,15 @@ class Pathoc(tcp.TCPClient):
|
|||||||
print >> fp, "%s=%s"%cn,
|
print >> fp, "%s=%s"%cn,
|
||||||
print >> fp
|
print >> fp
|
||||||
print >> fp, "\tVersion: %s"%i.get_version()
|
print >> fp, "\tVersion: %s"%i.get_version()
|
||||||
print >> fp, "\tValidity: %s - %s"%(i.get_notBefore(),i.get_notAfter())
|
print >> fp, "\tValidity: %s - %s"%(
|
||||||
|
i.get_notBefore(), i.get_notAfter()
|
||||||
|
)
|
||||||
print >> fp, "\tSerial: %s"%i.get_serial_number()
|
print >> fp, "\tSerial: %s"%i.get_serial_number()
|
||||||
print >> fp, "\tAlgorithm: %s"%i.get_signature_algorithm()
|
print >> fp, "\tAlgorithm: %s"%i.get_signature_algorithm()
|
||||||
pk = i.get_pubkey()
|
pk = i.get_pubkey()
|
||||||
types = {
|
types = {
|
||||||
OpenSSL.crypto.TYPE_RSA: "RSA",
|
OpenSSL.crypto.TYPE_RSA: "RSA",
|
||||||
OpenSSL.crypto.TYPE_DSA: "DSA"
|
OpenSSL.crypto.TYPE_DSA: "DSA"
|
||||||
}
|
}
|
||||||
t = types.get(pk.type(), "Uknown")
|
t = types.get(pk.type(), "Uknown")
|
||||||
print >> fp, "\tPubkey: %s bit %s"%(pk.bits(), t)
|
print >> fp, "\tPubkey: %s bit %s"%(pk.bits(), t)
|
||||||
@ -190,4 +220,68 @@ class Pathoc(tcp.TCPClient):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
memo = set([])
|
||||||
|
trycount = 0
|
||||||
|
try:
|
||||||
|
cnt = 0
|
||||||
|
while 1:
|
||||||
|
if trycount > args.memolimit:
|
||||||
|
print >> sys.stderr, "Memo limit exceeded..."
|
||||||
|
return
|
||||||
|
|
||||||
|
cnt += 1
|
||||||
|
if args.random:
|
||||||
|
playlist = [random.choice(args.requests)]
|
||||||
|
else:
|
||||||
|
playlist = args.requests
|
||||||
|
p = Pathoc(
|
||||||
|
(args.host, args.port),
|
||||||
|
ssl=args.ssl,
|
||||||
|
sni=args.sni,
|
||||||
|
sslversion=args.sslversion,
|
||||||
|
clientcert=args.clientcert,
|
||||||
|
ciphers=args.ciphers
|
||||||
|
)
|
||||||
|
if args.explain or args.memo:
|
||||||
|
playlist = [
|
||||||
|
i.freeze(p.settings, p.address.host) for i in playlist
|
||||||
|
]
|
||||||
|
if args.memo:
|
||||||
|
newlist = []
|
||||||
|
for spec in playlist:
|
||||||
|
h = hashlib.sha256(spec.spec()).digest()
|
||||||
|
if h not in memo:
|
||||||
|
memo.add(h)
|
||||||
|
newlist.append(spec)
|
||||||
|
playlist = newlist
|
||||||
|
if not playlist:
|
||||||
|
trycount += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
trycount = 0
|
||||||
|
try:
|
||||||
|
p.connect(args.connect_to)
|
||||||
|
except (tcp.NetLibError, PathocError), v:
|
||||||
|
print >> sys.stderr, str(v)
|
||||||
|
sys.exit(1)
|
||||||
|
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,
|
||||||
|
showssl=args.showssl,
|
||||||
|
hexdump=args.hexdump,
|
||||||
|
ignorecodes=args.ignorecodes,
|
||||||
|
ignoretimeout=args.ignoretimeout
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
if ret and args.oneshot:
|
||||||
|
sys.exit(0)
|
||||||
|
if cnt == args.repeat:
|
||||||
|
break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import urllib, threading, re, logging, os
|
import urllib
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
from netlib import tcp, http, wsgi, certutils
|
from netlib import tcp, http, wsgi, certutils
|
||||||
import netlib.utils
|
import netlib.utils
|
||||||
import version, app, language, utils
|
|
||||||
|
from . import version, app, language, utils
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CERT_DOMAIN = "pathod.net"
|
DEFAULT_CERT_DOMAIN = "pathod.net"
|
||||||
@ -12,7 +17,8 @@ CA_CERT_NAME = "mitmproxy-ca.pem"
|
|||||||
logger = logging.getLogger('pathod')
|
logger = logging.getLogger('pathod')
|
||||||
|
|
||||||
|
|
||||||
class PathodError(Exception): pass
|
class PathodError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SSLOptions:
|
class SSLOptions:
|
||||||
@ -64,7 +70,12 @@ class PathodHandler(tcp.BaseHandler):
|
|||||||
if self.server.explain and not isinstance(crafted, language.PathodErrorResponse):
|
if self.server.explain and not isinstance(crafted, language.PathodErrorResponse):
|
||||||
crafted = crafted.freeze(self.server.request_settings, None)
|
crafted = crafted.freeze(self.server.request_settings, None)
|
||||||
self.info(">> Spec: %s" % crafted.spec())
|
self.info(">> Spec: %s" % crafted.spec())
|
||||||
response_log = language.serve(crafted, self.wfile, self.server.request_settings, None)
|
response_log = language.serve(
|
||||||
|
crafted,
|
||||||
|
self.wfile,
|
||||||
|
self.server.request_settings,
|
||||||
|
None
|
||||||
|
)
|
||||||
if response_log["disconnect"]:
|
if response_log["disconnect"]:
|
||||||
return False, response_log
|
return False, response_log
|
||||||
return True, response_log
|
return True, response_log
|
||||||
@ -162,15 +173,14 @@ class PathodHandler(tcp.BaseHandler):
|
|||||||
for i in self.server.anchors:
|
for i in self.server.anchors:
|
||||||
if i[0].match(path):
|
if i[0].match(path):
|
||||||
self.info("crafting anchor: %s" % path)
|
self.info("crafting anchor: %s" % path)
|
||||||
aresp = language.parse_response(self.server.request_settings, i[1])
|
again, retlog["response"] = self.serve_crafted(i[1])
|
||||||
again, retlog["response"] = self.serve_crafted(aresp)
|
|
||||||
return again, retlog
|
return again, retlog
|
||||||
|
|
||||||
if not self.server.nocraft and path.startswith(self.server.craftanchor):
|
if not self.server.nocraft and path.startswith(self.server.craftanchor):
|
||||||
spec = urllib.unquote(path)[len(self.server.craftanchor):]
|
spec = urllib.unquote(path)[len(self.server.craftanchor):]
|
||||||
self.info("crafting spec: %s" % spec)
|
self.info("crafting spec: %s" % spec)
|
||||||
try:
|
try:
|
||||||
crafted = language.parse_response(self.server.request_settings, spec)
|
crafted = language.parse_response(spec)
|
||||||
except language.ParseException, v:
|
except language.ParseException, v:
|
||||||
self.info("Parse error: %s" % v.msg)
|
self.info("Parse error: %s" % v.msg)
|
||||||
crafted = language.make_error_response(
|
crafted = language.make_error_response(
|
||||||
@ -182,7 +192,10 @@ class PathodHandler(tcp.BaseHandler):
|
|||||||
elif self.server.noweb:
|
elif self.server.noweb:
|
||||||
crafted = language.make_error_response("Access Denied")
|
crafted = language.make_error_response("Access Denied")
|
||||||
language.serve(crafted, self.wfile, self.server.request_settings)
|
language.serve(crafted, self.wfile, self.server.request_settings)
|
||||||
return False, dict(type="error", msg="Access denied: web interface disabled")
|
return False, dict(
|
||||||
|
type="error",
|
||||||
|
msg="Access denied: web interface disabled"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.info("app: %s %s" % (method, path))
|
self.info("app: %s %s" % (method, path))
|
||||||
req = wsgi.Request("http", method, path, headers, content)
|
req = wsgi.Request("http", method, path, headers, content)
|
||||||
@ -252,19 +265,34 @@ class Pathod(tcp.TCPServer):
|
|||||||
LOGBUF = 500
|
LOGBUF = 500
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, addr, confdir=CONFDIR, ssl=False, ssloptions=None,
|
self,
|
||||||
craftanchor="/p/", staticdir=None, anchors=None,
|
addr,
|
||||||
sizelimit=None, noweb=False, nocraft=False, noapi=False,
|
confdir=CONFDIR,
|
||||||
nohang=False, timeout=None, logreq=False, logresp=False,
|
ssl=False,
|
||||||
explain=False, hexdump=False
|
ssloptions=None,
|
||||||
|
craftanchor="/p/",
|
||||||
|
staticdir=None,
|
||||||
|
anchors=(),
|
||||||
|
sizelimit=None,
|
||||||
|
noweb=False,
|
||||||
|
nocraft=False,
|
||||||
|
noapi=False,
|
||||||
|
nohang=False,
|
||||||
|
timeout=None,
|
||||||
|
logreq=False,
|
||||||
|
logresp=False,
|
||||||
|
explain=False,
|
||||||
|
hexdump=False
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
addr: (address, port) tuple. If port is 0, a free port will be
|
addr: (address, port) tuple. If port is 0, a free port will be
|
||||||
automatically chosen.
|
automatically chosen.
|
||||||
ssloptions: an SSLOptions object.
|
ssloptions: an SSLOptions object.
|
||||||
craftanchor: string specifying the path under which to anchor response generation.
|
craftanchor: string specifying the path under which to anchor
|
||||||
|
response generation.
|
||||||
staticdir: path to a directory of static resources, or None.
|
staticdir: path to a directory of static resources, or None.
|
||||||
anchors: A list of (regex, spec) tuples, or None.
|
anchors: List of (regex object, language.Request object) tuples, or
|
||||||
|
None.
|
||||||
sizelimit: Limit size of served data.
|
sizelimit: Limit size of served data.
|
||||||
nocraft: Disable response crafting.
|
nocraft: Disable response crafting.
|
||||||
noapi: Disable the API.
|
noapi: Disable the API.
|
||||||
@ -276,26 +304,17 @@ class Pathod(tcp.TCPServer):
|
|||||||
self.staticdir = staticdir
|
self.staticdir = staticdir
|
||||||
self.craftanchor = craftanchor
|
self.craftanchor = craftanchor
|
||||||
self.sizelimit = sizelimit
|
self.sizelimit = sizelimit
|
||||||
self.noweb, self.nocraft, self.noapi, self.nohang = noweb, nocraft, noapi, nohang
|
self.noweb, self.nocraft = noweb, nocraft
|
||||||
self.timeout, self.logreq, self.logresp, self.hexdump = timeout, logreq, logresp, hexdump
|
self.noapi, self.nohang = noapi, nohang
|
||||||
|
self.timeout, self.logreq = timeout, logreq
|
||||||
|
self.logresp, self.hexdump = logresp, hexdump
|
||||||
self.explain = explain
|
self.explain = explain
|
||||||
|
|
||||||
self.app = app.make_app(noapi)
|
self.app = app.make_app(noapi)
|
||||||
self.app.config["pathod"] = self
|
self.app.config["pathod"] = self
|
||||||
self.log = []
|
self.log = []
|
||||||
self.logid = 0
|
self.logid = 0
|
||||||
self.anchors = []
|
self.anchors = anchors
|
||||||
if anchors:
|
|
||||||
for i in anchors:
|
|
||||||
try:
|
|
||||||
arex = re.compile(i[0])
|
|
||||||
except re.error:
|
|
||||||
raise PathodError("Invalid regex in anchor: %s" % i[0])
|
|
||||||
try:
|
|
||||||
language.parse_response(self.request_settings, i[1])
|
|
||||||
except language.ParseException, v:
|
|
||||||
raise PathodError("Invalid page spec in anchor: '%s', %s" % (i[1], str(v)))
|
|
||||||
self.anchors.append((arex, i[1]))
|
|
||||||
|
|
||||||
def check_policy(self, req, settings):
|
def check_policy(self, req, settings):
|
||||||
"""
|
"""
|
||||||
@ -363,3 +382,72 @@ class Pathod(tcp.TCPServer):
|
|||||||
|
|
||||||
def get_log(self):
|
def get_log(self):
|
||||||
return self.log
|
return self.log
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
ssloptions = SSLOptions(
|
||||||
|
cn = args.cn,
|
||||||
|
confdir = args.confdir,
|
||||||
|
not_after_connect = args.ssl_not_after_connect,
|
||||||
|
ciphers = args.ciphers,
|
||||||
|
sslversion = utils.SSLVERSIONS[args.sslversion],
|
||||||
|
certs = args.ssl_certs
|
||||||
|
)
|
||||||
|
|
||||||
|
root = logging.getLogger()
|
||||||
|
if root.handlers:
|
||||||
|
for handler in root.handlers:
|
||||||
|
root.removeHandler(handler)
|
||||||
|
|
||||||
|
log = logging.getLogger('pathod')
|
||||||
|
log.setLevel(logging.DEBUG)
|
||||||
|
fmt = logging.Formatter(
|
||||||
|
'%(asctime)s: %(message)s',
|
||||||
|
datefmt='%d-%m-%y %H:%M:%S',
|
||||||
|
)
|
||||||
|
if args.logfile:
|
||||||
|
fh = logging.handlers.WatchedFileHandler(args.logfile)
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
log.addHandler(fh)
|
||||||
|
if not args.daemonize:
|
||||||
|
sh = logging.StreamHandler()
|
||||||
|
sh.setFormatter(fmt)
|
||||||
|
log.addHandler(sh)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pd = Pathod(
|
||||||
|
(args.address, args.port),
|
||||||
|
craftanchor = args.craftanchor,
|
||||||
|
ssl = args.ssl,
|
||||||
|
ssloptions = ssloptions,
|
||||||
|
staticdir = args.staticdir,
|
||||||
|
anchors = args.anchors,
|
||||||
|
sizelimit = args.sizelimit,
|
||||||
|
noweb = args.noweb,
|
||||||
|
nocraft = args.nocraft,
|
||||||
|
noapi = args.noapi,
|
||||||
|
nohang = args.nohang,
|
||||||
|
timeout = args.timeout,
|
||||||
|
logreq = args.logreq,
|
||||||
|
logresp = args.logresp,
|
||||||
|
hexdump = args.hexdump,
|
||||||
|
explain = args.explain,
|
||||||
|
)
|
||||||
|
except PathodError, v:
|
||||||
|
print >> sys.stderr, "Error: %s"%v
|
||||||
|
sys.exit(1)
|
||||||
|
except language.FileAccessDenied, v:
|
||||||
|
print >> sys.stderr, "Error: %s"%v
|
||||||
|
|
||||||
|
if args.daemonize:
|
||||||
|
utils.daemonize()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print "%s listening on %s:%s"%(
|
||||||
|
version.NAMEVERSION,
|
||||||
|
pd.address.host,
|
||||||
|
pd.address.port
|
||||||
|
)
|
||||||
|
pd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
@ -183,14 +183,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<section id="specifying_requests">
|
|
||||||
<div class="page-header">
|
|
||||||
<h1>Executing specs from file</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre class="example">+./path/to/spec</pre>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="specifying_requests">
|
<section id="specifying_requests">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import threading, Queue
|
import threading
|
||||||
|
import Queue
|
||||||
import requests
|
import requests
|
||||||
import requests.packages.urllib3
|
import requests.packages.urllib3
|
||||||
import pathod
|
import pathod
|
||||||
@ -6,7 +7,6 @@ import pathod
|
|||||||
requests.packages.urllib3.disable_warnings()
|
requests.packages.urllib3.disable_warnings()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Daemon:
|
class Daemon:
|
||||||
IFACE = "127.0.0.1"
|
IFACE = "127.0.0.1"
|
||||||
def __init__(self, ssl=None, **daemonargs):
|
def __init__(self, ssl=None, **daemonargs):
|
||||||
@ -14,7 +14,11 @@ class Daemon:
|
|||||||
self.thread = _PaThread(self.IFACE, self.q, ssl, daemonargs)
|
self.thread = _PaThread(self.IFACE, self.q, ssl, daemonargs)
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
self.port = self.q.get(True, 5)
|
self.port = self.q.get(True, 5)
|
||||||
self.urlbase = "%s://%s:%s"%("https" if ssl else "http", self.IFACE, self.port)
|
self.urlbase = "%s://%s:%s"%(
|
||||||
|
"https" if ssl else "http",
|
||||||
|
self.IFACE,
|
||||||
|
self.port
|
||||||
|
)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@ -80,6 +84,9 @@ class _PaThread(threading.Thread):
|
|||||||
ssl = self.ssl,
|
ssl = self.ssl,
|
||||||
**self.daemonargs
|
**self.daemonargs
|
||||||
)
|
)
|
||||||
self.name = "PathodThread (%s:%s)" % (self.server.address.host, self.server.address.port)
|
self.name = "PathodThread (%s:%s)" % (
|
||||||
|
self.server.address.host,
|
||||||
|
self.server.address.port
|
||||||
|
)
|
||||||
self.q.put(self.server.address.port)
|
self.q.put(self.server.address.port)
|
||||||
self.server.serve_forever()
|
self.server.serve_forever()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from netlib import tcp
|
from netlib import tcp
|
||||||
|
|
||||||
SSLVERSIONS = {
|
SSLVERSIONS = {
|
||||||
@ -110,3 +111,30 @@ class Data:
|
|||||||
|
|
||||||
|
|
||||||
data = Data(__name__)
|
data = Data(__name__)
|
||||||
|
|
||||||
|
def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
|
||||||
|
try:
|
||||||
|
pid = os.fork()
|
||||||
|
if pid > 0:
|
||||||
|
sys.exit(0)
|
||||||
|
except OSError, e:
|
||||||
|
sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||||
|
sys.exit(1)
|
||||||
|
os.chdir("/")
|
||||||
|
os.umask(0)
|
||||||
|
os.setsid()
|
||||||
|
try:
|
||||||
|
pid = os.fork()
|
||||||
|
if pid > 0:
|
||||||
|
sys.exit(0)
|
||||||
|
except OSError, e:
|
||||||
|
sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
|
||||||
|
sys.exit(1)
|
||||||
|
si = open(stdin, 'rb')
|
||||||
|
so = open(stdout, 'a+b')
|
||||||
|
se = open(stderr, 'a+b', 0)
|
||||||
|
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||||
|
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||||
|
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||||
|
|
||||||
|
|
||||||
|
5
pathoc
Executable file
5
pathoc
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from libpathod import cmdline
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmdline.go_pathoc()
|
5
pathod
Executable file
5
pathod
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from libpathod import cmdline
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cmdline.go_pathod()
|
12
setup.py
12
setup.py
@ -35,16 +35,12 @@ setup(
|
|||||||
|
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
scripts = ["pathod", "pathoc"],
|
||||||
entry_points={
|
|
||||||
'console_scripts': [
|
|
||||||
"pathod = libpathod.main:pathod",
|
|
||||||
"pathoc = libpathod.main:pathoc"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"netlib>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION),
|
"netlib>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION),
|
||||||
|
# It's INSANE that we have to do this, but...
|
||||||
|
# FIXME: Requirement to be removed at next release
|
||||||
|
"pip>=1.5.6",
|
||||||
"requests>=2.4.1",
|
"requests>=2.4.1",
|
||||||
"Flask>=0.10.1"
|
"Flask>=0.10.1"
|
||||||
],
|
],
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import tutils
|
import tutils
|
||||||
|
|
||||||
|
|
||||||
class TestApp(tutils.DaemonTests):
|
class TestApp(tutils.DaemonTests):
|
||||||
SSL = False
|
SSL = False
|
||||||
|
|
||||||
def test_index(self):
|
def test_index(self):
|
||||||
r = self.getpath("/")
|
r = self.getpath("/")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
@ -78,4 +80,3 @@ class TestApp(tutils.DaemonTests):
|
|||||||
r = self.getpath("/request_preview", params=dict(spec=""))
|
r = self.getpath("/request_preview", params=dict(spec=""))
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert 'empty spec' in r.content
|
assert 'empty spec' in r.content
|
||||||
|
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
import os, cStringIO
|
import os
|
||||||
|
import cStringIO
|
||||||
from libpathod import language, utils
|
from libpathod import language, utils
|
||||||
import tutils
|
import tutils
|
||||||
|
|
||||||
language.TESTING = True
|
language.TESTING = True
|
||||||
|
|
||||||
|
|
||||||
|
def test_quote():
|
||||||
|
assert language.quote("'\\\\'")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_request(s):
|
||||||
|
return language.parse_requests(s)[0]
|
||||||
|
|
||||||
|
|
||||||
class TestValueNakedLiteral:
|
class TestValueNakedLiteral:
|
||||||
def test_expr(self):
|
def test_expr(self):
|
||||||
v = language.ValueNakedLiteral("foo")
|
v = language.ValueNakedLiteral("foo")
|
||||||
@ -24,21 +33,35 @@ class TestValueLiteral:
|
|||||||
assert v.expr()
|
assert v.expr()
|
||||||
assert v.val == "foo"
|
assert v.val == "foo"
|
||||||
|
|
||||||
v = language.ValueLiteral(r"foo\n")
|
v = language.ValueLiteral("foo\n")
|
||||||
assert v.expr()
|
assert v.expr()
|
||||||
assert v.val == "foo\n"
|
assert v.val == "foo\n"
|
||||||
assert repr(v)
|
assert repr(v)
|
||||||
|
|
||||||
def test_spec(self):
|
def test_spec(self):
|
||||||
v = language.ValueLiteral("foo")
|
v = language.ValueLiteral("foo")
|
||||||
assert v.spec() == r'"foo"'
|
assert v.spec() == r"'foo'"
|
||||||
|
|
||||||
v = language.ValueLiteral("f\x00oo")
|
v = language.ValueLiteral("f\x00oo")
|
||||||
assert v.spec() == repr(v) == r'"f\x00oo"'
|
assert v.spec() == repr(v) == r"'f\x00oo'"
|
||||||
|
|
||||||
def test_freeze(self):
|
v = language.ValueLiteral("\"")
|
||||||
v = language.ValueLiteral("foo")
|
assert v.spec() == repr(v) == '\'"\''
|
||||||
assert v.freeze({}).val == v.val
|
|
||||||
|
def roundtrip(self, spec):
|
||||||
|
e = language.ValueLiteral.expr()
|
||||||
|
v = language.ValueLiteral(spec)
|
||||||
|
v2 = e.parseString(v.spec())
|
||||||
|
assert v.val == v2[0].val
|
||||||
|
assert v.spec() == v2[0].spec()
|
||||||
|
|
||||||
|
def test_roundtrip(self):
|
||||||
|
self.roundtrip("'")
|
||||||
|
self.roundtrip('\'')
|
||||||
|
self.roundtrip("a")
|
||||||
|
self.roundtrip("\"")
|
||||||
|
self.roundtrip(r"\\")
|
||||||
|
self.roundtrip("200:b'foo':i23,'\\''")
|
||||||
|
|
||||||
|
|
||||||
class TestValueGenerate:
|
class TestValueGenerate:
|
||||||
@ -181,7 +204,7 @@ class TestMisc:
|
|||||||
assert e.parseString("'get'")[0].value.val == "get"
|
assert e.parseString("'get'")[0].value.val == "get"
|
||||||
|
|
||||||
assert e.parseString("get")[0].spec() == "get"
|
assert e.parseString("get")[0].spec() == "get"
|
||||||
assert e.parseString("'foo'")[0].spec() == '"foo"'
|
assert e.parseString("'foo'")[0].spec() == "'foo'"
|
||||||
|
|
||||||
s = e.parseString("get")[0].spec()
|
s = e.parseString("get")[0].spec()
|
||||||
assert s == e.parseString(s)[0].spec()
|
assert s == e.parseString(s)[0].spec()
|
||||||
@ -218,6 +241,31 @@ class TestMisc:
|
|||||||
s = v.spec()
|
s = v.spec()
|
||||||
assert s == e.parseString(s)[0].spec()
|
assert s == e.parseString(s)[0].spec()
|
||||||
|
|
||||||
|
def test_pathodspec(self):
|
||||||
|
e = language.PathodSpec.expr()
|
||||||
|
v = e.parseString("s'200'")[0]
|
||||||
|
assert v.value.val == "200"
|
||||||
|
tutils.raises(
|
||||||
|
language.ParseException,
|
||||||
|
e.parseString,
|
||||||
|
"s'foo'"
|
||||||
|
)
|
||||||
|
|
||||||
|
v = e.parseString('s"200:b@1"')[0]
|
||||||
|
assert "@1" in v.spec()
|
||||||
|
f = v.freeze({})
|
||||||
|
assert "@1" not in f.spec()
|
||||||
|
|
||||||
|
def test_pathodspec_freeze(self):
|
||||||
|
e = language.PathodSpec(
|
||||||
|
language.ValueLiteral(
|
||||||
|
"200:b'foo':i10,'\\''".encode(
|
||||||
|
"string_escape"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert e.freeze({})
|
||||||
|
|
||||||
def test_code(self):
|
def test_code(self):
|
||||||
e = language.Code.expr()
|
e = language.Code.expr()
|
||||||
v = e.parseString("200")[0]
|
v = e.parseString("200")[0]
|
||||||
@ -298,11 +346,11 @@ class TestHeaders:
|
|||||||
assert v2.value.val == v3.value.val
|
assert v2.value.val == v3.value.val
|
||||||
|
|
||||||
def test_shortcuts(self):
|
def test_shortcuts(self):
|
||||||
assert language.parse_response({}, "400:c'foo'").headers[0].key.val == "Content-Type"
|
assert language.parse_response("400:c'foo'").headers[0].key.val == "Content-Type"
|
||||||
assert language.parse_response({}, "400:l'foo'").headers[0].key.val == "Location"
|
assert language.parse_response("400:l'foo'").headers[0].key.val == "Location"
|
||||||
|
|
||||||
assert 'Android' in language.parse_request({}, "get:/:ua").headers[0].value.val
|
assert 'Android' in parse_request("get:/:ua").headers[0].value.val
|
||||||
assert language.parse_request({}, "get:/:ua").headers[0].key.val == "User-Agent"
|
assert parse_request("get:/:ua").headers[0].key.val == "User-Agent"
|
||||||
|
|
||||||
|
|
||||||
class TestShortcutUserAgent:
|
class TestShortcutUserAgent:
|
||||||
@ -336,7 +384,7 @@ class Test_Action:
|
|||||||
assert l[0].offset == 0
|
assert l[0].offset == 0
|
||||||
|
|
||||||
def test_resolve(self):
|
def test_resolve(self):
|
||||||
r = language.parse_request({}, 'GET:"/foo"')
|
r = parse_request('GET:"/foo"')
|
||||||
e = language.DisconnectAt("r")
|
e = language.DisconnectAt("r")
|
||||||
ret = e.resolve(r, {})
|
ret = e.resolve(r, {})
|
||||||
assert isinstance(ret.offset, int)
|
assert isinstance(ret.offset, int)
|
||||||
@ -352,9 +400,9 @@ class Test_Action:
|
|||||||
|
|
||||||
class TestDisconnects:
|
class TestDisconnects:
|
||||||
def test_parse_response(self):
|
def test_parse_response(self):
|
||||||
a = language.parse_response({}, "400:d0").actions[0]
|
a = language.parse_response("400:d0").actions[0]
|
||||||
assert a.spec() == "d0"
|
assert a.spec() == "d0"
|
||||||
a = language.parse_response({}, "400:dr").actions[0]
|
a = language.parse_response("400:dr").actions[0]
|
||||||
assert a.spec() == "dr"
|
assert a.spec() == "dr"
|
||||||
|
|
||||||
def test_at(self):
|
def test_at(self):
|
||||||
@ -377,12 +425,12 @@ class TestDisconnects:
|
|||||||
|
|
||||||
class TestInject:
|
class TestInject:
|
||||||
def test_parse_response(self):
|
def test_parse_response(self):
|
||||||
a = language.parse_response({}, "400:ir,@100").actions[0]
|
a = language.parse_response("400:ir,@100").actions[0]
|
||||||
assert a.offset == "r"
|
assert a.offset == "r"
|
||||||
assert a.value.datatype == "bytes"
|
assert a.value.datatype == "bytes"
|
||||||
assert a.value.usize == 100
|
assert a.value.usize == 100
|
||||||
|
|
||||||
a = language.parse_response({}, "400:ia,@100").actions[0]
|
a = language.parse_response("400:ia,@100").actions[0]
|
||||||
assert a.offset == "a"
|
assert a.offset == "a"
|
||||||
|
|
||||||
def test_at(self):
|
def test_at(self):
|
||||||
@ -397,7 +445,7 @@ class TestInject:
|
|||||||
|
|
||||||
def test_serve(self):
|
def test_serve(self):
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:i0,'foo'")
|
r = language.parse_response("400:i0,'foo'")
|
||||||
assert language.serve(r, s, {})
|
assert language.serve(r, s, {})
|
||||||
|
|
||||||
def test_spec(self):
|
def test_spec(self):
|
||||||
@ -430,7 +478,7 @@ class TestPauses:
|
|||||||
assert v.offset == "a"
|
assert v.offset == "a"
|
||||||
|
|
||||||
def test_request(self):
|
def test_request(self):
|
||||||
r = language.parse_response({}, '400:p10,10')
|
r = language.parse_response('400:p10,10')
|
||||||
assert r.actions[0].spec() == "p10,10"
|
assert r.actions[0].spec() == "p10,10"
|
||||||
|
|
||||||
def test_spec(self):
|
def test_spec(self):
|
||||||
@ -444,30 +492,59 @@ class TestPauses:
|
|||||||
|
|
||||||
|
|
||||||
class TestRequest:
|
class TestRequest:
|
||||||
def test_file(self):
|
|
||||||
p = tutils.test_data.path("data")
|
|
||||||
d = dict(staticdir=p)
|
|
||||||
r = language.parse_request(d, "+request")
|
|
||||||
assert r.path.values({})[0][:] == "/foo"
|
|
||||||
|
|
||||||
def test_nonascii(self):
|
def test_nonascii(self):
|
||||||
tutils.raises("ascii", language.parse_request, {}, "get:\xf0")
|
tutils.raises("ascii", parse_request, "get:\xf0")
|
||||||
|
|
||||||
def test_err(self):
|
def test_err(self):
|
||||||
tutils.raises(language.ParseException, language.parse_request, {}, 'GET')
|
tutils.raises(language.ParseException, parse_request, 'GET')
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
r = language.parse_request({}, 'GET:"/foo"')
|
r = parse_request('GET:"/foo"')
|
||||||
assert r.method.string() == "GET"
|
assert r.method.string() == "GET"
|
||||||
assert r.path.string() == "/foo"
|
assert r.path.string() == "/foo"
|
||||||
r = language.parse_request({}, 'GET:/foo')
|
r = parse_request('GET:/foo')
|
||||||
assert r.path.string() == "/foo"
|
assert r.path.string() == "/foo"
|
||||||
r = language.parse_request({}, 'GET:@1k')
|
r = parse_request('GET:@1k')
|
||||||
assert len(r.path.string()) == 1024
|
assert len(r.path.string()) == 1024
|
||||||
|
|
||||||
|
def test_multiple(self):
|
||||||
|
r = language.parse_requests("GET:/ PUT:/")
|
||||||
|
assert r[0].method.string() == "GET"
|
||||||
|
assert r[1].method.string() == "PUT"
|
||||||
|
assert len(r) == 2
|
||||||
|
|
||||||
|
l = """
|
||||||
|
GET
|
||||||
|
"/foo"
|
||||||
|
ir,@1
|
||||||
|
|
||||||
|
PUT
|
||||||
|
|
||||||
|
"/foo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
bar"
|
||||||
|
|
||||||
|
ir,@1
|
||||||
|
"""
|
||||||
|
r = language.parse_requests(l)
|
||||||
|
assert len(r) == 2
|
||||||
|
assert r[0].method.string() == "GET"
|
||||||
|
assert r[1].method.string() == "PUT"
|
||||||
|
|
||||||
|
l = """
|
||||||
|
get:"http://localhost:9999/p/200":ir,@1
|
||||||
|
get:"http://localhost:9999/p/200":ir,@2
|
||||||
|
"""
|
||||||
|
r = language.parse_requests(l)
|
||||||
|
assert len(r) == 2
|
||||||
|
assert r[0].method.string() == "GET"
|
||||||
|
assert r[1].method.string() == "GET"
|
||||||
|
|
||||||
def test_render(self):
|
def test_render(self):
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_request({}, "GET:'/foo'")
|
r = parse_request("GET:'/foo'")
|
||||||
assert language.serve(r, s, {}, "foo.com")
|
assert language.serve(r, s, {}, "foo.com")
|
||||||
|
|
||||||
def test_multiline(self):
|
def test_multiline(self):
|
||||||
@ -476,12 +553,11 @@ class TestRequest:
|
|||||||
"/foo"
|
"/foo"
|
||||||
ir,@1
|
ir,@1
|
||||||
"""
|
"""
|
||||||
r = language.parse_request({}, l)
|
r = parse_request(l)
|
||||||
assert r.method.string() == "GET"
|
assert r.method.string() == "GET"
|
||||||
assert r.path.string() == "/foo"
|
assert r.path.string() == "/foo"
|
||||||
assert r.actions
|
assert r.actions
|
||||||
|
|
||||||
|
|
||||||
l = """
|
l = """
|
||||||
GET
|
GET
|
||||||
|
|
||||||
@ -493,24 +569,24 @@ class TestRequest:
|
|||||||
|
|
||||||
ir,@1
|
ir,@1
|
||||||
"""
|
"""
|
||||||
r = language.parse_request({}, l)
|
r = parse_request(l)
|
||||||
assert r.method.string() == "GET"
|
assert r.method.string() == "GET"
|
||||||
assert r.path.string().endswith("bar")
|
assert r.path.string().endswith("bar")
|
||||||
assert r.actions
|
assert r.actions
|
||||||
|
|
||||||
def test_spec(self):
|
def test_spec(self):
|
||||||
def rt(s):
|
def rt(s):
|
||||||
s = language.parse_request({}, s).spec()
|
s = parse_request(s).spec()
|
||||||
assert language.parse_request({}, s).spec() == s
|
assert parse_request(s).spec() == s
|
||||||
rt("get:/foo")
|
rt("get:/foo")
|
||||||
rt("get:/foo:da")
|
rt("get:/foo:da")
|
||||||
|
|
||||||
def test_freeze(self):
|
def test_freeze(self):
|
||||||
r = language.parse_request({}, "GET:/:b@100").freeze({})
|
r = parse_request("GET:/:b@100").freeze({})
|
||||||
assert len(r.spec()) > 100
|
assert len(r.spec()) > 100
|
||||||
|
|
||||||
def test_path_generator(self):
|
def test_path_generator(self):
|
||||||
r = language.parse_request({}, "GET:@100").freeze({})
|
r = parse_request("GET:@100").freeze({})
|
||||||
assert len(r.spec()) > 100
|
assert len(r.spec()) > 100
|
||||||
|
|
||||||
|
|
||||||
@ -580,72 +656,69 @@ class TestWriteValues:
|
|||||||
|
|
||||||
def test_write_values_after(self):
|
def test_write_values_after(self):
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:da")
|
r = language.parse_response("400:da")
|
||||||
language.serve(r, s, {})
|
language.serve(r, s, {})
|
||||||
|
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:pa,0")
|
r = language.parse_response("400:pa,0")
|
||||||
language.serve(r, s, {})
|
language.serve(r, s, {})
|
||||||
|
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:ia,'xx'")
|
r = language.parse_response("400:ia,'xx'")
|
||||||
language.serve(r, s, {})
|
language.serve(r, s, {})
|
||||||
assert s.getvalue().endswith('xx')
|
assert s.getvalue().endswith('xx')
|
||||||
|
|
||||||
|
|
||||||
class TestResponse:
|
class TestResponse:
|
||||||
def dummy_response(self):
|
def dummy_response(self):
|
||||||
return language.parse_response({}, "400'msg'")
|
return language.parse_response("400'msg'")
|
||||||
|
|
||||||
def test_file(self):
|
|
||||||
p = tutils.test_data.path("data")
|
|
||||||
d = dict(staticdir=p)
|
|
||||||
r = language.parse_response(d, "+response")
|
|
||||||
assert r.code.string() == "202"
|
|
||||||
|
|
||||||
def test_response(self):
|
def test_response(self):
|
||||||
r = language.parse_response({}, "400:m'msg'")
|
r = language.parse_response("400:m'msg'")
|
||||||
assert r.code.string() == "400"
|
assert r.code.string() == "400"
|
||||||
assert r.reason.string() == "msg"
|
assert r.reason.string() == "msg"
|
||||||
|
|
||||||
r = language.parse_response({}, "400:m'msg':b@100b")
|
r = language.parse_response("400:m'msg':b@100b")
|
||||||
assert r.reason.string() == "msg"
|
assert r.reason.string() == "msg"
|
||||||
assert r.body.values({})
|
assert r.body.values({})
|
||||||
assert str(r)
|
assert str(r)
|
||||||
|
|
||||||
r = language.parse_response({}, "200")
|
r = language.parse_response("200")
|
||||||
assert r.code.string() == "200"
|
assert r.code.string() == "200"
|
||||||
assert not r.reason
|
assert not r.reason
|
||||||
assert "OK" in [i[:] for i in r.preamble({})]
|
assert "OK" in [i[:] for i in r.preamble({})]
|
||||||
|
|
||||||
def test_render(self):
|
def test_render(self):
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:m'msg'")
|
r = language.parse_response("400:m'msg'")
|
||||||
assert language.serve(r, s, {})
|
assert language.serve(r, s, {})
|
||||||
|
|
||||||
|
r = language.parse_response("400:p0,100:dr")
|
||||||
|
assert "p0" in r.spec()
|
||||||
|
s = r.preview_safe()
|
||||||
|
assert "p0" not in s.spec()
|
||||||
|
|
||||||
def test_raw(self):
|
def test_raw(self):
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:b'foo'")
|
r = language.parse_response("400:b'foo'")
|
||||||
language.serve(r, s, {})
|
language.serve(r, s, {})
|
||||||
v = s.getvalue()
|
v = s.getvalue()
|
||||||
assert "Content-Length" in v
|
assert "Content-Length" in v
|
||||||
assert "Date" in v
|
|
||||||
|
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
r = language.parse_response({}, "400:b'foo':r")
|
r = language.parse_response("400:b'foo':r")
|
||||||
language.serve(r, s, {})
|
language.serve(r, s, {})
|
||||||
v = s.getvalue()
|
v = s.getvalue()
|
||||||
assert not "Content-Length" in v
|
assert not "Content-Length" in v
|
||||||
assert not "Date" in v
|
|
||||||
|
|
||||||
def test_length(self):
|
def test_length(self):
|
||||||
def testlen(x):
|
def testlen(x):
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
language.serve(x, s, {})
|
language.serve(x, s, {})
|
||||||
assert x.length({}) == len(s.getvalue())
|
assert x.length({}) == len(s.getvalue())
|
||||||
testlen(language.parse_response({}, "400:m'msg':r"))
|
testlen(language.parse_response("400:m'msg':r"))
|
||||||
testlen(language.parse_response({}, "400:m'msg':h'foo'='bar':r"))
|
testlen(language.parse_response("400:m'msg':h'foo'='bar':r"))
|
||||||
testlen(language.parse_response({}, "400:m'msg':h'foo'='bar':b@100b:r"))
|
testlen(language.parse_response("400:m'msg':h'foo'='bar':b@100b:r"))
|
||||||
|
|
||||||
def test_maximum_length(self):
|
def test_maximum_length(self):
|
||||||
def testlen(x):
|
def testlen(x):
|
||||||
@ -654,74 +727,84 @@ class TestResponse:
|
|||||||
language.serve(x, s, {})
|
language.serve(x, s, {})
|
||||||
assert m >= len(s.getvalue())
|
assert m >= len(s.getvalue())
|
||||||
|
|
||||||
r = language.parse_response({}, "400:m'msg':b@100:d0")
|
r = language.parse_response("400:m'msg':b@100:d0")
|
||||||
testlen(r)
|
testlen(r)
|
||||||
|
|
||||||
r = language.parse_response({}, "400:m'msg':b@100:d0:i0,'foo'")
|
r = language.parse_response("400:m'msg':b@100:d0:i0,'foo'")
|
||||||
testlen(r)
|
testlen(r)
|
||||||
|
|
||||||
r = language.parse_response({}, "400:m'msg':b@100:d0:i0,'foo'")
|
r = language.parse_response("400:m'msg':b@100:d0:i0,'foo'")
|
||||||
testlen(r)
|
testlen(r)
|
||||||
|
|
||||||
def test_render(self):
|
|
||||||
r = language.parse_response({}, "400:p0,100:dr")
|
|
||||||
assert "p0" in r.spec()
|
|
||||||
s = r.preview_safe()
|
|
||||||
assert not "p0" in s.spec()
|
|
||||||
|
|
||||||
def test_parse_err(self):
|
def test_parse_err(self):
|
||||||
tutils.raises(language.ParseException, language.parse_response, {}, "400:msg,b:")
|
tutils.raises(
|
||||||
|
language.ParseException, language.parse_response, "400:msg,b:"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
language.parse_response({}, "400'msg':b:")
|
language.parse_response("400'msg':b:")
|
||||||
except language.ParseException, v:
|
except language.ParseException, v:
|
||||||
assert v.marked()
|
assert v.marked()
|
||||||
assert str(v)
|
assert str(v)
|
||||||
|
|
||||||
def test_nonascii(self):
|
def test_nonascii(self):
|
||||||
tutils.raises("ascii", language.parse_response, {}, "foo:b\xf0")
|
tutils.raises("ascii", language.parse_response, "foo:b\xf0")
|
||||||
|
|
||||||
def test_parse_header(self):
|
def test_parse_header(self):
|
||||||
r = language.parse_response({}, '400:h"foo"="bar"')
|
r = language.parse_response('400:h"foo"="bar"')
|
||||||
assert utils.get_header("foo", r.headers)
|
assert utils.get_header("foo", r.headers)
|
||||||
|
|
||||||
def test_parse_pause_before(self):
|
def test_parse_pause_before(self):
|
||||||
r = language.parse_response({}, "400:p0,10")
|
r = language.parse_response("400:p0,10")
|
||||||
assert r.actions[0].spec() == "p0,10"
|
assert r.actions[0].spec() == "p0,10"
|
||||||
|
|
||||||
def test_parse_pause_after(self):
|
def test_parse_pause_after(self):
|
||||||
r = language.parse_response({}, "400:pa,10")
|
r = language.parse_response("400:pa,10")
|
||||||
assert r.actions[0].spec() == "pa,10"
|
assert r.actions[0].spec() == "pa,10"
|
||||||
|
|
||||||
def test_parse_pause_random(self):
|
def test_parse_pause_random(self):
|
||||||
r = language.parse_response({}, "400:pr,10")
|
r = language.parse_response("400:pr,10")
|
||||||
assert r.actions[0].spec() == "pr,10"
|
assert r.actions[0].spec() == "pr,10"
|
||||||
|
|
||||||
def test_parse_stress(self):
|
def test_parse_stress(self):
|
||||||
# While larger values are known to work on linux,
|
# While larger values are known to work on linux, len() technically
|
||||||
# len() technically returns an int and a python 2.7 int on windows has 32bit precision.
|
# returns an int and a python 2.7 int on windows has 32bit precision.
|
||||||
# Therefore, we should keep the body length < 2147483647 bytes in our tests.
|
# Therefore, we should keep the body length < 2147483647 bytes in our
|
||||||
r = language.parse_response({}, "400:b@1g")
|
# tests.
|
||||||
|
r = language.parse_response("400:b@1g")
|
||||||
assert r.length({})
|
assert r.length({})
|
||||||
|
|
||||||
def test_spec(self):
|
def test_spec(self):
|
||||||
def rt(s):
|
def rt(s):
|
||||||
s = language.parse_response({}, s).spec()
|
s = language.parse_response(s).spec()
|
||||||
assert language.parse_response({}, s).spec() == s
|
assert language.parse_response(s).spec() == s
|
||||||
rt("400:b@100g")
|
rt("400:b@100g")
|
||||||
rt("400")
|
rt("400")
|
||||||
rt("400:da")
|
rt("400:da")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_read_file():
|
def test_read_file():
|
||||||
tutils.raises(language.FileAccessDenied, language.read_file, {}, "=/foo")
|
tutils.raises(language.FileAccessDenied, language.read_file, {}, "=/foo")
|
||||||
p = tutils.test_data.path("data")
|
p = tutils.test_data.path("data")
|
||||||
d = dict(staticdir=p)
|
d = dict(staticdir=p)
|
||||||
assert language.read_file(d, "+./file").strip() == "testfile"
|
assert language.read_file(d, "+./file").strip() == "testfile"
|
||||||
assert language.read_file(d, "+file").strip() == "testfile"
|
assert language.read_file(d, "+file").strip() == "testfile"
|
||||||
tutils.raises(language.FileAccessDenied, language.read_file, d, "+./nonexistent")
|
tutils.raises(
|
||||||
tutils.raises(language.FileAccessDenied, language.read_file, d, "+/nonexistent")
|
language.FileAccessDenied,
|
||||||
|
language.read_file,
|
||||||
tutils.raises(language.FileAccessDenied, language.read_file, d, "+../test_language.py")
|
d,
|
||||||
|
"+./nonexistent"
|
||||||
|
)
|
||||||
|
tutils.raises(
|
||||||
|
language.FileAccessDenied,
|
||||||
|
language.read_file,
|
||||||
|
d,
|
||||||
|
"+/nonexistent"
|
||||||
|
)
|
||||||
|
tutils.raises(
|
||||||
|
language.FileAccessDenied,
|
||||||
|
language.read_file,
|
||||||
|
d,
|
||||||
|
"+../test_language.py"
|
||||||
|
)
|
||||||
d["unconstrained_file_access"] = True
|
d["unconstrained_file_access"] = True
|
||||||
assert language.read_file(d, "+../test_language.py")
|
assert language.read_file(d, "+../test_language.py")
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import cStringIO
|
import cStringIO
|
||||||
from libpathod import pathoc, test, version, pathod
|
import re
|
||||||
|
|
||||||
|
from libpathod import pathoc, test, version, pathod, language
|
||||||
import tutils
|
import tutils
|
||||||
|
|
||||||
|
|
||||||
@ -18,7 +20,9 @@ class _TestDaemon:
|
|||||||
ssl=self.ssl,
|
ssl=self.ssl,
|
||||||
ssloptions=self.ssloptions,
|
ssloptions=self.ssloptions,
|
||||||
staticdir=tutils.test_data.path("data"),
|
staticdir=tutils.test_data.path("data"),
|
||||||
anchors=[("/anchor/.*", "202")]
|
anchors=[
|
||||||
|
(re.compile("/anchor/.*"), language.parse_response("202"))
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -37,17 +41,29 @@ class _TestDaemon:
|
|||||||
r = c.request("get:/api/info")
|
r = c.request("get:/api/info")
|
||||||
assert tuple(json.loads(r.content)["version"]) == version.IVERSION
|
assert tuple(json.loads(r.content)["version"]) == version.IVERSION
|
||||||
|
|
||||||
def tval(self, requests, showreq=False, showresp=False, explain=False,
|
def tval(
|
||||||
showssl=False, hexdump=False, timeout=None, ignorecodes=None,
|
self,
|
||||||
ignoretimeout=None):
|
requests,
|
||||||
|
showreq=False,
|
||||||
|
showresp=False,
|
||||||
|
explain=False,
|
||||||
|
showssl=False,
|
||||||
|
hexdump=False,
|
||||||
|
timeout=None,
|
||||||
|
ignorecodes=None,
|
||||||
|
ignoretimeout=None
|
||||||
|
):
|
||||||
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)
|
||||||
c.connect()
|
c.connect()
|
||||||
if timeout:
|
if timeout:
|
||||||
c.settimeout(timeout)
|
c.settimeout(timeout)
|
||||||
s = cStringIO.StringIO()
|
s = cStringIO.StringIO()
|
||||||
for i in requests:
|
for i in requests:
|
||||||
|
r = language.parse_requests(i)[0]
|
||||||
|
if explain:
|
||||||
|
r = r.freeze({})
|
||||||
c.print_request(
|
c.print_request(
|
||||||
i,
|
r,
|
||||||
showreq = showreq,
|
showreq = showreq,
|
||||||
showresp = showresp,
|
showresp = showresp,
|
||||||
explain = explain,
|
explain = explain,
|
||||||
@ -125,25 +141,17 @@ class TestDaemon(_TestDaemon):
|
|||||||
assert "HTTP/" in v
|
assert "HTTP/" in v
|
||||||
|
|
||||||
def test_explain(self):
|
def test_explain(self):
|
||||||
reqs = [ "get:/p/200:b@100" ]
|
reqs = ["get:/p/200:b@100"]
|
||||||
assert not "b@100" in self.tval(reqs, explain=True)
|
assert "b@100" not in self.tval(reqs, explain=True)
|
||||||
|
|
||||||
def test_showreq(self):
|
def test_showreq(self):
|
||||||
reqs = [ "get:/api/info:p0,0", "get:/api/info:p0,0" ]
|
reqs = ["get:/api/info:p0,0", "get:/api/info:p0,0"]
|
||||||
assert self.tval(reqs, showreq=True).count("unprintables escaped") == 2
|
assert self.tval(reqs, showreq=True).count("unprintables escaped") == 2
|
||||||
assert self.tval(reqs, showreq=True, hexdump=True).count("hex dump") == 2
|
assert self.tval(reqs, showreq=True, hexdump=True).count("hex dump") == 2
|
||||||
|
|
||||||
def test_parse_err(self):
|
|
||||||
assert "Error parsing" in self.tval(["foo"])
|
|
||||||
|
|
||||||
def test_conn_err(self):
|
def test_conn_err(self):
|
||||||
assert "Invalid server response" in self.tval(["get:'/p/200:d2'"])
|
assert "Invalid server response" in self.tval(["get:'/p/200:d2'"])
|
||||||
|
|
||||||
def test_fileread(self):
|
|
||||||
d = tutils.test_data.path("data/request")
|
|
||||||
assert "foo" in self.tval(["+%s"%d], showreq=True)
|
|
||||||
assert "File" in self.tval(["+/nonexistent"])
|
|
||||||
|
|
||||||
def test_connect_fail(self):
|
def test_connect_fail(self):
|
||||||
to = ("foobar", 80)
|
to = ("foobar", 80)
|
||||||
c = pathoc.Pathoc(("127.0.0.1", self.d.port))
|
c = pathoc.Pathoc(("127.0.0.1", self.d.port))
|
||||||
@ -157,6 +165,3 @@ class TestDaemon(_TestDaemon):
|
|||||||
"HTTP/1.1 200 OK\r\n"
|
"HTTP/1.1 200 OK\r\n"
|
||||||
)
|
)
|
||||||
c.http_connect(to)
|
c.http_connect(to)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,19 +1,9 @@
|
|||||||
import pprint
|
|
||||||
from libpathod import pathod, version
|
from libpathod import pathod, version
|
||||||
from netlib import tcp, http, certutils
|
from netlib import tcp, http
|
||||||
import requests
|
|
||||||
import tutils
|
import tutils
|
||||||
|
|
||||||
class TestPathod:
|
|
||||||
def test_instantiation(self):
|
|
||||||
p = pathod.Pathod(
|
|
||||||
("127.0.0.1", 0),
|
|
||||||
anchors = [(".*", "200:da")]
|
|
||||||
)
|
|
||||||
assert p.anchors
|
|
||||||
tutils.raises("invalid regex", pathod.Pathod, ("127.0.0.1", 0), anchors=[("*", "200:da")])
|
|
||||||
tutils.raises("invalid page spec", pathod.Pathod, ("127.0.0.1", 0), anchors=[("foo", "bar")])
|
|
||||||
|
|
||||||
|
class TestPathod:
|
||||||
def test_logging(self):
|
def test_logging(self):
|
||||||
p = pathod.Pathod(("127.0.0.1", 0))
|
p = pathod.Pathod(("127.0.0.1", 0))
|
||||||
assert len(p.get_log()) == 0
|
assert len(p.get_log()) == 0
|
||||||
@ -30,6 +20,7 @@ class TestPathod:
|
|||||||
|
|
||||||
class TestNoWeb(tutils.DaemonTests):
|
class TestNoWeb(tutils.DaemonTests):
|
||||||
noweb = True
|
noweb = True
|
||||||
|
|
||||||
def test_noweb(self):
|
def test_noweb(self):
|
||||||
assert self.get("200:da").status_code == 200
|
assert self.get("200:da").status_code == 200
|
||||||
assert self.getpath("/").status_code == 800
|
assert self.getpath("/").status_code == 800
|
||||||
@ -37,6 +28,7 @@ class TestNoWeb(tutils.DaemonTests):
|
|||||||
|
|
||||||
class TestTimeout(tutils.DaemonTests):
|
class TestTimeout(tutils.DaemonTests):
|
||||||
timeout = 0.01
|
timeout = 0.01
|
||||||
|
|
||||||
def test_noweb(self):
|
def test_noweb(self):
|
||||||
# FIXME: Add float values to spec language, reduce test timeout to
|
# FIXME: Add float values to spec language, reduce test timeout to
|
||||||
# increase test performance
|
# increase test performance
|
||||||
@ -46,6 +38,7 @@ class TestTimeout(tutils.DaemonTests):
|
|||||||
|
|
||||||
class TestNoApi(tutils.DaemonTests):
|
class TestNoApi(tutils.DaemonTests):
|
||||||
noapi = True
|
noapi = True
|
||||||
|
|
||||||
def test_noapi(self):
|
def test_noapi(self):
|
||||||
assert self.getpath("/log").status_code == 404
|
assert self.getpath("/log").status_code == 404
|
||||||
r = self.getpath("/")
|
r = self.getpath("/")
|
||||||
@ -59,7 +52,10 @@ class TestNotAfterConnect(tutils.DaemonTests):
|
|||||||
not_after_connect = True
|
not_after_connect = True
|
||||||
)
|
)
|
||||||
def test_connect(self):
|
def test_connect(self):
|
||||||
r = self.pathoc(r"get:'http://foo.com/p/202':da", connect_to=("localhost", self.d.port))
|
r = self.pathoc(
|
||||||
|
r"get:'http://foo.com/p/202':da",
|
||||||
|
connect_to=("localhost", self.d.port)
|
||||||
|
)
|
||||||
assert r.status_code == 202
|
assert r.status_code == 202
|
||||||
|
|
||||||
|
|
||||||
@ -157,11 +153,15 @@ class CommonTests(tutils.DaemonTests):
|
|||||||
assert l["type"] == "error"
|
assert l["type"] == "error"
|
||||||
assert "foo" in l["msg"]
|
assert "foo" in l["msg"]
|
||||||
|
|
||||||
def test_invalid_body(self):
|
def test_invalid_content_length(self):
|
||||||
tutils.raises(http.HttpError, self.pathoc, "get:/:h'content-length'='foo'")
|
tutils.raises(
|
||||||
|
http.HttpError,
|
||||||
|
self.pathoc,
|
||||||
|
"get:/:h'content-length'='foo'"
|
||||||
|
)
|
||||||
l = self.d.last_log()
|
l = self.d.last_log()
|
||||||
assert l["type"] == "error"
|
assert l["type"] == "error"
|
||||||
assert "Invalid" in l["msg"]
|
assert "Content-Length unknown" in l["msg"]
|
||||||
|
|
||||||
def test_invalid_headers(self):
|
def test_invalid_headers(self):
|
||||||
tutils.raises(http.HttpError, self.pathoc, "get:/:h'\t'='foo'")
|
tutils.raises(http.HttpError, self.pathoc, "get:/:h'\t'='foo'")
|
||||||
@ -204,7 +204,7 @@ class TestDaemon(CommonTests):
|
|||||||
|
|
||||||
class TestDaemonSSL(CommonTests):
|
class TestDaemonSSL(CommonTests):
|
||||||
ssl = True
|
ssl = True
|
||||||
def test_ssl_conn_failure(self):
|
def _test_ssl_conn_failure(self):
|
||||||
c = tcp.TCPClient(("localhost", self.d.port))
|
c = tcp.TCPClient(("localhost", self.d.port))
|
||||||
c.rbufsize = 0
|
c.rbufsize = 0
|
||||||
c.wbufsize = 0
|
c.wbufsize = 0
|
||||||
@ -222,4 +222,3 @@ class TestDaemonSSL(CommonTests):
|
|||||||
r = self.pathoc(r"get:/p/202")
|
r = self.pathoc(r"get:/p/202")
|
||||||
assert r.status_code == 202
|
assert r.status_code == 202
|
||||||
assert self.d.last_log()["cipher"][1] > 0
|
assert self.d.last_log()["cipher"][1] > 0
|
||||||
|
|
||||||
|
@ -4,13 +4,18 @@ from libpathod import test
|
|||||||
import tutils
|
import tutils
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
class TestDaemonManual:
|
class TestDaemonManual:
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
with test.Daemon() as d:
|
with test.Daemon() as d:
|
||||||
rsp = requests.get("http://localhost:%s/p/202:da"%d.port)
|
rsp = requests.get("http://localhost:%s/p/202:da"%d.port)
|
||||||
assert rsp.ok
|
assert rsp.ok
|
||||||
assert rsp.status_code == 202
|
assert rsp.status_code == 202
|
||||||
tutils.raises(requests.ConnectionError, requests.get, "http://localhost:%s/p/202:da"%d.port)
|
tutils.raises(
|
||||||
|
"Connection aborted",
|
||||||
|
requests.get,
|
||||||
|
"http://localhost:%s/p/202:da"%d.port
|
||||||
|
)
|
||||||
|
|
||||||
def test_startstop_ssl(self):
|
def test_startstop_ssl(self):
|
||||||
d = test.Daemon(ssl=True)
|
d = test.Daemon(ssl=True)
|
||||||
@ -18,7 +23,11 @@ class TestDaemonManual:
|
|||||||
assert rsp.ok
|
assert rsp.ok
|
||||||
assert rsp.status_code == 202
|
assert rsp.status_code == 202
|
||||||
d.shutdown()
|
d.shutdown()
|
||||||
tutils.raises(requests.ConnectionError, requests.get, "http://localhost:%s/p/202:da"%d.port)
|
tutils.raises(
|
||||||
|
"Connection aborted",
|
||||||
|
requests.get,
|
||||||
|
"http://localhost:%s/p/202:da"%d.port
|
||||||
|
)
|
||||||
|
|
||||||
def test_startstop_ssl_explicit(self):
|
def test_startstop_ssl_explicit(self):
|
||||||
ssloptions = dict(
|
ssloptions = dict(
|
||||||
@ -31,6 +40,9 @@ class TestDaemonManual:
|
|||||||
assert rsp.ok
|
assert rsp.ok
|
||||||
assert rsp.status_code == 202
|
assert rsp.status_code == 202
|
||||||
d.shutdown()
|
d.shutdown()
|
||||||
tutils.raises(requests.ConnectionError, requests.get, "http://localhost:%s/p/202:da"%d.port)
|
tutils.raises(
|
||||||
|
"Connection aborted",
|
||||||
|
requests.get,
|
||||||
|
"http://localhost:%s/p/202:da"%d.port
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import tempfile, os, shutil
|
import tempfile
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from libpathod import utils, test, pathoc, pathod
|
from libpathod import utils, test, pathoc, pathod, language
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
class DaemonTests:
|
class DaemonTests:
|
||||||
noweb = False
|
noweb = False
|
||||||
noapi = False
|
noapi = False
|
||||||
@ -11,6 +15,7 @@ class DaemonTests:
|
|||||||
timeout = None
|
timeout = None
|
||||||
hexdump = False
|
hexdump = False
|
||||||
ssloptions = None
|
ssloptions = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpAll(self):
|
def setUpAll(self):
|
||||||
opts = self.ssloptions or {}
|
opts = self.ssloptions or {}
|
||||||
@ -19,7 +24,9 @@ class DaemonTests:
|
|||||||
so = pathod.SSLOptions(**opts)
|
so = pathod.SSLOptions(**opts)
|
||||||
self.d = test.Daemon(
|
self.d = test.Daemon(
|
||||||
staticdir=test_data.path("data"),
|
staticdir=test_data.path("data"),
|
||||||
anchors=[("/anchor/.*", "202:da")],
|
anchors=[
|
||||||
|
(re.compile("/anchor/.*"), language.parse_response("202:da"))
|
||||||
|
],
|
||||||
ssl = self.ssl,
|
ssl = self.ssl,
|
||||||
ssloptions = so,
|
ssloptions = so,
|
||||||
sizelimit=1*1024*1024,
|
sizelimit=1*1024*1024,
|
||||||
@ -45,7 +52,13 @@ class DaemonTests:
|
|||||||
def getpath(self, path, params=None):
|
def getpath(self, path, params=None):
|
||||||
scheme = "https" if self.ssl else "http"
|
scheme = "https" if self.ssl else "http"
|
||||||
return requests.get(
|
return requests.get(
|
||||||
"%s://localhost:%s/%s"%(scheme, self.d.port, path), verify=False, params=params
|
"%s://localhost:%s/%s"%(
|
||||||
|
scheme,
|
||||||
|
self.d.port,
|
||||||
|
path
|
||||||
|
),
|
||||||
|
verify=False,
|
||||||
|
params=params
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, spec):
|
def get(self, spec):
|
||||||
|
Loading…
Reference in New Issue
Block a user