fill in initiator flow, define relay API
This commit is contained in:
parent
246e080c7c
commit
f5a0b3e5c6
55
docs/api.md
55
docs/api.md
|
@ -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
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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
15
src/wormhole/codes.py
Normal 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
2
src/wormhole/const.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
RELAY = "baked in relay URL"
|
|
@ -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"))
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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,))
|
||||
|
||||
|
|
|
@ -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
106
src/wormhole/transcribe.py
Normal 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
|
27
src/wormhole/twisted/transcribe.py
Normal file
27
src/wormhole/twisted/transcribe.py
Normal 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
|
||||
|
Loading…
Reference in New Issue
Block a user