diff --git a/libpathod/language.py b/libpathod/language.py index e66f987fc..d4c5b880e 100644 --- a/libpathod/language.py +++ b/libpathod/language.py @@ -267,7 +267,7 @@ class _Token(object): """ return None - def resolve(self, msg, settings): + def resolve(self, settings, msg): """ Resolves this token to ready it for transmission. This means that the calculated offsets of actions are fixed. @@ -371,8 +371,6 @@ class ValueFile(_Token): def get_generator(self, settings): if not settings.staticdir: raise FileAccessDenied("File access disabled.") - sd = os.path.normpath(os.path.abspath(settings.staticdir)) - s = os.path.expanduser(self.path) s = os.path.normpath( os.path.abspath(os.path.join(settings.staticdir, s)) @@ -613,26 +611,34 @@ class Path(_Component): return Path(self.value.freeze(settings)) -class WS(_Component): +class _Token(_Component): def __init__(self, value): self.value = value @classmethod def expr(klass): - spec = pp.CaselessLiteral("ws") + spec = pp.CaselessLiteral(klass.TOK) spec = spec.setParseAction(lambda x: klass(*x)) return spec def values(self, settings): - return "ws" + return self.TOK def spec(self): - return "ws" + return self.TOK def freeze(self, settings): return self +class WS(_Token): + TOK = "ws" + + +class WF(_Token): + TOK = "wf" + + class Method(_Component): methods = [ "get", @@ -724,7 +730,7 @@ class _Action(_Token): def __init__(self, offset): self.offset = offset - def resolve(self, msg, settings): + def resolve(self, settings, msg): """ Resolves offset specifications to a numeric offset. Returns a copy of the action object. @@ -889,10 +895,6 @@ class _Message(object): l += len(i.value.get_generator(settings)) return l - @abc.abstractmethod - def preamble(self, settings): # pragma: no cover - pass - @classmethod def expr(klass): # pragma: no cover pass @@ -929,6 +931,10 @@ 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) @@ -985,7 +991,7 @@ class Response(_HTTPMessage): ) return l - def resolve(self, settings): + def resolve(self, settings, msg=None): tokens = self.tokens[:] if self.ws: if not settings.websocket_key: @@ -1017,7 +1023,7 @@ class Response(_HTTPMessage): ) intermediate = self.__class__(tokens) return self.__class__( - [i.resolve(intermediate, settings) for i in tokens] + [i.resolve(settings, intermediate) for i in tokens] ) @classmethod @@ -1035,6 +1041,7 @@ class Response(_HTTPMessage): pp.ZeroOrMore(Sep + atom) ] ) + resp = resp.setParseAction(klass) return resp def spec(self): @@ -1081,7 +1088,7 @@ class Request(_HTTPMessage): v.append(self.version) return v - def resolve(self, settings): + def resolve(self, settings, msg=None): tokens = self.tokens[:] if self.ws: if not self.method: @@ -1114,7 +1121,7 @@ class Request(_HTTPMessage): ) intermediate = self.__class__(tokens) return self.__class__( - [i.resolve(intermediate, settings) for i in tokens] + [i.resolve(settings, intermediate) for i in tokens] ) @classmethod @@ -1134,6 +1141,7 @@ class Request(_HTTPMessage): pp.ZeroOrMore(Sep + atom) ] ) + resp = resp.setParseAction(klass) return resp def spec(self): @@ -1155,13 +1163,27 @@ class WebsocketFrame(_Message): atom = pp.MatchFirst(parts) resp = pp.And( [ - pp.Literal("wf"), + WF.expr(), Sep, pp.ZeroOrMore(Sep + atom) ] ) + resp = resp.setParseAction(klass) return resp + def values(self, settings): + vals = [ + websockets.FrameHeader().to_bytes() + ] + if self.body: + vals.append(self.body.value.get_generator(settings)) + 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]) @@ -1205,7 +1227,7 @@ def parse_response(s): except UnicodeError: raise ParseException("Spec must be valid ASCII.", 0, 0) try: - return Response(Response.expr().parseString(s, parseAll=True)) + return Response.expr().parseString(s, parseAll=True)[0] except pp.ParseException, v: raise ParseException(v.msg, v.line, v.col) @@ -1219,16 +1241,13 @@ def parse_requests(s): except UnicodeError: raise ParseException("Spec must be valid ASCII.", 0, 0) try: - parts = pp.OneOrMore( - pp.Group( - pp.Or( - [ - Request.expr(), - WebsocketFrame.expr(), - ] - ) + return pp.OneOrMore( + pp.Or( + [ + WebsocketFrame.expr(), + Request.expr(), + ] ) ).parseString(s, parseAll=True) - return [Request(i) for i in parts] except pp.ParseException, v: raise ParseException(v.msg, v.line, v.col) diff --git a/libpathod/pathoc.py b/libpathod/pathoc.py index cf9be5b9d..89a8280bb 100644 --- a/libpathod/pathoc.py +++ b/libpathod/pathoc.py @@ -3,10 +3,11 @@ import os import hashlib import random import time +import threading import OpenSSL.crypto -from netlib import tcp, http, certutils +from netlib import tcp, http, certutils, websockets import netlib.utils import language @@ -75,6 +76,17 @@ class Response: return "Response(%s - %s)"%(self.status_code, self.msg) +class WebsocketFrameReader(threading.Thread): + def __init__(self, rfile, callback): + threading.Thread.__init__(self) + self.rfile, self.callback = rfile, callback + self.daemon = True + + def run(self): + while 1: + print websockets.Frame.from_file(self.rfile) + + class Pathoc(tcp.TCPClient): def __init__( self, @@ -184,13 +196,64 @@ class Pathoc(tcp.TCPClient): print >> fp, "%s (unprintables escaped):"%header print >> fp, netlib.utils.cleanBin(data) - def request(self, r): + def websocket_get_frame(self, frame): + """ + Called when a frame is received from the server. + """ + pass + + def websocket_send_frame(self, r): + """ + Sends a single websocket frame. + """ + req = None + if isinstance(r, basestring): + r = language.parse_requests(r)[0] + if self.showreq: + self.wfile.start_log() + try: + req = language.serve(r, self.wfile, self.settings) + self.wfile.flush() + except tcp.NetLibTimeout: + if self.ignoretimeout: + return None + if self.showsummary: + print >> self.fp, "<<", "Timeout" + raise + except tcp.NetLibDisconnect: # pragma: nocover + if self.showsummary: + print >> self.fp, "<<", "Disconnect" + raise + finally: + if req: + if self.explain: + print >> self.fp, ">> Spec:", r.spec() + if self.showreq: + self._show( + self.fp, ">> Request", + self.wfile.get_log(), + self.hexdump + ) + + def websocket_start(self, r, callback=None): + """ + Performs an HTTP request, and attempts to drop into websocket + connection. + """ + resp = self.http(r) + if resp.status_code == 101: + if self.showsummary: + print >> self.fp, "Websocket connection established..." + WebsocketFrameReader(self.rfile, self.websocket_get_frame).start() + return resp + + def http(self, r): """ Performs a single request. r: A language.Request object, or a string representing one request. - Returns True if we have a non-ignored response. + Returns Response if we have a non-ignored response. May raise http.HTTPError, tcp.NetLibError """ @@ -253,6 +316,26 @@ class Pathoc(tcp.TCPClient): ) return resp + def request(self, r): + """ + Performs a single request. + + r: A language.Request object, or a string representing one request. + + Returns Response if we have a non-ignored response. + + May raise http.HTTPError, tcp.NetLibError + """ + if isinstance(r, basestring): + r = language.parse_requests(r)[0] + if isinstance(r, language.Request): + if r.ws: + return self.websocket_start(r, self.websocket_get_frame) + else: + return self.http(r) + elif isinstance(r, language.WebsocketFrame): + self.websocket_send_frame(r) + def main(args): # pragma: nocover memo = set([]) diff --git a/test/test_language.py b/test/test_language.py index 919f5f654..0fb8479d4 100644 --- a/test/test_language.py +++ b/test/test_language.py @@ -406,7 +406,7 @@ class Test_Action: def test_resolve(self): r = parse_request('GET:"/foo"') e = language.DisconnectAt("r") - ret = e.resolve(r, {}) + ret = e.resolve({}, r) assert isinstance(ret.offset, int) def test_repr(self): @@ -637,12 +637,18 @@ class TestRequest: assert utils.get_header("Upgrade", res.headers).value.val == "websocket" - class TestWebsocketFrame: def test_spec(self): e = language.WebsocketFrame.expr() - assert e.parseString("wf:foo") + wf = e.parseString("wf:b'foo'") + assert wf + + assert parse_request("wf:b'foo'") + + def test_values(self): + r = parse_request("wf:b'foo'") + assert r.values(language.Settings()) class TestWriteValues: