diff --git a/.travis.yml b/.travis.yml index 94c559e..16a86d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - "3.4" - "3.5" install: - - pip install . Twisted pyflakes + - pip install . "Twisted>=15.5.0" pyflakes script: - pyflakes src - trial wormhole diff --git a/README.md b/README.md index 1ce1431..d24940c 100644 --- a/README.md +++ b/README.md @@ -137,9 +137,9 @@ using a key derived from the PAKE phase. See This library is released under the MIT license, see LICENSE for details. This library is compatible with python2.7, 3.3, 3.4, and 3.5 . It is probably -compatible with py2.6, but the latest Twisted (15.5.0) is not. The async -support does not yet work with py3, but will in the future once Twisted -itself is finished being ported. +compatible with py2.6, but the latest Twisted (15.5.0) is not. The +(daemonizing) 'wormhole server start' command does not yet work with py3, but +will in the future once Twisted itself is finished being ported. This package depends upon the SPAKE2, pynacl, requests, and argparse libraries. To run a relay server, use the async support, or run the unit diff --git a/setup.py b/setup.py index 216da9c..1f9407c 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,8 @@ setup(name="magic-wormhole", ["wormhole = wormhole.scripts.runner:entry"]}, install_requires=["spake2==0.3", "pynacl", "requests", "argparse", "six"], + # for Twisted support, we want Twisted>=15.5.0. Older Twisteds don't + # provide sufficient python3 compatibility. test_suite="wormhole.test", cmdclass=commands, ) diff --git a/src/wormhole/test/test_interop.py b/src/wormhole/test/test_interop.py index 7a8bab6..23daa9c 100644 --- a/src/wormhole/test/test_interop.py +++ b/src/wormhole/test/test_interop.py @@ -1,5 +1,4 @@ from __future__ import print_function -import sys from twisted.trial import unittest from twisted.internet.defer import gatherResults from twisted.internet.threads import deferToThread @@ -56,9 +55,3 @@ class Basic(ServerBase, unittest.TestCase): return self.doBoth([bw.close], tw.close()) d.addCallback(_done) return d - -if sys.version_info[0] >= 3: - Basic.skip = "twisted is not yet sufficiently ported to py3" - # as of 15.4.0, Twisted is still missing: - # * web.client.Agent (for all non-EventSource POSTs in transcribe.py) - # * python.logfile (to allow daemonization of 'wormhole server') diff --git a/src/wormhole/test/test_server.py b/src/wormhole/test/test_server.py index 0073f57..989de29 100644 --- a/src/wormhole/test/test_server.py +++ b/src/wormhole/test/test_server.py @@ -1,5 +1,5 @@ from __future__ import print_function -import sys, json +import json import requests from six.moves.urllib_parse import urlencode from twisted.trial import unittest @@ -14,7 +14,7 @@ from ..twisted.eventsource_twisted import EventSource class Reachable(ServerBase, unittest.TestCase): def test_getPage(self): - # client.getPage requires str/unicode URL, returns bytes + # client.getPage requires bytes URL, returns bytes url = self.relayurl.replace("wormhole-relay/", "").encode("ascii") d = getPage(url) def _got(res): @@ -23,14 +23,9 @@ class Reachable(ServerBase, unittest.TestCase): return d def test_agent(self): - # client.Agent is not yet ported: it wants URLs to be both unicode - # and bytes at the same time. - # https://twistedmatrix.com/trac/ticket/7407 - if sys.version_info[0] > 2: - raise unittest.SkipTest("twisted.web.client.Agent does not yet support py3") url = self.relayurl.replace("wormhole-relay/", "").encode("ascii") agent = Agent(reactor) - d = agent.request("GET", url) + d = agent.request(b"GET", url) def _check(resp): self.failUnlessEqual(resp.code, 200) return readBody(resp) @@ -281,9 +276,6 @@ class API(ServerBase, unittest.TestCase): return self._do_watch("watch") def _do_watch(self, endpoint_name): - if sys.version_info[0] >= 3: - raise unittest.SkipTest("twisted vs py3") - d = self.post("allocate", {"appid": "app1", "side": "abc"}) def _allocated(data): self.cid = data["channelid"] diff --git a/src/wormhole/test/test_twisted.py b/src/wormhole/test/test_twisted.py index 920522c..688b7a9 100644 --- a/src/wormhole/test/test_twisted.py +++ b/src/wormhole/test/test_twisted.py @@ -1,5 +1,5 @@ from __future__ import print_function -import sys, json +import json from twisted.trial import unittest from twisted.internet.defer import gatherResults, succeed from ..twisted.transcribe import (Wormhole, UsageError, ChannelManager, @@ -389,7 +389,7 @@ class Basic(ServerBase, unittest.TestCase): d.addCallback(_done) return d -data1 = u"""\ +data1 = b"""\ event: welcome data: one and a data: two @@ -420,10 +420,9 @@ class EventSourceClient(unittest.TestCase): (u"e2", u"four"), ]) -if sys.version_info[0] >= 3: - Channel.skip = "twisted is not yet sufficiently ported to py3" - Basic.skip = "twisted is not yet sufficiently ported to py3" - EventSourceClient.skip = "twisted is not yet sufficiently ported to py3" - # as of 15.4.0, Twisted is still missing: - # * web.client.Agent (for all non-EventSource POSTs in transcribe.py) - # * python.logfile (to allow daemonization of 'wormhole server') +# new py3 support in 15.5.0: web.client.Agent, w.c.downloadPage, twistd + +# However trying 'wormhole server start' with py3/twisted-15.5.0 throws an +# error in t.i._twistd_unix.UnixApplicationRunner.postApplication, it calls +# os.write with str, not bytes. This file does not cover that test (testing +# daemonization is hard). diff --git a/src/wormhole/twisted/eventsource_twisted.py b/src/wormhole/twisted/eventsource_twisted.py index af90819..0ea1647 100644 --- a/src/wormhole/twisted/eventsource_twisted.py +++ b/src/wormhole/twisted/eventsource_twisted.py @@ -15,7 +15,7 @@ from ..util.eventual import eventually class EventSourceParser(basic.LineOnlyReceiver): # http://www.w3.org/TR/eventsource/ - delimiter = "\n" + delimiter = b"\n" def __init__(self, handler): self.current_field = None @@ -97,8 +97,8 @@ class EventSource: # TODO: service.Service assert not self.started, "single-use" self.started = True assert self.url - d = self.agent.request("GET", self.url.encode("utf-8"), - Headers({"accept": ["text/event-stream"]})) + d = self.agent.request(b"GET", self.url.encode("utf-8"), + Headers({b"accept": [b"text/event-stream"]})) d.addCallback(self._connected) return d diff --git a/src/wormhole/twisted/transcribe.py b/src/wormhole/twisted/transcribe.py index db4a154..f5c98ff 100644 --- a/src/wormhole/twisted/transcribe.py +++ b/src/wormhole/twisted/transcribe.py @@ -45,7 +45,7 @@ class DataProducer: def post_json(agent, url, request_body): # POST a JSON body to a URL, parsing the response as JSON data = json.dumps(request_body).encode("utf-8") - d = agent.request("POST", url.encode("utf-8"), + d = agent.request(b"POST", url.encode("utf-8"), bodyProducer=DataProducer(data)) def _check_error(resp): if resp.code != 200: @@ -53,19 +53,19 @@ def post_json(agent, url, request_body): return resp d.addCallback(_check_error) d.addCallback(web_client.readBody) - d.addCallback(lambda data: json.loads(data)) + d.addCallback(lambda data: json.loads(data.decode("utf-8"))) return d def get_json(agent, url): # GET from a URL, parsing the response as JSON - d = agent.request("GET", url.encode("utf-8")) + d = agent.request(b"GET", url.encode("utf-8")) def _check_error(resp): if resp.code != 200: raise web_error.Error(resp.code, resp.phrase) return resp d.addCallback(_check_error) d.addCallback(web_client.readBody) - d.addCallback(lambda data: json.loads(data)) + d.addCallback(lambda data: json.loads(data.decode("utf-8"))) return d class Channel: @@ -100,6 +100,7 @@ class Channel: if not isinstance(phase, type(u"")): raise TypeError(type(phase)) if not isinstance(msg, type(b"")): raise TypeError(type(msg)) self._sent_messages.add( (phase,msg) ) + assert isinstance(self._side, type(u"")), type(self._side) payload = {"appid": self._appid, "channelid": self._channelid, "side": self._side, @@ -321,8 +322,8 @@ class Wormhole: "relay_url": self._relay_url, "code": self.code, "side": self._side, - "spake2": json.loads(self.sp.serialize()), - "msg1": self.msg1.encode("hex"), + "spake2": json.loads(self.sp.serialize().decode("ascii")), + "msg1": hexlify(self.msg1).decode("ascii"), } return json.dumps(data) @@ -330,10 +331,11 @@ class Wormhole: def from_serialized(klass, data): d = json.loads(data) self = klass(d["appid"], d["relay_url"]) - self._set_side(d["side"].encode("ascii")) + self._set_side(d["side"]) self._set_code_and_channelid(d["code"]) - self.sp = SPAKE2_Symmetric.from_serialized(json.dumps(d["spake2"])) - self.msg1 = d["msg1"].decode("hex") + sp_data = json.dumps(d["spake2"]).encode("ascii") + self.sp = SPAKE2_Symmetric.from_serialized(sp_data) + self.msg1 = unhexlify(d["msg1"].encode("ascii")) return self def derive_key(self, purpose, length=SecretBox.KEY_SIZE):