aboutsummaryrefslogtreecommitdiffstats
path: root/pathod/language/base.py
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2016-02-18 13:03:40 +0100
committerMaximilian Hils <git@maximilianhils.com>2016-02-18 13:03:40 +0100
commitd33d3663ecb166461d9cb5a78a29b44ee7a8fbb7 (patch)
treefe8856f65d1dafa946150c5acbaf6e942ba3c026 /pathod/language/base.py
parent294774d6f0dee95b02a93307ec493b111b7f171e (diff)
downloadmitmproxy-d33d3663ecb166461d9cb5a78a29b44ee7a8fbb7.tar.gz
mitmproxy-d33d3663ecb166461d9cb5a78a29b44ee7a8fbb7.tar.bz2
mitmproxy-d33d3663ecb166461d9cb5a78a29b44ee7a8fbb7.zip
combine projects
Diffstat (limited to 'pathod/language/base.py')
-rw-r--r--pathod/language/base.py576
1 files changed, 576 insertions, 0 deletions
diff --git a/pathod/language/base.py b/pathod/language/base.py
new file mode 100644
index 00000000..a4302998
--- /dev/null
+++ b/pathod/language/base.py
@@ -0,0 +1,576 @@
+import operator
+import os
+import abc
+import pyparsing as pp
+
+from .. import utils
+from . import generators, exceptions
+
+class Settings(object):
+
+ def __init__(
+ self,
+ is_client=False,
+ staticdir=None,
+ unconstrained_file_access=False,
+ request_host=None,
+ websocket_key=None,
+ protocol=None,
+ ):
+ self.is_client = is_client
+ self.staticdir = staticdir
+ self.unconstrained_file_access = unconstrained_file_access
+ self.request_host = request_host
+ self.websocket_key = websocket_key # TODO: refactor this into the protocol
+ self.protocol = protocol
+
+
+Sep = pp.Optional(pp.Literal(":")).suppress()
+
+
+v_integer = pp.Word(pp.nums)\
+ .setName("integer")\
+ .setParseAction(lambda toks: int(toks[0]))
+
+
+v_literal = pp.MatchFirst(
+ [
+ pp.QuotedString(
+ "\"",
+ unquoteResults=True,
+ multiline=True
+ ),
+ pp.QuotedString(
+ "'",
+ unquoteResults=True,
+ multiline=True
+ ),
+ ]
+)
+
+v_naked_literal = pp.MatchFirst(
+ [
+ v_literal,
+ pp.Word("".join(i for i in pp.printables if i not in ",:\n@\'\""))
+ ]
+)
+
+
+class Token(object):
+
+ """
+ A token in the specification language. Tokens are immutable. The token
+ classes have no meaning in and of themselves, and are combined into
+ Components and Actions to build the language.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ @classmethod
+ def expr(cls): # pragma: no cover
+ """
+ A parse expression.
+ """
+ return None
+
+ @abc.abstractmethod
+ def spec(self): # pragma: no cover
+ """
+ A parseable specification for this token.
+ """
+ return None
+
+ @property
+ def unique_name(self):
+ """
+ Controls uniqueness constraints for tokens. No two tokens with the
+ same name will be allowed. If no uniquness should be applied, this
+ should be None.
+ """
+ return self.__class__.__name__.lower()
+
+ def resolve(self, settings_, msg_):
+ """
+ Resolves this token to ready it for transmission. This means that
+ the calculated offsets of actions are fixed.
+
+ settings: a language.Settings instance
+ msg: The containing message
+ """
+ return self
+
+ def __repr__(self):
+ return self.spec()
+
+
+class _TokValueLiteral(Token):
+
+ def __init__(self, val):
+ self.val = val.decode("string_escape")
+
+ def get_generator(self, settings_):
+ return self.val
+
+ def freeze(self, settings_):
+ return self
+
+
+class TokValueLiteral(_TokValueLiteral):
+
+ """
+ A literal with Python-style string escaping
+ """
+ @classmethod
+ def expr(cls):
+ e = v_literal.copy()
+ return e.setParseAction(cls.parseAction)
+
+ @classmethod
+ def parseAction(cls, x):
+ v = cls(*x)
+ return v
+
+ def spec(self):
+ inner = self.val.encode("string_escape")
+ inner = inner.replace(r"\'", r"\x27")
+ return "'" + inner + "'"
+
+
+class TokValueNakedLiteral(_TokValueLiteral):
+
+ @classmethod
+ def expr(cls):
+ e = v_naked_literal.copy()
+ return e.setParseAction(lambda x: cls(*x))
+
+ def spec(self):
+ return self.val.encode("string_escape")
+
+
+class TokValueGenerate(Token):
+
+ def __init__(self, usize, unit, datatype):
+ if not unit:
+ unit = "b"
+ self.usize, self.unit, self.datatype = usize, unit, datatype
+
+ def bytes(self):
+ return self.usize * utils.SIZE_UNITS[self.unit]
+
+ def get_generator(self, settings_):
+ return generators.RandomGenerator(self.datatype, self.bytes())
+
+ def freeze(self, settings):
+ g = self.get_generator(settings)
+ return TokValueLiteral(g[:].encode("string_escape"))
+
+ @classmethod
+ def expr(cls):
+ e = pp.Literal("@").suppress() + v_integer
+
+ u = reduce(
+ operator.or_,
+ [pp.Literal(i) for i in utils.SIZE_UNITS.keys()]
+ ).leaveWhitespace()
+ e = e + pp.Optional(u, default=None)
+
+ s = pp.Literal(",").suppress()
+ s += reduce(
+ operator.or_,
+ [pp.Literal(i) for i in generators.DATATYPES.keys()]
+ )
+ e += pp.Optional(s, default="bytes")
+ return e.setParseAction(lambda x: cls(*x))
+
+ def spec(self):
+ s = "@%s" % self.usize
+ if self.unit != "b":
+ s += self.unit
+ if self.datatype != "bytes":
+ s += ",%s" % self.datatype
+ return s
+
+
+class TokValueFile(Token):
+
+ def __init__(self, path):
+ self.path = str(path)
+
+ @classmethod
+ def expr(cls):
+ e = pp.Literal("<").suppress()
+ e = e + v_naked_literal
+ return e.setParseAction(lambda x: cls(*x))
+
+ def freeze(self, settings_):
+ return self
+
+ def get_generator(self, settings):
+ if not settings.staticdir:
+ 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 exceptions.FileAccessDenied(
+ "File access outside of configured directory"
+ )
+ if not os.path.isfile(s):
+ raise exceptions.FileAccessDenied("File not readable")
+ return generators.FileGenerator(s)
+
+ def spec(self):
+ return "<'%s'" % self.path.encode("string_escape")
+
+
+TokValue = pp.MatchFirst(
+ [
+ TokValueGenerate.expr(),
+ TokValueFile.expr(),
+ TokValueLiteral.expr()
+ ]
+)
+
+
+TokNakedValue = pp.MatchFirst(
+ [
+ TokValueGenerate.expr(),
+ TokValueFile.expr(),
+ TokValueLiteral.expr(),
+ TokValueNakedLiteral.expr(),
+ ]
+)
+
+
+TokOffset = pp.MatchFirst(
+ [
+ v_integer,
+ pp.Literal("r"),
+ pp.Literal("a")
+ ]
+)
+
+
+class _Component(Token):
+
+ """
+ A value component of the primary specification of an message.
+ Components produce byte values desribe the bytes of the message.
+ """
+
+ def values(self, settings): # pragma: no cover
+ """
+ A sequence of values, which can either be strings or generators.
+ """
+ pass
+
+ def string(self, settings=None):
+ """
+ A string representation of the object.
+ """
+ return "".join(i[:] for i in self.values(settings or {}))
+
+
+class KeyValue(_Component):
+
+ """
+ A key/value pair.
+ cls.preamble: leader
+ """
+
+ def __init__(self, key, value):
+ self.key, self.value = key, value
+
+ @classmethod
+ def expr(cls):
+ e = pp.Literal(cls.preamble).suppress()
+ e += TokValue
+ e += pp.Literal("=").suppress()
+ e += TokValue
+ return e.setParseAction(lambda x: cls(*x))
+
+ def spec(self):
+ return "%s%s=%s" % (self.preamble, self.key.spec(), self.value.spec())
+
+ def freeze(self, settings):
+ return self.__class__(
+ self.key.freeze(settings), self.value.freeze(settings)
+ )
+
+
+class CaselessLiteral(_Component):
+
+ """
+ A caseless token that can take only one value.
+ """
+
+ def __init__(self, value):
+ self.value = value
+
+ @classmethod
+ def expr(cls):
+ spec = pp.CaselessLiteral(cls.TOK)
+ spec = spec.setParseAction(lambda x: cls(*x))
+ return spec
+
+ def values(self, settings):
+ return self.TOK
+
+ def spec(self):
+ return self.TOK
+
+ def freeze(self, settings_):
+ return self
+
+
+class OptionsOrValue(_Component):
+
+ """
+ Can be any of a specified set of options, or a value specifier.
+ """
+ preamble = ""
+ options = []
+
+ def __init__(self, value):
+ # If it's a string, we were passed one of the options, so we lower-case
+ # it to be canonical. The user can specify a different case by using a
+ # string value literal.
+ self.option_used = False
+ if isinstance(value, basestring):
+ for i in self.options:
+ # Find the exact option value in a case-insensitive way
+ if i.lower() == value.lower():
+ self.option_used = True
+ value = TokValueLiteral(i)
+ break
+ self.value = value
+
+ @classmethod
+ def expr(cls):
+ parts = [pp.CaselessLiteral(i) for i in cls.options]
+ m = pp.MatchFirst(parts)
+ spec = m | TokValue.copy()
+ spec = spec.setParseAction(lambda x: cls(*x))
+ if cls.preamble:
+ spec = pp.Literal(cls.preamble).suppress() + spec
+ return spec
+
+ def values(self, settings):
+ return [
+ self.value.get_generator(settings)
+ ]
+
+ def spec(self):
+ s = self.value.spec()
+ if s[1:-1].lower() in self.options:
+ s = s[1:-1].lower()
+ return "%s%s" % (self.preamble, s)
+
+ def freeze(self, settings):
+ return self.__class__(self.value.freeze(settings))
+
+
+class Integer(_Component):
+ bounds = (None, None)
+ preamble = ""
+
+ def __init__(self, value):
+ v = int(value)
+ outofbounds = any([
+ self.bounds[0] is not None and v < self.bounds[0],
+ self.bounds[1] is not None and v > self.bounds[1]
+ ])
+ if outofbounds:
+ raise exceptions.ParseException(
+ "Integer value must be between %s and %s." % self.bounds,
+ 0, 0
+ )
+ self.value = str(value)
+
+ @classmethod
+ def expr(cls):
+ e = v_integer.copy()
+ if cls.preamble:
+ e = pp.Literal(cls.preamble).suppress() + e
+ return e.setParseAction(lambda x: cls(*x))
+
+ def values(self, settings):
+ return self.value
+
+ def spec(self):
+ return "%s%s" % (self.preamble, self.value)
+
+ def freeze(self, settings_):
+ return self
+
+
+class Value(_Component):
+
+ """
+ A value component lead by an optional preamble.
+ """
+ preamble = ""
+
+ def __init__(self, value):
+ self.value = value
+
+ @classmethod
+ def expr(cls):
+ e = (TokValue | TokNakedValue)
+ if cls.preamble:
+ e = pp.Literal(cls.preamble).suppress() + e
+ return e.setParseAction(lambda x: cls(*x))
+
+ def values(self, settings):
+ return [self.value.get_generator(settings)]
+
+ def spec(self):
+ return "%s%s" % (self.preamble, self.value.spec())
+
+ def freeze(self, settings):
+ return self.__class__(self.value.freeze(settings))
+
+
+class FixedLengthValue(Value):
+
+ """
+ A value component lead by an optional preamble.
+ """
+ preamble = ""
+ length = None
+
+ def __init__(self, value):
+ Value.__init__(self, value)
+ lenguess = None
+ try:
+ lenguess = len(value.get_generator(Settings()))
+ except exceptions.RenderError:
+ pass
+ # This check will fail if we know the length upfront
+ if lenguess is not None and lenguess != self.length:
+ raise exceptions.RenderError(
+ "Invalid value length: '%s' is %s bytes, should be %s." % (
+ self.spec(),
+ lenguess,
+ self.length
+ )
+ )
+
+ def values(self, settings):
+ ret = Value.values(self, settings)
+ l = sum(len(i) for i in ret)
+ # This check will fail if we don't know the length upfront - i.e. for
+ # file inputs
+ if l != self.length:
+ raise exceptions.RenderError(
+ "Invalid value length: '%s' is %s bytes, should be %s." % (
+ self.spec(),
+ l,
+ self.length
+ )
+ )
+ return ret
+
+
+class Boolean(_Component):
+
+ """
+ A boolean flag.
+ name = true
+ -name = false
+ """
+ name = ""
+
+ def __init__(self, value):
+ self.value = value
+
+ @classmethod
+ def expr(cls):
+ e = pp.Optional(pp.Literal("-"), default=True)
+ e += pp.Literal(cls.name).suppress()
+
+ def parse(s_, loc_, toks):
+ val = True
+ if toks[0] == "-":
+ val = False
+ return cls(val)
+
+ return e.setParseAction(parse)
+
+ def spec(self):
+ return "%s%s" % ("-" if not self.value else "", self.name)
+
+
+class IntField(_Component):
+
+ """
+ An integer field, where values can optionally specified by name.
+ """
+ names = {}
+ max = 16
+ preamble = ""
+
+ def __init__(self, value):
+ self.origvalue = value
+ self.value = self.names.get(value, value)
+ if self.value > self.max:
+ raise exceptions.ParseException(
+ "Value can't exceed %s" % self.max, 0, 0
+ )
+
+ @classmethod
+ def expr(cls):
+ parts = [pp.CaselessLiteral(i) for i in cls.names.keys()]
+ m = pp.MatchFirst(parts)
+ spec = m | v_integer.copy()
+ spec = spec.setParseAction(lambda x: cls(*x))
+ if cls.preamble:
+ spec = pp.Literal(cls.preamble).suppress() + spec
+ return spec
+
+ def values(self, settings):
+ return [str(self.value)]
+
+ def spec(self):
+ return "%s%s" % (self.preamble, self.origvalue)
+
+
+class NestedMessage(Token):
+
+ """
+ A nested message, as an escaped string with a preamble.
+ """
+ preamble = ""
+ nest_type = None
+
+ def __init__(self, value):
+ Token.__init__(self)
+ self.value = value
+ try:
+ self.parsed = self.nest_type(
+ self.nest_type.expr().parseString(
+ value.val,
+ parseAll=True
+ )
+ )
+ except pp.ParseException as v:
+ raise exceptions.ParseException(v.msg, v.line, v.col)
+
+ @classmethod
+ def expr(cls):
+ e = pp.Literal(cls.preamble).suppress()
+ e = e + TokValueLiteral.expr()
+ return e.setParseAction(lambda x: cls(*x))
+
+ def values(self, settings):
+ return [
+ self.value.get_generator(settings),
+ ]
+
+ def spec(self):
+ return "%s%s" % (self.preamble, self.value.spec())
+
+ def freeze(self, settings):
+ f = self.parsed.freeze(settings).spec()
+ return self.__class__(TokValueLiteral(f.encode("string_escape")))