diff options
-rw-r--r-- | libpathod/language/__init__.py | 88 | ||||
-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.py | 21 | ||||
-rw-r--r-- | libpathod/language/generators.py | 99 | ||||
-rw-r--r-- | libpathod/language/http.py | 267 | ||||
-rw-r--r-- | libpathod/language/websockets.py | 59 | ||||
-rw-r--r-- | libpathod/language/writer.py | 61 | ||||
-rw-r--r-- | libpathod/pathoc.py | 11 | ||||
-rw-r--r-- | libpathod/pathod.py | 12 | ||||
-rw-r--r-- | libpathod/utils.py | 11 | ||||
-rw-r--r-- | test/test_language.py | 219 |
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") |