diff options
Diffstat (limited to 'libpathod/language/http.py')
-rw-r--r-- | libpathod/language/http.py | 267 |
1 files changed, 267 insertions, 0 deletions
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) |