fill in initiator flow, define relay API

This commit is contained in:
Brian Warner 2015-02-10 18:34:13 -08:00
parent 246e080c7c
commit f5a0b3e5c6
10 changed files with 197 additions and 38 deletions

View File

@ -29,44 +29,53 @@ The synchronous+blocking flow looks like this:
```python
from wormhole.transcribe import Initiator
blob = b"initiator's blob"
i = Initiator("appid", blob)
print("Invitation Code: %s" % i.start()
theirblob = i.finish()
print("Their blob: %s" % theirblob.decode("ascii"))
data = b"initiator's data"
i = Initiator("appid", data)
code = i.get_code()
print("Invitation Code: %s" % code)
theirdata = i.get_data()
print("Their data: %s" % theirdata.decode("ascii"))
```
```python
import sys
from wormhole.transcribe import Receiver
blob = b"receiver's blob"
data = b"receiver's data"
code = sys.argv[1]
r = Receiver("appid", code, blob)
theirblob = r.finish()
print("Their blob: %s" % theirblob.decode("ascii"))
r = Receiver("appid", code, data)
theirdata = r.get_data()
print("Their data: %s" % theirdata.decode("ascii"))
```
The Twisted-friendly flow looks like this:
```python
from wormhole.transcribe import Initiator
blob = b"initiator's blob"
i = Initiator("appid", blob)
d = i.start()
d.addCallback(lambda code: print("Invitation Code: %s" % code))
d.addCallback(lambda _: i.finish())
d.addCallback(lambda theirblob:
print("Their blob: %s" % theirblob.decode("ascii")))
from twisted.internet import reactor
from wormhole.transcribe import TwistedInitiator
data = b"initiator's data"
ti = TwistedInitiator("appid", data, reactor)
ti.startService()
d1 = ti.when_get_code()
d1.addCallback(lambda code: print("Invitation Code: %s" % code))
d2 = ti.when_get_data()
d2.addCallback(lambda theirdata:
print("Their data: %s" % theirdata.decode("ascii")))
d2.addCallback(labmda _: reactor.stop())
reactor.run()
```
```python
from wormhole.transcribe import Receiver
blob = b"receiver's blob"
from twisted.internet import reactor
from wormhole.transcribe import TwistedReceiver
data = b"receiver's data"
code = sys.argv[1]
r = Receiver("appid", code, blob)
d = r.finish()
d.addCallback(lambda theirblob:
print("Their blob: %s" % theirblob.decode("ascii")))
tr = TwistedReceiver("appid", code, data, reactor)
tr.startService()
d = tr.when_get_data()
d.addCallback(lambda theirdata:
print("Their data: %s" % theirdata.decode("ascii")))
d.addCallback(lambda _: reactor.stop())
reactor.run()
```
## Application Identifier

View File

@ -19,7 +19,7 @@ setup(name="wormhole-sync",
url="https://github.com/warner/wormhole-sync",
package_dir={"": "src"},
packages=["wormhole"],
install_requires=["spake2"],
install_requires=["spake2", "requests"],
test_suite="wormhole.test",
cmdclass=commands,
)

15
src/wormhole/codes.py Normal file
View File

@ -0,0 +1,15 @@
import os, random
WORDLIST = ["able", "baker", "charlie"] # TODO: 1024
def make_code(channel_id):
# TODO: confirm that random.choice() uses os.urandom properly and covers
# the entire range with minimal bias. Many random.py functions do not,
# but I think this one might. If not, build our own from os.urandom,
# convert-to-int, and modulo.
word = random.choice(WORDLIST)
return "%d-%s" % (channel_id, word)
def extract_channel_id(code):
channel_id = int(code.split("-")[0])
return channel_id

2
src/wormhole/const.py Normal file
View File

@ -0,0 +1,2 @@
RELAY = "baked in relay URL"

View File

@ -1,7 +1,7 @@
import os, sys, json
from binascii import unhexlify
from nacl.secret import SecretBox
from . import api
from .transcribe import Receiver
APPID = "lothar.com/wormhole/file-xfer"
RELAY = "example.com"
@ -9,8 +9,8 @@ RELAY = "example.com"
# we're receiving
code = sys.argv[1]
blob = b""
r = api.Receiver(APPID, blob, code)
them_bytes = r.finish()
r = Receiver(APPID, blob, code)
them_bytes = r.get_data()
them_d = json.loads(them_bytes.decode("utf-8"))
print("them: %r" % (them_d,))
xfer_key = unhexlify(them_d["xfer_key"].encode("ascii"))

View File

@ -1,12 +1,12 @@
import sys, json
from . import api
from .transcribe import Receiver
APPID = "lothar.com/wormhole/text-xfer"
# we're receiving
code = sys.argv[1]
blob = b""
r = api.Receiver(APPID, blob, code)
them_bytes = r.finish()
r = Receiver(APPID, blob, code)
them_bytes = r.get_data()
them_d = json.loads(them_bytes.decode("utf-8"))
print(them_d["message"])

View File

@ -2,7 +2,7 @@ import os, sys, json
from binascii import hexlify
from nacl.secret import SecretBox
from nacl import utils
from . import api
from .transcribe import Initiator
APPID = "lothar.com/wormhole/file-xfer"
RELAY = "example.com"
@ -16,14 +16,14 @@ blob = json.dumps({"xfer_key": hexlify(xfer_key),
"filesize": os.stat(filename).st_size,
"relay": RELAY,
}).encode("utf-8")
i = api.Initiator(APPID, blob)
code = i.start()
i = Initiator(APPID, blob)
code = i.get_code()
print("Wormhole code is '%s'" % code)
print("On the other computer, please run:")
print()
print(" wormhole-receive-file %s" % code)
print()
them_bytes = i.finish()
them_bytes = i.get_data()
them_d = json.loads(them_bytes.decode("utf-8"))
print("them: %r" % (them_d,))

View File

@ -1,5 +1,5 @@
import sys, json
from . import api
from .transcribe import Initiator
APPID = "lothar.com/wormhole/text-xfer"
@ -7,13 +7,13 @@ APPID = "lothar.com/wormhole/text-xfer"
message = sys.argv[1]
blob = json.dumps({"message": message,
}).encode("utf-8")
i = api.Initiator(APPID, blob)
code = i.start()
i = Initiator(APPID, blob)
code = i.get_code()
print("Wormhole code is '%s'" % code)
print("On the other computer, please run:")
print()
print(" wormhole-receive-text %s" % code)
print()
them_bytes = i.finish()
them_bytes = i.get_data()
them_d = json.loads(them_bytes.decode("utf-8"))
print("them: %r" % (them_d,))

106
src/wormhole/transcribe.py Normal file
View File

@ -0,0 +1,106 @@
import time, requests
from spake2 import SPAKE2_A, SPAKE2_B
from .const import RELAY
from .codes import make_code, extract_channel_id
SECOND = 1
MINUTE = 60*SECOND
class Timeout(Exception):
pass
# POST /allocate -> {channel-id: INT}
# POST /pake/post/CHANNEL-ID {side: STR, message: STR} -> {messages: [STR..]}
# POST /pake/poll/CHANNEL-ID {side: STR} -> {messages: [STR..]}
# POST /data/post/CHANNEL-ID {side: STR, message: STR} -> {messages: [STR..]}
# POST /data/poll/CHANNEL-ID {side: STR} -> {messages: [STR..]}
# POST /deallocate/CHANNEL-ID {side: STR} -> waiting | ok
class Initiator:
def __init__(self, appid, data, relay=RELAY):
self.appid = appid
self.data = data
self.relay = relay
self.started = time.time()
self.wait = 2*SECOND
self.timeout = 3*MINUTE
self.side = "initiator"
def get_code(self):
# allocate channel
r = requests.post(self.relay + "allocate", data="{}")
r.raise_for_status()
self.channel_id = r.json()["channel-id"]
self.code = codes.make_code(self.channel_id)
self.sp = SPAKE2_A(self.code.encode("ascii"),
idA=self.appid+":Initiator",
idB=self.appid+":Receiver")
msg = self.sp.start()
post_url = self.relay + "pake/post/%d" % self.channel_id
post_data = {"side": self.side,
"message": hexlify(msg).decode("ascii")}
r = requests.post(post_url, data=json.dumps(post_data))
r.raise_for_status()
return self.code
def get_data(self):
# poll for PAKE response
pake_url = self.relay + "pake/poll/%d" % self.channel_id
post_data = json.dumps({"side": self.side})
while True:
r = requests.post(pake_url, data=post_data)
r.raise_for_status()
msgs = r.json()["messages"]
if msgs:
break
if time.time() > (self.started + self.timeout):
raise Timeout
time.sleep(self.wait)
pake_msg = unhexlify(msgs[0].encode("ascii"))
self.key = self.sp.finish(pake_msg)
# post encrypted data
post_url = self.relay + "data/post/%d" % self.channel_id
post_data = json.dumps({"side": self.side,
"message": hexlify(self.data).decode("ascii")})
r = requests.post(post_url, data=post_data)
r.raise_for_status()
# poll for data message
data_url = self.relay + "data/poll/%d" % self.channel_id
post_data = json.dumps({"side": self.side})
while True:
r = requests.post(data_url, data=post_data)
r.raise_for_status()
msgs = r.json()["messages"]
if msgs:
break
if time.time() > (self.started + self.timeout):
raise Timeout
time.sleep(self.wait)
data = unhexlify(msgs[0].encode("ascii"))
# deallocate channel
deallocate_url = self.relay + "deallocate/%s" % self.channel_id
post_data = json.dumps({"side": self.side})
r = requests.post(deallocate, data=post_data)
r.raise_for_status()
return data
class Receiver:
def __init__(self, appid, data, code, relay=RELAY):
self.appid = appid
self.data = data
self.code = code
self.channel_id = extract_channel_id(code)
self.relay = relay
self.sp = SPAKE2_B(code.encode("ascii"),
idA=self.appid+":Initiator",
idB=self.appid+":Receiver")
def get_data(self):
# poll for PAKE response
# poll for data message
# deallocate channel
pass

View File

@ -0,0 +1,27 @@
from twisted.application import service
from ..const import RELAY
class TwistedInitiator(service.MultiService):
def __init__(self, appid, data, reactor, relay=RELAY):
self.appid = appid
self.data = data
self.reactor = reactor
self.relay = relay
def when_get_code(self):
pass # return Deferred
def when_get_data(self):
pass # return Deferred
class TwistedReceiver(service.MultiService):
def __init__(self, appid, data, code, reactor, relay=RELAY):
self.appid = appid
self.data = data
self.code = code
self.reactor = reactor
self.relay = relay
def when_get_data(self):
pass # return Deferred