rewrite welcome handler
This commit is contained in:
parent
152775c5c0
commit
76f5960517
69
docs/api.md
69
docs/api.md
|
@ -288,6 +288,75 @@ sides are the same, call `send()` to continue the protocol. If you call
|
||||||
`send()` before `verify()`, it will perform the complete protocol without
|
`send()` before `verify()`, it will perform the complete protocol without
|
||||||
pausing.
|
pausing.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
The welcome message serves three main purposes:
|
||||||
|
|
||||||
|
* notify users about important server changes, such as CAPTCHA requirements
|
||||||
|
driven by overload, or donation requests
|
||||||
|
* enable future protocol negotiation between clients and the server
|
||||||
|
* advise users of the CLI tools (`wormhole send`) to upgrade to a new version
|
||||||
|
|
||||||
|
There are three keys currently defined for the welcome message, all of which
|
||||||
|
are optional (the welcome message omits "error" and "motd" unless the server
|
||||||
|
operator needs to signal a problem).
|
||||||
|
|
||||||
|
* `motd`: if this key is present, it will be a string with embedded newlines.
|
||||||
|
The client should display this string to the user, including a note that it
|
||||||
|
comes from the magic-wormhole Rendezvous Server and that server's URL.
|
||||||
|
* `error`: if present, the server has decided it cannot service this client.
|
||||||
|
The string will be wrapped in a `WelcomeError` (which is a subclass of
|
||||||
|
`WormholeError`), and all API calls will signal errors (pending Deferreds
|
||||||
|
will errback). The rendezvous connection will be closed.
|
||||||
|
* `current_cli_version`: if present, the server is advising instances of the
|
||||||
|
CLI tools (the `wormhole` command included in the python distribution) that
|
||||||
|
there is a newer release available, thus users should upgrade if they can,
|
||||||
|
because more features will be available if both clients are running the
|
||||||
|
same version. The CLI tools compare this string against their `__version__`
|
||||||
|
and can print a short message to stderr if an upgrade is warranted.
|
||||||
|
|
||||||
|
There is currently no facility in the server to actually send `motd`, but a
|
||||||
|
static `error` string can be included by running the server with
|
||||||
|
`--signal-error=MESSAGE`.
|
||||||
|
|
||||||
|
The main idea of `error` is to allow the server to cleanly inform the client
|
||||||
|
about some necessary action it didn't take. The server currently sends the
|
||||||
|
welcome message as soon as the client connects (even before it receives the
|
||||||
|
"claim" request), but a future server could wait for a required client
|
||||||
|
message and signal an error (via the Welcome message) if it didn't see this
|
||||||
|
extra message before the CLAIM arrived.
|
||||||
|
|
||||||
|
This could enable changes to the protocol, e.g. requiring a CAPTCHA or
|
||||||
|
proof-of-work token when the server is under DoS attack. The new server would
|
||||||
|
send the current requirements in an initial message (which old clients would
|
||||||
|
ignore). New clients would be required to send the token before their "claim"
|
||||||
|
message. If the server sees "claim" before "token", it knows that the client
|
||||||
|
is too old to know about this protocol, and it could send a "welcome" with an
|
||||||
|
`error` field containing instructions (explaining to the user that the server
|
||||||
|
is under attack, and they must either upgrade to a client that can speak the
|
||||||
|
new protocol, or wait until the attack has passed). Either case is better
|
||||||
|
than an opaque exception later when the required message fails to arrive.
|
||||||
|
|
||||||
|
(Note that the server can also send an explicit ERROR message at any time,
|
||||||
|
and the client should react with a ServerError. Versions 0.9.2 and earlier of
|
||||||
|
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 default welcome handler will print `motd` to stderr, and will ignore
|
||||||
|
`current_cli_version`.
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
As the wormhole connection is established, several events may be dispatched
|
As the wormhole connection is established, several events may be dispatched
|
||||||
|
|
|
@ -22,6 +22,10 @@ digraph {
|
||||||
P_close_error -> S_closing
|
P_close_error -> S_closing
|
||||||
S0 -> P_close_lonely [label="close"]
|
S0 -> P_close_lonely [label="close"]
|
||||||
|
|
||||||
|
S0 -> P_close_unwelcome [label="rx_unwelcome"]
|
||||||
|
P_close_unwelcome [shape="box" label="T.close(unwelcome)"]
|
||||||
|
P_close_unwelcome -> S_closing
|
||||||
|
|
||||||
P0_build [shape="box" label="W.got_code"]
|
P0_build [shape="box" label="W.got_code"]
|
||||||
P0_build -> S1
|
P0_build -> S1
|
||||||
S1 [label="S1: lonely" color="orange"]
|
S1 [label="S1: lonely" color="orange"]
|
||||||
|
@ -30,6 +34,7 @@ digraph {
|
||||||
|
|
||||||
S1 -> P_close_error [label="rx_error"]
|
S1 -> P_close_error [label="rx_error"]
|
||||||
S1 -> P_close_scary [label="scared" color="red"]
|
S1 -> P_close_scary [label="scared" color="red"]
|
||||||
|
S1 -> P_close_unwelcome [label="rx_unwelcome"]
|
||||||
S1 -> P_close_lonely [label="close"]
|
S1 -> P_close_lonely [label="close"]
|
||||||
P_close_lonely [shape="box" label="T.close(lonely)"]
|
P_close_lonely [shape="box" label="T.close(lonely)"]
|
||||||
P_close_lonely -> S_closing
|
P_close_lonely -> S_closing
|
||||||
|
@ -52,6 +57,7 @@ digraph {
|
||||||
|
|
||||||
S2 -> P_close_error [label="rx_error"]
|
S2 -> P_close_error [label="rx_error"]
|
||||||
S2 -> P_close_scary [label="scared" color="red"]
|
S2 -> P_close_scary [label="scared" color="red"]
|
||||||
|
S2 -> P_close_unwelcome [label="rx_unwelcome"]
|
||||||
|
|
||||||
S_closing [label="closing"]
|
S_closing [label="closing"]
|
||||||
S_closing -> P_closed [label="closed\nerror"]
|
S_closing -> P_closed [label="closed\nerror"]
|
||||||
|
@ -67,7 +73,7 @@ digraph {
|
||||||
|
|
||||||
{rank=same; Other S_closed}
|
{rank=same; Other S_closed}
|
||||||
Other [shape="box" style="dashed"
|
Other [shape="box" style="dashed"
|
||||||
label="rx_welcome -> process\nsend -> S.send\ngot_message -> got_version or got_phase\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate_Code -> C.allocate_code\ninput_code -> C.input_code\nset_code -> C.set_code"
|
label="rx_welcome -> process (maybe rx_unwelcome)\nsend -> S.send\ngot_message -> got_version or got_phase\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate_Code -> C.allocate_code\ninput_code -> C.input_code\nset_code -> C.set_code"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ from ._code import Code
|
||||||
from ._terminator import Terminator
|
from ._terminator import Terminator
|
||||||
from ._wordlist import PGPWordList
|
from ._wordlist import PGPWordList
|
||||||
from .errors import (ServerError, LonelyError, WrongPasswordError,
|
from .errors import (ServerError, LonelyError, WrongPasswordError,
|
||||||
KeyFormatError, OnlyOneCodeError, _UnknownPhaseError)
|
KeyFormatError, OnlyOneCodeError, _UnknownPhaseError,
|
||||||
|
WelcomeError)
|
||||||
from .util import bytes_to_dict
|
from .util import bytes_to_dict
|
||||||
|
|
||||||
@attrs
|
@attrs
|
||||||
|
@ -162,11 +163,28 @@ class Boss(object):
|
||||||
@m.input()
|
@m.input()
|
||||||
def close(self): pass
|
def close(self): pass
|
||||||
|
|
||||||
# from RendezvousConnector. rx_error an error message from the server
|
# from RendezvousConnector:
|
||||||
# (probably because of something we did, or due to CrowdedError). error
|
# * "rx_welcome" is the Welcome message, which might signal an error, or
|
||||||
# is when an exception happened while it tried to deliver something else
|
# our welcome_handler might signal one
|
||||||
|
# * "rx_error" is error message from the server (probably because of
|
||||||
|
# something we said badly, or due to CrowdedError)
|
||||||
|
# * "error" is when an exception happened while it tried to deliver
|
||||||
|
# something else
|
||||||
|
def rx_welcome(self, welcome):
|
||||||
|
try:
|
||||||
|
if "error" in welcome:
|
||||||
|
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
|
||||||
|
# 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
|
||||||
|
except WelcomeError as welcome_error:
|
||||||
|
self.rx_unwelcome(welcome_error)
|
||||||
@m.input()
|
@m.input()
|
||||||
def rx_welcome(self, welcome): pass
|
def rx_unwelcome(self, welcome_error): pass
|
||||||
@m.input()
|
@m.input()
|
||||||
def rx_error(self, errmsg, orig): pass
|
def rx_error(self, errmsg, orig): pass
|
||||||
@m.input()
|
@m.input()
|
||||||
|
@ -207,11 +225,6 @@ class Boss(object):
|
||||||
@m.input()
|
@m.input()
|
||||||
def closed(self): pass
|
def closed(self): pass
|
||||||
|
|
||||||
|
|
||||||
@m.output()
|
|
||||||
def process_welcome(self, welcome):
|
|
||||||
self._welcome_handler(welcome)
|
|
||||||
|
|
||||||
@m.output()
|
@m.output()
|
||||||
def do_got_code(self, code):
|
def do_got_code(self, code):
|
||||||
self._W.got_code(code)
|
self._W.got_code(code)
|
||||||
|
@ -232,6 +245,11 @@ class Boss(object):
|
||||||
self._S.send("%d" % phase, plaintext)
|
self._S.send("%d" % phase, plaintext)
|
||||||
|
|
||||||
@m.output()
|
@m.output()
|
||||||
|
def close_unwelcome(self, welcome_error):
|
||||||
|
#assert isinstance(err, WelcomeError)
|
||||||
|
self._result = welcome_error
|
||||||
|
self._T.close("unwelcome")
|
||||||
|
@m.output()
|
||||||
def close_error(self, errmsg, orig):
|
def close_error(self, errmsg, orig):
|
||||||
self._result = ServerError(errmsg)
|
self._result = ServerError(errmsg)
|
||||||
self._T.close("errory")
|
self._T.close("errory")
|
||||||
|
@ -275,12 +293,12 @@ class Boss(object):
|
||||||
|
|
||||||
S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely])
|
S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely])
|
||||||
S0_empty.upon(send, enter=S0_empty, outputs=[S_send])
|
S0_empty.upon(send, enter=S0_empty, outputs=[S_send])
|
||||||
S0_empty.upon(rx_welcome, enter=S0_empty, outputs=[process_welcome])
|
S0_empty.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
|
||||||
S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code])
|
S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code])
|
||||||
S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
||||||
S0_empty.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
S0_empty.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
||||||
|
|
||||||
S1_lonely.upon(rx_welcome, enter=S1_lonely, outputs=[process_welcome])
|
S1_lonely.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
|
||||||
S1_lonely.upon(happy, enter=S2_happy, outputs=[])
|
S1_lonely.upon(happy, enter=S2_happy, outputs=[])
|
||||||
S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared])
|
S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared])
|
||||||
S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely])
|
S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely])
|
||||||
|
@ -290,7 +308,7 @@ class Boss(object):
|
||||||
S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
||||||
S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
||||||
|
|
||||||
S2_happy.upon(rx_welcome, enter=S2_happy, outputs=[process_welcome])
|
S2_happy.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
|
||||||
S2_happy.upon(_got_phase, enter=S2_happy, outputs=[W_received])
|
S2_happy.upon(_got_phase, enter=S2_happy, outputs=[W_received])
|
||||||
S2_happy.upon(_got_version, enter=S2_happy, outputs=[process_version])
|
S2_happy.upon(_got_version, enter=S2_happy, outputs=[process_version])
|
||||||
S2_happy.upon(scared, enter=S3_closing, outputs=[close_scared])
|
S2_happy.upon(scared, enter=S3_closing, outputs=[close_scared])
|
||||||
|
@ -299,7 +317,7 @@ class Boss(object):
|
||||||
S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_error])
|
||||||
S2_happy.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
S2_happy.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
||||||
|
|
||||||
S3_closing.upon(rx_welcome, enter=S3_closing, outputs=[])
|
S3_closing.upon(rx_unwelcome, enter=S3_closing, outputs=[])
|
||||||
S3_closing.upon(rx_error, enter=S3_closing, outputs=[])
|
S3_closing.upon(rx_error, enter=S3_closing, outputs=[])
|
||||||
S3_closing.upon(_got_phase, enter=S3_closing, outputs=[])
|
S3_closing.upon(_got_phase, enter=S3_closing, outputs=[])
|
||||||
S3_closing.upon(_got_version, enter=S3_closing, outputs=[])
|
S3_closing.upon(_got_version, enter=S3_closing, outputs=[])
|
||||||
|
@ -310,7 +328,7 @@ class Boss(object):
|
||||||
S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed])
|
S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed])
|
||||||
S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error])
|
||||||
|
|
||||||
S4_closed.upon(rx_welcome, enter=S4_closed, outputs=[])
|
S4_closed.upon(rx_unwelcome, enter=S4_closed, outputs=[])
|
||||||
S4_closed.upon(_got_phase, enter=S4_closed, outputs=[])
|
S4_closed.upon(_got_phase, enter=S4_closed, outputs=[])
|
||||||
S4_closed.upon(_got_version, enter=S4_closed, outputs=[])
|
S4_closed.upon(_got_version, enter=S4_closed, outputs=[])
|
||||||
S4_closed.upon(happy, enter=S4_closed, outputs=[])
|
S4_closed.upon(happy, enter=S4_closed, outputs=[])
|
||||||
|
|
|
@ -5,11 +5,12 @@ from humanize import naturalsize
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||||
from twisted.python import log
|
from twisted.python import log
|
||||||
from .. import wormhole
|
from .. import wormhole, __version__
|
||||||
from ..transit import TransitReceiver
|
from ..transit import TransitReceiver
|
||||||
from ..errors import TransferError, WormholeClosedError, NoTorError
|
from ..errors import TransferError, WormholeClosedError, NoTorError
|
||||||
from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr,
|
from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr,
|
||||||
estimate_free_space)
|
estimate_free_space)
|
||||||
|
from .welcome import CLIWelcomeHandler
|
||||||
|
|
||||||
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
||||||
VERIFY_TIMER = 1
|
VERIFY_TIMER = 1
|
||||||
|
@ -61,10 +62,13 @@ class TwistedReceiver:
|
||||||
# with the user handing off the wormhole code
|
# with the user handing off the wormhole code
|
||||||
yield self._tor_manager.start()
|
yield self._tor_manager.start()
|
||||||
|
|
||||||
|
wh = CLIWelcomeHandler(self.args.relay_url, __version__,
|
||||||
|
self.args.stderr)
|
||||||
w = wormhole.create(self.args.appid or APPID, self.args.relay_url,
|
w = wormhole.create(self.args.appid or APPID, self.args.relay_url,
|
||||||
self._reactor,
|
self._reactor,
|
||||||
tor_manager=self._tor_manager,
|
tor_manager=self._tor_manager,
|
||||||
timing=self.args.timing)
|
timing=self.args.timing,
|
||||||
|
welcome_handler=wh.handle_welcome)
|
||||||
# I wanted to do this instead:
|
# I wanted to do this instead:
|
||||||
#
|
#
|
||||||
# try:
|
# try:
|
||||||
|
|
|
@ -7,9 +7,10 @@ from twisted.protocols import basic
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||||
from ..errors import TransferError, WormholeClosedError, NoTorError
|
from ..errors import TransferError, WormholeClosedError, NoTorError
|
||||||
from .. import wormhole
|
from .. import wormhole, __version__
|
||||||
from ..transit import TransitSender
|
from ..transit import TransitSender
|
||||||
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
|
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr
|
||||||
|
from .welcome import CLIWelcomeHandler
|
||||||
|
|
||||||
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
APPID = u"lothar.com/wormhole/text-or-file-xfer"
|
||||||
VERIFY_TIMER = 1
|
VERIFY_TIMER = 1
|
||||||
|
@ -52,10 +53,13 @@ class Sender:
|
||||||
# with the user handing off the wormhole code
|
# with the user handing off the wormhole code
|
||||||
yield self._tor_manager.start()
|
yield self._tor_manager.start()
|
||||||
|
|
||||||
|
wh = CLIWelcomeHandler(self._args.relay_url, __version__,
|
||||||
|
self._args.stderr)
|
||||||
w = wormhole.create(self._args.appid or APPID, self._args.relay_url,
|
w = wormhole.create(self._args.appid or APPID, self._args.relay_url,
|
||||||
self._reactor,
|
self._reactor,
|
||||||
tor_manager=self._tor_manager,
|
tor_manager=self._tor_manager,
|
||||||
timing=self._timing)
|
timing=self._timing,
|
||||||
|
welcome_handler=wh.handle_welcome)
|
||||||
d = self._go(w)
|
d = self._go(w)
|
||||||
|
|
||||||
# if we succeed, we should close and return the w.close results
|
# if we succeed, we should close and return the w.close results
|
||||||
|
|
24
src/wormhole/cli/welcome.py
Normal file
24
src/wormhole/cli/welcome.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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)
|
||||||
|
|
45
src/wormhole/test/test_cli.py
Normal file
45
src/wormhole/test/test_cli.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from __future__ import print_function, absolute_import, unicode_literals
|
||||||
|
import io
|
||||||
|
from twisted.trial import unittest
|
||||||
|
from ..cli import welcome
|
||||||
|
|
||||||
|
class Welcome(unittest.TestCase):
|
||||||
|
def do(self, welcome_message, my_version="2.0", twice=False):
|
||||||
|
stderr = io.StringIO()
|
||||||
|
h = welcome.CLIWelcomeHandler("url", my_version, stderr)
|
||||||
|
h.handle_welcome(welcome_message)
|
||||||
|
if twice:
|
||||||
|
h.handle_welcome(welcome_message)
|
||||||
|
return stderr.getvalue()
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
stderr = self.do({})
|
||||||
|
self.assertEqual(stderr, "")
|
||||||
|
|
||||||
|
def test_version_current(self):
|
||||||
|
stderr = self.do({"current_cli_version": "2.0"})
|
||||||
|
self.assertEqual(stderr, "")
|
||||||
|
|
||||||
|
def test_version_old(self):
|
||||||
|
stderr = self.do({"current_cli_version": "3.0"})
|
||||||
|
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_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")
|
||||||
|
self.assertEqual(stderr, "")
|
||||||
|
|
||||||
|
def test_motd(self):
|
||||||
|
stderr = self.do({"motd": "hello"})
|
||||||
|
self.assertEqual(stderr, "Server (at url) says:\n hello\n")
|
|
@ -713,6 +713,9 @@ class NotWelcome(ServerBase, unittest.TestCase):
|
||||||
send_d = cmd_send.send(self.cfg)
|
send_d = cmd_send.send(self.cfg)
|
||||||
f = yield self.assertFailure(send_d, WelcomeError)
|
f = yield self.assertFailure(send_d, WelcomeError)
|
||||||
self.assertEqual(str(f), "please upgrade XYZ")
|
self.assertEqual(str(f), "please upgrade XYZ")
|
||||||
|
# TODO: this comes from log.err() in cmd_send.Sender.go._bad, and I'm
|
||||||
|
# undecided about whether that ought to be doing log.err or not
|
||||||
|
self.flushLoggedErrors(WelcomeError)
|
||||||
|
|
||||||
@inlineCallbacks
|
@inlineCallbacks
|
||||||
def test_receiver(self):
|
def test_receiver(self):
|
||||||
|
@ -721,7 +724,7 @@ class NotWelcome(ServerBase, unittest.TestCase):
|
||||||
receive_d = cmd_receive.receive(self.cfg)
|
receive_d = cmd_receive.receive(self.cfg)
|
||||||
f = yield self.assertFailure(receive_d, WelcomeError)
|
f = yield self.assertFailure(receive_d, WelcomeError)
|
||||||
self.assertEqual(str(f), "please upgrade XYZ")
|
self.assertEqual(str(f), "please upgrade XYZ")
|
||||||
NotWelcome.skip = "not yet"
|
self.flushLoggedErrors(WelcomeError)
|
||||||
|
|
||||||
class Cleanup(ServerBase, unittest.TestCase):
|
class Cleanup(ServerBase, unittest.TestCase):
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
import os, json, re, gc
|
import os, json, re, gc, io
|
||||||
from binascii import hexlify, unhexlify
|
from binascii import hexlify, unhexlify
|
||||||
import mock
|
import mock
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
|
@ -38,70 +38,20 @@ def response(w, **kwargs):
|
||||||
|
|
||||||
class Welcome(unittest.TestCase):
|
class Welcome(unittest.TestCase):
|
||||||
def test_tolerate_no_current_version(self):
|
def test_tolerate_no_current_version(self):
|
||||||
w = wormhole._WelcomeHandler("relay_url", "current_cli_version", None)
|
w = wormhole._WelcomeHandler("relay_url")
|
||||||
w.handle_welcome({})
|
w.handle_welcome({})
|
||||||
|
|
||||||
def test_print_motd(self):
|
def test_print_motd(self):
|
||||||
w = wormhole._WelcomeHandler("relay_url", "current_cli_version", None)
|
stderr = io.StringIO()
|
||||||
with mock.patch("sys.stderr") as stderr:
|
w = wormhole._WelcomeHandler("relay_url", stderr=stderr)
|
||||||
w.handle_welcome({"motd": "message of\nthe day"})
|
w.handle_welcome({"motd": "message of\nthe day"})
|
||||||
self.assertEqual(stderr.method_calls,
|
self.assertEqual(stderr.getvalue(),
|
||||||
[mock.call.write("Server (at relay_url) says:\n"
|
"Server (at relay_url) says:\n message of\n the day\n")
|
||||||
" message of\n the day"),
|
|
||||||
mock.call.write("\n")])
|
|
||||||
# motd can be displayed multiple times
|
# motd can be displayed multiple times
|
||||||
with mock.patch("sys.stderr") as stderr2:
|
w.handle_welcome({"motd": "second message"})
|
||||||
w.handle_welcome({"motd": "second message"})
|
self.assertEqual(stderr.getvalue(),
|
||||||
self.assertEqual(stderr2.method_calls,
|
("Server (at relay_url) says:\n message of\n the day\n"
|
||||||
[mock.call.write("Server (at relay_url) says:\n"
|
"Server (at relay_url) says:\n second message\n"))
|
||||||
" second message"),
|
|
||||||
mock.call.write("\n")])
|
|
||||||
|
|
||||||
def test_current_version(self):
|
|
||||||
w = wormhole._WelcomeHandler("relay_url", "2.0", None)
|
|
||||||
with mock.patch("sys.stderr") as stderr:
|
|
||||||
w.handle_welcome({"current_cli_version": "2.0"})
|
|
||||||
self.assertEqual(stderr.method_calls, [])
|
|
||||||
|
|
||||||
with mock.patch("sys.stderr") as stderr:
|
|
||||||
w.handle_welcome({"current_cli_version": "3.0"})
|
|
||||||
exp1 = ("Warning: errors may occur unless both sides are"
|
|
||||||
" running the same version")
|
|
||||||
exp2 = ("Server claims 3.0 is current, but ours is 2.0")
|
|
||||||
self.assertEqual(stderr.method_calls,
|
|
||||||
[mock.call.write(exp1),
|
|
||||||
mock.call.write("\n"),
|
|
||||||
mock.call.write(exp2),
|
|
||||||
mock.call.write("\n"),
|
|
||||||
])
|
|
||||||
|
|
||||||
# warning is only displayed once
|
|
||||||
with mock.patch("sys.stderr") as stderr:
|
|
||||||
w.handle_welcome({"current_cli_version": "3.0"})
|
|
||||||
self.assertEqual(stderr.method_calls, [])
|
|
||||||
|
|
||||||
def test_non_release_version(self):
|
|
||||||
w = wormhole._WelcomeHandler("relay_url", "2.0-dirty", None)
|
|
||||||
with mock.patch("sys.stderr") as stderr:
|
|
||||||
w.handle_welcome({"current_cli_version": "3.0"})
|
|
||||||
self.assertEqual(stderr.method_calls, [])
|
|
||||||
|
|
||||||
def test_signal_error(self):
|
|
||||||
se = mock.Mock()
|
|
||||||
w = wormhole._WelcomeHandler("relay_url", "2.0", se)
|
|
||||||
w.handle_welcome({})
|
|
||||||
self.assertEqual(se.mock_calls, [])
|
|
||||||
|
|
||||||
w.handle_welcome({"error": "oops"})
|
|
||||||
self.assertEqual(len(se.mock_calls), 1)
|
|
||||||
self.assertEqual(len(se.mock_calls[0][1]), 2) # posargs
|
|
||||||
we = se.mock_calls[0][1][0]
|
|
||||||
self.assertIsInstance(we, WelcomeError)
|
|
||||||
self.assertEqual(we.args, ("oops",))
|
|
||||||
mood = se.mock_calls[0][1][1]
|
|
||||||
self.assertEqual(mood, "unwelcome")
|
|
||||||
# alas WelcomeError instances don't compare against each other
|
|
||||||
#self.assertEqual(se.mock_calls, [mock.call(WelcomeError("oops"))])
|
|
||||||
|
|
||||||
class Basic(unittest.TestCase):
|
class Basic(unittest.TestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
|
|
@ -10,7 +10,7 @@ from .timing import DebugTiming
|
||||||
from .journal import ImmediateJournal
|
from .journal import ImmediateJournal
|
||||||
from ._boss import Boss
|
from ._boss import Boss
|
||||||
from ._key import derive_key
|
from ._key import derive_key
|
||||||
from .errors import WelcomeError, NoKeyError, WormholeClosed
|
from .errors import NoKeyError, WormholeClosed
|
||||||
from .util import to_bytes
|
from .util import to_bytes
|
||||||
|
|
||||||
# We can provide different APIs to different apps:
|
# We can provide different APIs to different apps:
|
||||||
|
@ -39,34 +39,16 @@ def _log(client_name, machine_name, old_state, input, new_state):
|
||||||
old_state, input, new_state))
|
old_state, input, new_state))
|
||||||
|
|
||||||
class _WelcomeHandler:
|
class _WelcomeHandler:
|
||||||
def __init__(self, url, current_version, signal_error):
|
def __init__(self, url, stderr=sys.stderr):
|
||||||
self._ws_url = url
|
self.relay_url = url
|
||||||
self._version_warning_displayed = False
|
self.stderr = stderr
|
||||||
self._current_version = current_version
|
|
||||||
self._signal_error = signal_error
|
|
||||||
|
|
||||||
def handle_welcome(self, welcome):
|
def handle_welcome(self, welcome):
|
||||||
if "motd" in welcome:
|
if "motd" in welcome:
|
||||||
motd_lines = welcome["motd"].splitlines()
|
motd_lines = welcome["motd"].splitlines()
|
||||||
motd_formatted = "\n ".join(motd_lines)
|
motd_formatted = "\n ".join(motd_lines)
|
||||||
print("Server (at %s) says:\n %s" %
|
print("Server (at %s) says:\n %s" %
|
||||||
(self._ws_url, motd_formatted), file=sys.stderr)
|
(self.relay_url, motd_formatted), file=self.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 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=sys.stderr)
|
|
||||||
print("Server claims %s is current, but ours is %s"
|
|
||||||
% (welcome["current_cli_version"], self._current_version),
|
|
||||||
file=sys.stderr)
|
|
||||||
self._version_warning_displayed = True
|
|
||||||
|
|
||||||
if "error" in welcome:
|
|
||||||
return self._signal_error(WelcomeError(welcome["error"]),
|
|
||||||
"unwelcome")
|
|
||||||
|
|
||||||
@attrs
|
@attrs
|
||||||
@implementer(IWormhole)
|
@implementer(IWormhole)
|
||||||
|
@ -118,8 +100,6 @@ class _DelegatedWormhole(object):
|
||||||
# from below
|
# from below
|
||||||
def got_code(self, code):
|
def got_code(self, code):
|
||||||
self._delegate.wormhole_code(code)
|
self._delegate.wormhole_code(code)
|
||||||
def got_welcome(self, welcome):
|
|
||||||
pass # TODO
|
|
||||||
def got_key(self, key):
|
def got_key(self, key):
|
||||||
self._key = key # for derive_key()
|
self._key = key # for derive_key()
|
||||||
def got_verifier(self, verifier):
|
def got_verifier(self, verifier):
|
||||||
|
@ -232,8 +212,6 @@ class _DeferredWormhole(object):
|
||||||
for d in self._code_observers:
|
for d in self._code_observers:
|
||||||
d.callback(code)
|
d.callback(code)
|
||||||
self._code_observers[:] = []
|
self._code_observers[:] = []
|
||||||
def got_welcome(self, welcome):
|
|
||||||
pass # TODO
|
|
||||||
def got_key(self, key):
|
def got_key(self, key):
|
||||||
self._key = key # for derive_key()
|
self._key = key # for derive_key()
|
||||||
def got_verifier(self, verifier):
|
def got_verifier(self, verifier):
|
||||||
|
@ -280,10 +258,7 @@ def create(appid, relay_url, reactor, versions={},
|
||||||
side = bytes_to_hexstr(os.urandom(5))
|
side = bytes_to_hexstr(os.urandom(5))
|
||||||
journal = journal or ImmediateJournal()
|
journal = journal or ImmediateJournal()
|
||||||
if not welcome_handler:
|
if not welcome_handler:
|
||||||
from . import __version__
|
welcome_handler = _WelcomeHandler(relay_url).handle_welcome
|
||||||
signal_error = NotImplemented # TODO
|
|
||||||
wh = _WelcomeHandler(relay_url, __version__, signal_error)
|
|
||||||
welcome_handler = wh.handle_welcome
|
|
||||||
if delegate:
|
if delegate:
|
||||||
w = _DelegatedWormhole(delegate)
|
w = _DelegatedWormhole(delegate)
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user