Merge branch 'new-api-welcome'

This fixes #145. I thought it would add a roundtrip, but it turns out that
websockets were already adding the same delay, so I can't avoid it without
switching from websockets to raw TCP. Closes #145.
This commit is contained in:
Brian Warner 2017-05-15 02:19:24 -07:00
commit d0b3e11950
10 changed files with 387 additions and 372 deletions

View File

@ -30,10 +30,10 @@ from twisted.internet.defer import inlineCallbacks
def go():
w = wormhole.create(appid, relay_url, reactor)
w.generate_code()
code = yield w.when_code()
code = yield w.get_code()
print "code:", code
w.send(b"outbound data")
inbound = yield w.when_received()
w.send_message(b"outbound data")
inbound = yield w.get_message()
yield w.close()
```
@ -55,8 +55,8 @@ Delegated mode:
class MyDelegate:
def wormhole_got_code(self, code):
print("code: %s" % code)
def wormhole_received(self, data): # called for each message
print("got data, %d bytes" % len(data))
def wormhole_got_message(self, msg): # called for each message
print("got data, %d bytes" % len(msg))
w = wormhole.create(appid, relay_url, reactor, delegate=MyDelegate())
w.generate_code()
@ -69,10 +69,10 @@ w = wormhole.create(appid, relay_url, reactor)
w.generate_code()
def print_code(code):
print("code: %s" % code)
w.when_code().addCallback(print_code)
def received(data):
print("got data, %d bytes" % len(data))
w.when_received().addCallback(received) # gets exactly one message
w.get_code().addCallback(print_code)
def received(msg):
print("got data, %d bytes" % len(msg))
w.get_message().addCallback(received) # gets exactly one message
```
## Application Identifier
@ -168,9 +168,9 @@ The Wormhole object has three APIs for generating or accepting a code:
far. A convenience wrapper is provided to attach this to the `rlcompleter`
function of libreadline.
No matter which mode is used, the `w.when_code()` Deferred (or
No matter which mode is used, the `w.get_code()` Deferred (or
`delegate.wormhole_got_code(code)` callback) will fire when the code is
known. `when_code` is clearly necessary for `generate_code`, since there's no
known. `get_code` is clearly necessary for `generate_code`, since there's no
other way to learn what code was created, but it may be useful in other modes
for consistency.
@ -224,7 +224,7 @@ The code-entry Helper object has the following API:
before display.
* `h.choose_words(words)`: call this when the user is finished typing in the
code. It does not return anything, but will cause the Wormhole's
`w.when_code()` (or corresponding delegate) to fire, and triggers the
`w.get_code()` (or corresponding delegate) to fire, and triggers the
wormhole connection process. This accepts a string like "purple-sausages",
without the nameplate. It must be called after `h.choose_nameplate()` or
`MustChooseNameplateFirstError` will be raised. May only be called once,
@ -237,7 +237,7 @@ code-entry helper to do tab completion of wormhole codes:
from wormhole import create, input_with_completion
w = create(appid, relay_url, reactor)
input_with_completion("Wormhole code:", w.input_code(), reactor)
d = w.when_code()
d = w.get_code()
```
This helper runs python's (raw) `input()` function inside a thread, since
@ -276,33 +276,13 @@ random values (e.g. by using the Diceware wordlist) unless that makes it
easier to transcribe: e.g. rolling 6 dice could result in a code like
"913-166532", and flipping 16 coins could result in "123-HTTHHHTTHTTHHTHH".
## Verifier
For extra protection against guessing attacks, Wormhole can provide a
"Verifier". This is a moderate-length series of bytes (a SHA256 hash) that is
derived from the supposedly-shared session key. If desired, both sides can
display this value, and the humans can manually compare them before allowing
the rest of the protocol to proceed. If they do not match, then the two
programs are not talking to each other (they may both be talking to a
man-in-the-middle attacker), and the protocol should be abandoned.
Deferred-mode applications can wait for `d=w.when_verified()`: the Deferred
it returns will fire with the verifier. You can turn this into hex or Base64
to print it, or render it as ASCII-art, etc.
Asking the wormhole object for the verifier does not affect the flow of the
protocol. To benefit from verification, applications must refrain from
sending any data (with `w.send(data)`) until after the verifiers are approved
by the user. In addition, applications must queue or otherwise ignore
incoming (received) messages until that point. However once the verifiers are
confirmed, previously-received messages can be considered valid and processed
as usual.
## Welcome Messages
The first message sent by the rendezvous server is a "welcome" message (a
dictionary). Clients should not wait for this message, but when it arrives,
they should process the keys it contains.
dictionary). This is sent as soon as the client connects to the server,
generally before the code is established. Clients should use
`d=get_welcome()` to get and process the `motd` key (and maybe
`current_cli_version`) inside the welcome message.
The welcome message serves three main purposes:
@ -357,17 +337,45 @@ the library did not pay attention to the ERROR message, hence the server
should deliver errors in a WELCOME message if at all possible)
The `error` field is handled internally by the Wormhole object. The other
fields are processed by an application-supplied "welcome handler" function,
supplied as an argument to the `wormhole()` constructor. This function will
be called with the full welcome dictionary, so any other keys that a future
server might send will be available to it. If the welcome handler raises
`WelcomeError`, the connection will be closed just as if an `error` key had
been received. The handler may be called multiple times (once per connection,
if the rendezvous connection is lost and then reestablished), so applications
should avoid presenting the user with redundant messages.
fields can be processed by application, by using `d=w.get_welcome()`. The
Deferred will fire with the full welcome dictionary, so any other keys that a
future server might send will be available to it.
The default welcome handler will print `motd` to stderr, and will ignore
`current_cli_version`.
Applications which need to display `motd` or an upgrade message, and wish to
do so before using stdin/stdout for interactive code entry (`w.input_code()`)
should wait for `get_welcome()` before starting the entry process:
```python
@inlineCallbacks
def go():
w = wormhole.create(appid, relay_url, reactor)
welcome = yield w.get_welcome()
if "motd" in welcome: print welcome["motd"]
input_with_completion("Wormhole code:", w.input_code(), reactor)
...
```
## Verifier
For extra protection against guessing attacks, Wormhole can provide a
"Verifier". This is a moderate-length series of bytes (a SHA256 hash) that is
derived from the supposedly-shared session key. If desired, both sides can
display this value, and the humans can manually compare them before allowing
the rest of the protocol to proceed. If they do not match, then the two
programs are not talking to each other (they may both be talking to a
man-in-the-middle attacker), and the protocol should be abandoned.
Deferred-mode applications can wait for `d=w.get_verifier()`: the Deferred
it returns will fire with the verifier. You can turn this into hex or Base64
to print it, or render it as ASCII-art, etc.
Asking the wormhole object for the verifier does not affect the flow of the
protocol. To benefit from verification, applications must refrain from
sending any data (with `w.send_message(data)`) until after the verifiers are
approved by the user. In addition, applications must queue or otherwise
ignore incoming (received) messages until that point. However once the
verifiers are confirmed, previously-received messages can be considered valid
and processed as usual.
## Events
@ -377,36 +385,45 @@ functions on the delegate object. In Deferred mode, the application retrieves
Deferred objects from the wormhole, and event dispatch is performed by firing
those Deferreds.
* got_code (`yield w.when_code()` / `dg.wormhole_code(code)`): fired when the
wormhole code is established, either after `w.generate_code()` finishes the
generation process, or when the Input Helper returned by `w.input_code()`
has been told `h.set_words()`, or immediately after `w.set_code(code)` is
called. This is most useful after calling `w.generate_code()`, to show the
generated code to the user so they can transcribe it to their peer.
* key (`yield w.when_key()` / `dg.wormhole_key()`): fired when the
key-exchange process has completed and a purported shared key is
established. At this point we do not know that anyone else actually shares
this key: the peer may have used the wrong code, or may have disappeared
altogether. To wait for proof that the key is shared, wait for
`when_verified` instead. This event is really only useful for detecting
that the initiating peer has disconnected after leaving the initial PAKE
Most applications will only use `code`, `received`, and `close`.
* code (`code = yield w.get_code()` / `dg.wormhole_got_code(code)`): fired
when the wormhole code is established, either after `w.generate_code()`
finishes the generation process, or when the Input Helper returned by
`w.input_code()` has been told `h.set_words()`, or immediately after
`w.set_code(code)` is called. This is most useful after calling
`w.generate_code()`, to show the generated code to the user so they can
transcribe it to their peer.
* key (`yield w.get_unverified_key()` /
`dg.wormhole_got_unverified_key(key)`): fired (with the raw master SPAKE2
key) when the key-exchange process has completed and a purported shared key
is established. At this point we do not know that anyone else actually
shares this key: the peer may have used the wrong code, or may have
disappeared altogether. To wait for proof that the key is shared, wait for
`get_verifier` instead. This event is really only useful for detecting that
the initiating peer has disconnected after leaving the initial PAKE
message, to display a pacifying message to the user.
* verified (`verifier = yield w.when_verified()` /
`dg.wormhole_verified(verifier)`: fired when the key-exchange process has
completed and a valid VERSION message has arrived. The "verifier" is a byte
string with a hash of the shared session key; clients can compare them
* verifier (`verifier = yield w.get_verifier()` /
`dg.wormhole_got_verifier(verifier)`: fired when the key-exchange process
has completed and a valid VERSION message has arrived. The "verifier" is a
byte string with a hash of the shared session key; clients can compare them
(probably as hex) to ensure that they're really talking to each other, and
not to a man-in-the-middle. When `got_verifier` happens, this side knows
not to a man-in-the-middle. When `get_verifier` happens, this side knows
that *someone* has used the correct wormhole code; if someone used the
wrong code, the VERSION message cannot be decrypted, and the wormhole will
be closed instead.
* version (`yield w.when_version()` / `dg.wormhole_version(versions)`: fired
when the VERSION message arrives from the peer. This fires at the same time
as `verified`, but delivers the "app_versions" data (as passed into
`wormhole.create(versions=)`) instead of the verifier string.
* received (`yield w.when_received()` / `dg.wormhole_received(data)`: fired
* versions (`versions = yield w.get_versions()` /
`dg.wormhole_got_versions(versions)`: fired when the VERSION message
arrives from the peer. This fires just after `verified`, but delivers the
"app_versions" data (as passed into `wormhole.create(versions=)`) instead
of the verifier string. This is mostly a hack to make room for
forwards-compatible changes to the CLI file-transfer protocol, which sends
a request in the first message (rather than merely sending the abilities of
each side).
* received (`yield w.get_message()` / `dg.wormhole_got_message(msg)`: fired
each time a data message arrives from the peer, with the bytestring that
the peer passed into `w.send(data)`.
the peer passed into `w.send_message(msg)`. This is the primary
data-transfer API.
* closed (`yield w.close()` / `dg.wormhole_closed(result)`: fired when
`w.close()` has finished shutting down the wormhole, which means all
nameplates and mailboxes have been deallocated, and the WebSocket
@ -419,27 +436,28 @@ those Deferreds.
## Sending Data
The main purpose of a Wormhole is to send data. At any point after
construction, callers can invoke `w.send(data)`. This will queue the message
if necessary, but (if all goes well) will eventually result in the peer
getting a `received` event and the data being delivered to the application.
construction, callers can invoke `w.send_message(msg)`. This will queue the
message if necessary, but (if all goes well) will eventually result in the
peer getting a `received` event and the data being delivered to the
application.
Since Wormhole provides an ordered record pipe, each call to `w.send` will
result in exactly one `received` event on the far side. Records are not
Since Wormhole provides an ordered record pipe, each call to `w.send_message`
will result in exactly one `received` event on the far side. Records are not
split, merged, dropped, or reordered.
Each side can do an arbitrary number of `send()` calls. The Wormhole is not
meant as a long-term communication channel, but some protocols work better if
they can exchange an initial pair of messages (perhaps offering some set of
negotiable capabilities), and then follow up with a second pair (to reveal
the results of the negotiation). The Rendezvous Server does not currently
enforce any particular limits on number of messages, size of messages, or
rate of transmission, but in general clients are expected to send fewer than
a dozen messages, of no more than perhaps 20kB in size (remember that all
these messages are temporarily stored in a SQLite database on the server). A
future version of the protocol may make these limits more explicit, and will
allow clients to ask for greater capacity when they connect (probably by
passing additional "mailbox attribute" parameters with the
`allocate`/`claim`/`open` messages).
Each side can do an arbitrary number of `send_message()` calls. The Wormhole
is not meant as a long-term communication channel, but some protocols work
better if they can exchange an initial pair of messages (perhaps offering
some set of negotiable capabilities), and then follow up with a second pair
(to reveal the results of the negotiation). The Rendezvous Server does not
currently enforce any particular limits on number of messages, size of
messages, or rate of transmission, but in general clients are expected to
send fewer than a dozen messages, of no more than perhaps 20kB in size
(remember that all these messages are temporarily stored in a SQLite database
on the server). A future version of the protocol may make these limits more
explicit, and will allow clients to ask for greater capacity when they
connect (probably by passing additional "mailbox attribute" parameters with
the `allocate`/`claim`/`open` messages).
For bulk data transfer, see "transit.md", or the "Dilation" section below.
@ -531,7 +549,7 @@ What's good about dilated wormholes?:
Use non-dilated wormholes when your application only needs to exchange a
couple of messages, for example to set up public keys or provision access
tokens. Use a dilated wormhole to move large files.
tokens. Use a dilated wormhole to move files.
Dilated wormholes can provide multiple "channels": these are multiplexed
through the single (encrypted) TCP connection. Each channel is a separate
@ -570,17 +588,19 @@ in python3):
## Full API list
action | Deferred-Mode | Delegated-Mode
------------------ | -------------------- | --------------
w.generate_code() | |
w.set_code(code) | |
h=w.input_code() | |
. | d=w.when_code() | dg.wormhole_code(code)
. | d=w.when_verified() | dg.wormhole_verified(verifier)
. | d=w.when_version() | dg.wormhole_version(version)
w.send(data) | |
. | d=w.when_received() | dg.wormhole_received(data)
key=w.derive_key(purpose, length) | |
w.close() | | dg.wormhole_closed(result)
. | d=w.close() |
action | Deferred-Mode | Delegated-Mode
------------------ | ------------------ | --------------
. | d=w.get_welcome() | dg.wormhole_got_welcome(welcome)
w.generate_code() | |
w.set_code(code) | |
h=w.input_code() | |
. | d=w.get_code() | dg.wormhole_got_code(code)
. | d=w.get_unverified_key() | dg.wormhole_got_unverified_key(key)
. | d=w.get_verifier() | dg.wormhole_got_verifier(verifier)
. | d=w.get_versions() | dg.wormhole_got_versions(versions)
key=w.derive_key(purpose, length) | |
w.send_message(msg) | |
. | d=w.get_message() | dg.wormhole_got_message(msg)
w.close() | | dg.wormhole_closed(result)
. | d=w.close() |

View File

@ -33,7 +33,6 @@ class Boss(object):
_url = attrib(validator=instance_of(type(u"")))
_appid = attrib(validator=instance_of(type(u"")))
_versions = attrib(validator=instance_of(dict))
_welcome_handler = attrib() # TODO: validator: callable
_reactor = attrib()
_journal = attrib(validator=provides(_interfaces.IJournal))
_tor_manager = attrib() # TODO: ITorManager or None
@ -185,11 +184,11 @@ class Boss(object):
raise WelcomeError(welcome["error"])
# TODO: it'd be nice to not call the handler when we're in
# S3_closing or S4_closed states. I tried to implement this with
# rx_Welcome as an @input, but in the error case I'd be
# rx_welcome as an @input, but in the error case I'd be
# delivering a new input (rx_error or something) while in the
# middle of processing the rx_welcome input, and I wasn't sure
# Automat would handle that correctly.
self._welcome_handler(welcome) # can raise WelcomeError too
self._W.got_welcome(welcome) # TODO: let this raise WelcomeError?
except WelcomeError as welcome_error:
self.rx_unwelcome(welcome_error)
@m.input()
@ -244,7 +243,7 @@ class Boss(object):
self._their_versions = bytes_to_dict(plaintext)
# but this part is app-to-app
app_versions = self._their_versions.get("app_versions", {})
self._W.got_version(app_versions)
self._W.got_versions(app_versions)
@m.output()
def S_send(self, plaintext):

View File

@ -10,7 +10,7 @@ from ..transit import TransitReceiver
from ..errors import TransferError, WormholeClosedError, NoTorError
from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr,
estimate_free_space)
from .welcome import CLIWelcomeHandler
from .welcome import handle_welcome
APPID = u"lothar.com/wormhole/text-or-file-xfer"
@ -68,13 +68,10 @@ class Receiver:
# with the user handing off the wormhole code
yield self._tor_manager.start()
wh = CLIWelcomeHandler(self.args.relay_url, __version__,
self.args.stderr)
w = create(self.args.appid or APPID, self.args.relay_url,
self._reactor,
tor_manager=self._tor_manager,
timing=self.args.timing,
welcome_handler=wh.handle_welcome)
timing=self.args.timing)
self._w = w # so tests can wait on events too
# I wanted to do this instead:
@ -112,6 +109,10 @@ class Receiver:
@inlineCallbacks
def _go(self, w):
welcome = yield w.get_welcome()
handle_welcome(welcome, self.args.relay_url, __version__,
self.args.stderr)
yield self._handle_code(w)
def on_slow_key():
@ -125,7 +126,7 @@ class Receiver:
# of that possibility, it's probably not appropriate to give up
# automatically after some timeout. The user can express their
# impatience by quitting the program with control-C.
yield w.when_key()
yield w.get_unverified_key()
finally:
if not notify.called:
notify.cancel()
@ -147,7 +148,7 @@ class Receiver:
# It would be reasonable to give up after waiting here for too
# long.
verifier_bytes = yield w.when_verified()
verifier_bytes = yield w.get_verifier()
finally:
if not notify.called:
notify.cancel()
@ -183,12 +184,12 @@ class Receiver:
def _send_data(self, data, w):
data_bytes = dict_to_bytes(data)
w.send(data_bytes)
w.send_message(data_bytes)
@inlineCallbacks
def _get_data(self, w):
# this may raise WrongPasswordError
them_bytes = yield w.when_received()
them_bytes = yield w.get_message()
them_d = bytes_to_dict(them_bytes)
if "error" in them_d:
raise TransferError(them_d["error"])
@ -210,7 +211,7 @@ class Receiver:
if not used_completion:
print(" (note: you can use <Tab> to complete words)",
file=self.args.stderr)
yield w.when_code()
yield w.get_code()
def _show_verifier(self, verifier_bytes):
verifier_hex = bytes_to_hexstr(verifier_bytes)

View File

@ -10,7 +10,7 @@ from ..errors import TransferError, WormholeClosedError, NoTorError
from wormhole import create, __version__
from ..transit import TransitSender
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
from .welcome import CLIWelcomeHandler
from .welcome import handle_welcome
APPID = u"lothar.com/wormhole/text-or-file-xfer"
VERIFY_TIMER = 1
@ -53,13 +53,10 @@ class Sender:
# with the user handing off the wormhole code
yield self._tor_manager.start()
wh = CLIWelcomeHandler(self._args.relay_url, __version__,
self._args.stderr)
w = create(self._args.appid or APPID, self._args.relay_url,
self._reactor,
tor_manager=self._tor_manager,
timing=self._timing,
welcome_handler=wh.handle_welcome)
timing=self._timing)
d = self._go(w)
# if we succeed, we should close and return the w.close results
@ -85,10 +82,14 @@ class Sender:
def _send_data(self, data, w):
data_bytes = dict_to_bytes(data)
w.send(data_bytes)
w.send_message(data_bytes)
@inlineCallbacks
def _go(self, w):
welcome = yield w.get_welcome()
handle_welcome(welcome, self._args.relay_url, __version__,
self._args.stderr)
# TODO: run the blocking zip-the-directory IO in a thread, let the
# wormhole exchange happen in parallel
offer, self._fd_to_send = self._build_offer()
@ -110,21 +111,21 @@ class Sender:
else:
w.allocate_code(args.code_length)
code = yield w.when_code()
code = yield w.get_code()
if not args.zeromode:
print(u"Wormhole code is: %s" % code, file=args.stderr)
# flush stderr so the code is displayed immediately
args.stderr.flush()
print(u"", file=args.stderr)
# We don't print a "waiting" message for when_key() here, even though
# we do that in cmd_receive.py, because it's not at all surprising to
# we waiting here for a long time. We'll sit in when_key() until the
# receiver has typed in the code and their PAKE message makes it to
# us.
yield w.when_key()
# We don't print a "waiting" message for get_unverified_key() here,
# even though we do that in cmd_receive.py, because it's not at all
# surprising to we waiting here for a long time. We'll sit in
# get_unverified_key() until the receiver has typed in the code and
# their PAKE message makes it to us.
yield w.get_unverified_key()
# TODO: don't stall on w.verify() unless they want it
# TODO: don't stall on w.get_verifier() unless they want it
def on_slow_connection():
print(u"Key established, waiting for confirmation...",
file=args.stderr)
@ -132,13 +133,13 @@ class Sender:
try:
# The usual sender-chooses-code sequence means the receiver's
# PAKE should be followed immediately by their VERSION, so
# w.when_verified() should fire right away. However if we're
# w.get_verifier() should fire right away. However if we're
# using the offline-codes sequence, and the receiver typed in
# their code first, and then they went offline, we might be
# sitting here for a while, so printing the "waiting" message
# seems like a good idea. It might even be appropriate to give up
# after a while.
verifier_bytes = yield w.when_verified() # might WrongPasswordError
verifier_bytes = yield w.get_verifier() # might WrongPasswordError
finally:
if not notify.called:
notify.cancel()
@ -162,7 +163,7 @@ class Sender:
}
self._send_data({u"transit": sender_transit}, w)
# TODO: move this down below w.get()
# TODO: move this down below w.get_message()
transit_key = w.derive_key(APPID+"/transit-key",
ts.TRANSIT_KEY_LENGTH)
ts.set_transit_key(transit_key)
@ -174,13 +175,13 @@ class Sender:
while True:
try:
them_d_bytes = yield w.when_received()
them_d_bytes = yield w.get_message()
except WormholeClosedError:
if done:
returnValue(None)
raise TransferError("unexpected close")
# TODO: when_received() fired, so now it's safe to use
# w.derive_key()
# TODO: get_message() fired, so get_verifier must have fired, so
# now it's safe to use w.derive_key()
them_d = bytes_to_dict(them_d_bytes)
#print("GOT", them_d)
recognized = False
@ -209,7 +210,7 @@ class Sender:
if ok.lower() == "no":
err = "sender rejected verification check, abandoned transfer"
reject_data = dict_to_bytes({"error": err})
w.send(reject_data)
w.send_message(reject_data)
raise TransferError(err)
def _handle_transit(self, receiver_transit):

View File

@ -1,24 +1,18 @@
from __future__ import print_function, absolute_import, unicode_literals
import sys
from ..wormhole import _WelcomeHandler
class CLIWelcomeHandler(_WelcomeHandler):
def __init__(self, url, cli_version, stderr=sys.stderr):
_WelcomeHandler.__init__(self, url, stderr)
self._current_version = cli_version
self._version_warning_displayed = False
def handle_welcome(self, welcome):
# Only warn if we're running a release version (e.g. 0.0.6, not
# 0.0.6+DISTANCE.gHASH). Only warn once.
if ("current_cli_version" in welcome
and "+" not in self._current_version
and not self._version_warning_displayed
and welcome["current_cli_version"] != self._current_version):
print("Warning: errors may occur unless both sides are running the same version", file=self.stderr)
print("Server claims %s is current, but ours is %s"
% (welcome["current_cli_version"], self._current_version),
file=self.stderr)
self._version_warning_displayed = True
_WelcomeHandler.handle_welcome(self, welcome)
def handle_welcome(welcome, relay_url, my_version, stderr):
if "motd" in welcome:
motd_lines = welcome["motd"].splitlines()
motd_formatted = "\n ".join(motd_lines)
print("Server (at %s) says:\n %s" % (relay_url, motd_formatted),
file=stderr)
# Only warn if we're running a release version (e.g. 0.0.6, not
# 0.0.6+DISTANCE.gHASH). Only warn once.
if ("current_cli_version" in welcome
and "+" not in my_version
and welcome["current_cli_version"] != my_version):
print("Warning: errors may occur unless both sides are running the same version", file=stderr)
print("Server claims %s is current, but ours is %s"
% (welcome["current_cli_version"], my_version),
file=stderr)

View File

@ -433,9 +433,16 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
receive_d = cmd_receive.receive(recv_cfg)
else:
KEY_TIMER = 0 if mode == "slow-sender-text" else 1.0
rxw = []
with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER):
send_d = cmd_send.send(send_cfg)
receive_d = cmd_receive.receive(recv_cfg)
receive_d = cmd_receive.receive(recv_cfg,
_debug_stash_wormhole=rxw)
# we need to keep KEY_TIMER patched until the receiver
# gets far enough to start the timer, which happens after
# the code is set
if mode == "slow-sender-text":
yield rxw[0].get_unverified_key()
# The sender might fail, leaving the receiver hanging, or vice
# versa. Make sure we don't wait on one side exclusively
@ -832,26 +839,26 @@ class NotWelcome(ServerBase, unittest.TestCase):
class Cleanup(ServerBase, unittest.TestCase):
def setUp(self):
d = super(Cleanup, self).setUp()
self.cfg = cfg = config("send")
def make_config(self):
cfg = config("send")
# common options for all tests in this suite
cfg.hide_progress = True
cfg.relay_url = self.relayurl
cfg.transit_helper = ""
cfg.stdout = io.StringIO()
cfg.stderr = io.StringIO()
return d
return cfg
@inlineCallbacks
@mock.patch('sys.stdout')
def test_text(self, stdout):
# the rendezvous channel should be deleted after success
self.cfg.text = "hello"
self.cfg.code = "1-abc"
cfg = self.make_config()
cfg.text = "hello"
cfg.code = "1-abc"
send_d = cmd_send.send(self.cfg)
receive_d = cmd_receive.receive(self.cfg)
send_d = cmd_send.send(cfg)
receive_d = cmd_receive.receive(cfg)
yield send_d
yield receive_d
@ -863,12 +870,14 @@ class Cleanup(ServerBase, unittest.TestCase):
def test_text_wrong_password(self):
# if the password was wrong, the rendezvous channel should still be
# deleted
self.cfg.text = "secret message"
self.cfg.code = "1-abc"
send_d = cmd_send.send(self.cfg)
send_cfg = self.make_config()
send_cfg.text = "secret message"
send_cfg.code = "1-abc"
send_d = cmd_send.send(send_cfg)
self.cfg.code = "1-WRONG"
receive_d = cmd_receive.receive(self.cfg)
rx_cfg = self.make_config()
rx_cfg.code = "1-WRONG"
receive_d = cmd_receive.receive(rx_cfg)
# both sides should be capable of detecting the mismatch
yield self.assertFailure(send_d, WrongPasswordError)
@ -951,12 +960,9 @@ class AppID(ServerBase, unittest.TestCase):
self.assertEqual(used[0]["app_id"], u"appid2")
class Welcome(unittest.TestCase):
def do(self, welcome_message, my_version="2.0", twice=False):
def do(self, welcome_message, my_version="2.0"):
stderr = io.StringIO()
h = welcome.CLIWelcomeHandler("url", my_version, stderr)
h.handle_welcome(welcome_message)
if twice:
h.handle_welcome(welcome_message)
welcome.handle_welcome(welcome_message, "url", my_version, stderr)
return stderr.getvalue()
def test_empty(self):
@ -973,15 +979,6 @@ class Welcome(unittest.TestCase):
"Server claims 3.0 is current, but ours is 2.0\n")
self.assertEqual(stderr, expected)
def test_version_old_twice(self):
stderr = self.do({"current_cli_version": "3.0"}, twice=True)
# the handler should only emit the version warning once, even if we
# get multiple Welcome messages (which could happen if we lose the
# connection and then reconnect)
expected = ("Warning: errors may occur unless both sides are running the same version\n" +
"Server claims 3.0 is current, but ours is 2.0\n")
self.assertEqual(stderr, expected)
def test_version_unreleased(self):
stderr = self.do({"current_cli_version": "3.0"},
my_version="2.5+middle.something")

View File

@ -1152,15 +1152,15 @@ class Boss(unittest.TestCase):
def build(self):
events = []
wormhole = Dummy("w", events, None,
"got_code", "got_key", "got_verifier", "got_version",
"got_welcome",
"got_code", "got_key", "got_verifier", "got_versions",
"received", "closed")
self._welcome_handler = mock.Mock()
versions = {"app": "version1"}
reactor = None
journal = ImmediateJournal()
tor_manager = None
b = MockBoss(wormhole, "side", "url", "appid", versions,
self._welcome_handler, reactor, journal, tor_manager,
reactor, journal, tor_manager,
timing.DebugTiming())
b._T = Dummy("t", events, ITerminator, "close")
b._S = Dummy("s", events, ISend, "send")
@ -1179,8 +1179,11 @@ class Boss(unittest.TestCase):
self.assertEqual(events, [("w.got_code", "1-code")])
events[:] = []
b.rx_welcome("welcome")
self.assertEqual(self._welcome_handler.mock_calls, [mock.call("welcome")])
welcome = {"howdy": "how are ya"}
b.rx_welcome(welcome)
self.assertEqual(events, [("w.got_welcome", welcome),
])
events[:] = []
# pretend a peer message was correctly decrypted
b.got_key(b"key")
@ -1190,7 +1193,7 @@ class Boss(unittest.TestCase):
b.got_message("0", b"msg1")
self.assertEqual(events, [("w.got_key", b"key"),
("w.got_verifier", b"verifier"),
("w.got_version", {}),
("w.got_versions", {}),
("w.received", b"msg1"),
])
events[:] = []
@ -1206,6 +1209,12 @@ class Boss(unittest.TestCase):
b.closed()
self.assertEqual(events, [("w.closed", "happy")])
def test_unwelcome(self):
b, events = self.build()
unwelcome = {"error": "go away"}
b.rx_welcome(unwelcome)
self.assertEqual(events, [("t.close", "unwelcome")])
def test_lonely(self):
b, events = self.build()
b.set_code("1-code")

View File

@ -12,23 +12,6 @@ from ..errors import (WrongPasswordError,
APPID = "appid"
class Welcome(unittest.TestCase):
def test_tolerate_no_current_version(self):
w = wormhole._WelcomeHandler("relay_url")
w.handle_welcome({})
def test_print_motd(self):
stderr = io.StringIO()
w = wormhole._WelcomeHandler("relay_url", stderr=stderr)
w.handle_welcome({"motd": "message of\nthe day"})
self.assertEqual(stderr.getvalue(),
"Server (at relay_url) says:\n message of\n the day\n")
# motd can be displayed multiple times
w.handle_welcome({"motd": "second message"})
self.assertEqual(stderr.getvalue(),
("Server (at relay_url) says:\n message of\n the day\n"
"Server (at relay_url) says:\n second message\n"))
# event orderings to exercise:
#
# * normal sender: set_code, send_phase1, connected, claimed, learn_msg2,
@ -42,21 +25,24 @@ class Welcome(unittest.TestCase):
class Delegate:
def __init__(self):
self.welcome = None
self.code = None
self.key = None
self.verifier = None
self.version = None
self.versions = None
self.messages = []
self.closed = None
def wormhole_code(self, code):
def wormhole_got_welcome(self, welcome):
self.welcome = welcome
def wormhole_got_code(self, code):
self.code = code
def wormhole_key(self, key):
def wormhole_got_unverified_key(self, key):
self.key = key
def wormhole_verified(self, verifier):
def wormhole_got_verifier(self, verifier):
self.verifier = verifier
def wormhole_version(self, version):
self.version = version
def wormhole_received(self, data):
def wormhole_got_versions(self, versions):
self.versions = versions
def wormhole_got_message(self, data):
self.messages.append(data)
def wormhole_closed(self, result):
self.closed = result
@ -76,12 +62,12 @@ class Delegated(ServerBase, unittest.TestCase):
w2.set_code(dg.code)
yield poll_until(lambda: dg.key is not None)
yield poll_until(lambda: dg.verifier is not None)
yield poll_until(lambda: dg.version is not None)
yield poll_until(lambda: dg.versions is not None)
w1.send(b"ping")
got = yield w2.when_received()
w1.send_message(b"ping")
got = yield w2.get_message()
self.assertEqual(got, b"ping")
w2.send(b"pong")
w2.send_message(b"pong")
yield poll_until(lambda: dg.messages)
self.assertEqual(dg.messages[0], b"pong")
@ -124,7 +110,7 @@ class Wormholes(ServerBase, unittest.TestCase):
def test_allocate_default(self):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
mo = re.search(r"^\d+-\w+-\w+$", code)
self.assert_(mo, code)
# w.close() fails because we closed before connecting
@ -134,7 +120,7 @@ class Wormholes(ServerBase, unittest.TestCase):
def test_allocate_more_words(self):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code(3)
code = yield w1.when_code()
code = yield w1.get_code()
mo = re.search(r"^\d+-\w+-\w+-\w+$", code)
self.assert_(mo, code)
yield self.assertFailure(w1.close(), LonelyError)
@ -149,11 +135,11 @@ class Wormholes(ServerBase, unittest.TestCase):
w2 = wormhole.create(APPID, self.relayurl, reactor)
#w2.debug_set_trace(" W2")
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code)
yield w1.when_key()
yield w2.when_key()
yield w1.get_unverified_key()
yield w2.get_unverified_key()
key1 = w1.derive_key("purpose", 16)
self.assertEqual(len(key1), 16)
@ -163,29 +149,29 @@ class Wormholes(ServerBase, unittest.TestCase):
with self.assertRaises(TypeError):
w1.derive_key(12345, 16)
verifier1 = yield w1.when_verified()
verifier2 = yield w2.when_verified()
verifier1 = yield w1.get_verifier()
verifier2 = yield w2.get_verifier()
self.assertEqual(verifier1, verifier2)
self.successResultOf(w1.when_key())
self.successResultOf(w2.when_key())
self.successResultOf(w1.get_unverified_key())
self.successResultOf(w2.get_unverified_key())
version1 = yield w1.when_version()
version2 = yield w2.when_version()
versions1 = yield w1.get_versions()
versions2 = yield w2.get_versions()
# app-versions are exercised properly in test_versions, this just
# tests the defaults
self.assertEqual(version1, {})
self.assertEqual(version2, {})
self.assertEqual(versions1, {})
self.assertEqual(versions2, {})
w1.send(b"data1")
w2.send(b"data2")
dataX = yield w1.when_received()
dataY = yield w2.when_received()
w1.send_message(b"data1")
w2.send_message(b"data2")
dataX = yield w1.get_message()
dataY = yield w2.get_message()
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
version1_again = yield w1.when_version()
self.assertEqual(version1, version1_again)
versions1_again = yield w1.get_versions()
self.assertEqual(versions1, versions1_again)
c1 = yield w1.close()
self.assertEqual(c1, "happy")
@ -193,19 +179,19 @@ class Wormholes(ServerBase, unittest.TestCase):
self.assertEqual(c2, "happy")
@inlineCallbacks
def test_when_code_early(self):
def test_get_code_early(self):
w1 = wormhole.create(APPID, self.relayurl, reactor)
d = w1.when_code()
d = w1.get_code()
w1.set_code("1-abc")
code = self.successResultOf(d)
self.assertEqual(code, "1-abc")
yield self.assertFailure(w1.close(), LonelyError)
@inlineCallbacks
def test_when_code_late(self):
def test_get_code_late(self):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("1-abc")
d = w1.when_code()
d = w1.get_code()
code = self.successResultOf(d)
self.assertEqual(code, "1-abc")
yield self.assertFailure(w1.close(), LonelyError)
@ -218,12 +204,12 @@ class Wormholes(ServerBase, unittest.TestCase):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code)
w1.send(b"data")
w2.send(b"data")
dataX = yield w1.when_received()
dataY = yield w2.when_received()
w1.send_message(b"data")
w2.send_message(b"data")
dataX = yield w1.get_message()
dataY = yield w2.get_message()
self.assertEqual(dataX, b"data")
self.assertEqual(dataY, b"data")
yield w1.close()
@ -234,13 +220,13 @@ class Wormholes(ServerBase, unittest.TestCase):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code)
w1.send(b"data1")
dataY = yield w2.when_received()
w1.send_message(b"data1")
dataY = yield w2.get_message()
self.assertEqual(dataY, b"data1")
d = w1.when_received()
w2.send(b"data2")
d = w1.get_message()
w2.send_message(b"data2")
dataX = yield d
self.assertEqual(dataX, b"data2")
yield w1.close()
@ -251,10 +237,10 @@ class Wormholes(ServerBase, unittest.TestCase):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code)
w1.send(b"data1")
dataY = yield w2.when_received()
w1.send_message(b"data1")
dataY = yield w2.get_message()
self.assertEqual(dataY, b"data1")
yield w1.close()
yield w2.close()
@ -262,9 +248,9 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks
def test_early(self):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w1.send(b"data1")
w1.send_message(b"data1")
w2 = wormhole.create(APPID, self.relayurl, reactor)
d = w2.when_received()
d = w2.get_message()
w1.set_code("123-abc-def")
w2.set_code("123-abc-def")
dataY = yield d
@ -278,8 +264,8 @@ class Wormholes(ServerBase, unittest.TestCase):
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
dl = yield self.doBoth(w1.when_received(), w2.when_received())
w1.send_message(b"data1"), w2.send_message(b"data2")
dl = yield self.doBoth(w1.get_message(), w2.get_message())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
@ -300,8 +286,8 @@ class Wormholes(ServerBase, unittest.TestCase):
yield poll_until(lambda: w2._boss._K._debug_pake_stashed)
h.choose_words("purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
dl = yield self.doBoth(w1.when_received(), w2.when_received())
w1.send_message(b"data1"), w2.send_message(b"data2")
dl = yield self.doBoth(w1.get_message(), w2.get_message())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
@ -315,13 +301,13 @@ class Wormholes(ServerBase, unittest.TestCase):
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
w1.send(b"data3"), w2.send(b"data4")
dl = yield self.doBoth(w1.when_received(), w2.when_received())
w1.send_message(b"data1"), w2.send_message(b"data2")
w1.send_message(b"data3"), w2.send_message(b"data4")
dl = yield self.doBoth(w1.get_message(), w2.get_message())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
dl = yield self.doBoth(w1.when_received(), w2.when_received())
dl = yield self.doBoth(w1.get_message(), w2.get_message())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data4")
self.assertEqual(dataY, b"data3")
@ -337,19 +323,19 @@ class Wormholes(ServerBase, unittest.TestCase):
w2.set_code("123-foo")
# let it connect and become HAPPY
yield w1.when_version()
yield w2.when_version()
yield w1.get_versions()
yield w2.get_versions()
yield w1.close()
yield w2.close()
# once closed, all Deferred-yielding API calls get an immediate error
f = self.failureResultOf(w1.when_code(), WormholeClosed)
f = self.failureResultOf(w1.get_code(), WormholeClosed)
self.assertEqual(f.value.args[0], "happy")
self.failureResultOf(w1.when_key(), WormholeClosed)
self.failureResultOf(w1.when_verified(), WormholeClosed)
self.failureResultOf(w1.when_version(), WormholeClosed)
self.failureResultOf(w1.when_received(), WormholeClosed)
self.failureResultOf(w1.get_unverified_key(), WormholeClosed)
self.failureResultOf(w1.get_verifier(), WormholeClosed)
self.failureResultOf(w1.get_versions(), WormholeClosed)
self.failureResultOf(w1.get_message(), WormholeClosed)
@inlineCallbacks
@ -357,20 +343,20 @@ class Wormholes(ServerBase, unittest.TestCase):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code+"not")
code2 = yield w2.when_code()
code2 = yield w2.get_code()
self.assertNotEqual(code, code2)
# That's enough to allow both sides to discover the mismatch, but
# only after the confirmation message gets through. API calls that
# don't wait will appear to work until the mismatched confirmation
# message arrives.
w1.send(b"should still work")
w2.send(b"should still work")
w1.send_message(b"should still work")
w2.send_message(b"should still work")
key2 = yield w2.when_key() # should work
key2 = yield w2.get_unverified_key() # should work
# w2 has just received w1.PAKE, and is about to send w2.VERSION
key1 = yield w1.when_key() # should work
key1 = yield w1.get_unverified_key() # should work
# w1 has just received w2.PAKE, and is about to send w1.VERSION, and
# then will receive w2.VERSION. When it sees w2.VERSION, it will
# learn about the WrongPasswordError.
@ -378,54 +364,54 @@ class Wormholes(ServerBase, unittest.TestCase):
# API calls that wait (i.e. get) will errback. We collect all these
# Deferreds early to exercise the wait-then-fail path
d1_verified = w1.when_verified()
d1_version = w1.when_version()
d1_received = w1.when_received()
d2_verified = w2.when_verified()
d2_version = w2.when_version()
d2_received = w2.when_received()
d1_verified = w1.get_verifier()
d1_versions = w1.get_versions()
d1_received = w1.get_message()
d2_verified = w2.get_verifier()
d2_versions = w2.get_versions()
d2_received = w2.get_message()
# wait for each side to notice the failure
yield self.assertFailure(w1.when_verified(), WrongPasswordError)
yield self.assertFailure(w2.when_verified(), WrongPasswordError)
yield self.assertFailure(w1.get_verifier(), WrongPasswordError)
yield self.assertFailure(w2.get_verifier(), WrongPasswordError)
# and then wait for the rest of the loops to fire. if we had+used
# eventual-send, this wouldn't be a problem
yield pause_one_tick()
# now all the rest should have fired already
self.failureResultOf(d1_verified, WrongPasswordError)
self.failureResultOf(d1_version, WrongPasswordError)
self.failureResultOf(d1_versions, WrongPasswordError)
self.failureResultOf(d1_received, WrongPasswordError)
self.failureResultOf(d2_verified, WrongPasswordError)
self.failureResultOf(d2_version, WrongPasswordError)
self.failureResultOf(d2_versions, WrongPasswordError)
self.failureResultOf(d2_received, WrongPasswordError)
# and at this point, with the failure safely noticed by both sides,
# new when_key() calls should signal the failure, even before we
# close
# new get_unverified_key() calls should signal the failure, even
# before we close
# any new calls in the error state should immediately fail
self.failureResultOf(w1.when_key(), WrongPasswordError)
self.failureResultOf(w1.when_verified(), WrongPasswordError)
self.failureResultOf(w1.when_version(), WrongPasswordError)
self.failureResultOf(w1.when_received(), WrongPasswordError)
self.failureResultOf(w2.when_key(), WrongPasswordError)
self.failureResultOf(w2.when_verified(), WrongPasswordError)
self.failureResultOf(w2.when_version(), WrongPasswordError)
self.failureResultOf(w2.when_received(), WrongPasswordError)
self.failureResultOf(w1.get_unverified_key(), WrongPasswordError)
self.failureResultOf(w1.get_verifier(), WrongPasswordError)
self.failureResultOf(w1.get_versions(), WrongPasswordError)
self.failureResultOf(w1.get_message(), WrongPasswordError)
self.failureResultOf(w2.get_unverified_key(), WrongPasswordError)
self.failureResultOf(w2.get_verifier(), WrongPasswordError)
self.failureResultOf(w2.get_versions(), WrongPasswordError)
self.failureResultOf(w2.get_message(), WrongPasswordError)
yield self.assertFailure(w1.close(), WrongPasswordError)
yield self.assertFailure(w2.close(), WrongPasswordError)
# API calls should still get the error, not WormholeClosed
self.failureResultOf(w1.when_key(), WrongPasswordError)
self.failureResultOf(w1.when_verified(), WrongPasswordError)
self.failureResultOf(w1.when_version(), WrongPasswordError)
self.failureResultOf(w1.when_received(), WrongPasswordError)
self.failureResultOf(w2.when_key(), WrongPasswordError)
self.failureResultOf(w2.when_verified(), WrongPasswordError)
self.failureResultOf(w2.when_version(), WrongPasswordError)
self.failureResultOf(w2.when_received(), WrongPasswordError)
self.failureResultOf(w1.get_unverified_key(), WrongPasswordError)
self.failureResultOf(w1.get_verifier(), WrongPasswordError)
self.failureResultOf(w1.get_versions(), WrongPasswordError)
self.failureResultOf(w1.get_message(), WrongPasswordError)
self.failureResultOf(w2.get_unverified_key(), WrongPasswordError)
self.failureResultOf(w2.get_verifier(), WrongPasswordError)
self.failureResultOf(w2.get_versions(), WrongPasswordError)
self.failureResultOf(w2.get_message(), WrongPasswordError)
@inlineCallbacks
def test_wrong_password_with_spaces(self):
@ -442,21 +428,21 @@ class Wormholes(ServerBase, unittest.TestCase):
w1 = wormhole.create(APPID, self.relayurl, reactor)
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code)
v1 = yield w1.when_verified() # early
v2 = yield w2.when_verified()
v1 = yield w1.get_verifier() # early
v2 = yield w2.get_verifier()
self.failUnlessEqual(type(v1), type(b""))
self.failUnlessEqual(v1, v2)
w1.send(b"data1")
w2.send(b"data2")
dataX = yield w1.when_received()
dataY = yield w2.when_received()
w1.send_message(b"data1")
w2.send_message(b"data2")
dataX = yield w1.get_message()
dataY = yield w2.get_message()
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
# calling when_verified() this late should fire right away
v1_late = self.successResultOf(w2.when_verified())
# calling get_verifier() this late should fire right away
v1_late = self.successResultOf(w2.get_verifier())
self.assertEqual(v1_late, v1)
yield w1.close()
@ -470,11 +456,11 @@ class Wormholes(ServerBase, unittest.TestCase):
w2 = wormhole.create(APPID, self.relayurl, reactor,
versions={"w2": 456})
w1.allocate_code()
code = yield w1.when_code()
code = yield w1.get_code()
w2.set_code(code)
w1_versions = yield w2.when_version()
w1_versions = yield w2.get_versions()
self.assertEqual(w1_versions, {"w1": 123})
w2_versions = yield w1.when_version()
w2_versions = yield w1.get_versions()
self.assertEqual(w2_versions, {"w2": 456})
yield w1.close()
yield w2.close()
@ -496,8 +482,8 @@ class Wormholes(ServerBase, unittest.TestCase):
w2 = wormhole.create(APPID, self.relayurl, reactor)
w1.set_code("123-purple-elephant")
w2.set_code("123-purple-elephant")
w1.send(b"data1"), w2.send(b"data2")
dl = yield self.doBoth(w1.when_received(), w2.when_received())
w1.send_message(b"data1"), w2.send_message(b"data2")
dl = yield self.doBoth(w1.get_message(), w2.get_message())
(dataX, dataY) = dl
self.assertEqual(dataX, b"data2")
self.assertEqual(dataY, b"data1")
@ -537,7 +523,7 @@ class Errors(ServerBase, unittest.TestCase):
def test_allocate_and_set_code(self):
w = wormhole.create(APPID, self.relayurl, reactor)
w.allocate_code()
yield w.when_code()
yield w.get_code()
with self.assertRaises(OnlyOneCodeError):
w.set_code("123-nope")
yield self.assertFailure(w.close(), LonelyError)
@ -550,8 +536,8 @@ class Reconnection(ServerBase, unittest.TestCase):
w1._boss._RC._debug_record_inbound_f = w1_in.append
#w1.debug_set_trace("W1")
w1.allocate_code()
code = yield w1.when_code()
w1.send(b"data1") # will be queued until wormhole is established
code = yield w1.get_code()
w1.send_message(b"data1") # queued until wormhole is established
# now wait until we've deposited all our messages on the server
def seen_our_pake():
@ -577,11 +563,11 @@ class Reconnection(ServerBase, unittest.TestCase):
#w2.debug_set_trace(" W2")
w2.set_code(code)
dataY = yield w2.when_received()
dataY = yield w2.get_message()
self.assertEqual(dataY, b"data1")
w2.send(b"data2")
dataX = yield w1.when_received()
w2.send_message(b"data2")
dataX = yield w1.get_message()
self.assertEqual(dataX, b"data2")
c1 = yield w1.close()

View File

@ -15,18 +15,18 @@ from .util import to_bytes
# We can provide different APIs to different apps:
# * Deferreds
# w.when_code().addCallback(print_code)
# w.send(data)
# w.when_received().addCallback(got_data)
# w.get_code().addCallback(print_code)
# w.send_message(data)
# w.get_message().addCallback(got_data)
# w.close().addCallback(closed)
# * delegate callbacks (better for journaled environments)
# w = wormhole(delegate=app)
# w.send(data)
# w.send_message(data)
# app.wormhole_got_code(code)
# app.wormhole_got_verifier(verifier)
# app.wormhole_got_version(versions)
# app.wormhole_receive(data)
# app.wormhole_got_versions(versions)
# app.wormhole_got_message(data)
# w.close()
# app.wormhole_closed()
#
@ -34,18 +34,6 @@ from .util import to_bytes
# wormhole(delegate=app, delegate_prefix="wormhole_",
# delegate_args=(args, kwargs))
class _WelcomeHandler:
def __init__(self, url, stderr=sys.stderr):
self.relay_url = url
self.stderr = stderr
def handle_welcome(self, welcome):
if "motd" in welcome:
motd_lines = welcome["motd"].splitlines()
motd_formatted = "\n ".join(motd_lines)
print("Server (at %s) says:\n %s" %
(self.relay_url, motd_formatted), file=self.stderr)
@attrs
@implementer(IWormhole)
class _DelegatedWormhole(object):
@ -72,7 +60,7 @@ class _DelegatedWormhole(object):
## }
## return s
def send(self, plaintext):
def send_message(self, plaintext):
self._boss.send(plaintext)
def derive_key(self, purpose, length):
@ -94,23 +82,27 @@ class _DelegatedWormhole(object):
self._boss._set_trace(client_name, which, file)
# from below
def got_welcome(self, welcome):
self._delegate.wormhole_got_welcome(welcome)
def got_code(self, code):
self._delegate.wormhole_code(code)
self._delegate.wormhole_got_code(code)
def got_key(self, key):
self._delegate.wormhole_key(key)
self._delegate.wormhole_got_unverified_key(key)
self._key = key # for derive_key()
def got_verifier(self, verifier):
self._delegate.wormhole_verified(verifier)
def got_version(self, versions):
self._delegate.wormhole_version(versions)
self._delegate.wormhole_got_verifier(verifier)
def got_versions(self, versions):
self._delegate.wormhole_got_versions(versions)
def received(self, plaintext):
self._delegate.wormhole_received(plaintext)
self._delegate.wormhole_got_message(plaintext)
def closed(self, result):
self._delegate.wormhole_closed(result)
@implementer(IWormhole)
class _DeferredWormhole(object):
def __init__(self):
self._welcome = None
self._welcome_observers = []
self._code = None
self._code_observers = []
self._key = None
@ -129,7 +121,7 @@ class _DeferredWormhole(object):
self._boss = boss
# from above
def when_code(self):
def get_code(self):
# TODO: consider throwing error unless one of allocate/set/input_code
# was called first. It's legit to grab the Deferred before triggering
# the process that will cause it to fire, but forbidding that
@ -143,7 +135,16 @@ class _DeferredWormhole(object):
self._code_observers.append(d)
return d
def when_key(self):
def get_welcome(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._welcome is not None:
return defer.succeed(self._welcome)
d = defer.Deferred()
self._welcome_observers.append(d)
return d
def get_unverified_key(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._key is not None:
@ -152,7 +153,7 @@ class _DeferredWormhole(object):
self._key_observers.append(d)
return d
def when_verified(self):
def get_verifier(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._verifier is not None:
@ -161,7 +162,7 @@ class _DeferredWormhole(object):
self._verifier_observers.append(d)
return d
def when_version(self):
def get_versions(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._versions is not None:
@ -170,7 +171,7 @@ class _DeferredWormhole(object):
self._version_observers.append(d)
return d
def when_received(self):
def get_message(self):
if self._observer_result is not None:
return defer.fail(self._observer_result)
if self._received_data:
@ -187,7 +188,8 @@ class _DeferredWormhole(object):
self._boss.set_code(code)
# no .serialize in Deferred-mode
def send(self, plaintext):
def send_message(self, plaintext):
self._boss.send(plaintext)
def derive_key(self, purpose, length):
@ -217,6 +219,11 @@ class _DeferredWormhole(object):
self._boss._set_trace(client_name, which, file)
# from below
def got_welcome(self, welcome):
self._welcome = welcome
for d in self._welcome_observers:
d.callback(welcome)
self._welcome_observers[:] = []
def got_code(self, code):
self._code = code
for d in self._code_observers:
@ -227,12 +234,13 @@ class _DeferredWormhole(object):
for d in self._key_observers:
d.callback(key)
self._key_observers[:] = []
def got_verifier(self, verifier):
self._verifier = verifier
for d in self._verifier_observers:
d.callback(verifier)
self._verifier_observers[:] = []
def got_version(self, versions):
def got_versions(self, versions):
self._versions = versions
for d in self._version_observers:
d.callback(versions)
@ -245,7 +253,7 @@ class _DeferredWormhole(object):
self._received_data.append(plaintext)
def closed(self, result):
#print("closed", result, type(result))
#print("closed", result, type(result), file=sys.stderr)
if isinstance(result, Exception):
self._observer_result = self._closed_result = failure.Failure(result)
else:
@ -253,6 +261,8 @@ class _DeferredWormhole(object):
self._observer_result = WormholeClosed(result)
# but w.close() only gets error if we're unhappy
self._closed_result = result
for d in self._welcome_observers:
d.errback(self._observer_result)
for d in self._code_observers:
d.errback(self._observer_result)
for d in self._key_observers:
@ -270,13 +280,11 @@ class _DeferredWormhole(object):
def create(appid, relay_url, reactor, # use keyword args for everything else
versions={},
delegate=None, journal=None, tor_manager=None,
timing=None, welcome_handler=None,
timing=None,
stderr=sys.stderr):
timing = timing or DebugTiming()
side = bytes_to_hexstr(os.urandom(5))
journal = journal or ImmediateJournal()
if not welcome_handler:
welcome_handler = _WelcomeHandler(relay_url).handle_welcome
if delegate:
w = _DelegatedWormhole(delegate)
else:
@ -284,8 +292,7 @@ def create(appid, relay_url, reactor, # use keyword args for everything else
wormhole_versions = {} # will be used to indicate Wormhole capabilities
wormhole_versions["app_versions"] = versions # app-specific capabilities
b = Boss(w, side, relay_url, appid, wormhole_versions,
welcome_handler, reactor, journal,
tor_manager, timing)
reactor, journal, tor_manager, timing)
w._set_boss(b)
b.start()
return w

View File

@ -41,14 +41,14 @@ def receive(reactor, appid, relay_url, code,
wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm)
if code is None:
wh.allocate_code()
code = yield wh.when_code()
code = yield wh.get_code()
else:
wh.set_code(code)
# we'll call this no matter what, even if you passed in a code --
# maybe it should be only in the 'if' block above?
if on_code:
on_code(code)
data = yield wh.when_received()
data = yield wh.get_message()
data = json.loads(data.decode("utf-8"))
offer = data.get('offer', None)
if not offer:
@ -58,7 +58,8 @@ def receive(reactor, appid, relay_url, code,
msg = None
if 'message' in offer:
msg = offer['message']
wh.send(json.dumps({"answer": {"message_ack": "ok"}}).encode("utf-8"))
wh.send_message(json.dumps({"answer":
{"message_ack": "ok"}}).encode("utf-8"))
else:
raise Exception(
@ -104,20 +105,20 @@ def send(reactor, appid, relay_url, data, code,
wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm)
if code is None:
wh.allocate_code()
code = yield wh.when_code()
code = yield wh.get_code()
else:
wh.set_code(code)
if on_code:
on_code(code)
wh.send(
wh.send_message(
json.dumps({
"offer": {
"message": data
}
}).encode("utf-8")
)
data = yield wh.when_received()
data = yield wh.get_message()
data = json.loads(data.decode("utf-8"))
answer = data.get('answer', None)
yield wh.close()