aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--libpathod/language/__init__.py88
-rw-r--r--libpathod/language/base.py (renamed from libpathod/language.py)585
-rw-r--r--libpathod/language/contrib/__init__.py (renamed from libpathod/contrib/__init__.py)0
-rw-r--r--libpathod/language/contrib/pyparsing.py (renamed from libpathod/contrib/pyparsing.py)0
-rw-r--r--libpathod/language/exceptions.py21
-rw-r--r--libpathod/language/generators.py99
-rw-r--r--libpathod/language/http.py267
-rw-r--r--libpathod/language/websockets.py59
-rw-r--r--libpathod/language/writer.py61
-rw-r--r--libpathod/pathoc.py11
-rw-r--r--libpathod/pathod.py12
-rw-r--r--libpathod/utils.py11
-rw-r--r--test/test_language.py219
13 files changed, 737 insertions, 696 deletions
diff --git a/libpathod/language/__init__.py b/libpathod/language/__init__.py
new file mode 100644
index 00000000..cc8428c9
--- /dev/null
+++ b/libpathod/language/__init__.py
@@ -0,0 +1,88 @@
+import time
+
+import contrib.pyparsing as pp
+
+from . import base, http, websockets, writer, exceptions
+
+from exceptions import *
+
+
+class Settings:
+ def __init__(
+ self,
+ staticdir = None,
+ unconstrained_file_access = False,
+ request_host = None,
+ websocket_key = None
+ ):
+ self.staticdir = staticdir
+ self.unconstrained_file_access = unconstrained_file_access
+ self.request_host = request_host
+ self.websocket_key = websocket_key
+
+
+def parse_response(s):
+ """
+ May raise ParseException
+ """
+ try:
+ s = s.decode("ascii")
+ except UnicodeError:
+ raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0)
+ try:
+ return http.Response.expr().parseString(s, parseAll=True)[0]
+ except pp.ParseException, v:
+ raise exceptions.ParseException(v.msg, v.line, v.col)
+
+
+def parse_requests(s):
+ """
+ May raise ParseException
+ """
+ try:
+ s = s.decode("ascii")
+ except UnicodeError:
+ raise exceptions.ParseException("Spec must be valid ASCII.", 0, 0)
+ try:
+ return pp.OneOrMore(
+ pp.Or(
+ [
+ websockets.WebsocketFrame.expr(),
+ http.Request.expr(),
+ ]
+ )
+ ).parseString(s, parseAll=True)
+ except pp.ParseException, v:
+ raise exceptions.ParseException(v.msg, v.line, v.col)
+
+
+def serve(msg, fp, settings):
+ """
+ fp: The file pointer to write to.
+
+ request_host: If this a request, this is the connecting host. If
+ None, we assume it's a response. Used to decide what standard
+ modifications to make if raw is not set.
+
+ Calling this function may modify the object.
+ """
+ msg = msg.resolve(settings)
+ started = time.time()
+
+ vals = msg.values(settings)
+ vals.reverse()
+
+ actions = msg.actions[:]
+ actions.sort()
+ actions.reverse()
+ actions = [i.intermediate(settings) for i in actions]
+
+ disconnect = writer.write_values(fp, vals, actions[:])
+ duration = time.time() - started
+ ret = dict(
+ disconnect = disconnect,
+ started = started,
+ duration = duration,
+ )
+ ret.update(msg.log(settings))
+ return ret
diff --git a/libpathod/language.py b/libpathod/language/base.py
index e4277eb2..407d5473 100644
--- a/libpathod/language.py
+++ b/libpathod/language/base.py
@@ -1,34 +1,17 @@
import operator
-import string
import random
-import mmap
import os
-import time
import copy
import abc
import contrib.pyparsing as pp
-from netlib import http_status, tcp, http_uastrings, websockets
+from netlib import http_uastrings
-import utils
+from .. import utils
+from . import generators, exceptions
-BLOCKSIZE = 1024
TRUNCATE = 1024
-class Settings:
- def __init__(
- self,
- staticdir = None,
- unconstrained_file_access = False,
- request_host = None,
- websocket_key = None
- ):
- self.staticdir = staticdir
- self.unconstrained_file_access = unconstrained_file_access
- self.request_host = request_host
- self.websocket_key = websocket_key
-
-
def quote(s):
quotechar = s[0]
s = s[1:-1]
@@ -36,131 +19,6 @@ def quote(s):
return quotechar + s + quotechar
-class RenderError(Exception):
- pass
-
-
-class FileAccessDenied(RenderError):
- pass
-
-
-class ParseException(Exception):
- def __init__(self, msg, s, col):
- Exception.__init__(self)
- self.msg = msg
- self.s = s
- self.col = col
-
- def marked(self):
- return "%s\n%s"%(self.s, " " * (self.col - 1) + "^")
-
- def __str__(self):
- return "%s at char %s"%(self.msg, self.col)
-
-
-def send_chunk(fp, val, blocksize, start, end):
- """
- (start, end): Inclusive lower bound, exclusive upper bound.
- """
- for i in range(start, end, blocksize):
- fp.write(
- val[i:min(i + blocksize, end)]
- )
- return end - start
-
-
-def write_values(fp, vals, actions, sofar=0, blocksize=BLOCKSIZE):
- """
- 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".
-
- Both vals and actions are in reverse order, with the first items last.
-
- Return True if connection should disconnect.
- """
- sofar = 0
- try:
- while vals:
- v = vals.pop()
- offset = 0
- while actions and actions[-1][0] < (sofar + len(v)):
- a = actions.pop()
- offset += send_chunk(
- fp,
- v,
- blocksize,
- offset,
- a[0] - sofar - offset
- )
- if a[1] == "pause":
- time.sleep(a[2])
- elif a[1] == "disconnect":
- return True
- elif a[1] == "inject":
- send_chunk(fp, a[2], blocksize, 0, len(a[2]))
- send_chunk(fp, v, blocksize, offset, len(v))
- sofar += len(v)
- # Remainders
- while actions:
- a = actions.pop()
- if a[1] == "pause":
- time.sleep(a[2])
- elif a[1] == "disconnect":
- return True
- elif a[1] == "inject":
- send_chunk(fp, a[2], blocksize, 0, len(a[2]))
- except tcp.NetLibDisconnect: # pragma: no cover
- return True
-
-
-def serve(msg, fp, settings):
- """
- fp: The file pointer to write to.
-
- request_host: If this a request, this is the connecting host. If
- None, we assume it's a response. Used to decide what standard
- modifications to make if raw is not set.
-
- Calling this function may modify the object.
- """
- msg = msg.resolve(settings)
- started = time.time()
-
- vals = msg.values(settings)
- vals.reverse()
-
- actions = msg.actions[:]
- actions.sort()
- actions.reverse()
- actions = [i.intermediate(settings) for i in actions]
-
- disconnect = write_values(fp, vals, actions[:])
- duration = time.time() - started
- ret = dict(
- disconnect = disconnect,
- started = started,
- duration = duration,
- )
- ret.update(msg.log(settings))
- return ret
-
-
-DATATYPES = dict(
- ascii_letters = string.ascii_letters,
- ascii_lowercase = string.ascii_lowercase,
- ascii_uppercase = string.ascii_uppercase,
- digits = string.digits,
- hexdigits = string.hexdigits,
- octdigits = string.octdigits,
- punctuation = string.punctuation,
- whitespace = string.whitespace,
- ascii = string.printable,
- bytes = "".join(chr(i) for i in range(256))
-)
-
-
v_integer = pp.Word(pp.nums)\
.setName("integer")\
.setParseAction(lambda toks: int(toks[0]))
@@ -191,89 +49,6 @@ v_naked_literal = pp.MatchFirst(
)
-class TransformGenerator:
- """
- Perform a byte-by-byte transform another generator - that is, for each
- input byte, the transformation must produce one output byte.
-
- gen: A generator to wrap
- transform: A function (offset, data) -> transformed
- """
- def __init__(self, gen, transform):
- self.gen = gen
- self.transform = transform
-
- def __len__(self):
- return len(self.gen)
-
- def __getitem__(self, x):
- d = self.gen.__getitem__(x)
- return self.transform(x, d)
-
- def __getslice__(self, a, b):
- d = self.gen.__getslice__(a, b)
- return self.transform(a, d)
-
- def __repr__(self):
- return "'%s'"%self.gen
-
-
-class LiteralGenerator:
- def __init__(self, s):
- self.s = s
-
- def __len__(self):
- return len(self.s)
-
- def __getitem__(self, x):
- return self.s.__getitem__(x)
-
- def __getslice__(self, a, b):
- return self.s.__getslice__(a, b)
-
- def __repr__(self):
- return "'%s'"%self.s
-
-
-class RandomGenerator:
- def __init__(self, dtype, length):
- self.dtype = dtype
- self.length = length
-
- def __len__(self):
- return self.length
-
- def __getitem__(self, x):
- return random.choice(DATATYPES[self.dtype])
-
- def __getslice__(self, a, b):
- b = min(b, self.length)
- chars = DATATYPES[self.dtype]
- return "".join(random.choice(chars) for x in range(a, b))
-
- def __repr__(self):
- return "%s random from %s"%(self.length, self.dtype)
-
-
-class FileGenerator:
- def __init__(self, path):
- self.path = path
- self.fp = file(path, "rb")
- self.map = mmap.mmap(self.fp.fileno(), 0, access=mmap.ACCESS_READ)
-
- def __len__(self):
- return len(self.map)
-
- def __getitem__(self, x):
- return self.map.__getitem__(x)
-
- def __getslice__(self, a, b):
- return self.map.__getslice__(a, b)
-
- def __repr__(self):
- return "<%s"%self.path
-
-
class _Token(object):
"""
A specification token. Tokens are immutable.
@@ -310,7 +85,7 @@ class _ValueLiteral(_Token):
self.val = val.decode("string_escape")
def get_generator(self, settings):
- return LiteralGenerator(self.val)
+ return generators.LiteralGenerator(self.val)
def freeze(self, settings):
return self
@@ -352,7 +127,7 @@ class ValueGenerate(_Token):
return self.usize * utils.SIZE_UNITS[self.unit]
def get_generator(self, settings):
- return RandomGenerator(self.datatype, self.bytes())
+ return generators.RandomGenerator(self.datatype, self.bytes())
def freeze(self, settings):
g = self.get_generator(settings)
@@ -369,7 +144,10 @@ class ValueGenerate(_Token):
e = e + pp.Optional(u, default=None)
s = pp.Literal(",").suppress()
- s += reduce(operator.or_, [pp.Literal(i) for i in DATATYPES.keys()])
+ s += reduce(
+ operator.or_,
+ [pp.Literal(i) for i in generators.DATATYPES.keys()]
+ )
e += pp.Optional(s, default="bytes")
return e.setParseAction(lambda x: klass(*x))
@@ -397,19 +175,19 @@ class ValueFile(_Token):
def get_generator(self, settings):
if not settings.staticdir:
- raise FileAccessDenied("File access disabled.")
+ raise exceptions.FileAccessDenied("File access disabled.")
s = os.path.expanduser(self.path)
s = os.path.normpath(
os.path.abspath(os.path.join(settings.staticdir, s))
)
uf = settings.unconstrained_file_access
if not uf and not s.startswith(settings.staticdir):
- raise FileAccessDenied(
+ raise exceptions.FileAccessDenied(
"File access outside of configured directory"
)
if not os.path.isfile(s):
- raise FileAccessDenied("File not readable")
- return FileGenerator(s)
+ raise exceptions.FileAccessDenied("File not readable")
+ return generators.FileGenerator(s)
def spec(self):
return "<'%s'"%self.path.encode("string_escape")
@@ -587,14 +365,15 @@ class PathodSpec(_Token):
def __init__(self, value):
self.value = value
try:
- self.parsed = Response(
- Response.expr().parseString(
+ import http
+ self.parsed = http.Response(
+ http.Response.expr().parseString(
value.val,
parseAll=True
)
)
except pp.ParseException, v:
- raise ParseException(v.msg, v.line, v.col)
+ raise exceptions.ParseException(v.msg, v.line, v.col)
@classmethod
def expr(klass):
@@ -719,7 +498,7 @@ class Code(_Component):
return e.setParseAction(lambda x: klass(*x))
def values(self, settings):
- return [LiteralGenerator(self.code)]
+ return [generators.LiteralGenerator(self.code)]
def spec(self):
return "%s"%(self.code)
@@ -956,339 +735,17 @@ class _Message(object):
Sep = pp.Optional(pp.Literal(":")).suppress()
-class _HTTPMessage(_Message):
- version = "HTTP/1.1"
- @abc.abstractmethod
- def preamble(self, settings): # pragma: no cover
- pass
-
- def values(self, settings):
- vals = self.preamble(settings)
- vals.append("\r\n")
- for h in self.headers:
- vals.extend(h.values(settings))
- vals.append("\r\n")
- if self.body:
- vals.append(self.body.value.get_generator(settings))
- return vals
-
-
-class Response(_HTTPMessage):
- comps = (
- Body,
- Header,
- PauseAt,
- DisconnectAt,
- InjectAt,
- ShortcutContentType,
- ShortcutLocation,
- Raw,
- Reason
- )
- logattrs = ["code", "reason", "version", "body"]
-
- @property
- def ws(self):
- return self.tok(WS)
-
- @property
- def code(self):
- return self.tok(Code)
-
- @property
- def reason(self):
- return self.tok(Reason)
-
- def preamble(self, settings):
- l = [self.version, " "]
- l.extend(self.code.values(settings))
- code = int(self.code.code)
- l.append(" ")
- if self.reason:
- l.extend(self.reason.values(settings))
- else:
- l.append(
- LiteralGenerator(
- http_status.RESPONSES.get(
- code,
- "Unknown code"
- )
- )
- )
- return l
-
- def resolve(self, settings, msg=None):
- tokens = self.tokens[:]
- if self.ws:
- if not settings.websocket_key:
- raise RenderError(
- "No websocket key - have we seen a client handshake?"
- )
- if not self.code:
- tokens.insert(
- 1,
- Code(101)
- )
- hdrs = websockets.server_handshake_headers(settings.websocket_key)
- for i in hdrs.lst:
- if not utils.get_header(i[0], self.headers):
- tokens.append(
- Header(ValueLiteral(i[0]), ValueLiteral(i[1]))
- )
- if not self.raw:
- if not utils.get_header("Content-Length", self.headers):
- if not self.body:
- length = 0
- else:
- length = len(self.body.value.get_generator(settings))
- tokens.append(
- Header(
- ValueLiteral("Content-Length"),
- ValueLiteral(str(length)),
- )
- )
- intermediate = self.__class__(tokens)
- return self.__class__(
- [i.resolve(settings, intermediate) for i in tokens]
- )
-
- @classmethod
- def expr(klass):
- parts = [i.expr() for i in klass.comps]
- atom = pp.MatchFirst(parts)
- resp = pp.And(
- [
- pp.MatchFirst(
- [
- WS.expr() + pp.Optional(Sep + Code.expr()),
- Code.expr(),
- ]
- ),
- pp.ZeroOrMore(Sep + atom)
- ]
- )
- resp = resp.setParseAction(klass)
- return resp
-
- def spec(self):
- return ":".join([i.spec() for i in self.tokens])
-
-
-class Request(_HTTPMessage):
- comps = (
- Body,
- Header,
- PauseAt,
- DisconnectAt,
- InjectAt,
- ShortcutContentType,
- ShortcutUserAgent,
- Raw,
- PathodSpec,
- )
- logattrs = ["method", "path", "body"]
-
- @property
- def ws(self):
- return self.tok(WS)
-
- @property
- def method(self):
- return self.tok(Method)
-
- @property
- def path(self):
- return self.tok(Path)
-
- @property
- def pathodspec(self):
- return self.tok(PathodSpec)
-
- def preamble(self, settings):
- v = self.method.values(settings)
- v.append(" ")
- v.extend(self.path.values(settings))
- if self.pathodspec:
- v.append(self.pathodspec.parsed.spec())
- v.append(" ")
- v.append(self.version)
- return v
-
- def resolve(self, settings, msg=None):
- tokens = self.tokens[:]
- if self.ws:
- if not self.method:
- tokens.insert(
- 1,
- Method("get")
- )
- for i in websockets.client_handshake_headers().lst:
- if not utils.get_header(i[0], self.headers):
- tokens.append(
- Header(ValueLiteral(i[0]), ValueLiteral(i[1]))
- )
- if not self.raw:
- if not utils.get_header("Content-Length", self.headers):
- if self.body:
- length = len(self.body.value.get_generator(settings))
- tokens.append(
- Header(
- ValueLiteral("Content-Length"),
- ValueLiteral(str(length)),
- )
- )
- if settings.request_host:
- if not utils.get_header("Host", self.headers):
- tokens.append(
- Header(
- ValueLiteral("Host"),
- ValueLiteral(settings.request_host)
- )
- )
- intermediate = self.__class__(tokens)
- return self.__class__(
- [i.resolve(settings, intermediate) for i in tokens]
- )
-
- @classmethod
- def expr(klass):
- parts = [i.expr() for i in klass.comps]
- atom = pp.MatchFirst(parts)
- resp = pp.And(
- [
- pp.MatchFirst(
- [
- WS.expr() + pp.Optional(Sep + Method.expr()),
- Method.expr(),
- ]
- ),
- Sep,
- Path.expr(),
- pp.ZeroOrMore(Sep + atom)
- ]
- )
- resp = resp.setParseAction(klass)
- return resp
-
- def spec(self):
- return ":".join([i.spec() for i in self.tokens])
-
-
-class WebsocketFrame(_Message):
- comps = (
- Body,
- PauseAt,
- DisconnectAt,
- InjectAt
- )
- logattrs = ["body"]
-
- @classmethod
- def expr(klass):
- parts = [i.expr() for i in klass.comps]
- atom = pp.MatchFirst(parts)
- resp = pp.And(
- [
- WF.expr(),
- Sep,
- pp.ZeroOrMore(Sep + atom)
- ]
- )
- resp = resp.setParseAction(klass)
- return resp
-
- def values(self, settings):
- vals = []
- if self.body:
- bodygen = self.body.value.get_generator(settings)
- length = len(self.body.value.get_generator(settings))
- else:
- bodygen = None
- length = 0
- frame = websockets.FrameHeader(
- mask = True,
- payload_length = length
- )
- vals = [frame.to_bytes()]
- if self.body:
- masker = websockets.Masker(frame.masking_key)
- vals.append(
- TransformGenerator(
- bodygen,
- masker.mask
- )
- )
- return vals
-
- def resolve(self, settings, msg=None):
- return self.__class__(
- [i.resolve(settings, msg) for i in self.tokens]
- )
-
- def spec(self):
- return ":".join([i.spec() for i in self.tokens])
-
-
-class PathodErrorResponse(Response):
- pass
-
-
-def make_error_response(reason, body=None):
- tokens = [
- Code("800"),
- Header(ValueLiteral("Content-Type"), ValueLiteral("text/plain")),
- Reason(ValueLiteral(reason)),
- Body(ValueLiteral("pathod error: " + (body or reason))),
- ]
- return PathodErrorResponse(tokens)
-
-
def read_file(settings, s):
uf = settings.get("unconstrained_file_access")
sd = settings.get("staticdir")
if not sd:
- raise FileAccessDenied("File access disabled.")
+ raise exceptions.FileAccessDenied("File access disabled.")
sd = os.path.normpath(os.path.abspath(sd))
s = s[1:]
s = os.path.expanduser(s)
s = os.path.normpath(os.path.abspath(os.path.join(sd, s)))
if not uf and not s.startswith(sd):
- raise FileAccessDenied("File access outside of configured directory")
+ raise exceptions.FileAccessDenied("File access outside of configured directory")
if not os.path.isfile(s):
- raise FileAccessDenied("File not readable")
+ raise exceptions.FileAccessDenied("File not readable")
return file(s, "rb").read()
-
-
-def parse_response(s):
- """
- May raise ParseException
- """
- try:
- s = s.decode("ascii")
- except UnicodeError:
- raise ParseException("Spec must be valid ASCII.", 0, 0)
- try:
- return Response.expr().parseString(s, parseAll=True)[0]
- except pp.ParseException, v:
- raise ParseException(v.msg, v.line, v.col)
-
-
-def parse_requests(s):
- """
- May raise ParseException
- """
- try:
- s = s.decode("ascii")
- except UnicodeError:
- raise ParseException("Spec must be valid ASCII.", 0, 0)
- try:
- return pp.OneOrMore(
- pp.Or(
- [
- WebsocketFrame.expr(),
- Request.expr(),
- ]
- )
- ).parseString(s, parseAll=True)
- except pp.ParseException, v:
- raise ParseException(v.msg, v.line, v.col)
diff --git a/libpathod/contrib/__init__.py b/libpathod/language/contrib/__init__.py
index e69de29b..e69de29b 100644
--- a/libpathod/contrib/__init__.py
+++ b/libpathod/language/contrib/__init__.py
diff --git a/libpathod/contrib/pyparsing.py b/libpathod/language/contrib/pyparsing.py
index 7dfe1043..7dfe1043 100644
--- a/libpathod/contrib/pyparsing.py
+++ b/libpathod/language/contrib/pyparsing.py
diff --git a/libpathod/language/exceptions.py b/libpathod/language/exceptions.py
new file mode 100644
index 00000000..c9d0b2f0
--- /dev/null
+++ b/libpathod/language/exceptions.py
@@ -0,0 +1,21 @@
+
+class RenderError(Exception):
+ pass
+
+
+class FileAccessDenied(RenderError):
+ pass
+
+
+class ParseException(Exception):
+ def __init__(self, msg, s, col):
+ Exception.__init__(self)
+ self.msg = msg
+ self.s = s
+ self.col = col
+
+ def marked(self):
+ return "%s\n%s"%(self.s, " " * (self.col - 1) + "^")
+
+ def __str__(self):
+ return "%s at char %s"%(self.msg, self.col)
diff --git a/libpathod/language/generators.py b/libpathod/language/generators.py
new file mode 100644
index 00000000..ae6a0530
--- /dev/null
+++ b/libpathod/language/generators.py
@@ -0,0 +1,99 @@
+import string
+import random
+import mmap
+
+DATATYPES = dict(
+ ascii_letters = string.ascii_letters,
+ ascii_lowercase = string.ascii_lowercase,
+ ascii_uppercase = string.ascii_uppercase,
+ digits = string.digits,
+ hexdigits = string.hexdigits,
+ octdigits = string.octdigits,
+ punctuation = string.punctuation,
+ whitespace = string.whitespace,
+ ascii = string.printable,
+ bytes = "".join(chr(i) for i in range(256))
+)
+
+
+class TransformGenerator:
+ """
+ Perform a byte-by-byte transform another generator - that is, for each
+ input byte, the transformation must produce one output byte.
+
+ gen: A generator to wrap
+ transform: A function (offset, data) -> transformed
+ """
+ def __init__(self, gen, transform):
+ self.gen = gen
+ self.transform = transform
+
+ def __len__(self):
+ return len(self.gen)
+
+ def __getitem__(self, x):
+ d = self.gen.__getitem__(x)
+ return self.transform(x, d)
+
+ def __getslice__(self, a, b):
+ d = self.gen.__getslice__(a, b)
+ return self.transform(a, d)
+
+ def __repr__(self):
+ return "'%s'"%self.gen
+
+
+class LiteralGenerator:
+ def __init__(self, s):
+ self.s = s
+
+ def __len__(self):
+ return len(self.s)
+
+ def __getitem__(self, x):
+ return self.s.__getitem__(x)
+
+ def __getslice__(self, a, b):
+ return self.s.__getslice__(a, b)
+
+ def __repr__(self):
+ return "'%s'"%self.s
+
+
+class RandomGenerator:
+ def __init__(self, dtype, length):
+ self.dtype = dtype
+ self.length = length
+
+ def __len__(self):
+ return self.length
+
+ def __getitem__(self, x):
+ return random.choice(DATATYPES[self.dtype])
+
+ def __getslice__(self, a, b):
+ b = min(b, self.length)
+ chars = DATATYPES[self.dtype]
+ return "".join(random.choice(chars) for x in range(a, b))
+
+ def __repr__(self):
+ return "%s random from %s"%(self.length, self.dtype)
+
+
+class FileGenerator:
+ def __init__(self, path):
+ self.path = path
+ self.fp = file(path, "rb")
+ self.map = mmap.mmap(self.fp.fileno(), 0, access=mmap.ACCESS_READ)
+
+ def __len__(self):
+ return len(self.map)
+
+ def __getitem__(self, x):
+ return self.map.__getitem__(x)
+
+ def __getslice__(self, a, b):
+ return self.map.__getslice__(a, b)
+
+ def __repr__(self):
+ return "<%s"%self.path
diff --git a/libpathod/language/http.py b/libpathod/language/http.py
new file mode 100644
index 00000000..df5d8ba8
--- /dev/null
+++ b/libpathod/language/http.py
@@ -0,0 +1,267 @@
+
+import abc
+
+import contrib.pyparsing as pp
+
+import netlib.websockets
+from netlib import http_status
+from . import base, generators, exceptions
+
+
+def get_header(val, headers):
+ """
+ Header keys may be Values, so we have to "generate" them as we try the
+ match.
+ """
+ for h in headers:
+ k = h.key.get_generator({})
+ if len(k) == len(val) and k[:].lower() == val.lower():
+ return h
+ return None
+
+
+class _HTTPMessage(base._Message):
+ version = "HTTP/1.1"
+
+ @abc.abstractmethod
+ def preamble(self, settings): # pragma: no cover
+ pass
+
+ def values(self, settings):
+ vals = self.preamble(settings)
+ vals.append("\r\n")
+ for h in self.headers:
+ vals.extend(h.values(settings))
+ vals.append("\r\n")
+ if self.body:
+ vals.append(self.body.value.get_generator(settings))
+ return vals
+
+
+class Response(_HTTPMessage):
+ comps = (
+ base.Body,
+ base.Header,
+ base.PauseAt,
+ base.DisconnectAt,
+ base.InjectAt,
+ base.ShortcutContentType,
+ base.ShortcutLocation,
+ base.Raw,
+ base.Reason
+ )
+ logattrs = ["code", "reason", "version", "body"]
+
+ @property
+ def ws(self):
+ return self.tok(base.WS)
+
+ @property
+ def code(self):
+ return self.tok(base.Code)
+
+ @property
+ def reason(self):
+ return self.tok(base.Reason)
+
+ def preamble(self, settings):
+ l = [self.version, " "]
+ l.extend(self.code.values(settings))
+ code = int(self.code.code)
+ l.append(" ")
+ if self.reason:
+ l.extend(self.reason.values(settings))
+ else:
+ l.append(
+ generators.LiteralGenerator(
+ http_status.RESPONSES.get(
+ code,
+ "Unknown code"
+ )
+ )
+ )
+ return l
+
+ def resolve(self, settings, msg=None):
+ tokens = self.tokens[:]
+ if self.ws:
+ if not settings.websocket_key:
+ raise exceptions.RenderError(
+ "No websocket key - have we seen a client handshake?"
+ )
+ if not self.code:
+ tokens.insert(
+ 1,
+ base.Code(101)
+ )
+ hdrs = netlib.websockets.server_handshake_headers(
+ settings.websocket_key
+ )
+ for i in hdrs.lst:
+ if not get_header(i[0], self.headers):
+ tokens.append(
+ base.Header(
+ base.ValueLiteral(i[0]),
+ base.ValueLiteral(i[1]))
+ )
+ if not self.raw:
+ if not get_header("Content-Length", self.headers):
+ if not self.body:
+ length = 0
+ else:
+ length = len(self.body.value.get_generator(settings))
+ tokens.append(
+ base.Header(
+ base.ValueLiteral("Content-Length"),
+ base.ValueLiteral(str(length)),
+ )
+ )
+ intermediate = self.__class__(tokens)
+ return self.__class__(
+ [i.resolve(settings, intermediate) for i in tokens]
+ )
+
+ @classmethod
+ def expr(klass):
+ parts = [i.expr() for i in klass.comps]
+ atom = pp.MatchFirst(parts)
+ resp = pp.And(
+ [
+ pp.MatchFirst(
+ [
+ base.WS.expr() + pp.Optional(
+ base.Sep + base.Code.expr()
+ ),
+ base.Code.expr(),
+ ]
+ ),
+ pp.ZeroOrMore(base.Sep + atom)
+ ]
+ )
+ resp = resp.setParseAction(klass)
+ return resp
+
+ def spec(self):
+ return ":".join([i.spec() for i in self.tokens])
+
+
+class Request(_HTTPMessage):
+ comps = (
+ base.Body,
+ base.Header,
+ base.PauseAt,
+ base.DisconnectAt,
+ base.InjectAt,
+ base.ShortcutContentType,
+ base.ShortcutUserAgent,
+ base.Raw,
+ base.PathodSpec,
+ )
+ logattrs = ["method", "path", "body"]
+
+ @property
+ def ws(self):
+ return self.tok(base.WS)
+
+ @property
+ def method(self):
+ return self.tok(base.Method)
+
+ @property
+ def path(self):
+ return self.tok(base.Path)
+
+ @property
+ def pathodspec(self):
+ return self.tok(base.PathodSpec)
+
+ def preamble(self, settings):
+ v = self.method.values(settings)
+ v.append(" ")
+ v.extend(self.path.values(settings))
+ if self.pathodspec:
+ v.append(self.pathodspec.parsed.spec())
+ v.append(" ")
+ v.append(self.version)
+ return v
+
+ def resolve(self, settings, msg=None):
+ tokens = self.tokens[:]
+ if self.ws:
+ if not self.method:
+ tokens.insert(
+ 1,
+ base.Method("get")
+ )
+ for i in netlib.websockets.client_handshake_headers().lst:
+ if not get_header(i[0], self.headers):
+ tokens.append(
+ base.Header(
+ base.ValueLiteral(i[0]),
+ base.ValueLiteral(i[1])
+ )
+ )
+ if not self.raw:
+ if not get_header("Content-Length", self.headers):
+ if self.body:
+ length = len(self.body.value.get_generator(settings))
+ tokens.append(
+ base.Header(
+ base.ValueLiteral("Content-Length"),
+ base.ValueLiteral(str(length)),
+ )
+ )
+ if settings.request_host:
+ if not get_header("Host", self.headers):
+ tokens.append(
+ base.Header(
+ base.ValueLiteral("Host"),
+ base.ValueLiteral(settings.request_host)
+ )
+ )
+ intermediate = self.__class__(tokens)
+ return self.__class__(
+ [i.resolve(settings, intermediate) for i in tokens]
+ )
+
+ @classmethod
+ def expr(klass):
+ parts = [i.expr() for i in klass.comps]
+ atom = pp.MatchFirst(parts)
+ resp = pp.And(
+ [
+ pp.MatchFirst(
+ [
+ base.WS.expr() + pp.Optional(
+ base.Sep + base.Method.expr()
+ ),
+ base.Method.expr(),
+ ]
+ ),
+ base.Sep,
+ base.Path.expr(),
+ pp.ZeroOrMore(base.Sep + atom)
+ ]
+ )
+ resp = resp.setParseAction(klass)
+ return resp
+
+ def spec(self):
+ return ":".join([i.spec() for i in self.tokens])
+
+
+class PathodErrorResponse(Response):
+ pass
+
+
+def make_error_response(reason, body=None):
+ tokens = [
+ base.Code("800"),
+ base.Header(
+ base.ValueLiteral("Content-Type"),
+ base.ValueLiteral("text/plain")
+ ),
+ base.Reason(base.ValueLiteral(reason)),
+ base.Body(base.ValueLiteral("pathod error: " + (body or reason))),
+ ]
+ return PathodErrorResponse(tokens)
diff --git a/libpathod/language/websockets.py b/libpathod/language/websockets.py
new file mode 100644
index 00000000..29b7311c
--- /dev/null
+++ b/libpathod/language/websockets.py
@@ -0,0 +1,59 @@
+
+import netlib.websockets
+import contrib.pyparsing as pp
+from . import base, generators
+
+
+class WebsocketFrame(base._Message):
+ comps = (
+ base.Body,
+ base.PauseAt,
+ base.DisconnectAt,
+ base.InjectAt
+ )
+ logattrs = ["body"]
+
+ @classmethod
+ def expr(klass):
+ parts = [i.expr() for i in klass.comps]
+ atom = pp.MatchFirst(parts)
+ resp = pp.And(
+ [
+ base.WF.expr(),
+ base.Sep,
+ pp.ZeroOrMore(base.Sep + atom)
+ ]
+ )
+ resp = resp.setParseAction(klass)
+ return resp
+
+ def values(self, settings):
+ vals = []
+ if self.body:
+ bodygen = self.body.value.get_generator(settings)
+ length = len(self.body.value.get_generator(settings))
+ else:
+ bodygen = None
+ length = 0
+ frame = netlib.websockets.FrameHeader(
+ mask = True,
+ payload_length = length
+ )
+ vals = [frame.to_bytes()]
+ if self.body:
+ masker = netlib.websockets.Masker(frame.masking_key)
+ vals.append(
+ generators.TransformGenerator(
+ bodygen,
+ masker.mask
+ )
+ )
+ return vals
+
+ def resolve(self, settings, msg=None):
+ return self.__class__(
+ [i.resolve(settings, msg) for i in self.tokens]
+ )
+
+ def spec(self):
+ return ":".join([i.spec() for i in self.tokens])
diff --git a/libpathod/language/writer.py b/libpathod/language/writer.py
new file mode 100644
index 00000000..24f4330b
--- /dev/null
+++ b/libpathod/language/writer.py
@@ -0,0 +1,61 @@
+import time
+import netlib.tcp
+
+BLOCKSIZE = 1024
+
+
+def send_chunk(fp, val, blocksize, start, end):
+ """
+ (start, end): Inclusive lower bound, exclusive upper bound.
+ """
+ for i in range(start, end, blocksize):
+ fp.write(
+ val[i:min(i + blocksize, end)]
+ )
+ return end - start
+
+
+def write_values(fp, vals, actions, sofar=0, blocksize=BLOCKSIZE):
+ """
+ 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".
+
+ Both vals and actions are in reverse order, with the first items last.
+
+ Return True if connection should disconnect.
+ """
+ sofar = 0
+ try:
+ while vals:
+ v = vals.pop()
+ offset = 0
+ while actions and actions[-1][0] < (sofar + len(v)):
+ a = actions.pop()
+ offset += send_chunk(
+ fp,
+ v,
+ blocksize,
+ offset,
+ a[0] - sofar - offset
+ )
+ if a[1] == "pause":
+ time.sleep(a[2])
+ elif a[1] == "disconnect":
+ return True
+ elif a[1] == "inject":
+ send_chunk(fp, a[2], blocksize, 0, len(a[2]))
+ send_chunk(fp, v, blocksize, offset, len(v))
+ sofar += len(v)
+ # Remainders
+ while actions:
+ a = actions.pop()
+ if a[1] == "pause":
+ time.sleep(a[2])
+ elif a[1] == "disconnect":
+ return True
+ elif a[1] == "inject":
+ send_chunk(fp, a[2], blocksize, 0, len(a[2]))
+ except netlib.tcp.NetLibDisconnect: # pragma: no cover
+ return True
diff --git a/libpathod/pathoc.py b/libpathod/pathoc.py
index 6c01a68f..e874412d 100644
--- a/libpathod/pathoc.py
+++ b/libpathod/pathoc.py
@@ -12,7 +12,8 @@ import OpenSSL.crypto
from netlib import tcp, http, certutils, websockets
import netlib.utils
-import language
+import language.http
+import language.websockets
import utils
@@ -346,7 +347,7 @@ class Pathoc(tcp.TCPClient):
"""
Performs a single request.
- r: A language.Request object, or a string representing one request.
+ r: A language.http.Request object, or a string representing one request.
Returns Response if we have a non-ignored response.
@@ -385,7 +386,7 @@ class Pathoc(tcp.TCPClient):
"""
Performs a single request.
- r: A language.Request object, or a string representing one request.
+ r: A language.http.Request object, or a string representing one request.
Returns Response if we have a non-ignored response.
@@ -393,12 +394,12 @@ class Pathoc(tcp.TCPClient):
"""
if isinstance(r, basestring):
r = language.parse_requests(r)[0]
- if isinstance(r, language.Request):
+ if isinstance(r, language.http.Request):
if r.ws:
return self.websocket_start(r, self.websocket_get_frame)
else:
return self.http(r)
- elif isinstance(r, language.WebsocketFrame):
+ elif isinstance(r, language.websockets.WebsocketFrame):
self.websocket_send_frame(r)
diff --git a/libpathod/pathod.py b/libpathod/pathod.py
index dbcb807d..4e856f10 100644
--- a/libpathod/pathod.py
+++ b/libpathod/pathod.py
@@ -8,6 +8,7 @@ from netlib import tcp, http, wsgi, certutils, websockets
import netlib.utils
from . import version, app, language, utils
+import language.http
DEFAULT_CERT_DOMAIN = "pathod.net"
@@ -75,7 +76,7 @@ class PathodHandler(tcp.BaseHandler):
crafted, self.settings
)
if error:
- err = language.make_error_response(error)
+ err = language.http.make_error_response(error)
language.serve(err, self.wfile, self.settings)
log = dict(
type="error",
@@ -83,7 +84,7 @@ class PathodHandler(tcp.BaseHandler):
)
return False, log
- if self.server.explain and not isinstance(crafted, language.PathodErrorResponse):
+ if self.server.explain and not isinstance(crafted, language.http.PathodErrorResponse):
crafted = crafted.freeze(self.settings)
self.info(">> Spec: %s" % crafted.spec())
response_log = language.serve(
@@ -212,7 +213,7 @@ class PathodHandler(tcp.BaseHandler):
crafted = language.parse_response(spec)
except language.ParseException, v:
self.info("Parse error: %s" % v.msg)
- crafted = language.make_error_response(
+ crafted = language.http.make_error_response(
"Parse Error",
"Error parsing response spec: %s\n" % v.msg + v.marked()
)
@@ -220,7 +221,7 @@ class PathodHandler(tcp.BaseHandler):
self.addlog(retlog)
return again
elif self.server.noweb:
- crafted = language.make_error_response("Access Denied")
+ crafted = language.http.make_error_response("Access Denied")
language.serve(crafted, self.wfile, self.settings)
self.addlog(dict(
type="error",
@@ -364,7 +365,8 @@ class Pathod(tcp.TCPServer):
return "File access denied.", None
if self.sizelimit and l > self.sizelimit:
return "Response too large.", None
- if self.nohang and any([isinstance(i, language.PauseAt) for i in req.actions]):
+ pauses = [isinstance(i, language.base.PauseAt) for i in req.actions]
+ if self.nohang and any(pauses):
return "Pauses have been disabled.", None
return None, req
diff --git a/libpathod/utils.py b/libpathod/utils.py
index 431ba747..e1ec013f 100644
--- a/libpathod/utils.py
+++ b/libpathod/utils.py
@@ -44,17 +44,6 @@ def parse_size(s):
raise ValueError("Invalid size specification.")
-def get_header(val, headers):
- """
- Header keys may be Values, so we have to "generate" them as we try the match.
- """
- for h in headers:
- k = h.key.get_generator({})
- if len(k) == len(val) and k[:].lower() == val.lower():
- return h
- return None
-
-
def parse_anchor_spec(s):
"""
Return a tuple, or None on error.
diff --git a/test/test_language.py b/test/test_language.py
index c0eafcaa..514c2180 100644
--- a/test/test_language.py
+++ b/test/test_language.py
@@ -1,63 +1,60 @@
import os
import cStringIO
-from libpathod import language, utils
+from libpathod import language
+from libpathod.language import generators, base, http, websockets, writer, exceptions
import tutils
language.TESTING = True
-def test_quote():
- assert language.quote("'\\\\'")
-
-
def parse_request(s):
return language.parse_requests(s)[0]
class TestWS:
def test_expr(self):
- v = language.WS("foo")
+ v = base.WS("foo")
assert v.expr()
assert v.values(language.Settings())
class TestValueNakedLiteral:
def test_expr(self):
- v = language.ValueNakedLiteral("foo")
+ v = base.ValueNakedLiteral("foo")
assert v.expr()
def test_spec(self):
- v = language.ValueNakedLiteral("foo")
+ v = base.ValueNakedLiteral("foo")
assert v.spec() == repr(v) == "foo"
- v = language.ValueNakedLiteral("f\x00oo")
+ v = base.ValueNakedLiteral("f\x00oo")
assert v.spec() == repr(v) == r"f\x00oo"
class TestValueLiteral:
def test_espr(self):
- v = language.ValueLiteral("foo")
+ v = base.ValueLiteral("foo")
assert v.expr()
assert v.val == "foo"
- v = language.ValueLiteral("foo\n")
+ v = base.ValueLiteral("foo\n")
assert v.expr()
assert v.val == "foo\n"
assert repr(v)
def test_spec(self):
- v = language.ValueLiteral("foo")
+ v = base.ValueLiteral("foo")
assert v.spec() == r"'foo'"
- v = language.ValueLiteral("f\x00oo")
+ v = base.ValueLiteral("f\x00oo")
assert v.spec() == repr(v) == r"'f\x00oo'"
- v = language.ValueLiteral("\"")
+ v = base.ValueLiteral("\"")
assert v.spec() == repr(v) == '\'"\''
def roundtrip(self, spec):
- e = language.ValueLiteral.expr()
- v = language.ValueLiteral(spec)
+ e = base.ValueLiteral.expr()
+ v = base.ValueLiteral(spec)
v2 = e.parseString(v.spec())
assert v.val == v2[0].val
assert v.spec() == v2[0].spec()
@@ -73,56 +70,56 @@ class TestValueLiteral:
class TestValueGenerate:
def test_basic(self):
- v = language.Value.parseString("@10b")[0]
+ v = base.Value.parseString("@10b")[0]
assert v.usize == 10
assert v.unit == "b"
assert v.bytes() == 10
- v = language.Value.parseString("@10")[0]
+ v = base.Value.parseString("@10")[0]
assert v.unit == "b"
- v = language.Value.parseString("@10k")[0]
+ v = base.Value.parseString("@10k")[0]
assert v.bytes() == 10240
- v = language.Value.parseString("@10g")[0]
+ v = base.Value.parseString("@10g")[0]
assert v.bytes() == 1024**3 * 10
- v = language.Value.parseString("@10g,digits")[0]
+ v = base.Value.parseString("@10g,digits")[0]
assert v.datatype == "digits"
g = v.get_generator({})
assert g[:100]
- v = language.Value.parseString("@10,digits")[0]
+ v = base.Value.parseString("@10,digits")[0]
assert v.unit == "b"
assert v.datatype == "digits"
def test_spec(self):
- v = language.ValueGenerate(1, "b", "bytes")
+ v = base.ValueGenerate(1, "b", "bytes")
assert v.spec() == repr(v) == "@1"
- v = language.ValueGenerate(1, "k", "bytes")
+ v = base.ValueGenerate(1, "k", "bytes")
assert v.spec() == repr(v) == "@1k"
- v = language.ValueGenerate(1, "k", "ascii")
+ v = base.ValueGenerate(1, "k", "ascii")
assert v.spec() == repr(v) == "@1k,ascii"
- v = language.ValueGenerate(1, "b", "ascii")
+ v = base.ValueGenerate(1, "b", "ascii")
assert v.spec() == repr(v) == "@1,ascii"
def test_freeze(self):
- v = language.ValueGenerate(100, "b", "ascii")
+ v = base.ValueGenerate(100, "b", "ascii")
f = v.freeze(language.Settings())
assert len(f.val) == 100
class TestValueFile:
def test_file_value(self):
- v = language.Value.parseString("<'one two'")[0]
+ v = base.Value.parseString("<'one two'")[0]
assert str(v)
assert v.path == "one two"
- v = language.Value.parseString("<path")[0]
+ v = base.Value.parseString("<path")[0]
assert v.path == "path"
def test_access_control(self):
- v = language.Value.parseString("<path")[0]
+ v = base.Value.parseString("<path")[0]
with tutils.tmpdir() as t:
p = os.path.join(t, "path")
with open(p, "wb") as f:
@@ -130,9 +127,9 @@ class TestValueFile:
assert v.get_generator(language.Settings(staticdir=t))
- v = language.Value.parseString("<path2")[0]
+ v = base.Value.parseString("<path2")[0]
tutils.raises(
- language.FileAccessDenied,
+ exceptions.FileAccessDenied,
v.get_generator,
language.Settings(staticdir=t)
)
@@ -142,7 +139,7 @@ class TestValueFile:
language.Settings()
)
- v = language.Value.parseString("</outside")[0]
+ v = base.Value.parseString("</outside")[0]
tutils.raises(
"outside",
v.get_generator,
@@ -150,24 +147,24 @@ class TestValueFile:
)
def test_spec(self):
- v = language.Value.parseString("<'one two'")[0]
- v2 = language.Value.parseString(v.spec())[0]
+ v = base.Value.parseString("<'one two'")[0]
+ v2 = base.Value.parseString(v.spec())[0]
assert v2.path == "one two"
def test_freeze(self):
- v = language.Value.parseString("<'one two'")[0]
+ v = base.Value.parseString("<'one two'")[0]
v2 = v.freeze({})
assert v2.path == v.path
class TestMisc:
def test_generators(self):
- v = language.Value.parseString("'val'")[0]
+ v = base.Value.parseString("'val'")[0]
g = v.get_generator({})
assert g[:] == "val"
def test_randomgenerator(self):
- g = language.RandomGenerator("bytes", 100)
+ g = generators.RandomGenerator("bytes", 100)
assert repr(g)
assert len(g[:10]) == 10
assert len(g[1:10]) == 9
@@ -176,7 +173,7 @@ class TestMisc:
assert g[0]
def test_literalgenerator(self):
- g = language.LiteralGenerator("one")
+ g = generators.LiteralGenerator("one")
assert repr(g)
assert g[:] == "one"
assert g[1] == "n"
@@ -187,7 +184,7 @@ class TestMisc:
f = open(path, "wb")
f.write("x"*10000)
f.close()
- g = language.FileGenerator(path)
+ g = generators.FileGenerator(path)
assert len(g) == 10000
assert g[0] == "x"
assert g[-1] == "x"
@@ -196,15 +193,15 @@ class TestMisc:
del g # remove all references to FileGenerator instance to close the file handle.
def test_value(self):
- assert language.Value.parseString("'val'")[0].val == "val"
- assert language.Value.parseString('"val"')[0].val == "val"
- assert language.Value.parseString('"\'val\'"')[0].val == "'val'"
+ assert base.Value.parseString("'val'")[0].val == "val"
+ assert base.Value.parseString('"val"')[0].val == "val"
+ assert base.Value.parseString('"\'val\'"')[0].val == "'val'"
def test_path(self):
- e = language.Path.expr()
+ e = base.Path.expr()
assert e.parseString('"/foo"')[0].value.val == "/foo"
- v = language.Path("/foo")
+ v = base.Path("/foo")
assert v.value.val == "/foo"
v = e.parseString("@100")[0]
@@ -217,7 +214,7 @@ class TestMisc:
assert s == v.expr().parseString(s)[0].spec()
def test_method(self):
- e = language.Method.expr()
+ e = base.Method.expr()
assert e.parseString("get")[0].value.val == "GET"
assert e.parseString("'foo'")[0].value.val == "foo"
assert e.parseString("'get'")[0].value.val == "get"
@@ -237,13 +234,13 @@ class TestMisc:
assert v2.value.val == v3.value.val
def test_raw(self):
- e = language.Raw.expr().parseString("r")[0]
+ e = base.Raw.expr().parseString("r")[0]
assert e
assert e.spec() == "r"
assert e.freeze({}).spec() == "r"
def test_body(self):
- e = language.Body.expr()
+ e = base.Body.expr()
v = e.parseString("b'foo'")[0]
assert v.value.val == "foo"
@@ -261,7 +258,7 @@ class TestMisc:
assert s == e.parseString(s)[0].spec()
def test_pathodspec(self):
- e = language.PathodSpec.expr()
+ e = base.PathodSpec.expr()
v = e.parseString("s'200'")[0]
assert v.value.val == "200"
tutils.raises(
@@ -276,8 +273,8 @@ class TestMisc:
assert "@1" not in f.spec()
def test_pathodspec_freeze(self):
- e = language.PathodSpec(
- language.ValueLiteral(
+ e = base.PathodSpec(
+ base.ValueLiteral(
"200:b'foo':i10,'\\''".encode(
"string_escape"
)
@@ -287,7 +284,7 @@ class TestMisc:
assert e.values({})
def test_code(self):
- e = language.Code.expr()
+ e = base.Code.expr()
v = e.parseString("200")[0]
assert v.string() == "200"
assert v.spec() == "200"
@@ -295,7 +292,7 @@ class TestMisc:
assert v.freeze({}).code == v.code
def test_reason(self):
- e = language.Reason.expr()
+ e = base.Reason.expr()
v = e.parseString("m'msg'")[0]
assert v.value.val == "msg"
@@ -309,13 +306,13 @@ class TestMisc:
def test_internal_response(self):
d = cStringIO.StringIO()
- s = language.make_error_response("foo")
+ s = http.make_error_response("foo")
language.serve(s, d, {})
class TestHeaders:
def test_header(self):
- e = language.Header.expr()
+ e = base.Header.expr()
v = e.parseString("h'foo'='bar'")[0]
assert v.key.val == "foo"
assert v.value.val == "bar"
@@ -328,7 +325,7 @@ class TestHeaders:
assert s == e.parseString(s)[0].spec()
def test_header_freeze(self):
- e = language.Header.expr()
+ e = base.Header.expr()
v = e.parseString("h@10=@10'")[0]
v2 = v.freeze({})
v3 = v2.freeze({})
@@ -336,7 +333,7 @@ class TestHeaders:
assert v2.value.val == v3.value.val
def test_ctype_shortcut(self):
- e = language.ShortcutContentType.expr()
+ e = base.ShortcutContentType.expr()
v = e.parseString("c'foo'")[0]
assert v.key.val == "Content-Type"
assert v.value.val == "foo"
@@ -344,14 +341,14 @@ class TestHeaders:
s = v.spec()
assert s == e.parseString(s)[0].spec()
- e = language.ShortcutContentType.expr()
+ e = base.ShortcutContentType.expr()
v = e.parseString("c@100")[0]
v2 = v.freeze({})
v3 = v2.freeze({})
assert v2.value.val == v3.value.val
def test_location_shortcut(self):
- e = language.ShortcutLocation.expr()
+ e = base.ShortcutLocation.expr()
v = e.parseString("l'foo'")[0]
assert v.key.val == "Location"
assert v.value.val == "foo"
@@ -359,7 +356,7 @@ class TestHeaders:
s = v.spec()
assert s == e.parseString(s)[0].spec()
- e = language.ShortcutLocation.expr()
+ e = base.ShortcutLocation.expr()
v = e.parseString("l@100")[0]
v2 = v.freeze({})
v3 = v2.freeze({})
@@ -375,7 +372,7 @@ class TestHeaders:
class TestShortcutUserAgent:
def test_location_shortcut(self):
- e = language.ShortcutUserAgent.expr()
+ e = base.ShortcutUserAgent.expr()
v = e.parseString("ua")[0]
assert "Android" in str(v.value)
assert v.spec() == "ua"
@@ -394,9 +391,9 @@ class TestShortcutUserAgent:
class Test_Action:
def test_cmp(self):
- a = language.DisconnectAt(0)
- b = language.DisconnectAt(1)
- c = language.DisconnectAt(0)
+ a = base.DisconnectAt(0)
+ b = base.DisconnectAt(1)
+ c = base.DisconnectAt(0)
assert a < b
assert a == c
l = [b, a]
@@ -405,16 +402,16 @@ class Test_Action:
def test_resolve(self):
r = parse_request('GET:"/foo"')
- e = language.DisconnectAt("r")
+ e = base.DisconnectAt("r")
ret = e.resolve({}, r)
assert isinstance(ret.offset, int)
def test_repr(self):
- e = language.DisconnectAt("r")
+ e = base.DisconnectAt("r")
assert repr(e)
def test_freeze(self):
- l = language.DisconnectAt(5)
+ l = base.DisconnectAt(5)
assert l.freeze({}).spec() == l.spec()
@@ -426,21 +423,21 @@ class TestDisconnects:
assert a.spec() == "dr"
def test_at(self):
- e = language.DisconnectAt.expr()
+ e = base.DisconnectAt.expr()
v = e.parseString("d0")[0]
- assert isinstance(v, language.DisconnectAt)
+ assert isinstance(v, base.DisconnectAt)
assert v.offset == 0
v = e.parseString("d100")[0]
assert v.offset == 100
- e = language.DisconnectAt.expr()
+ e = base.DisconnectAt.expr()
v = e.parseString("dr")[0]
assert v.offset == "r"
def test_spec(self):
- assert language.DisconnectAt("r").spec() == "dr"
- assert language.DisconnectAt(10).spec() == "d10"
+ assert base.DisconnectAt("r").spec() == "dr"
+ assert base.DisconnectAt(10).spec() == "d10"
class TestInject:
@@ -454,11 +451,11 @@ class TestInject:
assert a.offset == "a"
def test_at(self):
- e = language.InjectAt.expr()
+ e = base.InjectAt.expr()
v = e.parseString("i0,'foo'")[0]
assert v.value.val == "foo"
assert v.offset == 0
- assert isinstance(v, language.InjectAt)
+ assert isinstance(v, base.InjectAt)
v = e.parseString("ir,'foo'")[0]
assert v.offset == "r"
@@ -469,12 +466,12 @@ class TestInject:
assert language.serve(r, s, {})
def test_spec(self):
- e = language.InjectAt.expr()
+ e = base.InjectAt.expr()
v = e.parseString("i0,'foo'")[0]
assert v.spec() == 'i0,"foo"'
def test_spec(self):
- e = language.InjectAt.expr()
+ e = base.InjectAt.expr()
v = e.parseString("i0,@100")[0]
v2 = v.freeze({})
v3 = v2.freeze({})
@@ -483,7 +480,7 @@ class TestInject:
class TestPauses:
def test_parse_response(self):
- e = language.PauseAt.expr()
+ e = base.PauseAt.expr()
v = e.parseString("p10,10")[0]
assert v.seconds == 10
assert v.offset == 10
@@ -502,12 +499,12 @@ class TestPauses:
assert r.actions[0].spec() == "p10,10"
def test_spec(self):
- assert language.PauseAt("r", 5).spec() == "pr,5"
- assert language.PauseAt(0, 5).spec() == "p0,5"
- assert language.PauseAt(0, "f").spec() == "p0,f"
+ assert base.PauseAt("r", 5).spec() == "pr,5"
+ assert base.PauseAt(0, 5).spec() == "p0,5"
+ assert base.PauseAt(0, "f").spec() == "p0,f"
def test_freeze(self):
- l = language.PauseAt("r", 5)
+ l = base.PauseAt("r", 5)
assert l.freeze({}).spec() == l.spec()
@@ -567,7 +564,7 @@ class TestRequest:
r = language.parse_requests(l)
assert len(r) == 1
assert len(r[0].tokens) == 3
- assert isinstance(r[0].tokens[2], language.PathodSpec)
+ assert isinstance(r[0].tokens[2], base.PathodSpec)
assert r[0].values({})
def test_render(self):
@@ -625,21 +622,21 @@ class TestRequest:
r = parse_request('ws:/path/')
res = r.resolve(language.Settings())
assert res.method.string().lower() == "get"
- assert res.tok(language.Path).value.val == "/path/"
- assert res.tok(language.Method).value.val.lower() == "get"
- assert utils.get_header("Upgrade", res.headers).value.val == "websocket"
+ assert res.tok(base.Path).value.val == "/path/"
+ assert res.tok(base.Method).value.val.lower() == "get"
+ assert http.get_header("Upgrade", res.headers).value.val == "websocket"
r = parse_request('ws:put:/path/')
res = r.resolve(language.Settings())
assert r.method.string().lower() == "put"
- assert res.tok(language.Path).value.val == "/path/"
- assert res.tok(language.Method).value.val.lower() == "put"
- assert utils.get_header("Upgrade", res.headers).value.val == "websocket"
+ assert res.tok(base.Path).value.val == "/path/"
+ assert res.tok(base.Method).value.val.lower() == "put"
+ assert http.get_header("Upgrade", res.headers).value.val == "websocket"
class TestWebsocketFrame:
def test_spec(self):
- e = language.WebsocketFrame.expr()
+ e = websockets.WebsocketFrame.expr()
wf = e.parseString("wf:b'foo'")
assert wf
@@ -656,45 +653,45 @@ class TestWriteValues:
v = "foobarfoobar"
for bs in range(1, len(v) + 2):
s = cStringIO.StringIO()
- language.send_chunk(s, v, bs, 0, len(v))
+ writer.send_chunk(s, v, bs, 0, len(v))
assert s.getvalue() == v
for start in range(len(v)):
for end in range(len(v)):
s = cStringIO.StringIO()
- language.send_chunk(s, v, bs, start, end)
+ writer.send_chunk(s, v, bs, start, end)
assert s.getvalue() == v[start:end]
def test_write_values_inject(self):
tst = "foo"
s = cStringIO.StringIO()
- language.write_values(s, [tst], [(0, "inject", "aaa")], blocksize=5)
+ writer.write_values(s, [tst], [(0, "inject", "aaa")], blocksize=5)
assert s.getvalue() == "aaafoo"
s = cStringIO.StringIO()
- language.write_values(s, [tst], [(1, "inject", "aaa")], blocksize=5)
+ writer.write_values(s, [tst], [(1, "inject", "aaa")], blocksize=5)
assert s.getvalue() == "faaaoo"
s = cStringIO.StringIO()
- language.write_values(s, [tst], [(1, "inject", "aaa")], blocksize=5)
+ writer.write_values(s, [tst], [(1, "inject", "aaa")], blocksize=5)
assert s.getvalue() == "faaaoo"
def test_write_values_disconnects(self):
s = cStringIO.StringIO()
tst = "foo" * 100
- language.write_values(s, [tst], [(0, "disconnect")], blocksize=5)
+ writer.write_values(s, [tst], [(0, "disconnect")], blocksize=5)
assert not s.getvalue()
def test_write_values(self):
tst = "foobarvoing"
s = cStringIO.StringIO()
- language.write_values(s, [tst], [])
+ writer.write_values(s, [tst], [])
assert s.getvalue() == tst
for bs in range(1, len(tst) + 2):
for off in range(len(tst)):
s = cStringIO.StringIO()
- language.write_values(
+ writer.write_values(
s, [tst], [(off, "disconnect")], blocksize=bs
)
assert s.getvalue() == tst[:off]
@@ -703,20 +700,20 @@ class TestWriteValues:
tst = "".join(str(i) for i in range(10))
for i in range(2, 10):
s = cStringIO.StringIO()
- language.write_values(
+ writer.write_values(
s, [tst], [(2, "pause", 0), (1, "pause", 0)], blocksize=i
)
assert s.getvalue() == tst
for i in range(2, 10):
s = cStringIO.StringIO()
- language.write_values(s, [tst], [(1, "pause", 0)], blocksize=i)
+ writer.write_values(s, [tst], [(1, "pause", 0)], blocksize=i)
assert s.getvalue() == tst
tst = ["".join(str(i) for i in range(10))] * 5
for i in range(2, 10):
s = cStringIO.StringIO()
- language.write_values(s, tst[:], [(1, "pause", 0)], blocksize=i)
+ writer.write_values(s, tst[:], [(1, "pause", 0)], blocksize=i)
assert s.getvalue() == "".join(tst)
def test_write_values_after(self):
@@ -816,7 +813,7 @@ class TestResponse:
def test_parse_header(self):
r = language.parse_response('400:h"foo"="bar"')
- assert utils.get_header("foo", r.headers)
+ assert http.get_header("foo", r.headers)
def test_parse_pause_before(self):
r = language.parse_response("400:p0,10")
@@ -854,28 +851,28 @@ class TestResponse:
def test_read_file():
- tutils.raises(language.FileAccessDenied, language.read_file, {}, "=/foo")
+ tutils.raises(exceptions.FileAccessDenied, base.read_file, {}, "=/foo")
p = tutils.test_data.path("data")
d = dict(staticdir=p)
- assert language.read_file(d, "+./file").strip() == "testfile"
- assert language.read_file(d, "+file").strip() == "testfile"
+ assert base.read_file(d, "+./file").strip() == "testfile"
+ assert base.read_file(d, "+file").strip() == "testfile"
tutils.raises(
- language.FileAccessDenied,
- language.read_file,
+ exceptions.FileAccessDenied,
+ base.read_file,
d,
"+./nonexistent"
)
tutils.raises(
- language.FileAccessDenied,
- language.read_file,
+ exceptions.FileAccessDenied,
+ base.read_file,
d,
"+/nonexistent"
)
tutils.raises(
- language.FileAccessDenied,
- language.read_file,
+ exceptions.FileAccessDenied,
+ base.read_file,
d,
"+../test_language.py"
)
d["unconstrained_file_access"] = True
- assert language.read_file(d, "+../test_language.py")
+ assert base.read_file(d, "+../test_language.py")