From 94b1ed873954d1f260cba4880ed503d72ca17c25 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 3 Jun 2016 22:32:40 -0700 Subject: [PATCH 001/176] starting to draw new state machines --- .gitignore | 1 + docs/states-code.dot | 18 ++++++++++++++++++ src/wormhole/states.py | 11 +++++++++++ 3 files changed, 30 insertions(+) create mode 100644 docs/states-code.dot create mode 100644 src/wormhole/states.py diff --git a/.gitignore b/.gitignore index 9db0baa..c4cf83b 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ target/ /relay.sqlite /misc/node_modules/ /docs/events.png +/docs/states-code.png diff --git a/docs/states-code.dot b/docs/states-code.dot new file mode 100644 index 0000000..c32bca2 --- /dev/null +++ b/docs/states-code.dot @@ -0,0 +1,18 @@ +/* this state machine is just about the code */ + +digraph { + need_code [label="need\ncode"] + asking_for_code [label="asking\nuser\nfor\ncode"] + creating_code [label="allocating\nnameplate"] + creating_code2 [label="generating\nsecret"] + know_code + + need_code -> know_code [label="set_code()"] + + need_code -> asking_for_code [label="input_code()"] + asking_for_code -> know_code [label="user typed code"] + + need_code -> creating_code [label="get_code()"] + creating_code -> creating_code2 [label="rx allocation"] + creating_code2 -> know_code [label="generated secret"] +} diff --git a/src/wormhole/states.py b/src/wormhole/states.py new file mode 100644 index 0000000..8ad8089 --- /dev/null +++ b/src/wormhole/states.py @@ -0,0 +1,11 @@ + +from automat import MethodicalMachine + +class WormholeState(object): + _machine = MethodicalMachine() + + @_machine.state(initial=True) + def start(self): + pass + + From 73f3d8610748a40c1dd9fc792d6aba4055d46bc0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 15 Dec 2016 00:04:17 -0800 Subject: [PATCH 002/176] state machine should be complete, I think --- setup.py | 1 + src/wormhole/wormhole.py | 116 +++++++++++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index e8ff35b..24101ac 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ setup(name="magic-wormhole", "six", "twisted[tls]", "autobahn[twisted] >= 0.14.1", + "automat", "hkdf", "tqdm", "click", "humanize", diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 7c6de48..c3cc111 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -43,27 +43,29 @@ def make_confmsg(confkey, nonce): class WSClient(websocket.WebSocketClientProtocol): def onOpen(self): - self.wormhole_open = True - self.factory.d.callback(self) + #self.wormhole_open = True + self.connection_machine.onOpen() + #self.factory.d.callback(self) def onMessage(self, payload, isBinary): assert not isBinary self.wormhole._ws_dispatch_response(payload) def onClose(self, wasClean, code, reason): - if self.wormhole_open: - self.wormhole._ws_closed(wasClean, code, reason) - else: - # we closed before establishing a connection (onConnect) or - # finishing WebSocket negotiation (onOpen): errback - self.factory.d.errback(error.ConnectError(reason)) + self.connection_machine.onClose() + #if self.wormhole_open: + # self.wormhole._ws_closed(wasClean, code, reason) + #else: + # # we closed before establishing a connection (onConnect) or + # # finishing WebSocket negotiation (onOpen): errback + # self.factory.d.errback(error.ConnectError(reason)) class WSFactory(websocket.WebSocketClientFactory): protocol = WSClient def buildProtocol(self, addr): proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) - proto.wormhole = self.wormhole - proto.wormhole_open = False + proto.connection_machine = self.connection_machine + #proto.wormhole_open = False return proto @@ -215,6 +217,100 @@ class _WelcomeHandler: # states for nameplates, mailboxes, and the websocket connection (CLOSED, OPENING, OPEN, CLOSING) = ("closed", "opening", "open", "closing") +from automat import MethodicalMachine +# pip install (path to automat checkout)[visualize] +# automat-visualize wormhole.wormhole + +class _ConnectionMachine(object): + m = MethodicalMachine() + + def __init__(self, ws_url): + self._f = f = WSFactory(ws_url) + f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + f.connection_machine = self # calls onOpen and onClose + p = urlparse(ws_url) + self._ep = self._make_endpoint(p.hostname, p.port or 80) + self._connector = None + + @m.state(initial=True) + def initial(self): pass + @m.state() + def first_time_connecting(self): pass + @m.state() + def negotiating(self): pass + @m.state() + def open(self): pass + @m.state() + def waiting(self): pass + @m.state() + def connecting(self): pass + @m.state() + def disconnecting(self): pass + @m.state() + def disconnecting2(self): pass + @m.state(terminal=True) + def failed(self): pass + @m.state(terminal=True) + def closed(self): pass + + + @m.input() + def start(self): pass + @m.input() + def d_callback(self, p): pass + @m.input() + def d_errback(self, f): pass + @m.input() + def d_cancel(self): pass + @m.input() + def onOpen(self, ws): pass + @m.input() + def onClose(self): pass + @m.input() + def expire(self): pass + @m.input() + def close(self): pass + + @m.output() + def ep_connect(self): + "ep.connect()" + self._d = self._ep.connect(self._f) + self._d.addBoth(self.d_callback, self.d_errback) + @m.output() + def handle_connection(self, ws): + pass + @m.output() + def start_timer(self): + pass + @m.output() + def cancel_timer(self): + pass + @m.output() + def dropConnection(self): + pass + @m.output() + def notify_fail(self): + pass + + initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) + first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) + first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) + first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + disconnecting2.upon(d_errback, enter=closed, outputs=[]) + + negotiating.upon(onOpen, enter=open, outputs=[handle_connection]) + negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) + negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) + + open.upon(onClose, enter=waiting, outputs=[start_timer]) + open.upon(close, enter=disconnecting, outputs=[dropConnection]) + connecting.upon(d_callback, enter=negotiating, outputs=[]) + connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) + connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + + waiting.upon(expire, enter=connecting, outputs=[ep_connect]) + waiting.upon(close, enter=closed, outputs=[cancel_timer]) + disconnecting.upon(onClose, enter=closed, outputs=[]) class _Wormhole: DEBUG = False From d136028fa807dcb4c023178f6fdcbfb53edb0fe5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 16 Dec 2016 15:37:34 -0800 Subject: [PATCH 003/176] try adding args --- src/wormhole/wormhole.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index c3cc111..2ddfdc7 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -289,7 +289,7 @@ class _ConnectionMachine(object): def dropConnection(self): pass @m.output() - def notify_fail(self): + def notify_fail(self, f): pass initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) From b826e8c73c1eede126bd26dcb8d39bf4fc31d60e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 16 Dec 2016 17:26:06 -0800 Subject: [PATCH 004/176] hack args till they work, add ALLOW_CLOSE the diagram is a lot simpler if the only way to shut things down is to terminate the whole process --- src/wormhole/wormhole.py | 46 +++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 2ddfdc7..24cfcc4 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -223,6 +223,7 @@ from automat import MethodicalMachine class _ConnectionMachine(object): m = MethodicalMachine() + ALLOW_CLOSE = True def __init__(self, ws_url): self._f = f = WSFactory(ws_url) @@ -238,20 +239,21 @@ class _ConnectionMachine(object): def first_time_connecting(self): pass @m.state() def negotiating(self): pass + @m.state(terminal=True) + def failed(self): pass @m.state() def open(self): pass @m.state() def waiting(self): pass @m.state() def connecting(self): pass - @m.state() - def disconnecting(self): pass - @m.state() - def disconnecting2(self): pass - @m.state(terminal=True) - def failed(self): pass - @m.state(terminal=True) - def closed(self): pass + if ALLOW_CLOSE: + @m.state() + def disconnecting(self): pass + @m.state() + def disconnecting2(self): pass + @m.state(terminal=True) + def closed(self): pass @m.input() @@ -265,11 +267,12 @@ class _ConnectionMachine(object): @m.input() def onOpen(self, ws): pass @m.input() - def onClose(self): pass + def onClose(self, f): pass @m.input() def expire(self): pass - @m.input() - def close(self): pass + if ALLOW_CLOSE: + @m.input() + def close(self): pass @m.output() def ep_connect(self): @@ -280,7 +283,7 @@ class _ConnectionMachine(object): def handle_connection(self, ws): pass @m.output() - def start_timer(self): + def start_timer(self, f): pass @m.output() def cancel_timer(self): @@ -295,22 +298,27 @@ class _ConnectionMachine(object): initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) - first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) - disconnecting2.upon(d_errback, enter=closed, outputs=[]) + if ALLOW_CLOSE: + first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + disconnecting2.upon(d_errback, enter=closed, outputs=[]) negotiating.upon(onOpen, enter=open, outputs=[handle_connection]) - negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) + if ALLOW_CLOSE: + negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) open.upon(onClose, enter=waiting, outputs=[start_timer]) - open.upon(close, enter=disconnecting, outputs=[dropConnection]) + if ALLOW_CLOSE: + open.upon(close, enter=disconnecting, outputs=[dropConnection]) connecting.upon(d_callback, enter=negotiating, outputs=[]) connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) - connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + if ALLOW_CLOSE: + connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) waiting.upon(expire, enter=connecting, outputs=[ep_connect]) - waiting.upon(close, enter=closed, outputs=[cancel_timer]) - disconnecting.upon(onClose, enter=closed, outputs=[]) + if ALLOW_CLOSE: + waiting.upon(close, enter=closed, outputs=[cancel_timer]) + disconnecting.upon(onClose, enter=closed, outputs=[]) class _Wormhole: DEBUG = False From 14c8e7636400ae80f68a20b4523731c579629686 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 16 Dec 2016 18:52:22 -0800 Subject: [PATCH 005/176] onConnect, start manual tests, doesn't work yet --- src/wormhole/wormhole.py | 41 ++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 24cfcc4..d869822 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -1,7 +1,7 @@ from __future__ import print_function, absolute_import, unicode_literals import os, sys, re from six.moves.urllib_parse import urlparse -from twisted.internet import defer, endpoints, error +from twisted.internet import defer, endpoints #, error from twisted.internet.threads import deferToThread, blockingCallFromThread from twisted.internet.defer import inlineCallbacks, returnValue from twisted.python import log, failure @@ -43,16 +43,19 @@ def make_confmsg(confkey, nonce): class WSClient(websocket.WebSocketClientProtocol): def onOpen(self): - #self.wormhole_open = True - self.connection_machine.onOpen() + self.wormhole_open = True + ##self.connection_machine.onOpen(self) #self.factory.d.callback(self) + def onConnect(self): + self.connection_machine.onConnect(self) + def onMessage(self, payload, isBinary): assert not isBinary self.wormhole._ws_dispatch_response(payload) def onClose(self, wasClean, code, reason): - self.connection_machine.onClose() + self.connection_machine.onClose(f=None) #if self.wormhole_open: # self.wormhole._ws_closed(wasClean, code, reason) #else: @@ -225,13 +228,17 @@ class _ConnectionMachine(object): m = MethodicalMachine() ALLOW_CLOSE = True - def __init__(self, ws_url): + def __init__(self, ws_url, reactor): + self._reactor = reactor self._f = f = WSFactory(ws_url) f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) f.connection_machine = self # calls onOpen and onClose p = urlparse(ws_url) self._ep = self._make_endpoint(p.hostname, p.port or 80) self._connector = None + self._done_d = defer.Deferred() + def _make_endpoint(self, hostname, port): + return endpoints.HostnameEndpoint(self._reactor, hostname, port) @m.state(initial=True) def initial(self): pass @@ -257,15 +264,15 @@ class _ConnectionMachine(object): @m.input() - def start(self): pass + def start(self): pass ; print("in start") @m.input() - def d_callback(self, p): pass + def d_callback(self, p): pass ; print("in d_callback") @m.input() - def d_errback(self, f): pass + def d_errback(self, f): pass ; print("in d_errback") @m.input() def d_cancel(self): pass @m.input() - def onOpen(self, ws): pass + def onConnect(self, ws): pass ; print("in onConnect") @m.input() def onClose(self, f): pass @m.input() @@ -277,6 +284,7 @@ class _ConnectionMachine(object): @m.output() def ep_connect(self): "ep.connect()" + print("ep_connect()") self._d = self._ep.connect(self._f) self._d.addBoth(self.d_callback, self.d_errback) @m.output() @@ -298,11 +306,12 @@ class _ConnectionMachine(object): initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) + first_time_connecting.upon(onClose, enter=failed, outputs=[notify_fail]) if ALLOW_CLOSE: first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) disconnecting2.upon(d_errback, enter=closed, outputs=[]) - negotiating.upon(onOpen, enter=open, outputs=[handle_connection]) + negotiating.upon(onConnect, enter=open, outputs=[handle_connection]) if ALLOW_CLOSE: negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) @@ -312,6 +321,7 @@ class _ConnectionMachine(object): open.upon(close, enter=disconnecting, outputs=[dropConnection]) connecting.upon(d_callback, enter=negotiating, outputs=[]) connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) + connecting.upon(onClose, enter=waiting, outputs=[start_timer]) if ALLOW_CLOSE: connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) @@ -320,6 +330,17 @@ class _ConnectionMachine(object): waiting.upon(close, enter=closed, outputs=[cancel_timer]) disconnecting.upon(onClose, enter=closed, outputs=[]) +def tryit(reactor): + cm = _ConnectionMachine("ws://127.0.0.1:4000/v1", reactor) + print("_ConnectionMachine created") + cm.start() + print("waiting on _done_d to finish") + return cm._done_d + +if __name__ == "__main__": + from twisted.internet.task import react + react(tryit) + class _Wormhole: DEBUG = False From 3c9c0e58abf9c2b9da6ca4117716af7d0defd88f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 17 Dec 2016 12:29:10 -0800 Subject: [PATCH 006/176] move to _connection.py, add more state machines Starting on defining manager state machines for nameplates, mailboxes, the PAKE key-establishment process, and the bit that knows it can drop the connection when both nameplates and mailboxes have been released. --- src/wormhole/_connection.py | 403 ++++++++++++++++++++++++++++++++++++ src/wormhole/wormhole.py | 153 -------------- 2 files changed, 403 insertions(+), 153 deletions(-) create mode 100644 src/wormhole/_connection.py diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py new file mode 100644 index 0000000..6dcee77 --- /dev/null +++ b/src/wormhole/_connection.py @@ -0,0 +1,403 @@ + +from six.moves.urllib_parse import urlparse +from attr import attrs, attrib +from twisted.internet import protocol, reactor +from twisted.internet import defer, endpoints #, error +from autobahn.twisted import websocket +from automat import MethodicalMachine + +class WSClient(websocket.WebSocketClientProtocol): + def onConnect(self, response): + # this fires during WebSocket negotiation, and isn't very useful + # unless you want to modify the protocol settings + print("onConnect", response) + #self.connection_machine.onConnect(self) + + def onOpen(self, *args): + # this fires when the WebSocket is ready to go. No arguments + print("onOpen", args) + #self.wormhole_open = True + self.connection_machine.onOpen(self) + #self.factory.d.callback(self) + + def onMessage(self, payload, isBinary): + print("onMessage") + return + assert not isBinary + self.wormhole._ws_dispatch_response(payload) + + def onClose(self, wasClean, code, reason): + print("onClose") + self.connection_machine.onClose(f=None) + #if self.wormhole_open: + # self.wormhole._ws_closed(wasClean, code, reason) + #else: + # # we closed before establishing a connection (onConnect) or + # # finishing WebSocket negotiation (onOpen): errback + # self.factory.d.errback(error.ConnectError(reason)) + +class WSFactory(websocket.WebSocketClientFactory): + protocol = WSClient + def buildProtocol(self, addr): + proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) + proto.connection_machine = self.connection_machine + #proto.wormhole_open = False + return proto + + +class Dummy(protocol.Protocol): + def connectionMade(self): + print("connectionMade") + reactor.callLater(1.0, self.factory.cm.onConnect, "fake ws") + reactor.callLater(2.0, self.transport.loseConnection) + def connectionLost(self, why): + self.factory.cm.onClose(why) + +# pip install (path to automat checkout)[visualize] +# automat-visualize wormhole.wormhole + +class _WebSocketMachine(object): + m = MethodicalMachine() + ALLOW_CLOSE = True + + def __init__(self, ws_url, reactor): + self._reactor = reactor + self._f = f = WSFactory(ws_url) + f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + f.connection_machine = self # calls onOpen and onClose + #self._f = protocol.ClientFactory() + #self._f.cm = self + #self._f.protocol = Dummy + p = urlparse(ws_url) + self._ep = self._make_endpoint(p.hostname, p.port or 80) + self._connector = None + self._done_d = defer.Deferred() + def _make_endpoint(self, hostname, port): + return endpoints.HostnameEndpoint(self._reactor, hostname, port) + + @m.state(initial=True) + def initial(self): pass + @m.state() + def first_time_connecting(self): pass + @m.state() + def negotiating(self): pass + @m.state(terminal=True) + def failed(self): pass + @m.state() + def open(self): pass + @m.state() + def waiting(self): pass + @m.state() + def connecting(self): pass + if ALLOW_CLOSE: + @m.state() + def disconnecting(self): pass + @m.state() + def disconnecting2(self): pass + @m.state(terminal=True) + def closed(self): pass + + + @m.input() + def start(self): pass ; print("in start") + @m.input() + def d_callback(self, p): pass ; print("in d_callback", p) + @m.input() + def d_errback(self, f): pass ; print("in d_errback", f) + @m.input() + def d_cancel(self): pass + @m.input() + def onOpen(self, ws): pass ; print("in onOpen") + @m.input() + def onClose(self, f): pass + @m.input() + def expire(self): pass + if ALLOW_CLOSE: + @m.input() + def close(self): pass + + @m.output() + def ep_connect(self): + "ep.connect()" + print("ep_connect()") + self._d = self._ep.connect(self._f) + self._d.addCallbacks(self.d_callback, self.d_errback) + @m.output() + def handle_connection(self, ws): + print("handle_connection", ws) + #self._wormhole.new_connection(Connection(ws)) + @m.output() + def start_timer(self, f): + print("start_timer") + self._t = self._reactor.callLater(3.0, self.expire) + @m.output() + def cancel_timer(self): + print("cancel_timer") + self._t.cancel() + self._t = None + @m.output() + def dropConnection(self): + print("dropConnection") + self._ws.dropConnection() + @m.output() + def notify_fail(self, f): + print("notify_fail", f.value) + self._done_d.errback(f) + + initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) + first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) + first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) + first_time_connecting.upon(onClose, enter=failed, outputs=[notify_fail]) + if ALLOW_CLOSE: + first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + disconnecting2.upon(d_errback, enter=closed, outputs=[]) + + negotiating.upon(onOpen, enter=open, outputs=[handle_connection]) + if ALLOW_CLOSE: + negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) + negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) + + open.upon(onClose, enter=waiting, outputs=[start_timer]) + if ALLOW_CLOSE: + open.upon(close, enter=disconnecting, outputs=[dropConnection]) + connecting.upon(d_callback, enter=negotiating, outputs=[]) + connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) + connecting.upon(onClose, enter=waiting, outputs=[start_timer]) + if ALLOW_CLOSE: + connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + + waiting.upon(expire, enter=connecting, outputs=[ep_connect]) + if ALLOW_CLOSE: + waiting.upon(close, enter=closed, outputs=[cancel_timer]) + disconnecting.upon(onClose, enter=closed, outputs=[]) + +def tryit(reactor): + cm = _WebSocketMachine("ws://127.0.0.1:4000/v1", reactor) + print("_ConnectionMachine created") + print("start:", cm.start()) + print("waiting on _done_d to finish") + return cm._done_d + +# success: d_callback, onConnect(response), onOpen(), onMessage() +# negotifail: d_callback, onClose() +# noconnect: d_errback + +def tryws(reactor): + ws_url = "ws://127.0.0.1:40001/v1" + f = WSFactory(ws_url) + p = urlparse(ws_url) + ep = endpoints.HostnameEndpoint(reactor, p.hostname, p.port or 80) + d = ep.connect(f) + def _good(p): print("_good", p) + def _bad(f): print("_bad", f) + d.addCallbacks(_good, _bad) + return defer.Deferred() + +if __name__ == "__main__": + import sys + from twisted.python import log + log.startLogging(sys.stdout) + from twisted.internet.task import react + react(tryit) + +@attrs +class Connection(object): + _ws = attrib() + _appid = attrib() + _side = attrib() + _ws_machine = attrib() + m = MethodicalMachine() + + @m.state(initial=True) + def unbound(self): pass + @m.state() + def binding(self): pass + @m.state() + def neither(self): pass + @m.state() + def has_nameplate(self): pass + @m.state() + def has_mailbox(self): pass + @m.state() + def has_both(self): pass + @m.state() + def closing(self): pass + @m.state() + def closed(self): pass + + @m.input() + def bind(self): pass + @m.input() + def ack_bind(self): pass + @m.input() + def c_set_nameplate(self): pass + @m.input() + def c_set_mailbox(self, mailbox): pass + @m.input() + def c_remove_nameplate(self): pass + @m.input() + def c_remove_mailbox(self): pass + @m.input() + def ack_close(self): pass + + @m.output() + def send_bind(self): + self._ws_send_command("bind", appid=self._appid, side=self._side) + @m.output() + def notify_bound(self): + self._nameplate_machine.bound() + @m.output() + def m_set_mailbox(self, mailbox): + self._mailbox_machine.m_set_mailbox(mailbox) + @m.output() + def request_close(self): + self._ws_machine.close() + @m.output() + def notify_close(self): + pass + + unbound.upon(bind, enter=binding, outputs=[send_bind]) + binding.upon(ack_bind, enter=neither, outputs=[notify_bound]) + neither.upon(c_set_nameplate, enter=has_nameplate, outputs=[]) + neither.upon(c_set_mailbox, enter=has_mailbox, outputs=[m_set_mailbox]) + has_nameplate.upon(c_set_mailbox, enter=has_both, outputs=[m_set_mailbox]) + has_nameplate.upon(c_remove_nameplate, enter=closing, outputs=[request_close]) + has_mailbox.upon(c_set_nameplate, enter=has_both, outputs=[]) + has_mailbox.upon(c_remove_mailbox, enter=closing, outputs=[request_close]) + has_both.upon(c_remove_nameplate, enter=has_mailbox, outputs=[]) + has_both.upon(c_remove_mailbox, enter=has_nameplate, outputs=[]) + closing.upon(ack_close, enter=closed, outputs=[]) + +class NameplateMachine(object): + m = MethodicalMachine() + + def bound(self): + pass + + @m.state(initial=True) + def unclaimed(self): pass # but bound + @m.state() + def claiming(self): pass + @m.state() + def claimed(self): pass + @m.state() + def releasing(self): pass + + @m.input() + def list_nameplates(self): pass + @m.input() + def got_nameplates(self, nameplates): pass # response("nameplates") + @m.input() + def learned_nameplate(self, nameplate): + """Call learned_nameplate() when you learn the nameplate: either + through allocation or code entry""" + pass + @m.input() + def claim_acked(self, mailbox): pass # response("claimed") + @m.input() + def release(self): pass + @m.input() + def release_acked(self): pass # response("released") + + @m.output() + def send_list_nameplates(self): + self._ws_send_command("list") + @m.output() + def notify_nameplates(self, nameplates): + # tell somebody + pass + @m.output() + def send_claim(self, nameplate): + self._ws_send_command("claim", nameplate=nameplate) + @m.output() + def c_set_nameplate(self, mailbox): + self._connection_machine.set_nameplate() + @m.output() + def c_set_mailbox(self, mailbox): + self._connection_machine.set_mailbox() + @m.output() + def send_release(self): + self._ws_send_command("release") + @m.output() + def notify_released(self): + # let someone know, when both the mailbox and the nameplate are + # released, the websocket can be closed, and we're done + pass + + unclaimed.upon(list_nameplates, enter=unclaimed, outputs=[send_list_nameplates]) + unclaimed.upon(got_nameplates, enter=unclaimed, outputs=[notify_nameplates]) + unclaimed.upon(learned_nameplate, enter=claiming, outputs=[send_claim]) + claiming.upon(claim_acked, enter=claimed, outputs=[c_set_nameplate, + c_set_mailbox]) + claiming.upon(learned_nameplate, enter=claiming, outputs=[]) + claimed.upon(release, enter=releasing, outputs=[send_release]) + claimed.upon(learned_nameplate, enter=claimed, outputs=[]) + releasing.upon(release, enter=releasing, outputs=[]) + releasing.upon(release_acked, enter=unclaimed, outputs=[notify_released]) + releasing.upon(learned_nameplate, enter=releasing, outputs=[]) + + + +class MailboxMachine(object): + m = MethodicalMachine() + + @m.state() + def closed(initial=True): pass + @m.state() + def open(): pass + @m.state() + def key_established(): pass + @m.state() + def key_verified(): pass + + @m.input() + def m_set_code(self, code): pass + + @m.input() + def m_set_mailbox(self, mailbox): + """Call m_set_mailbox() when you learn the mailbox id, either from + the response to claim_nameplate, or because we started from a + Wormhole Seed""" + pass + @m.input() + def message_pake(self, pake): pass # reponse["message"][phase=pake] + @m.input() + def message_version(self, version): # response["message"][phase=version] + pass + @m.input() + def message_app(self, msg): # response["message"][phase=\d+] + pass + @m.input() + def close(self): pass + + @m.output() + def send_pake(self, pake): + self._ws_send_command("add", phase="pake", body=XXX(pake)) + @m.output() + def send_version(self, pake): # XXX remove pake= + plaintext = dict_to_bytes(self._my_versions) + phase = "version" + data_key = self._derive_phase_key(self._side, phase) + encrypted = self._encrypt_data(data_key, plaintext) + self._msg_send(phase, encrypted) + @m.output() + def c_remove_mailbox(self): + self._connection.c_remove_mailbox() + + # decrypt, deliver up to app + + + + @m.output() + def open_mailbox(self, mailbox): + self._ws_send_command("open", mailbox=mailbox) + + @m.output() + def close_mailbox(self, mood): + self._ws_send_command("close", mood=mood) + + closed.upon(m_set_mailbox, enter=open, outputs=[open_mailbox]) + open.upon(message_pake, enter=key_established, outputs=[send_pake, + send_version]) + key_established.upon(message_version, enter=key_verified, outputs=[]) + key_verified.upon(close, enter=closed, outputs=[c_remove_mailbox]) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index d869822..ddcf290 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -5,7 +5,6 @@ from twisted.internet import defer, endpoints #, error from twisted.internet.threads import deferToThread, blockingCallFromThread from twisted.internet.defer import inlineCallbacks, returnValue from twisted.python import log, failure -from autobahn.twisted import websocket from nacl.secret import SecretBox from nacl.exceptions import CryptoError from nacl import utils @@ -41,37 +40,6 @@ def make_confmsg(confkey, nonce): # phase=version: version data, key verification (HKDF(key, nonce)+nonce) # phase=1,2,3,..: application messages -class WSClient(websocket.WebSocketClientProtocol): - def onOpen(self): - self.wormhole_open = True - ##self.connection_machine.onOpen(self) - #self.factory.d.callback(self) - - def onConnect(self): - self.connection_machine.onConnect(self) - - def onMessage(self, payload, isBinary): - assert not isBinary - self.wormhole._ws_dispatch_response(payload) - - def onClose(self, wasClean, code, reason): - self.connection_machine.onClose(f=None) - #if self.wormhole_open: - # self.wormhole._ws_closed(wasClean, code, reason) - #else: - # # we closed before establishing a connection (onConnect) or - # # finishing WebSocket negotiation (onOpen): errback - # self.factory.d.errback(error.ConnectError(reason)) - -class WSFactory(websocket.WebSocketClientFactory): - protocol = WSClient - def buildProtocol(self, addr): - proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) - proto.connection_machine = self.connection_machine - #proto.wormhole_open = False - return proto - - class _GetCode: def __init__(self, code_length, send_command, timing): self._code_length = code_length @@ -220,127 +188,6 @@ class _WelcomeHandler: # states for nameplates, mailboxes, and the websocket connection (CLOSED, OPENING, OPEN, CLOSING) = ("closed", "opening", "open", "closing") -from automat import MethodicalMachine -# pip install (path to automat checkout)[visualize] -# automat-visualize wormhole.wormhole - -class _ConnectionMachine(object): - m = MethodicalMachine() - ALLOW_CLOSE = True - - def __init__(self, ws_url, reactor): - self._reactor = reactor - self._f = f = WSFactory(ws_url) - f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - f.connection_machine = self # calls onOpen and onClose - p = urlparse(ws_url) - self._ep = self._make_endpoint(p.hostname, p.port or 80) - self._connector = None - self._done_d = defer.Deferred() - def _make_endpoint(self, hostname, port): - return endpoints.HostnameEndpoint(self._reactor, hostname, port) - - @m.state(initial=True) - def initial(self): pass - @m.state() - def first_time_connecting(self): pass - @m.state() - def negotiating(self): pass - @m.state(terminal=True) - def failed(self): pass - @m.state() - def open(self): pass - @m.state() - def waiting(self): pass - @m.state() - def connecting(self): pass - if ALLOW_CLOSE: - @m.state() - def disconnecting(self): pass - @m.state() - def disconnecting2(self): pass - @m.state(terminal=True) - def closed(self): pass - - - @m.input() - def start(self): pass ; print("in start") - @m.input() - def d_callback(self, p): pass ; print("in d_callback") - @m.input() - def d_errback(self, f): pass ; print("in d_errback") - @m.input() - def d_cancel(self): pass - @m.input() - def onConnect(self, ws): pass ; print("in onConnect") - @m.input() - def onClose(self, f): pass - @m.input() - def expire(self): pass - if ALLOW_CLOSE: - @m.input() - def close(self): pass - - @m.output() - def ep_connect(self): - "ep.connect()" - print("ep_connect()") - self._d = self._ep.connect(self._f) - self._d.addBoth(self.d_callback, self.d_errback) - @m.output() - def handle_connection(self, ws): - pass - @m.output() - def start_timer(self, f): - pass - @m.output() - def cancel_timer(self): - pass - @m.output() - def dropConnection(self): - pass - @m.output() - def notify_fail(self, f): - pass - - initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) - first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) - first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) - first_time_connecting.upon(onClose, enter=failed, outputs=[notify_fail]) - if ALLOW_CLOSE: - first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) - disconnecting2.upon(d_errback, enter=closed, outputs=[]) - - negotiating.upon(onConnect, enter=open, outputs=[handle_connection]) - if ALLOW_CLOSE: - negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) - negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) - - open.upon(onClose, enter=waiting, outputs=[start_timer]) - if ALLOW_CLOSE: - open.upon(close, enter=disconnecting, outputs=[dropConnection]) - connecting.upon(d_callback, enter=negotiating, outputs=[]) - connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) - connecting.upon(onClose, enter=waiting, outputs=[start_timer]) - if ALLOW_CLOSE: - connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) - - waiting.upon(expire, enter=connecting, outputs=[ep_connect]) - if ALLOW_CLOSE: - waiting.upon(close, enter=closed, outputs=[cancel_timer]) - disconnecting.upon(onClose, enter=closed, outputs=[]) - -def tryit(reactor): - cm = _ConnectionMachine("ws://127.0.0.1:4000/v1", reactor) - print("_ConnectionMachine created") - cm.start() - print("waiting on _done_d to finish") - return cm._done_d - -if __name__ == "__main__": - from twisted.internet.task import react - react(tryit) - class _Wormhole: DEBUG = False From 9e5bf452e3becb8e1c870a42e5bf0c05190aca33 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 17 Dec 2016 12:31:32 -0800 Subject: [PATCH 007/176] rename --- src/wormhole/_connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 6dcee77..5218e4a 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -54,9 +54,9 @@ class Dummy(protocol.Protocol): self.factory.cm.onClose(why) # pip install (path to automat checkout)[visualize] -# automat-visualize wormhole.wormhole +# automat-visualize wormhole._connection -class _WebSocketMachine(object): +class WebSocketMachine(object): m = MethodicalMachine() ALLOW_CLOSE = True @@ -172,7 +172,7 @@ class _WebSocketMachine(object): disconnecting.upon(onClose, enter=closed, outputs=[]) def tryit(reactor): - cm = _WebSocketMachine("ws://127.0.0.1:4000/v1", reactor) + cm = WebSocketMachine("ws://127.0.0.1:4000/v1", reactor) print("_ConnectionMachine created") print("start:", cm.start()) print("waiting on _done_d to finish") From 63ae3c63fcb093eb3d6db66b61ecb556307e0853 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 17 Dec 2016 17:30:18 -0800 Subject: [PATCH 008/176] notes --- src/wormhole/_connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 5218e4a..c3c73e6 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -178,8 +178,10 @@ def tryit(reactor): print("waiting on _done_d to finish") return cm._done_d +# http://autobahn-python.readthedocs.io/en/latest/websocket/programming.html +# observed sequence of events: # success: d_callback, onConnect(response), onOpen(), onMessage() -# negotifail: d_callback, onClose() +# negotifail (non-websocket): d_callback, onClose() # noconnect: d_errback def tryws(reactor): From 18f7ab93086015a2759d67fb740ea3a24600bbdd Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 18 Dec 2016 21:20:26 -0800 Subject: [PATCH 009/176] more state-machine work --- src/wormhole/_connection.py | 413 +++++++++++++++++++++++++----------- 1 file changed, 295 insertions(+), 118 deletions(-) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index c3c73e6..e50b5a8 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -1,7 +1,6 @@ from six.moves.urllib_parse import urlparse from attr import attrs, attrib -from twisted.internet import protocol, reactor from twisted.internet import defer, endpoints #, error from autobahn.twisted import websocket from automat import MethodicalMachine @@ -45,30 +44,26 @@ class WSFactory(websocket.WebSocketClientFactory): return proto -class Dummy(protocol.Protocol): - def connectionMade(self): - print("connectionMade") - reactor.callLater(1.0, self.factory.cm.onConnect, "fake ws") - reactor.callLater(2.0, self.transport.loseConnection) - def connectionLost(self, why): - self.factory.cm.onClose(why) - # pip install (path to automat checkout)[visualize] # automat-visualize wormhole._connection -class WebSocketMachine(object): +# We have one WSRelayClient for each wsurl we know about, and it lasts +# as long as its parent Wormhole does. + +@attrs +class WSRelayClient(object): + _wormhole = attrib() + _ws_url = attrib() + _reactor = attrib() + m = MethodicalMachine() ALLOW_CLOSE = True - def __init__(self, ws_url, reactor): - self._reactor = reactor - self._f = f = WSFactory(ws_url) + def __init__(self): + self._f = f = WSFactory(self._ws_url) f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) f.connection_machine = self # calls onOpen and onClose - #self._f = protocol.ClientFactory() - #self._f.cm = self - #self._f.protocol = Dummy - p = urlparse(ws_url) + p = urlparse(self._ws_url) self._ep = self._make_endpoint(p.hostname, p.port or 80) self._connector = None self._done_d = defer.Deferred() @@ -105,16 +100,16 @@ class WebSocketMachine(object): @m.input() def d_errback(self, f): pass ; print("in d_errback", f) @m.input() - def d_cancel(self): pass + def d_cancel(self, f): pass # XXX remove f @m.input() def onOpen(self, ws): pass ; print("in onOpen") @m.input() - def onClose(self, f): pass + def onClose(self, f): pass # XXX maybe remove f @m.input() def expire(self): pass if ALLOW_CLOSE: @m.input() - def close(self): pass + def close(self, f): pass @m.output() def ep_connect(self): @@ -123,20 +118,26 @@ class WebSocketMachine(object): self._d = self._ep.connect(self._f) self._d.addCallbacks(self.d_callback, self.d_errback) @m.output() - def handle_connection(self, ws): - print("handle_connection", ws) - #self._wormhole.new_connection(Connection(ws)) + def add_connection(self, ws): + print("add_connection", ws) + self._connection = WSConnection(ws, self._wormhole.appid, + self._wormhole.side, self) + self._wormhole.add_connection(self._connection) @m.output() - def start_timer(self, f): + def remove_connection(self, f): # XXX remove f + self._wormhole.remove_connection(self._connection) + self._connection = None + @m.output() + def start_timer(self, f): # XXX remove f print("start_timer") self._t = self._reactor.callLater(3.0, self.expire) @m.output() - def cancel_timer(self): + def cancel_timer(self, f): # XXX remove f print("cancel_timer") self._t.cancel() self._t = None @m.output() - def dropConnection(self): + def dropConnection(self, f): # XXX remove f print("dropConnection") self._ws.dropConnection() @m.output() @@ -149,17 +150,19 @@ class WebSocketMachine(object): first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) first_time_connecting.upon(onClose, enter=failed, outputs=[notify_fail]) if ALLOW_CLOSE: - first_time_connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + first_time_connecting.upon(close, enter=disconnecting2, + outputs=[d_cancel]) disconnecting2.upon(d_errback, enter=closed, outputs=[]) - negotiating.upon(onOpen, enter=open, outputs=[handle_connection]) + negotiating.upon(onOpen, enter=open, outputs=[add_connection]) if ALLOW_CLOSE: negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) - open.upon(onClose, enter=waiting, outputs=[start_timer]) + open.upon(onClose, enter=waiting, outputs=[remove_connection, start_timer]) if ALLOW_CLOSE: - open.upon(close, enter=disconnecting, outputs=[dropConnection]) + open.upon(close, enter=disconnecting, + outputs=[dropConnection, remove_connection]) connecting.upon(d_callback, enter=negotiating, outputs=[]) connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) connecting.upon(onClose, enter=waiting, outputs=[start_timer]) @@ -172,7 +175,7 @@ class WebSocketMachine(object): disconnecting.upon(onClose, enter=closed, outputs=[]) def tryit(reactor): - cm = WebSocketMachine("ws://127.0.0.1:4000/v1", reactor) + cm = WSRelayClient(None, "ws://127.0.0.1:4000/v1", reactor) print("_ConnectionMachine created") print("start:", cm.start()) print("waiting on _done_d to finish") @@ -202,12 +205,14 @@ if __name__ == "__main__": from twisted.internet.task import react react(tryit) +# a new WSConnection is created each time the WSRelayClient gets through +# negotiation @attrs -class Connection(object): +class WSConnection(object): _ws = attrib() _appid = attrib() _side = attrib() - _ws_machine = attrib() + _wsrc = attrib() m = MethodicalMachine() @m.state(initial=True) @@ -232,13 +237,13 @@ class Connection(object): @m.input() def ack_bind(self): pass @m.input() - def c_set_nameplate(self): pass + def wsc_set_nameplate(self): pass @m.input() - def c_set_mailbox(self, mailbox): pass + def wsc_set_mailbox(self, mailbox): pass @m.input() - def c_remove_nameplate(self): pass + def wsc_release_nameplate(self): pass @m.input() - def c_remove_mailbox(self): pass + def wsc_release_mailbox(self): pass @m.input() def ack_close(self): pass @@ -248,34 +253,32 @@ class Connection(object): @m.output() def notify_bound(self): self._nameplate_machine.bound() + self._connection.make_listing_machine() @m.output() def m_set_mailbox(self, mailbox): self._mailbox_machine.m_set_mailbox(mailbox) @m.output() def request_close(self): - self._ws_machine.close() + self._wsrc.close() @m.output() def notify_close(self): pass unbound.upon(bind, enter=binding, outputs=[send_bind]) binding.upon(ack_bind, enter=neither, outputs=[notify_bound]) - neither.upon(c_set_nameplate, enter=has_nameplate, outputs=[]) - neither.upon(c_set_mailbox, enter=has_mailbox, outputs=[m_set_mailbox]) - has_nameplate.upon(c_set_mailbox, enter=has_both, outputs=[m_set_mailbox]) - has_nameplate.upon(c_remove_nameplate, enter=closing, outputs=[request_close]) - has_mailbox.upon(c_set_nameplate, enter=has_both, outputs=[]) - has_mailbox.upon(c_remove_mailbox, enter=closing, outputs=[request_close]) - has_both.upon(c_remove_nameplate, enter=has_mailbox, outputs=[]) - has_both.upon(c_remove_mailbox, enter=has_nameplate, outputs=[]) + neither.upon(wsc_set_nameplate, enter=has_nameplate, outputs=[]) + neither.upon(wsc_set_mailbox, enter=has_mailbox, outputs=[m_set_mailbox]) + has_nameplate.upon(wsc_set_mailbox, enter=has_both, outputs=[m_set_mailbox]) + has_nameplate.upon(wsc_release_nameplate, enter=closing, outputs=[request_close]) + has_mailbox.upon(wsc_set_nameplate, enter=has_both, outputs=[]) + has_mailbox.upon(wsc_release_mailbox, enter=closing, outputs=[request_close]) + has_both.upon(wsc_release_nameplate, enter=has_mailbox, outputs=[]) + has_both.upon(wsc_release_mailbox, enter=has_nameplate, outputs=[]) closing.upon(ack_close, enter=closed, outputs=[]) class NameplateMachine(object): m = MethodicalMachine() - def bound(self): - pass - @m.state(initial=True) def unclaimed(self): pass # but bound @m.state() @@ -284,112 +287,135 @@ class NameplateMachine(object): def claimed(self): pass @m.state() def releasing(self): pass + @m.state(terminal=True) + def done(self): pass - @m.input() - def list_nameplates(self): pass - @m.input() - def got_nameplates(self, nameplates): pass # response("nameplates") @m.input() def learned_nameplate(self, nameplate): """Call learned_nameplate() when you learn the nameplate: either through allocation or code entry""" pass @m.input() - def claim_acked(self, mailbox): pass # response("claimed") + def rx_claimed(self, mailbox): pass # response("claimed") @m.input() - def release(self): pass + def nm_release_nameplate(self): pass @m.input() def release_acked(self): pass # response("released") - @m.output() - def send_list_nameplates(self): - self._ws_send_command("list") - @m.output() - def notify_nameplates(self, nameplates): - # tell somebody - pass @m.output() def send_claim(self, nameplate): self._ws_send_command("claim", nameplate=nameplate) @m.output() - def c_set_nameplate(self, mailbox): - self._connection_machine.set_nameplate() + def wsc_set_nameplate(self, mailbox): + self._connection_machine.wsc_set_nameplate() @m.output() - def c_set_mailbox(self, mailbox): - self._connection_machine.set_mailbox() + def wsc_set_mailbox(self, mailbox): + self._connection_machine.wsc_set_mailbox() + @m.output() + def mm_set_mailbox(self, mailbox): + self._mm.mm_set_mailbox() @m.output() def send_release(self): self._ws_send_command("release") @m.output() - def notify_released(self): + def wsc_release_nameplate(self): # let someone know, when both the mailbox and the nameplate are # released, the websocket can be closed, and we're done - pass + self._wsc.wsc_release_nameplate() - unclaimed.upon(list_nameplates, enter=unclaimed, outputs=[send_list_nameplates]) - unclaimed.upon(got_nameplates, enter=unclaimed, outputs=[notify_nameplates]) unclaimed.upon(learned_nameplate, enter=claiming, outputs=[send_claim]) - claiming.upon(claim_acked, enter=claimed, outputs=[c_set_nameplate, - c_set_mailbox]) - claiming.upon(learned_nameplate, enter=claiming, outputs=[]) - claimed.upon(release, enter=releasing, outputs=[send_release]) - claimed.upon(learned_nameplate, enter=claimed, outputs=[]) - releasing.upon(release, enter=releasing, outputs=[]) - releasing.upon(release_acked, enter=unclaimed, outputs=[notify_released]) - releasing.upon(learned_nameplate, enter=releasing, outputs=[]) + claiming.upon(rx_claimed, enter=claimed, outputs=[wsc_set_nameplate, + mm_set_mailbox, + wsc_set_mailbox]) + #claiming.upon(learned_nameplate, enter=claiming, outputs=[]) + claimed.upon(nm_release_nameplate, enter=releasing, outputs=[send_release]) + #claimed.upon(learned_nameplate, enter=claimed, outputs=[]) + #releasing.upon(release, enter=releasing, outputs=[]) + releasing.upon(release_acked, enter=done, outputs=[wsc_release_nameplate]) + #releasing.upon(learned_nameplate, enter=releasing, outputs=[]) +class NameplateListingMachine(object): + m = MethodicalMachine() + def __init__(self): + self._list_nameplate_waiters = [] + # Ideally, each API request would spawn a new "list_nameplates" message + # to the server, so the response would be maximally fresh, but that would + # require correlating server request+response messages, and the protocol + # is intended to be less stateful than that. So we offer a weaker + # freshness property: if no server requests are in flight, then a new API + # request will provoke a new server request, and the result will be + # fresh. But if a server request is already in flight when a second API + # request arrives, both requests will be satisfied by the same response. + + @m.state(initial=True) + def idle(self): pass + @m.state() + def requesting(self): pass + + @m.input() + def list_nameplates(self): pass # returns Deferred + @m.input() + def response(self, message): pass + + @m.output() + def add_deferred(self): + d = defer.Deferred() + self._list_nameplate_waiters.append(d) + return d + @m.output() + def send_request(self): + self._connection.send_command("list") + @m.output() + def distribute_response(self, message): + nameplates = parse(message) + waiters = self._list_nameplate_waiters + self._list_nameplate_waiters = [] + for d in waiters: + d.callback(nameplates) + + idle.upon(list_nameplates, enter=requesting, + outputs=[add_deferred, send_request], + collector=lambda outs: outs[0]) + idle.upon(response, enter=idle, outputs=[]) + requesting.upon(list_nameplates, enter=requesting, + outputs=[add_deferred], + collector=lambda outs: outs[0]) + requesting.upon(response, enter=idle, outputs=[distribute_response]) + + # nlm._connection = c = Connection(ws) + # nlm.list_nameplates().addCallback(display_completions) + # c.register_dispatch("nameplates", nlm.response) class MailboxMachine(object): m = MethodicalMachine() @m.state() - def closed(initial=True): pass + def unknown(initial=True): pass @m.state() - def open(): pass + def mailbox_unused(): pass @m.state() - def key_established(): pass - @m.state() - def key_verified(): pass + def mailbox_used(): pass @m.input() - def m_set_code(self, code): pass + def mm_set_mailbox(self, mailbox): pass + @m.input() + def add_connection(self, connection): pass + @m.input() + def rx_message(self): pass - @m.input() - def m_set_mailbox(self, mailbox): - """Call m_set_mailbox() when you learn the mailbox id, either from - the response to claim_nameplate, or because we started from a - Wormhole Seed""" - pass - @m.input() - def message_pake(self, pake): pass # reponse["message"][phase=pake] - @m.input() - def message_version(self, version): # response["message"][phase=version] - pass - @m.input() - def message_app(self, msg): # response["message"][phase=\d+] - pass @m.input() def close(self): pass @m.output() - def send_pake(self, pake): - self._ws_send_command("add", phase="pake", body=XXX(pake)) + def open_mailbox(self): + self._mm.mm_set_mailbox(self._mailbox) @m.output() - def send_version(self, pake): # XXX remove pake= - plaintext = dict_to_bytes(self._my_versions) - phase = "version" - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) - self._msg_send(phase, encrypted) + def nm_release_nameplate(self): + self._nm.nm_release_nameplate() @m.output() - def c_remove_mailbox(self): - self._connection.c_remove_mailbox() - - # decrypt, deliver up to app - - - + def wsc_release_mailbox(self): + self._wsc.wsc_release_mailbox() @m.output() def open_mailbox(self, mailbox): self._ws_send_command("open", mailbox=mailbox) @@ -398,8 +424,159 @@ class MailboxMachine(object): def close_mailbox(self, mood): self._ws_send_command("close", mood=mood) - closed.upon(m_set_mailbox, enter=open, outputs=[open_mailbox]) - open.upon(message_pake, enter=key_established, outputs=[send_pake, - send_version]) - key_established.upon(message_version, enter=key_verified, outputs=[]) - key_verified.upon(close, enter=closed, outputs=[c_remove_mailbox]) + unknown.upon(mm_set_mailbox, enter=mailbox_unused, outputs=[open_mailbox]) + mailbox_unused.upon(rx_message, enter=mailbox_used, + outputs=[nm_release_nameplate]) + #open.upon(message_pake, enter=key_established, outputs=[send_pake, + # send_version]) + #key_established.upon(message_version, enter=key_verified, outputs=[]) + #key_verified.upon(close, enter=closed, outputs=[wsc_release_mailbox]) + +class Wormhole: + m = MethodicalMachine() + + def __init__(self, ws_url, reactor): + self._relay_client = WSRelayClient(self, ws_url, reactor) + # This records all the messages we want the relay to have. Each time + # we establish a connection, we'll send them all (and the relay + # server will filter out duplicates). If we add any while a + # connection is established, we'll send the new ones. + self._outbound_messages = [] + + def start(self): + self._relay_client.start() + + @m.state() + def closed(initial=True): pass + @m.state() + def know_code_not_mailbox(): pass + @m.state() + def know_code_and_mailbox(): pass # no longer need nameplate + @m.state() + def waiting_to_verify(): pass # key is established, want any message + @m.state() + def open(): pass # key is verified, can post app messages + @m.state(terminal=True) + def failed(): pass + + @m.input() + def deliver_message(self, message): pass + + def w_set_seed(self, code, mailbox): + """Call w_set_seed when we sprout a Wormhole Seed, which + contains both the code and the mailbox""" + self.w_set_code(code) + self.w_set_mailbox(mailbox) + + @m.input() + def w_set_code(self, code): + """Call w_set_code when you learn the code, probably because the user + typed it in.""" + @m.input() + def w_set_mailbox(self, mailbox): + """Call w_set_mailbox() when you learn the mailbox id, from the + response to claim_nameplate""" + pass + + + @m.input() + def rx_pake(self, pake): pass # reponse["message"][phase=pake] + def rx_version(self, version): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass + + @m.input() + def verify_good(self, verifier): pass + @m.input() + def verify_bad(self, f): pass + + @m.output() + def compute_and_post_pake(self, code): + self._code = code + self._pake = compute(code) + self._post(pake=self._pake) + self._ws_send_command("add", phase="pake", body=XXX(pake)) + @m.output() + def set_mailbox(self, mailbox): + self._mailbox = mailbox + @m.output() + def set_seed(self, code, mailbox): + self._code = code + self._mailbox = mailbox + + @m.output() + def deliver_message(self, message): + self._qc.deliver_message(message) + + @m.output() + def notify_verified(self, verifier): + for d in self._verify_waiters: + d.callback(verifier) + @m.output() + def notify_failed(self, f): + for d in self._verify_waiters: + d.errback(f) + + @m.output() + def compute_key_and_post_version(self, pake): + self._key = x + self._verifier = x + plaintext = dict_to_bytes(self._my_versions) + phase = "version" + data_key = self._derive_phase_key(self._side, phase) + encrypted = self._encrypt_data(data_key, plaintext) + self._msg_send(phase, encrypted) + + closed.upon(w_set_code, enter=know_code_not_mailbox, + outputs=[compute_and_post_pake]) + know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, + outputs=[set_mailbox]) + know_code_and_mailbox.upon(rx_pake, enter=waiting_to_verify, + outputs=[compute_key_and_post_version]) + waiting_to_verify.upon(verify_good, enter=open, outputs=[notify_verified]) + waiting_to_verify.upon(verify_bad, enter=failed, outputs=[notify_failed]) + +class QueueConnect: + m = MethodicalMachine() + def __init__(self): + self._outbound_messages = [] + self._connection = None + @m.state() + def disconnected(): pass + @m.state() + def connected(): pass + + @m.input() + def deliver_message(self, message): pass + @m.input() + def connect(self, connection): pass + @m.input() + def disconnect(self): pass + + @m.output() + def remember_connection(self, connection): + self._connection = connection + @m.output() + def forget_connection(self): + self._connection = None + @m.output() + def queue_message(self, message): + self._outbound_messages.append(message) + @m.output() + def send_message(self, message): + self._connection.send(message) + @m.output() + def send_queued_messages(self, connection): + for m in self._outbound_messages: + connection.send(m) + + disconnected.upon(deliver_message, enter=disconnected, outputs=[queue_message]) + disconnected.upon(connect, enter=connected, outputs=[remember_connection, + send_queued_messages]) + connected.upon(deliver_message, enter=connected, + outputs=[queue_message, send_message]) + connected.upon(disconnect, enter=disconnected, outputs=[forget_connection]) From 057f616765cd01807aa8ae9899e30daee4c80ee5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 18 Dec 2016 21:33:01 -0800 Subject: [PATCH 010/176] more experimentation --- src/wormhole/_connection.py | 60 +++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index e50b5a8..4bb60b2 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -453,7 +453,11 @@ class Wormhole: @m.state() def know_code_and_mailbox(): pass # no longer need nameplate @m.state() - def waiting_to_verify(): pass # key is established, want any message + def waiting_first_msg(): pass # key is established, want any message + @m.state() + def processing_version(): pass + @m.state() + def processing_phase(): pass @m.state() def open(): pass # key is verified, can post app messages @m.state(terminal=True) @@ -481,19 +485,22 @@ class Wormhole: @m.input() def rx_pake(self, pake): pass # reponse["message"][phase=pake] - def rx_version(self, version): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) - pass + @m.input() + def rx_version(self, version): # response["message"][phase=version] + pass @m.input() def verify_good(self, verifier): pass @m.input() def verify_bad(self, f): pass + @m.input() + def rx_phase(self, message): pass + @m.input() + def phase_good(self, message): pass + @m.input() + def phase_bad(self, f): pass + @m.output() def compute_and_post_pake(self, code): self._code = code @@ -509,8 +516,13 @@ class Wormhole: self._mailbox = mailbox @m.output() - def deliver_message(self, message): - self._qc.deliver_message(message) + def process_version(self, version): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass @m.output() def notify_verified(self, verifier): @@ -521,6 +533,23 @@ class Wormhole: for d in self._verify_waiters: d.errback(f) + @m.output() + def process_phase(self, message): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass + + @m.output() + def post_inbound(self, message): + pass + + @m.output() + def deliver_message(self, message): + self._qc.deliver_message(message) + @m.output() def compute_key_and_post_version(self, pake): self._key = x @@ -535,10 +564,15 @@ class Wormhole: outputs=[compute_and_post_pake]) know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, outputs=[set_mailbox]) - know_code_and_mailbox.upon(rx_pake, enter=waiting_to_verify, + know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg, outputs=[compute_key_and_post_version]) - waiting_to_verify.upon(verify_good, enter=open, outputs=[notify_verified]) - waiting_to_verify.upon(verify_bad, enter=failed, outputs=[notify_failed]) + waiting_first_msg.upon(rx_version, enter=processing_version, + outputs=[process_version]) + processing_version.upon(verify_good, enter=open, outputs=[notify_verified]) + processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed]) + open.upon(rx_phase, enter=processing_phase, outputs=[process_phase]) + processing_phase.upon(phase_good, enter=open, outputs=[post_inbound]) + processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed]) class QueueConnect: m = MethodicalMachine() From 8a9b50b320d1868d0a4bdc62a3f686652e04889d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 21 Dec 2016 01:23:36 -0500 Subject: [PATCH 011/176] adding high-level state diagram Automat doesn't let me combine flowcharts with state machines in a way that's useful for documenting my thoughts. --- docs/w.dot | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 docs/w.dot diff --git a/docs/w.dot b/docs/w.dot new file mode 100644 index 0000000..8c7351d --- /dev/null +++ b/docs/w.dot @@ -0,0 +1,122 @@ +digraph { + W_S_nothing [label="know\nnothing"] + W_S_nothing -> W_S_know_nameplate [label="set_nameplate()"] + W_S_know_nameplate [label="know (just)\nnameplate"] + + W_P_build_and_post_pake [label="build_pake()\nMM_send(pake)" shape="box"] + W_S_know_code [label="know code\n"] + W_P_compute_key [label="compute_key()" shape="box"] + W_P_post_version [label="MM_send(version)\nnotify_verifier()" shape="box"] + W_S_know_key [label="know_key\nunverified" color="orange"] + W_P_verify [label="verify(msg)" shape="box"] + W_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] + W_S_verified_key [color="green"] + W_P_mood_scary [shape="box" label="mood(scary)"] + W_P_notify_failure [shape="box" label="notify_failure()" color="red"] + W_P_mood_happy [shape="box" label="mood(happy)"] + W_S_closed [label="closed"] + W_P_mood_lonely [shape="box" label="mood(lonely)"] + W_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] + W_P_queue2 [shape="box" style="dotted" label="queue\noutbound msg"] + W_P_queue3 [shape="box" style="dotted" label="queue\noutbound msg"] + W_P_queue4 [shape="box" style="dotted" label="queue\noutbound msg"] + W_P_send [shape="box" label="MM_send(msg)"] + W_P_send_queued [shape="box" label="MM_send()\nany queued\nmessages"] + + W_S_nothing -> W_P_build_and_post_pake [label="set_code()"] + W_S_nothing -> W_P_queue1 [label="API_send" style="dotted"] + W_P_queue1 -> W_S_nothing [style="dotted"] + W_S_know_nameplate -> W_P_got_pakeinfo [label="W_rx_pake"] + W_S_know_nameplate -> W_P_build_and_post_pake [label="set_code()"] + W_P_got_pakeinfo [shape="box" label="got wordlist"] + W_P_got_pakeinfo -> W_S_know_pake2 + W_S_know_pake2 [label="know their pake"] + W_S_know_pake2 -> W_P_compute_key [label="set_code()"] + W_S_know_pake2 -> W_P_queue4 [label="API_send" style="dotted"] + W_P_queue4 -> W_S_know_pake2 [style="dotted"] + W_P_build_and_post_pake -> W_S_know_code + W_S_know_code -> W_P_compute_key [label="W_rx_pake"] + W_S_know_code -> W_P_queue2 [label="API_send" style="dotted"] + W_P_queue2 -> W_S_know_code [style="dotted"] + W_P_compute_key -> W_P_post_version [label="pake ok"] + W_P_post_version -> W_S_know_key + W_P_compute_key -> W_P_mood_scary [label="pake bad"] + W_P_mood_scary -> W_P_notify_failure + W_P_notify_failure -> W_S_closed + W_S_know_key -> W_P_verify [label="W_rx_msg()"] /* version or phase */ + W_S_know_key -> W_P_mood_lonely [label="close"] /* more like impatient */ + W_S_know_key -> W_P_queue3 [label="API_send" style="dotted"] + W_P_queue3 -> W_S_know_key [style="dotted"] + W_P_verify -> W_P_accept_msg [label="verify good"] + W_P_accept_msg -> W_P_send_queued + W_P_send_queued -> W_S_verified_key + W_P_verify -> W_P_mood_scary [label="verify bad"] + W_S_verified_key -> W_P_verify [label="W_rx_msg()"] /* probably phase */ + W_S_verified_key -> W_P_mood_happy [label="close"] + W_P_mood_happy -> W_S_closed + W_S_verified_key -> W_P_send [label="API_send"] + W_P_send -> W_S_verified_key + W_S_know_code -> W_P_mood_lonely [label="close"] + W_P_mood_lonely -> W_S_closed + + NM_S_unclaimed [label="no nameplate"] + NM_S_unclaimed -> NM_S_unclaimed [label="NM_release()"] + NM_P_set_nameplate [shape="box" label="post_claim()"] + NM_S_unclaimed -> NM_P_set_nameplate [label="set_nameplate()"] + NM_S_claiming [label="claim pending"] + NM_P_set_nameplate -> NM_S_claiming + NM_S_claiming -> NM_P_rx_claimed [label="rx claimed"] + NM_P_rx_claimed [label="set_mailbox()" shape="box"] + NM_P_rx_claimed -> NM_S_claimed + NM_S_claimed [label="claimed"] + NM_S_claimed -> NM_P_release [label="NM_release()"] + NM_P_release [shape="box" label="post_release()"] + NM_P_release -> NM_S_releasing + NM_S_releasing [label="release pending"] + NM_S_releasing -> NM_S_releasing [label="NM_release()"] + NM_S_releasing -> NM_S_released [label="rx released"] + NM_S_released [label="released"] + NM_S_released -> NM_S_released [label="NM_release()"] + + + MM_S_want_mailbox [label="want mailbox"] + MM_S_want_mailbox -> MM_P_queue1 [label="MM_send()" style="dotted"] + MM_P_queue1 [shape="box" style="dotted" label="queue message"] + MM_P_queue1 -> MM_S_want_mailbox [style="dotted"] + MM_P_open_mailbox [shape="box" label="post_open()"] + MM_S_want_mailbox -> MM_P_open_mailbox [label="set_mailbox()"] + MM_P_send_queued [shape="box" label="post add() for\nqueued messages"] + MM_P_open_mailbox -> MM_P_send_queued + MM_P_send_queued -> MM_S_open + MM_S_open [label="open\nunused"] + MM_S_open -> MM_P_send_queued [label="MM_send()"] + + MM_S_open -> MM_P_rx [label="rx message"] + MM_P_rx [shape="box" label="W_rx_pake()\nor W_rx_msg()"] + MM_P_rx -> MM_P_release + MM_P_release [shape="box" label="NM_release()"] + MM_P_release -> MM_S_used + MM_S_used [label="open\nused"] + MM_S_used -> MM_P_rx [label="rx message"] + MM_S_used -> MM_P_send [label="MM_send()"] + MM_P_send [shape="box" label="post message"] + MM_P_send -> MM_S_used + + /* upgrading to new PAKE algorithm */ + P2_start [label="(PAKE\nupgrade)\nstart"] + P2_start -> P2_P_send_abilities [label="set_code()"] + P2_P_send_abilities [shape="box" label="send pake_abilities"] + P2_P_send_abilities -> P2_wondering + P2_wondering [label="waiting\nwondering"] + P2_wondering -> P2_P_send_pakev1 [label="rx pake_v1"] + P2_P_send_pakev1 [shape="box" label="send pake_v1"] + P2_P_send_pakev1 -> P2_P_process_v1 + P2_P_process_v1 [shape="box" label="process v1"] + P2_wondering -> P2_P_find_max [label="rx pake_abilities"] + P2_P_find_max [shape="box" label="find max"] + P2_P_find_max -> P2_P_send_pakev2 + P2_P_send_pakev2 + P2_P_send_pakev2 [shape="box" label="send pake_v2"] + P2_P_send_pakev2 -> P2_P_process_v2 [label="rx pake_v2"] + P2_P_process_v2 [shape="box" label="process v2"] +} From b1f313b116edd2b68ae291db1d57d9c4588ab041 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 22 Dec 2016 00:07:22 -0500 Subject: [PATCH 012/176] more diagram work --- docs/w.dot | 222 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 151 insertions(+), 71 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index 8c7351d..1e85bb4 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -1,72 +1,134 @@ digraph { - W_S_nothing [label="know\nnothing"] - W_S_nothing -> W_S_know_nameplate [label="set_nameplate()"] - W_S_know_nameplate [label="know (just)\nnameplate"] + /* could shave a RTT by committing to the nameplate early, before + finishing the rest of the code input. While the user is still + typing/completing the code, we claim the nameplate, open the mailbox, + and retrieve the peer's PAKE message. Then as soon as the user + finishes entering the code, we build our own PAKE message, send PAKE, + compute the key, send VERSION. Starting from the Return, this saves + two round trips. OTOH it adds consequences to hitting Tab. */ - W_P_build_and_post_pake [label="build_pake()\nMM_send(pake)" shape="box"] - W_S_know_code [label="know code\n"] - W_P_compute_key [label="compute_key()" shape="box"] - W_P_post_version [label="MM_send(version)\nnotify_verifier()" shape="box"] - W_S_know_key [label="know_key\nunverified" color="orange"] - W_P_verify [label="verify(msg)" shape="box"] - W_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] - W_S_verified_key [color="green"] - W_P_mood_scary [shape="box" label="mood(scary)"] - W_P_notify_failure [shape="box" label="notify_failure()" color="red"] - W_P_mood_happy [shape="box" label="mood(happy)"] - W_S_closed [label="closed"] - W_P_mood_lonely [shape="box" label="mood(lonely)"] - W_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] - W_P_queue2 [shape="box" style="dotted" label="queue\noutbound msg"] - W_P_queue3 [shape="box" style="dotted" label="queue\noutbound msg"] - W_P_queue4 [shape="box" style="dotted" label="queue\noutbound msg"] - W_P_send [shape="box" label="MM_send(msg)"] - W_P_send_queued [shape="box" label="MM_send()\nany queued\nmessages"] + WM_start [label="Wormhole\nMachine" style="dotted"] + WM_start -> WM_S_nothing [style="invis"] - W_S_nothing -> W_P_build_and_post_pake [label="set_code()"] - W_S_nothing -> W_P_queue1 [label="API_send" style="dotted"] - W_P_queue1 -> W_S_nothing [style="dotted"] - W_S_know_nameplate -> W_P_got_pakeinfo [label="W_rx_pake"] - W_S_know_nameplate -> W_P_build_and_post_pake [label="set_code()"] - W_P_got_pakeinfo [shape="box" label="got wordlist"] - W_P_got_pakeinfo -> W_S_know_pake2 - W_S_know_pake2 [label="know their pake"] - W_S_know_pake2 -> W_P_compute_key [label="set_code()"] - W_S_know_pake2 -> W_P_queue4 [label="API_send" style="dotted"] - W_P_queue4 -> W_S_know_pake2 [style="dotted"] - W_P_build_and_post_pake -> W_S_know_code - W_S_know_code -> W_P_compute_key [label="W_rx_pake"] - W_S_know_code -> W_P_queue2 [label="API_send" style="dotted"] - W_P_queue2 -> W_S_know_code [style="dotted"] - W_P_compute_key -> W_P_post_version [label="pake ok"] - W_P_post_version -> W_S_know_key - W_P_compute_key -> W_P_mood_scary [label="pake bad"] - W_P_mood_scary -> W_P_notify_failure - W_P_notify_failure -> W_S_closed - W_S_know_key -> W_P_verify [label="W_rx_msg()"] /* version or phase */ - W_S_know_key -> W_P_mood_lonely [label="close"] /* more like impatient */ - W_S_know_key -> W_P_queue3 [label="API_send" style="dotted"] - W_P_queue3 -> W_S_know_key [style="dotted"] - W_P_verify -> W_P_accept_msg [label="verify good"] - W_P_accept_msg -> W_P_send_queued - W_P_send_queued -> W_S_verified_key - W_P_verify -> W_P_mood_scary [label="verify bad"] - W_S_verified_key -> W_P_verify [label="W_rx_msg()"] /* probably phase */ - W_S_verified_key -> W_P_mood_happy [label="close"] - W_P_mood_happy -> W_S_closed - W_S_verified_key -> W_P_send [label="API_send"] - W_P_send -> W_S_verified_key - W_S_know_code -> W_P_mood_lonely [label="close"] - W_P_mood_lonely -> W_S_closed + WM_S_nothing [label="know\nnothing"] + WM_S_nothing -> WM_P_queue1 [label="API_send" style="dotted"] + WM_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] + WM_P_queue1 -> WM_S_nothing [style="dotted"] + WM_S_nothing -> WM_P_build_and_post_pake [label="WM_set_code()"] + WM_P_build_and_post_pake [label="NM_set_nameplate()\nbuild_pake()\nMM_send(pake)" shape="box"] + WM_P_build_and_post_pake -> WM_S_know_code + + WM_S_know_code [label="know code\n"] + WM_S_know_code -> WM_P_queue2 [label="API_send" style="dotted"] + WM_P_queue2 [shape="box" style="dotted" label="queue\noutbound msg"] + WM_P_queue2 -> WM_S_know_code [style="dotted"] + WM_S_know_code -> WM_P_compute_key [label="WM_rx_pake"] + WM_S_know_code -> WM_P_mood_lonely [label="close"] + + WM_P_compute_key [label="compute_key()" shape="box"] + WM_P_compute_key -> WM_P_post_version [label="pake ok"] + WM_P_compute_key -> WM_P_mood_scary [label="pake bad"] + + WM_P_mood_scary [shape="box" label="MM_close()\nmood=scary"] + WM_P_mood_scary -> WM_P_notify_failure + + WM_P_notify_failure [shape="box" label="notify_failure()" color="red"] + WM_P_notify_failure -> WM_S_closed + + WM_P_post_version [label="MM_send(version)\nnotify_verifier()" shape="box"] + WM_P_post_version -> WM_S_know_key + + WM_S_know_key [label="know_key\nunverified" color="orange"] + WM_S_know_key -> WM_P_queue3 [label="API_send" style="dotted"] + WM_P_queue3 [shape="box" style="dotted" label="queue\noutbound msg"] + WM_P_queue3 -> WM_S_know_key [style="dotted"] + WM_S_know_key -> WM_P_verify [label="WM_rx_msg()"] /* version or phase */ + WM_S_know_key -> WM_P_mood_lonely [label="close"] /* more like impatient */ + + WM_P_verify [label="verify(msg)" shape="box"] + WM_P_verify -> WM_P_accept_msg [label="verify good"] + WM_P_verify -> WM_P_mood_scary [label="verify bad"] + + WM_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] + WM_P_accept_msg -> WM_P_send_queued + + WM_P_send_queued [shape="box" label="MM_send()\nany queued\nmessages"] + WM_P_send_queued -> WM_S_verified_key + + WM_S_verified_key [color="green"] + WM_S_verified_key -> WM_P_verify [label="WM_rx_msg()"] /* probably phase */ + WM_S_verified_key -> WM_P_mood_happy [label="close"] + WM_S_verified_key -> WM_P_send [label="API_send"] + + WM_P_mood_happy [shape="box" label="MM_close()\nmood=happy"] + WM_P_mood_happy -> WM_S_closed + + WM_P_mood_lonely [shape="box" label="MM_close()\nmood=lonely"] + WM_P_mood_lonely -> WM_S_closed + + WM_P_send [shape="box" label="MM_send(msg)"] + WM_P_send -> WM_S_verified_key + + WM_S_closed [label="closed"] + + + WCM_start [label="Wormhole Code\nMachine" style="dotted"] + WCM_start -> WCM_S_unknown [style="invis"] + WCM_S_unknown [label="unknown"] + WCM_S_unknown -> WCM_P_set_code [label="set"] + WCM_P_set_code [shape="box" label="WM_set_code()"] + WCM_P_set_code -> WCM_S_known + WCM_S_known [label="known"] + + WCM_S_unknown -> WCM_P_list_nameplates [label="input"] + WCM_S_typing_nameplate [label="typing\nnameplate"] + + WCM_S_typing_nameplate -> WCM_P_nameplate_completion [label=""] + WCM_P_nameplate_completion [shape="box" label="completion?"] + WCM_P_nameplate_completion -> WCM_P_list_nameplates + WCM_P_list_nameplates [shape="box" label="NLM_update_nameplates()"] + WCM_P_list_nameplates -> WCM_S_typing_nameplate + + WCM_S_typing_nameplate -> WCM_P_got_nameplates [label="C_rx_nameplates()"] + WCM_P_got_nameplates [shape="box" label="stash nameplates\nfor completion"] + WCM_P_got_nameplates -> WCM_S_typing_nameplate + WCM_S_typing_nameplate -> WCM_P_finish_nameplate [label="finished\nnameplate"] + WCM_P_finish_nameplate [shape="box" label="lookup wordlist\nfor completion"] + WCM_P_finish_nameplate -> WCM_S_typing_code + WCM_S_typing_code [label="typing\ncode"] + WCM_S_typing_code -> WCM_P_code_completion [label=""] + WCM_P_code_completion [shape="box" label="completion"] + WCM_P_code_completion -> WCM_S_typing_code + + WCM_S_typing_code -> WCM_P_set_code [label="finished\ncode"] + + WCM_S_unknown -> WCM_P_allocate [label="allocate"] + WCM_P_allocate [shape="box" label="C_allocate_nameplate()"] + WCM_P_allocate -> WCM_S_allocate_waiting + WCM_S_allocate_waiting [label="waiting"] + WCM_S_allocate_waiting -> WCM_P_allocate_generate [label="WCM_rx_allocation()"] + WCM_P_allocate_generate [shape="box" label="generate\nrandom code"] + WCM_P_allocate_generate -> WCM_P_set_code + + /* ConnectionMachine */ + /*WCM_S_known -> CM_start [style="invis"] + CM_start [label="Connection\nMachine" style="dotted"] + CM_start -> CM_S_neither [style="invis"] + CM_S_neither [label="neither"]*/ + + + + NM_start [label="Nameplate\nMachine" style="dotted"] + NM_start -> NM_S_unclaimed [style="invis"] NM_S_unclaimed [label="no nameplate"] NM_S_unclaimed -> NM_S_unclaimed [label="NM_release()"] NM_P_set_nameplate [shape="box" label="post_claim()"] - NM_S_unclaimed -> NM_P_set_nameplate [label="set_nameplate()"] + NM_S_unclaimed -> NM_P_set_nameplate [label="NM_set_nameplate()"] NM_S_claiming [label="claim pending"] NM_P_set_nameplate -> NM_S_claiming NM_S_claiming -> NM_P_rx_claimed [label="rx claimed"] - NM_P_rx_claimed [label="set_mailbox()" shape="box"] + NM_P_rx_claimed [label="MM_set_mailbox()" shape="box"] NM_P_rx_claimed -> NM_S_claimed NM_S_claimed [label="claimed"] NM_S_claimed -> NM_P_release [label="NM_release()"] @@ -78,7 +140,9 @@ digraph { NM_S_released [label="released"] NM_S_released -> NM_S_released [label="NM_release()"] - + + MM_start [label="Mailbox\nMachine" style="dotted"] + MM_start -> MM_S_want_mailbox [style="invis"] MM_S_want_mailbox [label="want mailbox"] MM_S_want_mailbox -> MM_P_queue1 [label="MM_send()" style="dotted"] MM_P_queue1 [shape="box" style="dotted" label="queue message"] @@ -88,21 +152,36 @@ digraph { MM_P_send_queued [shape="box" label="post add() for\nqueued messages"] MM_P_open_mailbox -> MM_P_send_queued MM_P_send_queued -> MM_S_open - MM_S_open [label="open\nunused"] - MM_S_open -> MM_P_send_queued [label="MM_send()"] + MM_S_open [label="open\n(unused)"] + MM_S_open -> MM_P_send1 [label="MM_send()"] + MM_P_send1 [shape="box" label="post add()\nfor message"] + MM_P_send1 -> MM_S_open + MM_S_open -> MM_P_release1 [label="MM_close()"] + MM_P_release1 [shape="box" label="NM_release()"] + MM_P_release1 -> MM_P_close MM_S_open -> MM_P_rx [label="rx message"] - MM_P_rx [shape="box" label="W_rx_pake()\nor W_rx_msg()"] - MM_P_rx -> MM_P_release - MM_P_release [shape="box" label="NM_release()"] - MM_P_release -> MM_S_used - MM_S_used [label="open\nused"] + MM_P_rx [shape="box" label="WM_rx_pake()\nor WM_rx_msg()"] + MM_P_rx -> MM_P_release2 + MM_P_release2 [shape="box" label="NM_release()"] + MM_P_release2 -> MM_S_used + MM_S_used [label="open\n(used)"] MM_S_used -> MM_P_rx [label="rx message"] - MM_S_used -> MM_P_send [label="MM_send()"] - MM_P_send [shape="box" label="post message"] - MM_P_send -> MM_S_used + MM_S_used -> MM_P_send2 [label="MM_send()"] + MM_P_send2 [shape="box" label="post add()\nfor message"] + MM_P_send2 -> MM_S_used + MM_S_used -> MM_P_close [label="MM_close()"] + MM_P_close [shape="box" label="post_close(mood)"] + MM_P_close -> MM_S_closing + MM_S_closing [label="waiting"] + MM_S_closing -> MM_S_closing [label="MM_close()"] + MM_S_closing -> MM_S_closed [label="rx closed"] + MM_S_closed [label="closed"] + MM_S_closed -> MM_S_closed [label="MM_close()"] - /* upgrading to new PAKE algorithm */ + /* upgrading to new PAKE algorithm, the slower form (the faster form + puts the pake_abilities record in the nameplate_info message) */ + /* P2_start [label="(PAKE\nupgrade)\nstart"] P2_start -> P2_P_send_abilities [label="set_code()"] P2_P_send_abilities [shape="box" label="send pake_abilities"] @@ -119,4 +198,5 @@ digraph { P2_P_send_pakev2 [shape="box" label="send pake_v2"] P2_P_send_pakev2 -> P2_P_process_v2 [label="rx pake_v2"] P2_P_process_v2 [shape="box" label="process v2"] + */ } From 3ec7747b1e829c47bd03087a4ea050c0657afc26 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 22 Dec 2016 12:24:22 -0500 Subject: [PATCH 013/176] more --- docs/w.dot | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index 1e85bb4..da84c89 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -110,12 +110,6 @@ digraph { WCM_S_allocate_waiting -> WCM_P_allocate_generate [label="WCM_rx_allocation()"] WCM_P_allocate_generate [shape="box" label="generate\nrandom code"] WCM_P_allocate_generate -> WCM_P_set_code - - /* ConnectionMachine */ - /*WCM_S_known -> CM_start [style="invis"] - CM_start [label="Connection\nMachine" style="dotted"] - CM_start -> CM_S_neither [style="invis"] - CM_S_neither [label="neither"]*/ @@ -199,4 +193,10 @@ digraph { P2_P_send_pakev2 -> P2_P_process_v2 [label="rx pake_v2"] P2_P_process_v2 [shape="box" label="process v2"] */ + + /* ConnectionMachine */ + WCM_S_known -> CM_start [style="invis"] + CM_start [label="Connection\nMachine" style="dotted"] + CM_start -> CM_S_neither [style="invis"] + CM_S_neither [label="neither"] } From 6c77f33cdf81f085380efc3549f4bf55c8150071 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 22 Dec 2016 16:13:07 -0500 Subject: [PATCH 014/176] start on connection machine --- docs/w.dot | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/w.dot b/docs/w.dot index da84c89..4534ddd 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -199,4 +199,11 @@ digraph { CM_start [label="Connection\nMachine" style="dotted"] CM_start -> CM_S_neither [style="invis"] CM_S_neither [label="neither"] + CM_S_neither -> CM_S_unbound + CM_S_unbound [label="unbound"] + CM_S_unbound -> CM_P_bind + CM_P_bind [shape="box" label="C_send(bind)"] + CM_P_bind -> CM_S_bound + CM_S_bound [label="bound"] + } From 78fcb6b8094295730fe4371c3618c4ad3d8d0aa9 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 24 Dec 2016 12:10:33 -0500 Subject: [PATCH 015/176] new approach, thinking about connections up front --- docs/w2.dot | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/w2.dot diff --git a/docs/w2.dot b/docs/w2.dot new file mode 100644 index 0000000..1cc1b54 --- /dev/null +++ b/docs/w2.dot @@ -0,0 +1,126 @@ +digraph { + /* new idea */ + + foo [label="whole\ncode"] + foo -> S1 + allocation -> S1B + interactive -> S1B + + {rank=same; S1 S1B} + S1 [label="1: know nothing"] + S1B [label="1: know nothing\n(bound)"] + + S1 -> S1B [label="connect()"] + S1B -> S1 [label="lose()"] + + {rank=same; S2 S2B P_claim2} + S2 [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open"] + S2B [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open\n(bound)"] + S1 -> S2 [label="set_nameplate()"] + S2 -> P_claim2 [label="connect()"] + S2B -> S2 [label="lose()"] + S1B -> P_claim1 [label="set_nameplate()"] + P_claim1 [shape="box" label="tx claim()"] + P_claim1 -> S2B + P_claim2 [shape="box" label="tx claim()"] + P_claim2 -> S2B + S2 -> P2_queue [label="M_send(msg)"] + P2_queue [shape="box" label="queue"] + P2_queue -> S2 + + {rank=same; S3 S3B P3_open P3_send} + S3 [label="3: claimed\nknown mailbox\nwant open"] + S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] + S2 -> S3 [label="(none)" style="invis"] + S2B -> P_open [label="rx_claimed()"] + P_open [shape="box" label="store mailbox\ntx open()\ntx add(queued)"] + P_open -> S3B + S3 -> P3_open [label="connect()"] + S3B -> S3 [label="lose()"] + /*S3B -> S2 [label="lose()"]*/ /* not worth it */ + P3_open [shape="box" label="tx open()\ntx add(queued)"] + P3_open -> S3B + /*S3B -> S3B [label="rx_claimed()"] */ + S3B -> P3_send [label="M_send(msg)"] + P3_send [shape="box" label="queue\ntx add(msg)"] + P3_send -> S3B + S3 -> P3_queue [label="M_send(msg)"] + P3_queue [shape="box" label="queue"] + P3_queue -> S3 + + {rank=same; S4 P4_release S4B P4_process P4_send P4_queue} + S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] + S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] + S3 -> S4 [label="(none)" style="invis"] + S3B -> P_release [label="rx_message()"] + P_release [shape="box" label="tx release()"] + P_release -> P3_process + P3_process [shape="box" label="process message"] + P3_process -> S4B + S4 -> P4_release [label="connect()"] + /* it is currently an error to release a nameplate you aren't + currently claiming, so release() is not idempotent. #118 fixes that */ + P4_release [shape="box" label="tx claim() *#118\ntx release()\ntx open()\ntx add(queued)"] + S4B -> S4B [label="rx_claimed() *#118"] + S4B -> P_close [label="M_close()"] + S4B -> P4_send [label="M_send(msg)"] + P4_send [shape="box" label="queue\ntx add(msg)"] + P4_send -> S4B + S4 -> P4_queue [label="M_send(msg)"] + P4_queue [shape="box" label="queue"] + P4_queue -> S4 + + P4_release -> S4B + S4B -> S4 [label="lose()"] + S4B -> P4_process [label="rx_message()"] + P4_process [shape="box" label="process message"] + P4_process -> S4B + + {rank=same; S5 S5B P5_open P5_process} + S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] + S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] + S4 -> S5 [label="(none)" style="invis"] + S4B -> S5B [label="rx_released()"] + S5 -> P5_open [label="connect()"] + P5_open [shape="box" label="tx open()\ntx add(queued)"] + P5_open -> S5B + S5B -> S5 [label="lose()"] + S5B -> P5_process [label="rx_message()"] + P5_process [shape="box" label="process message"] + P5_process -> S5B + S5B -> P5_send [label="M_send(msg)"] + P5_send [shape="box" label="queue\ntx add(msg)"] + P5_send -> S5B + S5 -> P5_queue [label="M_send(msg)"] + P5_queue [shape="box" label="queue"] + P5_queue -> S5 + + {rank=same; S6 P6_close S6B} + S6 [label="6: closing\nunwant mailbox\nopen\n"] + S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] + S5 -> S6 [label="M_close()"] + S5B -> P_close [label="M_close()"] + P_close [shape="box" label="tx close()"] + P_close -> S6B + S6 -> P6_close [label="connect()"] + P6_close [shape="box" label="tx close()"] + P6_close -> S6B + S6B -> S6 [label="lose()"] + S6B -> S6B [label="rx_released()"] + S6B -> S6B [label="M_close()"] + S6B -> S6B [label="M_send()"] + S6 -> S6 [label="M_send()"] + + {rank=same; S7 S7B} + S7 [label="7: closed\n"] + S7B [label="7: closed\n(bound)"] + S6 -> S7 [label="(none)" style="invis"] + S6B -> P7_drop [label="rx_closed()"] + P7_drop [shape="box" label="C_drop()"] + P7_drop -> S7B + S7 -> S7B [label="connect()" style="invis"] + S7B -> S7 [label="lose()"] + S7B -> S7B [label="M_close()"] + S7B -> S7B [label="M_send()"] + +} From f27e601e41778910c0811ba71ef0e01fa7219b4c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 24 Dec 2016 12:18:06 -0500 Subject: [PATCH 016/176] more --- docs/w2.dot | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/w2.dot b/docs/w2.dot index 1cc1b54..4452d11 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -9,32 +9,36 @@ digraph { {rank=same; S1 S1B} S1 [label="1: know nothing"] S1B [label="1: know nothing\n(bound)"] - S1 -> S1B [label="connect()"] S1B -> S1 [label="lose()"] + S1 -> S2 [label="set_nameplate()"] + S1B -> P_claim1 [label="set_nameplate()"] + P_claim1 [shape="box" label="tx claim()"] + P_claim1 -> S2B + {rank=same; S2 S2B P_claim2} S2 [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open"] S2B [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open\n(bound)"] - S1 -> S2 [label="set_nameplate()"] S2 -> P_claim2 [label="connect()"] S2B -> S2 [label="lose()"] - S1B -> P_claim1 [label="set_nameplate()"] - P_claim1 [shape="box" label="tx claim()"] - P_claim1 -> S2B P_claim2 [shape="box" label="tx claim()"] P_claim2 -> S2B S2 -> P2_queue [label="M_send(msg)"] P2_queue [shape="box" label="queue"] P2_queue -> S2 + S2B -> P2B_queue [label="M_send(msg)"] + P2B_queue [shape="box" label="queue"] + P2B_queue -> S2B - {rank=same; S3 S3B P3_open P3_send} - S3 [label="3: claimed\nknown mailbox\nwant open"] - S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] S2 -> S3 [label="(none)" style="invis"] S2B -> P_open [label="rx_claimed()"] P_open [shape="box" label="store mailbox\ntx open()\ntx add(queued)"] P_open -> S3B + + {rank=same; S3 S3B P3_open P3_send} + S3 [label="3: claimed\nknown mailbox\nwant open"] + S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] S3 -> P3_open [label="connect()"] S3B -> S3 [label="lose()"] /*S3B -> S2 [label="lose()"]*/ /* not worth it */ @@ -48,15 +52,14 @@ digraph { P3_queue [shape="box" label="queue"] P3_queue -> S3 + S3 -> S4 [label="(none)" style="invis"] + S3B -> P3_release_process [label="rx_message()"] + P3_release_process [shape="box" label="tx release()\nprocess message"] + P3_release_process -> S4B + {rank=same; S4 P4_release S4B P4_process P4_send P4_queue} S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] - S3 -> S4 [label="(none)" style="invis"] - S3B -> P_release [label="rx_message()"] - P_release [shape="box" label="tx release()"] - P_release -> P3_process - P3_process [shape="box" label="process message"] - P3_process -> S4B S4 -> P4_release [label="connect()"] /* it is currently an error to release a nameplate you aren't currently claiming, so release() is not idempotent. #118 fixes that */ @@ -76,11 +79,12 @@ digraph { P4_process [shape="box" label="process message"] P4_process -> S4B + S4 -> S5 [label="(none)" style="invis"] + S4B -> S5B [label="rx_released()"] + {rank=same; S5 S5B P5_open P5_process} S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] - S4 -> S5 [label="(none)" style="invis"] - S4B -> S5B [label="rx_released()"] S5 -> P5_open [label="connect()"] P5_open [shape="box" label="tx open()\ntx add(queued)"] P5_open -> S5B @@ -95,13 +99,14 @@ digraph { P5_queue [shape="box" label="queue"] P5_queue -> S5 - {rank=same; S6 P6_close S6B} - S6 [label="6: closing\nunwant mailbox\nopen\n"] - S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] S5 -> S6 [label="M_close()"] S5B -> P_close [label="M_close()"] P_close [shape="box" label="tx close()"] P_close -> S6B + + {rank=same; S6 P6_close S6B} + S6 [label="6: closing\nunwant mailbox\nopen\n"] + S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] S6 -> P6_close [label="connect()"] P6_close [shape="box" label="tx close()"] P6_close -> S6B @@ -111,13 +116,14 @@ digraph { S6B -> S6B [label="M_send()"] S6 -> S6 [label="M_send()"] - {rank=same; S7 S7B} - S7 [label="7: closed\n"] - S7B [label="7: closed\n(bound)"] S6 -> S7 [label="(none)" style="invis"] S6B -> P7_drop [label="rx_closed()"] P7_drop [shape="box" label="C_drop()"] P7_drop -> S7B + + {rank=same; S7 S7B} + S7 [label="7: closed\n"] + S7B [label="7: closed\n(bound)"] S7 -> S7B [label="connect()" style="invis"] S7B -> S7 [label="lose()"] S7B -> S7B [label="M_close()"] From f85e2ec68aa51d6fb8c5a8dfa77e94a3c8d76ac3 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 24 Dec 2016 13:17:48 -0500 Subject: [PATCH 017/176] more --- docs/w.dot | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/w.dot b/docs/w.dot index 4534ddd..00d3cbf 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -1,4 +1,5 @@ digraph { + /* could shave a RTT by committing to the nameplate early, before finishing the rest of the code input. While the user is still typing/completing the code, we claim the nameplate, open the mailbox, @@ -195,6 +196,7 @@ digraph { */ /* ConnectionMachine */ + /* WCM_S_known -> CM_start [style="invis"] CM_start [label="Connection\nMachine" style="dotted"] CM_start -> CM_S_neither [style="invis"] @@ -205,5 +207,5 @@ digraph { CM_P_bind [shape="box" label="C_send(bind)"] CM_P_bind -> CM_S_bound CM_S_bound [label="bound"] - + */ } From e7b2a7bbf9d17c8b9e4ff0833fa933a1a38fb9ad Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 25 Dec 2016 09:38:46 -0500 Subject: [PATCH 018/176] fixing 118 is the key --- docs/w2.dot | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/w2.dot b/docs/w2.dot index 4452d11..e7a02b5 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -3,7 +3,7 @@ digraph { foo [label="whole\ncode"] foo -> S1 - allocation -> S1B + allocation -> S1B [label="already\nconnected"] interactive -> S1B {rank=same; S1 S1B} @@ -44,7 +44,7 @@ digraph { /*S3B -> S2 [label="lose()"]*/ /* not worth it */ P3_open [shape="box" label="tx open()\ntx add(queued)"] P3_open -> S3B - /*S3B -> S3B [label="rx_claimed()"] */ + S3B -> S3B [label="rx_claimed()"] S3B -> P3_send [label="M_send(msg)"] P3_send [shape="box" label="queue\ntx add(msg)"] P3_send -> S3B @@ -53,18 +53,22 @@ digraph { P3_queue -> S3 S3 -> S4 [label="(none)" style="invis"] - S3B -> P3_release_process [label="rx_message()"] - P3_release_process [shape="box" label="tx release()\nprocess message"] - P3_release_process -> S4B + S3B -> P3_process_ours [label="rx_message(ours)"] + P3_process_ours [shape="box" label="process message"] + P3_process_ours -> S3B + S3B -> P3_process_theirs [label="rx_message(theirs)"] + P3_process_theirs [shape="box" label="tx release()\nprocess message"] + P3_process_theirs -> S4B {rank=same; S4 P4_release S4B P4_process P4_send P4_queue} S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] + S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] S4 -> P4_release [label="connect()"] /* it is currently an error to release a nameplate you aren't currently claiming, so release() is not idempotent. #118 fixes that */ - P4_release [shape="box" label="tx claim() *#118\ntx release()\ntx open()\ntx add(queued)"] - S4B -> S4B [label="rx_claimed() *#118"] + P4_release [shape="box" label="tx open()\ntx add(queued)\ntx release()"] + /*S4B -> S4B [label="rx_claimed() *#118"]*/ S4B -> P_close [label="M_close()"] S4B -> P4_send [label="M_send(msg)"] P4_send [shape="box" label="queue\ntx add(msg)"] @@ -75,6 +79,7 @@ digraph { P4_release -> S4B S4B -> S4 [label="lose()"] + /*S4B -> S2 [label="lose()"]*/ S4B -> P4_process [label="rx_message()"] P4_process [shape="box" label="process message"] P4_process -> S4B @@ -82,7 +87,9 @@ digraph { S4 -> S5 [label="(none)" style="invis"] S4B -> S5B [label="rx_released()"] - {rank=same; S5 S5B P5_open P5_process} + seed [label="from Seed?"] + seed -> S5 + {rank=same; seed S5 S5B P5_open P5_process} S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] S5 -> P5_open [label="connect()"] From fa76b579769161ee22657bbfe6c6476cb375d257 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 26 Dec 2016 13:45:03 -0500 Subject: [PATCH 019/176] w2.dot: add M_ prefix --- docs/w2.dot | 237 +++++++++++++++++++++++++++------------------------- 1 file changed, 121 insertions(+), 116 deletions(-) diff --git a/docs/w2.dot b/docs/w2.dot index e7a02b5..29aa198 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -1,139 +1,144 @@ digraph { /* new idea */ - foo [label="whole\ncode"] - foo -> S1 - allocation -> S1B [label="already\nconnected"] - interactive -> S1B + {rank=same; M_entry_whole_code M_title M_entry_allocation M_entry_interactive} + M_entry_whole_code [label="whole\ncode"] + M_entry_whole_code -> M_S1 + M_title [label="Message\nMachine" style="dotted"] + M_entry_whole_code -> M_title [style="invis"] + M_entry_allocation [label="allocation"] + M_entry_allocation -> M_S1B [label="already\nconnected"] + M_entry_interactive [label="interactive"] + M_entry_interactive -> M_S1B - {rank=same; S1 S1B} - S1 [label="1: know nothing"] - S1B [label="1: know nothing\n(bound)"] - S1 -> S1B [label="connect()"] - S1B -> S1 [label="lose()"] + {rank=same; M_S1 M_S1B} + M_S1 [label="1: know nothing"] + M_S1B [label="1: know nothing\n(bound)"] + M_S1 -> M_S1B [label="connect()"] + M_S1B -> M_S1 [label="lose()"] - S1 -> S2 [label="set_nameplate()"] - S1B -> P_claim1 [label="set_nameplate()"] - P_claim1 [shape="box" label="tx claim()"] - P_claim1 -> S2B + M_S1 -> M_S2 [label="M_set_nameplate()"] + M_S1B -> M_P_claim1 [label="M_set_nameplate()"] + M_P_claim1 [shape="box" label="tx claim()"] + M_P_claim1 -> M_S2B - {rank=same; S2 S2B P_claim2} - S2 [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open"] - S2B [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open\n(bound)"] - S2 -> P_claim2 [label="connect()"] - S2B -> S2 [label="lose()"] - P_claim2 [shape="box" label="tx claim()"] - P_claim2 -> S2B - S2 -> P2_queue [label="M_send(msg)"] - P2_queue [shape="box" label="queue"] - P2_queue -> S2 - S2B -> P2B_queue [label="M_send(msg)"] - P2B_queue [shape="box" label="queue"] - P2B_queue -> S2B + {rank=same; M_S2 M_S2B M_P_claim2} + M_S2 [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open"] + M_S2B [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open\n(bound)"] + M_S2 -> M_P_claim2 [label="connect()"] + M_S2B -> M_S2 [label="lose()"] + M_P_claim2 [shape="box" label="tx claim()"] + M_P_claim2 -> M_S2B + M_S2 -> M_P2_queue [label="M_send(msg)"] + M_P2_queue [shape="box" label="queue"] + M_P2_queue -> M_S2 + M_S2B -> M_P2B_queue [label="M_send(msg)"] + M_P2B_queue [shape="box" label="queue"] + M_P2B_queue -> M_S2B - S2 -> S3 [label="(none)" style="invis"] - S2B -> P_open [label="rx_claimed()"] - P_open [shape="box" label="store mailbox\ntx open()\ntx add(queued)"] - P_open -> S3B + M_S2 -> M_S3 [label="(none)" style="invis"] + M_S2B -> M_P_open [label="rx_claimed()"] + M_P_open [shape="box" label="store mailbox\ntx open()\ntx add(queued)"] + M_P_open -> M_S3B - {rank=same; S3 S3B P3_open P3_send} - S3 [label="3: claimed\nknown mailbox\nwant open"] - S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] - S3 -> P3_open [label="connect()"] - S3B -> S3 [label="lose()"] - /*S3B -> S2 [label="lose()"]*/ /* not worth it */ - P3_open [shape="box" label="tx open()\ntx add(queued)"] - P3_open -> S3B - S3B -> S3B [label="rx_claimed()"] - S3B -> P3_send [label="M_send(msg)"] - P3_send [shape="box" label="queue\ntx add(msg)"] - P3_send -> S3B - S3 -> P3_queue [label="M_send(msg)"] - P3_queue [shape="box" label="queue"] - P3_queue -> S3 + {rank=same; M_S3 M_S3B M_P3_open M_P3_send} + M_S3 [label="3: claimed\nknown mailbox\nwant open"] + M_S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] + M_S3 -> M_P3_open [label="connect()"] + M_S3B -> M_S3 [label="lose()"] + /*M_S3B -> M_S2 [label="lose()"]*/ /* not worth it */ + M_P3_open [shape="box" label="tx open()\ntx add(queued)"] + M_P3_open -> M_S3B + M_S3B -> M_S3B [label="rx_claimed()"] + M_S3B -> M_P3_send [label="M_send(msg)"] + M_P3_send [shape="box" label="queue\ntx add(msg)"] + M_P3_send -> M_S3B + M_S3 -> M_P3_queue [label="M_send(msg)"] + M_P3_queue [shape="box" label="queue"] + M_P3_queue -> M_S3 - S3 -> S4 [label="(none)" style="invis"] - S3B -> P3_process_ours [label="rx_message(ours)"] - P3_process_ours [shape="box" label="process message"] - P3_process_ours -> S3B - S3B -> P3_process_theirs [label="rx_message(theirs)"] - P3_process_theirs [shape="box" label="tx release()\nprocess message"] - P3_process_theirs -> S4B + M_S3 -> M_S4 [label="(none)" style="invis"] + M_S3B -> M_P3_process_ours [label="rx_message(side=me)"] + M_P3_process_ours [shape="box" label="process message"] + M_P3_process_ours -> M_S3B + M_S3B -> M_P3_process_theirs [label="rx_message(side!=me)"] + M_P3_process_theirs [shape="box" label="tx release()\nprocess message"] + M_P3_process_theirs -> M_S4B - {rank=same; S4 P4_release S4B P4_process P4_send P4_queue} - S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] + {rank=same; M_S4 M_P4_release M_S4B M_P4_process M_P4_send M_P4_queue} + M_S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] - S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] - S4 -> P4_release [label="connect()"] + M_S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] + M_S4 -> M_P4_release [label="connect()"] /* it is currently an error to release a nameplate you aren't currently claiming, so release() is not idempotent. #118 fixes that */ - P4_release [shape="box" label="tx open()\ntx add(queued)\ntx release()"] - /*S4B -> S4B [label="rx_claimed() *#118"]*/ - S4B -> P_close [label="M_close()"] - S4B -> P4_send [label="M_send(msg)"] - P4_send [shape="box" label="queue\ntx add(msg)"] - P4_send -> S4B - S4 -> P4_queue [label="M_send(msg)"] - P4_queue [shape="box" label="queue"] - P4_queue -> S4 + M_P4_release [shape="box" label="tx open()\ntx add(queued)\ntx release()"] + /*M_S4B -> M_S4B [label="rx_claimed() *#118"]*/ + M_S4B -> M_P_close [label="M_close()"] + M_S4B -> M_P4_send [label="M_send(msg)"] + M_P4_send [shape="box" label="queue\ntx add(msg)"] + M_P4_send -> M_S4B + M_S4 -> M_P4_queue [label="M_send(msg)"] + M_P4_queue [shape="box" label="queue"] + M_P4_queue -> M_S4 - P4_release -> S4B - S4B -> S4 [label="lose()"] - /*S4B -> S2 [label="lose()"]*/ - S4B -> P4_process [label="rx_message()"] - P4_process [shape="box" label="process message"] - P4_process -> S4B + M_P4_release -> M_S4B + M_S4B -> M_S4 [label="lose()"] + /*M_S4B -> M_S2 [label="lose()"]*/ + M_S4B -> M_P4_process [label="rx_message()"] + M_P4_process [shape="box" label="process message"] + M_P4_process -> M_S4B - S4 -> S5 [label="(none)" style="invis"] - S4B -> S5B [label="rx_released()"] + M_S4 -> M_S5 [label="(none)" style="invis"] + M_S4B -> M_S5B [label="rx_released()"] seed [label="from Seed?"] - seed -> S5 - {rank=same; seed S5 S5B P5_open P5_process} - S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] - S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] - S5 -> P5_open [label="connect()"] - P5_open [shape="box" label="tx open()\ntx add(queued)"] - P5_open -> S5B - S5B -> S5 [label="lose()"] - S5B -> P5_process [label="rx_message()"] - P5_process [shape="box" label="process message"] - P5_process -> S5B - S5B -> P5_send [label="M_send(msg)"] - P5_send [shape="box" label="queue\ntx add(msg)"] - P5_send -> S5B - S5 -> P5_queue [label="M_send(msg)"] - P5_queue [shape="box" label="queue"] - P5_queue -> S5 + seed -> M_S5 + {rank=same; seed M_S5 M_S5B M_P5_open M_P5_process} + M_S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] + M_S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] + M_S5 -> M_P5_open [label="connect()"] + M_P5_open [shape="box" label="tx open()\ntx add(queued)"] + M_P5_open -> M_S5B + M_S5B -> M_S5 [label="lose()"] + M_S5B -> M_P5_process [label="rx_message()"] + M_P5_process [shape="box" label="process message"] + M_P5_process -> M_S5B + M_S5B -> M_P5_send [label="M_send(msg)"] + M_P5_send [shape="box" label="queue\ntx add(msg)"] + M_P5_send -> M_S5B + M_S5 -> M_P5_queue [label="M_send(msg)"] + M_P5_queue [shape="box" label="queue"] + M_P5_queue -> M_S5 - S5 -> S6 [label="M_close()"] - S5B -> P_close [label="M_close()"] - P_close [shape="box" label="tx close()"] - P_close -> S6B + M_S5 -> M_S6 [label="M_close()"] + M_S5B -> M_P_close [label="M_close()"] + M_P_close [shape="box" label="tx close()"] + M_P_close -> M_S6B - {rank=same; S6 P6_close S6B} - S6 [label="6: closing\nunwant mailbox\nopen\n"] - S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] - S6 -> P6_close [label="connect()"] - P6_close [shape="box" label="tx close()"] - P6_close -> S6B - S6B -> S6 [label="lose()"] - S6B -> S6B [label="rx_released()"] - S6B -> S6B [label="M_close()"] - S6B -> S6B [label="M_send()"] - S6 -> S6 [label="M_send()"] + {rank=same; M_S6 M_P6_close M_S6B} + M_S6 [label="6: closing\nunwant mailbox\nopen\n"] + M_S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] + M_S6 -> M_P6_close [label="connect()"] + M_P6_close [shape="box" label="tx close()"] + M_P6_close -> M_S6B + M_S6B -> M_S6 [label="lose()"] + M_S6B -> M_S6B [label="rx_released()"] + M_S6B -> M_S6B [label="M_close()"] + M_S6B -> M_S6B [label="M_send()"] + M_S6 -> M_S6 [label="M_send()"] - S6 -> S7 [label="(none)" style="invis"] - S6B -> P7_drop [label="rx_closed()"] - P7_drop [shape="box" label="C_drop()"] - P7_drop -> S7B + M_S6 -> M_S7 [label="(none)" style="invis"] + M_S6B -> M_P7_drop [label="rx_closed()"] + M_P7_drop [shape="box" label="C_drop()"] + M_P7_drop -> M_S7B - {rank=same; S7 S7B} - S7 [label="7: closed\n"] - S7B [label="7: closed\n(bound)"] - S7 -> S7B [label="connect()" style="invis"] - S7B -> S7 [label="lose()"] - S7B -> S7B [label="M_close()"] - S7B -> S7B [label="M_send()"] + {rank=same; M_S7 M_S7B} + M_S7 [label="7: closed\n"] + M_S7B [label="7: closed\n(bound)"] + M_S7 -> M_S7B [label="connect()" style="invis"] + M_S7B -> M_S7 [label="lose()"] + M_S7B -> M_S7B [label="M_close()"] + M_S7B -> M_S7B [label="M_send()"] } From 5b82b424a03b4771ffcffd9cd1d0938563125dd9 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 26 Dec 2016 13:45:19 -0500 Subject: [PATCH 020/176] w.dot: comment out things that seem superceded by w2.dot --- docs/w.dot | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index 00d3cbf..09bebfa 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -17,7 +17,7 @@ digraph { WM_P_queue1 -> WM_S_nothing [style="dotted"] WM_S_nothing -> WM_P_build_and_post_pake [label="WM_set_code()"] - WM_P_build_and_post_pake [label="NM_set_nameplate()\nbuild_pake()\nMM_send(pake)" shape="box"] + WM_P_build_and_post_pake [label="M_set_nameplate()\nbuild_pake()\nM_send(pake)" shape="box"] WM_P_build_and_post_pake -> WM_S_know_code WM_S_know_code [label="know code\n"] @@ -37,7 +37,7 @@ digraph { WM_P_notify_failure [shape="box" label="notify_failure()" color="red"] WM_P_notify_failure -> WM_S_closed - WM_P_post_version [label="MM_send(version)\nnotify_verifier()" shape="box"] + WM_P_post_version [label="M_send(version)\nnotify_verifier()" shape="box"] WM_P_post_version -> WM_S_know_key WM_S_know_key [label="know_key\nunverified" color="orange"] @@ -54,7 +54,7 @@ digraph { WM_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] WM_P_accept_msg -> WM_P_send_queued - WM_P_send_queued [shape="box" label="MM_send()\nany queued\nmessages"] + WM_P_send_queued [shape="box" label="M_send()\nany queued\nmessages"] WM_P_send_queued -> WM_S_verified_key WM_S_verified_key [color="green"] @@ -68,7 +68,7 @@ digraph { WM_P_mood_lonely [shape="box" label="MM_close()\nmood=lonely"] WM_P_mood_lonely -> WM_S_closed - WM_P_send [shape="box" label="MM_send(msg)"] + WM_P_send [shape="box" label="M_send(msg)"] WM_P_send -> WM_S_verified_key WM_S_closed [label="closed"] @@ -113,7 +113,7 @@ digraph { WCM_P_allocate_generate -> WCM_P_set_code - + /* NM_start [label="Nameplate\nMachine" style="dotted"] NM_start -> NM_S_unclaimed [style="invis"] NM_S_unclaimed [label="no nameplate"] @@ -134,8 +134,9 @@ digraph { NM_S_releasing -> NM_S_released [label="rx released"] NM_S_released [label="released"] NM_S_released -> NM_S_released [label="NM_release()"] + */ - + /* MM_start [label="Mailbox\nMachine" style="dotted"] MM_start -> MM_S_want_mailbox [style="invis"] MM_S_want_mailbox [label="want mailbox"] @@ -173,6 +174,7 @@ digraph { MM_S_closing -> MM_S_closed [label="rx closed"] MM_S_closed [label="closed"] MM_S_closed -> MM_S_closed [label="MM_close()"] + */ /* upgrading to new PAKE algorithm, the slower form (the faster form puts the pake_abilities record in the nameplate_info message) */ From 2cfc990d5edfdbdef4f85ffbc77485d35e30a45a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 27 Dec 2016 00:37:43 -0500 Subject: [PATCH 021/176] more --- docs/w.dot | 39 ++++++++++++++++++++++----------------- docs/w2.dot | 52 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index 09bebfa..37e71e4 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -31,7 +31,7 @@ digraph { WM_P_compute_key -> WM_P_post_version [label="pake ok"] WM_P_compute_key -> WM_P_mood_scary [label="pake bad"] - WM_P_mood_scary [shape="box" label="MM_close()\nmood=scary"] + WM_P_mood_scary [shape="box" label="M_close()\nmood=scary"] WM_P_mood_scary -> WM_P_notify_failure WM_P_notify_failure [shape="box" label="notify_failure()" color="red"] @@ -54,7 +54,7 @@ digraph { WM_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] WM_P_accept_msg -> WM_P_send_queued - WM_P_send_queued [shape="box" label="M_send()\nany queued\nmessages"] + WM_P_send_queued [shape="box" label="M_send()\nqueued"] WM_P_send_queued -> WM_S_verified_key WM_S_verified_key [color="green"] @@ -62,10 +62,10 @@ digraph { WM_S_verified_key -> WM_P_mood_happy [label="close"] WM_S_verified_key -> WM_P_send [label="API_send"] - WM_P_mood_happy [shape="box" label="MM_close()\nmood=happy"] + WM_P_mood_happy [shape="box" label="M_close()\nmood=happy"] WM_P_mood_happy -> WM_S_closed - WM_P_mood_lonely [shape="box" label="MM_close()\nmood=lonely"] + WM_P_mood_lonely [shape="box" label="M_close()\nmood=lonely"] WM_P_mood_lonely -> WM_S_closed WM_P_send [shape="box" label="M_send(msg)"] @@ -80,7 +80,7 @@ digraph { WCM_S_unknown -> WCM_P_set_code [label="set"] WCM_P_set_code [shape="box" label="WM_set_code()"] WCM_P_set_code -> WCM_S_known - WCM_S_known [label="known"] + WCM_S_known [label="known" color="green"] WCM_S_unknown -> WCM_P_list_nameplates [label="input"] WCM_S_typing_nameplate [label="typing\nnameplate"] @@ -198,16 +198,21 @@ digraph { */ /* ConnectionMachine */ - /* - WCM_S_known -> CM_start [style="invis"] - CM_start [label="Connection\nMachine" style="dotted"] - CM_start -> CM_S_neither [style="invis"] - CM_S_neither [label="neither"] - CM_S_neither -> CM_S_unbound - CM_S_unbound [label="unbound"] - CM_S_unbound -> CM_P_bind - CM_P_bind [shape="box" label="C_send(bind)"] - CM_P_bind -> CM_S_bound - CM_S_bound [label="bound"] - */ + C_start [label="Connection\nMachine" style="dotted"] + C_start -> C_S_connecting [label="C_start()"] + C_S_connecting [label="connecting"] + C_S_connecting -> C_S_connected [label="connected"] + C_S_connecting -> C_P_stop_connecting [label="C_stop()"] + C_P_stop_connecting [shape="box" label="cancel\nconnection\nattempt"] + C_P_stop_connecting -> C_S_stopped + C_S_connected [label="connected" color="green"] + C_S_connected -> C_S_waiting [label="lost"] + C_S_waiting [label="waiting"] + C_S_waiting -> C_S_connecting [label="expire"] + C_S_waiting -> C_S_stopped [label="C_stop()"] + C_S_connected -> C_S_stopping [label="C_stop()"] + C_S_stopping [label="stopping"] + C_S_stopping -> C_S_stopped [label="stopped"] + C_S_stopped [label="stopped"] + } diff --git a/docs/w2.dot b/docs/w2.dot index 29aa198..b9aa9c2 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -29,12 +29,12 @@ digraph { M_S2B -> M_S2 [label="lose()"] M_P_claim2 [shape="box" label="tx claim()"] M_P_claim2 -> M_S2B - M_S2 -> M_P2_queue [label="M_send(msg)"] - M_P2_queue [shape="box" label="queue"] - M_P2_queue -> M_S2 - M_S2B -> M_P2B_queue [label="M_send(msg)"] - M_P2B_queue [shape="box" label="queue"] - M_P2B_queue -> M_S2B + M_S2 -> M_P2_queue [label="M_send(msg)" style="dotted"] + M_P2_queue [shape="box" label="queue" style="dotted"] + M_P2_queue -> M_S2 [style="dotted"] + M_S2B -> M_P2B_queue [label="M_send(msg)" style="dotted"] + M_P2B_queue [shape="box" label="queue" style="dotted"] + M_P2B_queue -> M_S2B [style="dotted"] M_S2 -> M_S3 [label="(none)" style="invis"] M_S2B -> M_P_open [label="rx_claimed()"] @@ -53,17 +53,20 @@ digraph { M_S3B -> M_P3_send [label="M_send(msg)"] M_P3_send [shape="box" label="queue\ntx add(msg)"] M_P3_send -> M_S3B - M_S3 -> M_P3_queue [label="M_send(msg)"] - M_P3_queue [shape="box" label="queue"] - M_P3_queue -> M_S3 + M_S3 -> M_P3_queue [label="M_send(msg)" style="dotted"] + M_P3_queue [shape="box" label="queue" style="dotted"] + M_P3_queue -> M_S3 [style="dotted"] M_S3 -> M_S4 [label="(none)" style="invis"] M_S3B -> M_P3_process_ours [label="rx_message(side=me)"] - M_P3_process_ours [shape="box" label="process message"] + M_P3_process_ours [shape="box" label="dequeue"] M_P3_process_ours -> M_S3B M_S3B -> M_P3_process_theirs [label="rx_message(side!=me)"] M_P3_process_theirs [shape="box" label="tx release()\nprocess message"] M_P3_process_theirs -> M_S4B + M_S3B -> M_P3_close [label="M_close()"] + M_P3_close [shape="box" label="tx release()\n"] + M_P3_close -> M_P_close {rank=same; M_S4 M_P4_release M_S4B M_P4_process M_P4_send M_P4_queue} M_S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] @@ -78,9 +81,9 @@ digraph { M_S4B -> M_P4_send [label="M_send(msg)"] M_P4_send [shape="box" label="queue\ntx add(msg)"] M_P4_send -> M_S4B - M_S4 -> M_P4_queue [label="M_send(msg)"] - M_P4_queue [shape="box" label="queue"] - M_P4_queue -> M_S4 + M_S4 -> M_P4_queue [label="M_send(msg)" style="dotted"] + M_P4_queue [shape="box" label="queue" style="dotted"] + M_P4_queue -> M_S4 [style="dotted"] M_P4_release -> M_S4B M_S4B -> M_S4 [label="lose()"] @@ -96,7 +99,7 @@ digraph { seed -> M_S5 {rank=same; seed M_S5 M_S5B M_P5_open M_P5_process} M_S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] - M_S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] + M_S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)" color="green"] M_S5 -> M_P5_open [label="connect()"] M_P5_open [shape="box" label="tx open()\ntx add(queued)"] M_P5_open -> M_S5B @@ -107,11 +110,12 @@ digraph { M_S5B -> M_P5_send [label="M_send(msg)"] M_P5_send [shape="box" label="queue\ntx add(msg)"] M_P5_send -> M_S5B - M_S5 -> M_P5_queue [label="M_send(msg)"] - M_P5_queue [shape="box" label="queue"] - M_P5_queue -> M_S5 + M_S5 -> M_P5_queue [label="M_send(msg)" style="dotted"] + M_P5_queue [shape="box" label="queue" style="dotted"] + M_P5_queue -> M_S5 [style="dotted"] M_S5 -> M_S6 [label="M_close()"] + /*M_S5 -> M_P7_drop [label="M_close()"]*/ M_S5B -> M_P_close [label="M_close()"] M_P_close [shape="box" label="tx close()"] M_P_close -> M_S6B @@ -129,8 +133,9 @@ digraph { M_S6 -> M_S6 [label="M_send()"] M_S6 -> M_S7 [label="(none)" style="invis"] + /*M_S6 -> M_P7_drop [label="M_close()"]*/ M_S6B -> M_P7_drop [label="rx_closed()"] - M_P7_drop [shape="box" label="C_drop()"] + M_P7_drop [shape="box" label="C_stop()"] M_P7_drop -> M_S7B {rank=same; M_S7 M_S7B} @@ -141,4 +146,15 @@ digraph { M_S7B -> M_S7B [label="M_close()"] M_S7B -> M_S7B [label="M_send()"] + + M_process [shape="box" label="process"] + M_process_me [shape="box" label="dequeue"] + M_process -> M_process_me [label="side == me"] + M_process_them [shape="box" label="" style="dotted"] + M_process -> M_process_them [label="side != me"] + M_process_them -> M_process_pake [label="phase == pake"] + M_process_pake [shape="box" label="WM_rx_pake()"] + M_process_them -> M_process_other [label="phase in (version,numbered)"] + M_process_other [shape="box" label="WM_rx_msg()"] + } From 86f246dbdb270f6c50d48c2c977ebb425a03195c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 27 Dec 2016 01:32:01 -0500 Subject: [PATCH 022/176] just might work. close() mapped out. Starting to draw a distinction between clean-close and abrupt-halt. At least, if we're in the connected state, wormhole.close() should take its time and free up server-side resources (nameplate/mailbox) right away, rather than relying on GC/timeouts to release them. It might be useful to make separate "clean" wormhole.close() and "abrupt" wormhole.halt() API calls, except that really when would you ever call halt? To be realistic, only one of two things will happen: * connection happens normally, app finishes, calls "clean" close() * app terminates suddenly, via exception or SIGINT The problem with defining .close() is that I have to make it work sensibly from any state, not just the one plausible "connected" state. Providing .halt() requires defining its behavior from everywhere else. --- docs/w.dot | 10 +++++--- docs/w2.dot | 69 +++++++++++++++++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index 37e71e4..db5c49b 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -201,18 +201,22 @@ digraph { C_start [label="Connection\nMachine" style="dotted"] C_start -> C_S_connecting [label="C_start()"] C_S_connecting [label="connecting"] - C_S_connecting -> C_S_connected [label="connected"] + C_S_connecting -> C_P_connected [label="onConnect"] + C_P_connected [shape="box" label="M_connected()"] + C_P_connected -> C_S_connected C_S_connecting -> C_P_stop_connecting [label="C_stop()"] C_P_stop_connecting [shape="box" label="cancel\nconnection\nattempt"] C_P_stop_connecting -> C_S_stopped C_S_connected [label="connected" color="green"] - C_S_connected -> C_S_waiting [label="lost"] + C_S_connected -> C_P_lost [label="onClose"] + C_P_lost [shape="box" label="M_lost()\nstart timer"] + C_P_lost -> C_S_waiting C_S_waiting [label="waiting"] C_S_waiting -> C_S_connecting [label="expire"] C_S_waiting -> C_S_stopped [label="C_stop()"] C_S_connected -> C_S_stopping [label="C_stop()"] C_S_stopping [label="stopping"] - C_S_stopping -> C_S_stopped [label="stopped"] + C_S_stopping -> C_S_stopped [label="onClose"] C_S_stopped [label="stopped"] } diff --git a/docs/w2.dot b/docs/w2.dot index b9aa9c2..1e85c3b 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -14,8 +14,8 @@ digraph { {rank=same; M_S1 M_S1B} M_S1 [label="1: know nothing"] M_S1B [label="1: know nothing\n(bound)"] - M_S1 -> M_S1B [label="connect()"] - M_S1B -> M_S1 [label="lose()"] + M_S1 -> M_S1B [label="M_connected()"] + M_S1B -> M_S1 [label="M_lost()"] M_S1 -> M_S2 [label="M_set_nameplate()"] M_S1B -> M_P_claim1 [label="M_set_nameplate()"] @@ -25,8 +25,8 @@ digraph { {rank=same; M_S2 M_S2B M_P_claim2} M_S2 [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open"] M_S2B [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open\n(bound)"] - M_S2 -> M_P_claim2 [label="connect()"] - M_S2B -> M_S2 [label="lose()"] + M_S2 -> M_P_claim2 [label="M_connected()"] + M_S2B -> M_S2 [label="M_lost()"] M_P_claim2 [shape="box" label="tx claim()"] M_P_claim2 -> M_S2B M_S2 -> M_P2_queue [label="M_send(msg)" style="dotted"] @@ -44,9 +44,9 @@ digraph { {rank=same; M_S3 M_S3B M_P3_open M_P3_send} M_S3 [label="3: claimed\nknown mailbox\nwant open"] M_S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] - M_S3 -> M_P3_open [label="connect()"] - M_S3B -> M_S3 [label="lose()"] - /*M_S3B -> M_S2 [label="lose()"]*/ /* not worth it */ + M_S3 -> M_P3_open [label="M_connected()"] + M_S3B -> M_S3 [label="M_lost()"] + /*M_S3B -> M_S2 [label="M_lost()"]*/ /* not worth it */ M_P3_open [shape="box" label="tx open()\ntx add(queued)"] M_P3_open -> M_S3B M_S3B -> M_S3B [label="rx_claimed()"] @@ -63,8 +63,15 @@ digraph { M_P3_process_ours -> M_S3B M_S3B -> M_P3_process_theirs [label="rx_message(side!=me)"] M_P3_process_theirs [shape="box" label="tx release()\nprocess message"] + /* pay attention to the race here: this process_message() will + deliver msg_pake to the WormholeMachine, which will compute_key() and + M_send(version), and we're inbetween M_S2 (where M_send gets queued) + and M_S3 (where M_send gets sent and queued), and we're no longer + passing through the M_P3_open phase (which drains the queue). So + there's a real possibility of the outbound msg_version getting + dropped on the floor, or put in a queue but never delivered. */ M_P3_process_theirs -> M_S4B - M_S3B -> M_P3_close [label="M_close()"] + M_S3B -> M_P3_close [label="M_close(mood)"] M_P3_close [shape="box" label="tx release()\n"] M_P3_close -> M_P_close @@ -72,12 +79,12 @@ digraph { M_S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] M_S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] - M_S4 -> M_P4_release [label="connect()"] + M_S4 -> M_P4_release [label="M_connected()"] /* it is currently an error to release a nameplate you aren't currently claiming, so release() is not idempotent. #118 fixes that */ M_P4_release [shape="box" label="tx open()\ntx add(queued)\ntx release()"] /*M_S4B -> M_S4B [label="rx_claimed() *#118"]*/ - M_S4B -> M_P_close [label="M_close()"] + M_S4B -> M_P_close [label="M_close(mood)"] M_S4B -> M_P4_send [label="M_send(msg)"] M_P4_send [shape="box" label="queue\ntx add(msg)"] M_P4_send -> M_S4B @@ -86,8 +93,8 @@ digraph { M_P4_queue -> M_S4 [style="dotted"] M_P4_release -> M_S4B - M_S4B -> M_S4 [label="lose()"] - /*M_S4B -> M_S2 [label="lose()"]*/ + M_S4B -> M_S4 [label="M_lost()"] + /*M_S4B -> M_S2 [label="M_lost()"]*/ M_S4B -> M_P4_process [label="rx_message()"] M_P4_process [shape="box" label="process message"] M_P4_process -> M_S4B @@ -96,14 +103,16 @@ digraph { M_S4B -> M_S5B [label="rx_released()"] seed [label="from Seed?"] + M_S3 -> seed [style="invis"] + M_S4 -> seed [style="invis"] seed -> M_S5 {rank=same; seed M_S5 M_S5B M_P5_open M_P5_process} M_S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] M_S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)" color="green"] - M_S5 -> M_P5_open [label="connect()"] + M_S5 -> M_P5_open [label="M_connected()"] M_P5_open [shape="box" label="tx open()\ntx add(queued)"] M_P5_open -> M_S5B - M_S5B -> M_S5 [label="lose()"] + M_S5B -> M_S5 [label="M_lost()"] M_S5B -> M_P5_process [label="rx_message()"] M_P5_process [shape="box" label="process message"] M_P5_process -> M_S5B @@ -114,26 +123,40 @@ digraph { M_P5_queue [shape="box" label="queue" style="dotted"] M_P5_queue -> M_S5 [style="dotted"] - M_S5 -> M_S6 [label="M_close()"] - /*M_S5 -> M_P7_drop [label="M_close()"]*/ - M_S5B -> M_P_close [label="M_close()"] - M_P_close [shape="box" label="tx close()"] + M_S5 -> M_S6 [style="invis"] + M_S5B -> M_P_close [label="M_close(mood)"] + M_P_close [shape="box" label="tx close(mood)"] M_P_close -> M_S6B {rank=same; M_S6 M_P6_close M_S6B} M_S6 [label="6: closing\nunwant mailbox\nopen\n"] M_S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] - M_S6 -> M_P6_close [label="connect()"] + M_S6 -> M_P6_close [label="M_connected()"] M_P6_close [shape="box" label="tx close()"] M_P6_close -> M_S6B - M_S6B -> M_S6 [label="lose()"] + M_S6B -> M_S6 [label="M_lost()"] M_S6B -> M_S6B [label="rx_released()"] M_S6B -> M_S6B [label="M_close()"] M_S6B -> M_S6B [label="M_send()"] M_S6 -> M_S6 [label="M_send()"] M_S6 -> M_S7 [label="(none)" style="invis"] - /*M_S6 -> M_P7_drop [label="M_close()"]*/ + {rank=same; M_other_closes M_P7_drop} + M_other_closes [label="all\nother\nunbound\nstates" style="dotted"] + M_other_closes -> M_P7_stop [label="M_close()" style="dotted"] + M_P7_stop [shape="box" label="C_stop()"] + M_P7_stop -> M_S7 + /*M_S1 -> M_other_closes [label="M_close()"] + M_S2 -> M_other_closes [label="M_close()"] + M_S3 -> M_other_closes [label="M_close()"] + M_S4 -> M_other_closes [label="M_close()"]*/ + M_S5 -> M_other_closes [style="dotted"] + /*M_S6 -> M_P7_stop [label="M_close()"]*/ + M_S6 -> M_other_closes [style="dotted"] + M_S1B -> M_P7_drop [label="M_close()"] + M_S2B -> M_P2_drop [label="M_close()"] + M_P2_drop [shape="box" label="tx release()"] + M_P2_drop -> M_P7_drop M_S6B -> M_P7_drop [label="rx_closed()"] M_P7_drop [shape="box" label="C_stop()"] M_P7_drop -> M_S7B @@ -141,8 +164,8 @@ digraph { {rank=same; M_S7 M_S7B} M_S7 [label="7: closed\n"] M_S7B [label="7: closed\n(bound)"] - M_S7 -> M_S7B [label="connect()" style="invis"] - M_S7B -> M_S7 [label="lose()"] + M_S7 -> M_S7B [style="invis"] + M_S7B -> M_S7 [label="M_lost()"] M_S7B -> M_S7B [label="M_close()"] M_S7B -> M_S7B [label="M_send()"] From 0b281379484e17c4cdde1f508225cdc178389e3d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 27 Dec 2016 23:23:35 -0500 Subject: [PATCH 023/176] w3.dot: figure out close() pathways d=M_close() will always do the verbose clean shutdown thing, and the Deferred won't fire (e.g. we won't move to state "Ss") until we've deallocated our server resources (nameplates and mailboxes), and we've finished shutting down our websocket connection. So integration tests should wait on the Deferred to make sure everything has stopped moving and the reactor is clean. CLI applications that are following the success path can use M_close() and wait on the Deferred before terminating. CLI applications that wind up on some error path can either use M_close(), or just SIGINT and leave the server to GC stuff later. GUI applications can use M_close() but ignore the Deferred, and assume that the program will keep running long enough to get the deallocation messages through. GUI+DB applications can use M_close() and then stop recording state changes, and if the program remains running long enough, everything will be deallocated, but if it terminates too soon, the server will have to GC. --- docs/w2.dot | 222 +++++++++++++++++++++------------------------------- docs/w3.dot | 76 ++++++++++++++++++ 2 files changed, 166 insertions(+), 132 deletions(-) create mode 100644 docs/w3.dot diff --git a/docs/w2.dot b/docs/w2.dot index 1e85c3b..9f13362 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -3,174 +3,132 @@ digraph { {rank=same; M_entry_whole_code M_title M_entry_allocation M_entry_interactive} M_entry_whole_code [label="whole\ncode"] - M_entry_whole_code -> M_S1 + M_entry_whole_code -> M_S1A M_title [label="Message\nMachine" style="dotted"] M_entry_whole_code -> M_title [style="invis"] - M_entry_allocation [label="allocation"] - M_entry_allocation -> M_S1B [label="already\nconnected"] - M_entry_interactive [label="interactive"] - M_entry_interactive -> M_S1B + M_entry_allocation [label="allocation" color="orange"] + M_entry_allocation -> M_S1B [label="already\nconnected" color="orange" fontcolor="orange"] + M_entry_interactive [label="interactive" color="orange"] + M_entry_interactive -> M_S1B [color="orange"] - {rank=same; M_S1 M_S1B} - M_S1 [label="1: know nothing"] - M_S1B [label="1: know nothing\n(bound)"] - M_S1 -> M_S1B [label="M_connected()"] - M_S1B -> M_S1 [label="M_lost()"] + {rank=same; M_S1A M_S1B} + M_S1A [label="S1A:\nknow nothing"] + M_S1B [label="S1B:\nknow nothing\n(bound)" color="orange"] + M_S1A -> M_S1B [label="M_connected()"] + M_S1B -> M_S1A [label="M_lost()"] - M_S1 -> M_S2 [label="M_set_nameplate()"] - M_S1B -> M_P_claim1 [label="M_set_nameplate()"] - M_P_claim1 [shape="box" label="tx claim()"] - M_P_claim1 -> M_S2B + M_S1A -> M_S2A [label="M_set_nameplate()"] + M_S1B -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] - {rank=same; M_S2 M_S2B M_P_claim2} - M_S2 [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open"] - M_S2B [label="2: know nameplate\nwant claim\nunknown mailbox\nwant open\n(bound)"] - M_S2 -> M_P_claim2 [label="M_connected()"] - M_S2B -> M_S2 [label="M_lost()"] - M_P_claim2 [shape="box" label="tx claim()"] - M_P_claim2 -> M_S2B - M_S2 -> M_P2_queue [label="M_send(msg)" style="dotted"] - M_P2_queue [shape="box" label="queue" style="dotted"] - M_P2_queue -> M_S2 [style="dotted"] + {rank=same; M_S2A M_S2B M_S2C M_P2_claim} + M_S2A [label="S2A:\nnot claimed"] + M_S2C [label="S2C:\nmaybe claimed"] + M_S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] + M_S2A -> M_P2_claim [label="M_connected()"] + M_S2A -> M_C_stop [label="M_close()" style="dashed"] + M_C_stop [shape="box" label="C_stop()" style="dashed"] + M_S2B -> M_SrB [label="M_close()" style="dashed"] + M_SrB [label="SrB" style="dashed"] + M_S2C -> M_SrA [label="M_close()" style="dashed"] + M_SrA [label="SrA" style="dashed"] + + M_S2C -> M_P2_claim [label="M_connected()"] + M_S2B -> M_S2C [label="M_lost()"] + M_P2_claim [shape="box" label="tx claim" color="orange"] + M_P2_claim -> M_S2B [color="orange"] + M_S2A -> M_P2A_queue [label="M_send(msg)" style="dotted"] + M_P2A_queue [shape="box" label="queue" style="dotted"] + M_P2A_queue -> M_S2A [style="dotted"] + M_S2C -> M_P2C_queue [label="M_send(msg)" style="dotted"] + M_P2C_queue [shape="box" label="queue" style="dotted"] + M_P2C_queue -> M_S2C [style="dotted"] M_S2B -> M_P2B_queue [label="M_send(msg)" style="dotted"] M_P2B_queue [shape="box" label="queue" style="dotted"] M_P2B_queue -> M_S2B [style="dotted"] - M_S2 -> M_S3 [label="(none)" style="invis"] - M_S2B -> M_P_open [label="rx_claimed()"] - M_P_open [shape="box" label="store mailbox\ntx open()\ntx add(queued)"] - M_P_open -> M_S3B + M_S2A -> M_S3A [label="(none)" style="invis"] + M_S2B -> M_P_open [label="rx claimed" color="orange" fontcolor="orange"] + M_P_open [shape="box" label="store mailbox\ntx open\ntx add(queued)" color="orange"] + M_P_open -> M_S3B [color="orange"] - {rank=same; M_S3 M_S3B M_P3_open M_P3_send} - M_S3 [label="3: claimed\nknown mailbox\nwant open"] - M_S3B [label="3: claimed\nknown mailbox\nwant open\n(bound)"] - M_S3 -> M_P3_open [label="M_connected()"] - M_S3B -> M_S3 [label="M_lost()"] - /*M_S3B -> M_S2 [label="M_lost()"]*/ /* not worth it */ - M_P3_open [shape="box" label="tx open()\ntx add(queued)"] + {rank=same; M_S3A M_S3B M_P3_open M_P3_send} + M_S3A [label="S3A:\nclaimed\nmaybe open"] + M_S3B [label="S3B:\nclaimed\nmaybe open\n(bound)" color="orange"] + M_S3A -> M_P3_open [label="M_connected()"] + M_S3B -> M_S3A [label="M_lost()"] + M_P3_open [shape="box" label="tx open\ntx add(queued)"] M_P3_open -> M_S3B - M_S3B -> M_S3B [label="rx_claimed()"] + M_S3B -> M_S3B [label="rx claimed"] M_S3B -> M_P3_send [label="M_send(msg)"] M_P3_send [shape="box" label="queue\ntx add(msg)"] M_P3_send -> M_S3B - M_S3 -> M_P3_queue [label="M_send(msg)" style="dotted"] + M_S3A -> M_P3_queue [label="M_send(msg)" style="dotted"] M_P3_queue [shape="box" label="queue" style="dotted"] - M_P3_queue -> M_S3 [style="dotted"] + M_P3_queue -> M_S3A [style="dotted"] - M_S3 -> M_S4 [label="(none)" style="invis"] - M_S3B -> M_P3_process_ours [label="rx_message(side=me)"] + M_S3A -> M_S4A [label="(none)" style="invis"] + M_S3B -> M_P3_process_ours [label="rx message(side=me)"] M_P3_process_ours [shape="box" label="dequeue"] M_P3_process_ours -> M_S3B - M_S3B -> M_P3_process_theirs [label="rx_message(side!=me)"] - M_P3_process_theirs [shape="box" label="tx release()\nprocess message"] + M_S3B -> M_P3_process_theirs1 [label="rx message(side!=me)" color="orange" fontcolor="orange"] + M_P3_process_theirs1 [shape="box" label="tx release" color="orange"] + M_P3_process_theirs1 -> M_P3_process_theirs2 [color="orange"] + M_P3_process_theirs2 [shape="octagon" label="process message" color="orange"] /* pay attention to the race here: this process_message() will deliver msg_pake to the WormholeMachine, which will compute_key() and - M_send(version), and we're inbetween M_S2 (where M_send gets queued) - and M_S3 (where M_send gets sent and queued), and we're no longer - passing through the M_P3_open phase (which drains the queue). So - there's a real possibility of the outbound msg_version getting + M_send(version), and we're in between M_S2A (where M_send gets + queued) and M_S3A (where M_send gets sent and queued), and we're no + longer passing through the M_P3_open phase (which drains the queue). + So there's a real possibility of the outbound msg_version getting dropped on the floor, or put in a queue but never delivered. */ - M_P3_process_theirs -> M_S4B - M_S3B -> M_P3_close [label="M_close(mood)"] - M_P3_close [shape="box" label="tx release()\n"] - M_P3_close -> M_P_close + M_P3_process_theirs2 -> M_S4B [color="orange"] - {rank=same; M_S4 M_P4_release M_S4B M_P4_process M_P4_send M_P4_queue} - M_S4 [label="4: released\nunwant nameplate\nwant mailbox\nopen\n"] + {rank=same; M_S4A M_P4_release M_S4B M_P4_process M_P4_send M_P4_queue} + M_S4A [label="S4A:\nmaybe released\nmaybe open\n"] - M_S4B [label="4: released\nunwant nameplate\nwant mailbox\nopen\n(bound)"] - M_S4 -> M_P4_release [label="M_connected()"] - /* it is currently an error to release a nameplate you aren't - currently claiming, so release() is not idempotent. #118 fixes that */ - M_P4_release [shape="box" label="tx open()\ntx add(queued)\ntx release()"] - /*M_S4B -> M_S4B [label="rx_claimed() *#118"]*/ - M_S4B -> M_P_close [label="M_close(mood)"] + M_S4B [label="S4B:\nmaybe released\nmaybe open\n(bound)" color="orange"] + M_S4A -> M_P4_release [label="M_connected()"] + M_P4_release [shape="box" label="tx open\ntx add(queued)\ntx release"] M_S4B -> M_P4_send [label="M_send(msg)"] M_P4_send [shape="box" label="queue\ntx add(msg)"] M_P4_send -> M_S4B - M_S4 -> M_P4_queue [label="M_send(msg)" style="dotted"] + M_S4A -> M_P4_queue [label="M_send(msg)" style="dotted"] M_P4_queue [shape="box" label="queue" style="dotted"] - M_P4_queue -> M_S4 [style="dotted"] + M_P4_queue -> M_S4A [style="dotted"] M_P4_release -> M_S4B - M_S4B -> M_S4 [label="M_lost()"] - /*M_S4B -> M_S2 [label="M_lost()"]*/ - M_S4B -> M_P4_process [label="rx_message()"] - M_P4_process [shape="box" label="process message"] + M_S4B -> M_S4A [label="M_lost()"] + M_S4B -> M_P4_process [label="rx message"] + M_P4_process [shape="octagon" label="process message"] M_P4_process -> M_S4B - M_S4 -> M_S5 [label="(none)" style="invis"] - M_S4B -> M_S5B [label="rx_released()"] + M_S4A -> M_S5A [label="(none)" style="invis"] + M_S4B -> M_S5B [label="rx released" color="orange" fontcolor="orange"] seed [label="from Seed?"] - M_S3 -> seed [style="invis"] - M_S4 -> seed [style="invis"] - seed -> M_S5 - {rank=same; seed M_S5 M_S5B M_P5_open M_P5_process} - M_S5 [label="5: released\nunwant nameplate\nwant mailbox\nopen\n"] - M_S5B [label="5: released\nunwant nameplate\nwant mailbox\nopen\n(bound)" color="green"] - M_S5 -> M_P5_open [label="M_connected()"] - M_P5_open [shape="box" label="tx open()\ntx add(queued)"] + M_S3A -> seed [style="invis"] + M_S4A -> seed [style="invis"] + seed -> M_S5A + {rank=same; seed M_S5A M_S5B M_P5_open M_process} + M_S5A [label="S5A:\nreleased\nmaybe open"] + M_S5B [label="S5B:\nreleased\nmaybe open\n(bound)" color="green"] + M_S5A -> M_P5_open [label="M_connected()"] + M_P5_open [shape="box" label="tx open\ntx add(queued)"] M_P5_open -> M_S5B - M_S5B -> M_S5 [label="M_lost()"] - M_S5B -> M_P5_process [label="rx_message()"] - M_P5_process [shape="box" label="process message"] - M_P5_process -> M_S5B - M_S5B -> M_P5_send [label="M_send(msg)"] - M_P5_send [shape="box" label="queue\ntx add(msg)"] - M_P5_send -> M_S5B - M_S5 -> M_P5_queue [label="M_send(msg)" style="dotted"] + M_S5B -> M_S5A [label="M_lost()"] + M_S5B -> M_process [label="rx message" color="green" fontcolor="green"] + M_process [shape="octagon" label="process message" color="green"] + M_process -> M_S5B [color="green"] + M_S5B -> M_P5_send [label="M_send(msg)" color="green" fontcolor="green"] + M_P5_send [shape="box" label="queue\ntx add(msg)" color="green"] + M_P5_send -> M_S5B [color="green"] + M_S5A -> M_P5_queue [label="M_send(msg)" style="dotted"] M_P5_queue [shape="box" label="queue" style="dotted"] - M_P5_queue -> M_S5 [style="dotted"] + M_P5_queue -> M_S5A [style="dotted"] + M_S5B -> M_CcB_P_close [label="M_close()" style="dashed" color="orange" fontcolor="orange"] + M_CcB_P_close [label="tx close" style="dashed" color="orange"] - M_S5 -> M_S6 [style="invis"] - M_S5B -> M_P_close [label="M_close(mood)"] - M_P_close [shape="box" label="tx close(mood)"] - M_P_close -> M_S6B - - {rank=same; M_S6 M_P6_close M_S6B} - M_S6 [label="6: closing\nunwant mailbox\nopen\n"] - M_S6B [label="6: closing\nunwant mailbox\nopen\n(bound)"] - M_S6 -> M_P6_close [label="M_connected()"] - M_P6_close [shape="box" label="tx close()"] - M_P6_close -> M_S6B - M_S6B -> M_S6 [label="M_lost()"] - M_S6B -> M_S6B [label="rx_released()"] - M_S6B -> M_S6B [label="M_close()"] - M_S6B -> M_S6B [label="M_send()"] - M_S6 -> M_S6 [label="M_send()"] - - M_S6 -> M_S7 [label="(none)" style="invis"] - {rank=same; M_other_closes M_P7_drop} - M_other_closes [label="all\nother\nunbound\nstates" style="dotted"] - M_other_closes -> M_P7_stop [label="M_close()" style="dotted"] - M_P7_stop [shape="box" label="C_stop()"] - M_P7_stop -> M_S7 - /*M_S1 -> M_other_closes [label="M_close()"] - M_S2 -> M_other_closes [label="M_close()"] - M_S3 -> M_other_closes [label="M_close()"] - M_S4 -> M_other_closes [label="M_close()"]*/ - M_S5 -> M_other_closes [style="dotted"] - /*M_S6 -> M_P7_stop [label="M_close()"]*/ - M_S6 -> M_other_closes [style="dotted"] - M_S1B -> M_P7_drop [label="M_close()"] - M_S2B -> M_P2_drop [label="M_close()"] - M_P2_drop [shape="box" label="tx release()"] - M_P2_drop -> M_P7_drop - M_S6B -> M_P7_drop [label="rx_closed()"] - M_P7_drop [shape="box" label="C_stop()"] - M_P7_drop -> M_S7B - - {rank=same; M_S7 M_S7B} - M_S7 [label="7: closed\n"] - M_S7B [label="7: closed\n(bound)"] - M_S7 -> M_S7B [style="invis"] - M_S7B -> M_S7 [label="M_lost()"] - M_S7B -> M_S7B [label="M_close()"] - M_S7B -> M_S7B [label="M_send()"] - - - M_process [shape="box" label="process"] + M_process [shape="octagon" label="process message"] M_process_me [shape="box" label="dequeue"] M_process -> M_process_me [label="side == me"] M_process_them [shape="box" label="" style="dotted"] diff --git a/docs/w3.dot b/docs/w3.dot new file mode 100644 index 0000000..8568c24 --- /dev/null +++ b/docs/w3.dot @@ -0,0 +1,76 @@ +digraph { + /* M_close pathways */ + + /* All dashed states are from the main Mailbox Machine diagram, and + all dashed lines indicate M_close() pathways in from those states. + Within this graph, all M_close() events leave the state unchanged. */ + + {rank=same; MC_SrA MC_SrB} + MC_SrA [label="SrA:\nwaiting for:\nrelease"] + MC_SrA -> MC_Pr [label="M_connected()"] + MC_Pr [shape="box" label="tx release" color="orange"] + MC_Pr -> MC_SrB [color="orange"] + MC_SrB [label="SrB:\nwaiting for:\nrelease" color="orange"] + MC_SrB -> MC_SrA [label="M_lost()"] + MC_SrB -> MC_P_stop [label="rx released" color="orange" fontcolor="orange"] + + /*{rank=same; MC_ScA MC_ScB}*/ + MC_ScA [label="ScA:\nwaiting for:\nclosed"] + MC_ScA -> MC_Pc [label="M_connected()"] + MC_Pc [shape="box" label="tx close" color="orange"] + MC_Pc -> MC_ScB [color="orange"] + MC_ScB [label="ScB:\nwaiting for:\nclosed" color="orange"] + MC_ScB -> MC_ScA [label="M_lost()"] + MC_ScB -> MC_P_stop [label="rx closed" color="orange" fontcolor="orange"] + + {rank=same; MC_SrcA MC_SrcB} + MC_SrcA [label="SrcA:\nwaiting for:\nrelease\nclose"] + MC_SrcA -> MC_Prc [label="M_connected()"] + MC_Prc [shape="box" label="tx release\ntx close" color="orange"] + MC_Prc -> MC_SrcB [color="orange"] + MC_SrcB [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] + MC_SrcB -> MC_SrcA [label="M_lost()"] + MC_SrcB -> MC_ScB [label="rx released" color="orange" fontcolor="orange"] + MC_SrcB -> MC_SrB [label="rx closed" color="orange" fontcolor="orange"] + + + MC_P_stop [shape="box" label="C_stop()"] + MC_P_stop -> MC_SsB + + MC_SsB -> MC_Ss [label="MC_stopped()"] + MC_SsB [label="SsB: closed\nstopping"] + + MC_Ss [label="Ss: closed" color="green"] + + + MC_S1A [label="S1A" style="dashed"] + MC_S1A -> MC_P_stop [style="dashed"] + MC_S1B [label="S1B" color="orange" style="dashed"] + MC_S1B -> MC_P_stop [style="dashed" color="orange"] + + {rank=same; MC_S2A MC_S2B MC_S2C} + MC_S2A [label="S2A" style="dashed"] + MC_S2A -> MC_P_stop [style="dashed"] + MC_S2C [label="S2C" style="dashed"] + MC_S2C -> MC_SrA [style="dashed"] + MC_S2B [label="S2B" color="orange" style="dashed"] + MC_S2B -> MC_SrB [color="orange" style="dashed"] + + {rank=same; MC_S3A MC_S4A MC_S3B MC_S4B} + MC_S3A [label="S3A" style="dashed"] + MC_S3B [label="S3B" color="orange" style="dashed"] + MC_S3A -> MC_SrcA [style="dashed"] + MC_S3B -> MC_Prc [color="orange" style="dashed"] + + MC_S4A [label="S4A" style="dashed"] + MC_S4B [label="S4B" color="orange" style="dashed"] + MC_S4A -> MC_SrcA [style="dashed"] + MC_S4B -> MC_Prc [color="orange" style="dashed"] + + {rank=same; MC_S5A MC_S5B} + MC_S5A [label="S5A" style="dashed"] + MC_S5B [label="S5B" color="green" style="dashed"] + MC_S5A -> MC_ScA [style="dashed"] + MC_S5B -> MC_Pc [color="green"] + +} From a9a0bc43c7475786dff6bf49055a93b741bfcfd0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Dec 2016 01:58:25 -0500 Subject: [PATCH 024/176] w4.dot: redraw Connection Machine to match --- docs/w.dot | 31 ++++++++------------------- docs/w3.dot | 4 +++- docs/w4.dot | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 docs/w4.dot diff --git a/docs/w.dot b/docs/w.dot index db5c49b..e62400e 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -197,26 +197,13 @@ digraph { P2_P_process_v2 [shape="box" label="process v2"] */ - /* ConnectionMachine */ - C_start [label="Connection\nMachine" style="dotted"] - C_start -> C_S_connecting [label="C_start()"] - C_S_connecting [label="connecting"] - C_S_connecting -> C_P_connected [label="onConnect"] - C_P_connected [shape="box" label="M_connected()"] - C_P_connected -> C_S_connected - C_S_connecting -> C_P_stop_connecting [label="C_stop()"] - C_P_stop_connecting [shape="box" label="cancel\nconnection\nattempt"] - C_P_stop_connecting -> C_S_stopped - C_S_connected [label="connected" color="green"] - C_S_connected -> C_P_lost [label="onClose"] - C_P_lost [shape="box" label="M_lost()\nstart timer"] - C_P_lost -> C_S_waiting - C_S_waiting [label="waiting"] - C_S_waiting -> C_S_connecting [label="expire"] - C_S_waiting -> C_S_stopped [label="C_stop()"] - C_S_connected -> C_S_stopping [label="C_stop()"] - C_S_stopping [label="stopping"] - C_S_stopping -> C_S_stopped [label="onClose"] - C_S_stopped [label="stopped"] - + WCM_S_known -> O_WM [style="invis"] + O_WM [label="Wormhole\nMachine" style="dotted"] + O_WM -> O_MM [style="dotted"] + O_WM -> O_MCM [style="dotted"] + O_MM -> O_MCM [style="dotted"] + O_MM [label="Mailbox\nMachine" style="dotted"] + O_MCM [label="Mailbox\nClose\nMachine" style="dotted"] + O_MM -> O_CM [style="dotted"] + O_CM [label="Connection\nMachine" style="dotted"] } diff --git a/docs/w3.dot b/docs/w3.dot index 8568c24..89a8bdf 100644 --- a/docs/w3.dot +++ b/docs/w3.dot @@ -1,5 +1,7 @@ digraph { /* M_close pathways */ + MC_title [label="Mailbox\nClose\nMachine" style="dotted"] + MC_title -> MC_S2B [style="invis"] /* All dashed states are from the main Mailbox Machine diagram, and all dashed lines indicate M_close() pathways in from those states. @@ -56,7 +58,7 @@ digraph { MC_S2B [label="S2B" color="orange" style="dashed"] MC_S2B -> MC_SrB [color="orange" style="dashed"] - {rank=same; MC_S3A MC_S4A MC_S3B MC_S4B} + {rank=same; MC_title MC_S3A MC_S4A MC_S3B MC_S4B} MC_S3A [label="S3A" style="dashed"] MC_S3B [label="S3B" color="orange" style="dashed"] MC_S3A -> MC_SrcA [style="dashed"] diff --git a/docs/w4.dot b/docs/w4.dot new file mode 100644 index 0000000..7b4e75d --- /dev/null +++ b/docs/w4.dot @@ -0,0 +1,61 @@ +digraph { + + + /* ConnectionMachine */ + C_start [label="Connection\nMachine" style="dotted"] + C_start -> C_Pc1 [label="CM_start()" color="orange" fontcolor="orange"] + C_Pc1 [shape="box" label="ep.connect()" color="orange"] + C_Pc1 -> C_Sc1 [color="orange"] + C_Sc1 [label="connecting\n(1st time)" color="orange"] + C_Sc1 -> C_S_negotiating [label="d.callback" color="orange" fontcolor="orange"] + C_Sc1 -> C_P_failed [label="d.errback" color="red"] + C_Sc1 -> C_P_failed [label="p.onClose" color="red"] + C_Sc1 -> C_P_cancel [label="C_stop()"] + C_P_cancel [shape="box" label="d.cancel()"] + C_P_cancel -> C_S_cancelling + C_S_cancelling [label="cancelling"] + C_S_cancelling -> C_P_stopped [label="d.errback"] + + C_S_negotiating [label="negotiating" color="orange"] + C_S_negotiating -> C_P_failed [label="p.onClose"] + C_S_negotiating -> C_P_connected [label="p.onOpen" color="orange" fontcolor="orange"] + C_S_negotiating -> C_P_drop2 [label="C_stop()"] + C_P_drop2 [shape="box" label="p.dropConnection()"] + C_P_drop2 -> C_S_disconnecting + C_P_connected [shape="box" label="tx bind\nM_connected()" color="orange"] + C_P_connected -> C_S_open [color="orange"] + + C_S_open [label="open" color="green"] + C_S_open -> C_P_lost [label="p.onClose" color="blue" fontcolor="blue"] + C_S_open -> C_P_drop [label="C_stop()" color="orange" fontcolor="orange"] + C_P_drop [shape="box" label="p.dropConnection()\nM_lost()" color="orange"] + C_P_drop -> C_S_disconnecting [color="orange"] + C_S_disconnecting [label="disconnecting" color="orange"] + C_S_disconnecting -> C_P_stopped [label="p.onClose" color="orange" fontcolor="orange"] + + C_P_lost [shape="box" label="M_lost()" color="blue"] + C_P_lost -> C_P_wait [color="blue"] + C_P_wait [shape="box" label="start timer" color="blue"] + C_P_wait -> C_S_waiting [color="blue"] + C_S_waiting [label="waiting" color="blue"] + C_S_waiting -> C_Pc2 [label="expire" color="blue" fontcolor="blue"] + C_S_waiting -> C_P_stop_timer [label="C_stop()"] + C_P_stop_timer [shape="box" label="timer.cancel()"] + C_P_stop_timer -> C_P_stopped + C_Pc2 [shape="box" label="ep.connect()" color="blue"] + C_Pc2 -> C_Sc2 [color="blue"] + C_Sc2 [label="connecting" color="blue"] + C_Sc2 -> C_P_reset [label="d.callback" color="blue" fontcolor="blue"] + C_P_reset [shape="box" label="reset\ntimer" color="blue"] + C_P_reset -> C_S_negotiating [color="blue"] + C_Sc2 -> C_P_wait [label="d.errback"] + C_Sc2 -> C_P_cancel [label="C_stop()"] + + C_P_stopped [shape="box" label="MC_stopped()" color="orange"] + C_P_stopped -> C_S_stopped [color="orange"] + C_S_stopped [label="stopped" color="orange"] + + C_P_failed [shape="box" label="notify_fail" color="red"] + C_P_failed -> C_S_failed + C_S_failed [label="failed" color="red"] +} From 0b05c9ca5a765b0eb94a2b3f7f520f2d55e50ec3 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Dec 2016 02:53:16 -0500 Subject: [PATCH 025/176] new experimental state-machine language I think I want to express actions as transient states. _c2.py matches w4.dot --- src/wormhole/_c2.py | 103 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/wormhole/_c2.py diff --git a/src/wormhole/_c2.py b/src/wormhole/_c2.py new file mode 100644 index 0000000..a07cb86 --- /dev/null +++ b/src/wormhole/_c2.py @@ -0,0 +1,103 @@ + +class ConnectionMachine: + def __init__(self): + self._f = f = WSFactory(self._ws_url) + f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + f.connection_machine = self # calls onOpen and onClose + p = urlparse(self._ws_url) + self._ep = self._make_endpoint(p.hostname, p.port or 80) + self._connector = None + self._done_d = defer.Deferred() + + # "@action" marks a method as doing something, then moving to a state or + # another action. "=State()" marks a state, where we want for an event. + # "=Event()" marks an event, which causes us to move out of a state, + # through zero or more actions, and eventually landing in some other + # state. + + starting = State(initial=True) + connecting = State() + negotiating = State() + open = State() + waiting = State() + reconnecting = State() + disconnecting = State() + cancelling = State() + stopped = State() + + CM_start = Event() + d_callback = Event() + d_errback = Event() + onOpen = Event() + onClose = Event() + stop = Event() + expire = Event() + + @action(goto=connecting) + def connect1(self): + d = self._ep.connect() + d.addCallbacks(self.c1_d_callback, self.c1_d_errback) + @action(goto=failed) + def notify_fail(self, ARGS?): + stuff() + @action(goto=open) + def opened(self): + tx_bind() + M_connected() + @action(goto=disconnecting) + def dropConnectionWhileNegotiating(self): + p.dropConnection() + @action(goto=disconnecting) + def dropOpenConnection(self): + p.dropOpenConnection() + M_lost() + @action(goto=start_timer) + def lostConnection(self): + M_lost() + @action(goto=waiting): + def start_timer(self): + self._timer = reactor.callLater(self._timeout, self.expire) + @action(goto=reconnecting) + def reconnect(self): + d = self._ep.connect() + d.addCallbacks(self.c1_d_callback, self.c1_d_errback) + @action(goto=negotiating) + def reset_timer(self): + self._timeout = self.INITIAL_TIMEOUT + @action(goto=MC_stopped) + def cancel_timer(self): + self._timer.cancel() + @action(goto=cancelling) + def d_cancel(self): + self._d.cancel() + @action(goto=stopped) + def MC_stopped(self): + self.MC.stopped() + + def c1_d_callback(self, p): + self.d_callback() + def c1_d_errback(self, f): + self.d_errback() + def p_onClose(self, why): + self.onClose() + def p_onOpen(self): + self.onOpen() + + starting.upon(CM_start, goto=connect1) + connecting.upon(d_callback, goto=negotiating) + connecting.upon(d_errback, goto=notify_fail) + connecting.upon(onClose, goto=notify_fail) + negotiating.upon(onOpen, goto=opened) + negotiating.upon(onClose, goto=notify_fail) + negotiating.upon(stop, goto=dropConnectionWhileNegotiating) + open.upon(onClose, goto=lostConnection) + open.upon(stop, goto=dropOpenConnection) + waiting.upon(expire, goto=reconnect) + waiting.upon(stop, goto=cancel_timer) + reconnecting.upon(d_callback, goto=reset_timer) + reconnecting.upon(stop, goto=d_cancel) + disconnecting.upon(onClose, goto=MC_stopped) + cancelling.upon(d_errback, goto=MC_stopped) + +CM = ConnectionMachine() +CM.CM_start() From 0fe6cfd99498ef2881ec4592c0e5ba84f2e13ed9 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Dec 2016 02:54:28 -0500 Subject: [PATCH 026/176] tweaks --- src/wormhole/_connection.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 4bb60b2..a58238d 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -88,7 +88,7 @@ class WSRelayClient(object): @m.state() def disconnecting(self): pass @m.state() - def disconnecting2(self): pass + def cancelling(self): pass @m.state(terminal=True) def closed(self): pass @@ -100,16 +100,17 @@ class WSRelayClient(object): @m.input() def d_errback(self, f): pass ; print("in d_errback", f) @m.input() + def d_cancel(self, f): pass # XXX remove f @m.input() def onOpen(self, ws): pass ; print("in onOpen") @m.input() - def onClose(self, f): pass # XXX maybe remove f + def onClose(self, f): pass # XXX p.onClose does cm.onClose("made up failure") @m.input() def expire(self): pass if ALLOW_CLOSE: @m.input() - def close(self, f): pass + def stop(self, f): pass @m.output() def ep_connect(self): @@ -124,8 +125,8 @@ class WSRelayClient(object): self._wormhole.side, self) self._wormhole.add_connection(self._connection) @m.output() - def remove_connection(self, f): # XXX remove f - self._wormhole.remove_connection(self._connection) + def M_lost(self, f): # XXX remove f + self._wormhole.M_lost(self._connection) self._connection = None @m.output() def start_timer(self, f): # XXX remove f @@ -144,35 +145,38 @@ class WSRelayClient(object): def notify_fail(self, f): print("notify_fail", f.value) self._done_d.errback(f) + @m.output() + def MC_stopped(self): + pass initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) first_time_connecting.upon(onClose, enter=failed, outputs=[notify_fail]) if ALLOW_CLOSE: - first_time_connecting.upon(close, enter=disconnecting2, + first_time_connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) - disconnecting2.upon(d_errback, enter=closed, outputs=[]) + cancelling.upon(d_errback, enter=closed, outputs=[]) negotiating.upon(onOpen, enter=open, outputs=[add_connection]) if ALLOW_CLOSE: - negotiating.upon(close, enter=disconnecting, outputs=[dropConnection]) + negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) - open.upon(onClose, enter=waiting, outputs=[remove_connection, start_timer]) + open.upon(onClose, enter=waiting, outputs=[M_lost, start_timer]) if ALLOW_CLOSE: - open.upon(close, enter=disconnecting, - outputs=[dropConnection, remove_connection]) + open.upon(stop, enter=disconnecting, + outputs=[dropConnection, M_lost]) connecting.upon(d_callback, enter=negotiating, outputs=[]) connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) connecting.upon(onClose, enter=waiting, outputs=[start_timer]) if ALLOW_CLOSE: - connecting.upon(close, enter=disconnecting2, outputs=[d_cancel]) + connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) waiting.upon(expire, enter=connecting, outputs=[ep_connect]) if ALLOW_CLOSE: - waiting.upon(close, enter=closed, outputs=[cancel_timer]) - disconnecting.upon(onClose, enter=closed, outputs=[]) + waiting.upon(stop, enter=closed, outputs=[cancel_timer]) + disconnecting.upon(onClose, enter=closed, outputs=[]) #MC_stopped def tryit(reactor): cm = WSRelayClient(None, "ws://127.0.0.1:4000/v1", reactor) From 35324a791105eb5445b49549d148d90511301d63 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Dec 2016 16:04:20 -0500 Subject: [PATCH 027/176] my StateMachine can now render .dot --- src/wormhole/_c2.py | 289 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 236 insertions(+), 53 deletions(-) diff --git a/src/wormhole/_c2.py b/src/wormhole/_c2.py index a07cb86..5fd2519 100644 --- a/src/wormhole/_c2.py +++ b/src/wormhole/_c2.py @@ -1,76 +1,240 @@ +class StateMachineError(Exception): + pass + +class _Transition: + def __init__(self, goto, color=None): + self._goto = goto + self._extra_dot_attrs = {} + if color: + self._extra_dot_attrs["color"] = color + self._extra_dot_attrs["fontcolor"] = color + def _dot_attrs(self): + return self._extra_dot_attrs + +class _State: + def __init__(self, m, name, extra_dot_attrs): + assert isinstance(m, Machine) + self.m = m + self._name = name + self._extra_dot_attrs = extra_dot_attrs + self.eventmap = {} + def upon(self, event, goto, color=None): + if event in self.eventmap: + raise StateMachineError("event already registered") + t = _Transition(goto, color=color) + self.eventmap[event] = t + def _dot_name(self): + return "S_"+self._name + def _dot_attrs(self): + attrs = {"label": self._name} + attrs.update(self._extra_dot_attrs) + return attrs + +class _Event: + def __init__(self, m, name): + assert isinstance(m, Machine) + self.m = m + self._name = name + def __call__(self): # *args, **kwargs + self.m._handle_event(self) + # return value? + def _dot_name(self): + return "E_"+self._name + def _dot_attrs(self): + return {"label": self._name} + +class _Action: + def __init__(self, m, f, extra_dot_attrs): + self.m = m + self.f = f + self._extra_dot_attrs = extra_dot_attrs + self.next_goto = None + self._name = f.__name__ + def goto(self, next_goto, color=None): + if self.next_goto: + raise StateMachineError("Action.goto() called twice") + self.next_goto = _Transition(next_goto, color=color) + def __call__(self): # *args, **kwargs ? + raise StateMachineError("don't call Actions directly") + def _dot_name(self): + return "A_"+self._name + def _dot_attrs(self): + attrs = {"shape": "box", "label": self._name} + attrs.update(self._extra_dot_attrs) + return attrs + +def format_attrs(**kwargs): + # return "", or "[attr=value attr=value]" + if not kwargs or all([not(v) for v in kwargs.values()]): + return "" + def escape(s): + return s.replace('\n', r'\n').replace('"', r'\"') + pieces = ['%s="%s"' % (k, escape(kwargs[k])) + for k in sorted(kwargs) + if kwargs[k]] + body = " ".join(pieces) + return "[%s]" % body + +class Machine: + def __init__(self): + self._initial_state = None + self._states = set() + self._events = set() + self._actions = set() + self._current_state = None + + def _maybe_start(self): + if self._current_state: + return + if not self._initial_state: + raise StateMachineError("no initial state") + self._current_state = self._initial_state + + def _handle_event(self, event): # other args? + self._maybe_start() + assert event in self._events + goto = self._current_state.eventmap.get(event) + if not goto: + raise StateMachineError("no transition for event %s from state %s" + % (event, self._current_state)) + # execute: ordering concerns here + while not isinstance(goto, _State): + assert isinstance(goto, _Action) + next_goto = goto.next_goto + goto.f() # args? + goto = next_goto + assert isinstance(goto, _State) + self._current_state = goto + + def _describe(self): + print "current state:", self._current_state + + def _dump_dot(self, f): + f.write("digraph {\n") + for s in sorted(self._states): + f.write(" %s %s\n" % (s._dot_name(), format_attrs(**s._dot_attrs()))) + f.write("\n") + for a in sorted(self._actions): + f.write(" %s %s\n" % (a._dot_name(), format_attrs(**a._dot_attrs()))) + f.write("\n") + for s in sorted(self._states): + for e in sorted(s.eventmap): + t = s.eventmap[e] + goto = t._goto + attrs = {"label": e._name} + attrs.update(t._dot_attrs()) + f.write(" %s -> %s %s\n" % (s._dot_name(), goto._dot_name(), + format_attrs(**attrs))) + f.write("\n") + for a in sorted(self._actions): + t = a.next_goto + f.write(" %s -> %s %s\n" % (a._dot_name(), t._goto._dot_name(), + format_attrs(**t._dot_attrs()))) + f.write("}\n") + + + def State(self, name, initial=False, **dot_attrs): + s = _State(self, name, dot_attrs) + if initial: + if self._initial_state: + raise StateMachineError("duplicate initial state") + self._initial_state = s + self._states.add(s) + return s + + def Event(self, name): + e = _Event(self, name) + self._events.add(e) + return e + + def action(self, **dotattrs): + def wrap(f): + a = _Action(self, f, dotattrs) + self._actions.add(a) + return a + return wrap + +from six.moves.urllib_parse import urlparse +from twisted.internet import defer, reactor + class ConnectionMachine: - def __init__(self): - self._f = f = WSFactory(self._ws_url) - f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - f.connection_machine = self # calls onOpen and onClose + def __init__(self, ws_url): + self._ws_url = ws_url + #self._f = f = WSFactory(self._ws_url) + #f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + #f.connection_machine = self # calls onOpen and onClose p = urlparse(self._ws_url) self._ep = self._make_endpoint(p.hostname, p.port or 80) self._connector = None self._done_d = defer.Deferred() + def _make_endpoint(self, hostname, port): + return None + # "@action" marks a method as doing something, then moving to a state or # another action. "=State()" marks a state, where we want for an event. # "=Event()" marks an event, which causes us to move out of a state, # through zero or more actions, and eventually landing in some other # state. - starting = State(initial=True) - connecting = State() - negotiating = State() - open = State() - waiting = State() - reconnecting = State() - disconnecting = State() - cancelling = State() - stopped = State() + m = Machine() + starting = m.State("starting", initial=True, color="orange") + connecting = m.State("connecting", color="orange") + negotiating = m.State("negotiating", color="orange") + open = m.State("open", color="green") + waiting = m.State("waiting", color="blue") + reconnecting = m.State("reconnecting", color="blue") + disconnecting = m.State("disconnecting", color="orange") + cancelling = m.State("cancelling") + stopped = m.State("stopped", color="orange") - CM_start = Event() - d_callback = Event() - d_errback = Event() - onOpen = Event() - onClose = Event() - stop = Event() - expire = Event() + CM_start = m.Event("CM_start") + d_callback = m.Event("d_callback") + d_errback = m.Event("d_errback") + onOpen = m.Event("onOpen") + onClose = m.Event("onClose") + stop = m.Event("stop") + expire = m.Event("expire") - @action(goto=connecting) + @m.action(color="orange") def connect1(self): d = self._ep.connect() d.addCallbacks(self.c1_d_callback, self.c1_d_errback) - @action(goto=failed) - def notify_fail(self, ARGS?): - stuff() - @action(goto=open) + @m.action(color="red") + def notify_fail(self, ARGS): + self._done_d.errback("ERR") + @m.action(color="orange") def opened(self): - tx_bind() - M_connected() - @action(goto=disconnecting) + self._p.send("bind") + self._M.connected() + @m.action() def dropConnectionWhileNegotiating(self): - p.dropConnection() - @action(goto=disconnecting) + self._p.dropConnection() + @m.action(color="orange") def dropOpenConnection(self): - p.dropOpenConnection() - M_lost() - @action(goto=start_timer) + self._p.dropOpenConnection() + self._M.lost() + @m.action(color="blue") def lostConnection(self): - M_lost() - @action(goto=waiting): + self._M.lost() + @m.action(color="blue") def start_timer(self): self._timer = reactor.callLater(self._timeout, self.expire) - @action(goto=reconnecting) + @m.action(color="blue") def reconnect(self): d = self._ep.connect() d.addCallbacks(self.c1_d_callback, self.c1_d_errback) - @action(goto=negotiating) + @m.action(color="blue") def reset_timer(self): self._timeout = self.INITIAL_TIMEOUT - @action(goto=MC_stopped) + @m.action() def cancel_timer(self): self._timer.cancel() - @action(goto=cancelling) + @m.action() def d_cancel(self): self._d.cancel() - @action(goto=stopped) + @m.action(color="orange") def MC_stopped(self): self.MC.stopped() @@ -83,21 +247,40 @@ class ConnectionMachine: def p_onOpen(self): self.onOpen() - starting.upon(CM_start, goto=connect1) - connecting.upon(d_callback, goto=negotiating) - connecting.upon(d_errback, goto=notify_fail) - connecting.upon(onClose, goto=notify_fail) - negotiating.upon(onOpen, goto=opened) - negotiating.upon(onClose, goto=notify_fail) + starting.upon(CM_start, goto=connect1, color="orange") + connecting.upon(d_callback, goto=negotiating, color="orange") + connecting.upon(d_errback, goto=notify_fail, color="red") + connecting.upon(onClose, goto=notify_fail, color="red") + connecting.upon(stop, goto=d_cancel) + negotiating.upon(onOpen, goto=opened, color="orange") + negotiating.upon(onClose, goto=notify_fail, color="red") negotiating.upon(stop, goto=dropConnectionWhileNegotiating) - open.upon(onClose, goto=lostConnection) - open.upon(stop, goto=dropOpenConnection) - waiting.upon(expire, goto=reconnect) + open.upon(onClose, goto=lostConnection, color="blue") + open.upon(stop, goto=dropOpenConnection, color="orange") + waiting.upon(expire, goto=reconnect, color="blue") waiting.upon(stop, goto=cancel_timer) - reconnecting.upon(d_callback, goto=reset_timer) + reconnecting.upon(d_callback, goto=reset_timer, color="blue") reconnecting.upon(stop, goto=d_cancel) - disconnecting.upon(onClose, goto=MC_stopped) + disconnecting.upon(onClose, goto=MC_stopped, color="orange") cancelling.upon(d_errback, goto=MC_stopped) -CM = ConnectionMachine() -CM.CM_start() + connect1.goto(connecting, color="orange") + notify_fail.goto(MC_stopped, color="red") + opened.goto(open, color="orange") + dropConnectionWhileNegotiating.goto(disconnecting) + dropOpenConnection.goto(disconnecting, color="orange") + lostConnection.goto(start_timer, color="blue") + start_timer.goto(waiting, color="blue") + reconnect.goto(reconnecting, color="blue") + reset_timer.goto(negotiating, color="blue") + cancel_timer.goto(MC_stopped) + d_cancel.goto(cancelling) + MC_stopped.goto(stopped, color="orange") + + +CM = ConnectionMachine("ws://host") +#CM.CM_start() + +if __name__ == "__main__": + import sys + CM.m._dump_dot(sys.stdout) From 17a90d87acb26c5f36729d8d24ef31a724116ab5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Dec 2016 16:44:48 -0500 Subject: [PATCH 028/176] tweaks --- src/wormhole/_c2.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/wormhole/_c2.py b/src/wormhole/_c2.py index 5fd2519..b1eb720 100644 --- a/src/wormhole/_c2.py +++ b/src/wormhole/_c2.py @@ -133,6 +133,11 @@ class Machine: format_attrs(**t._dot_attrs()))) f.write("}\n") + # all descriptions are from the state machine's point of view + # States are gerunds: Foo-ing + # Events are past-tense verbs: Foo-ed, as in "I have been Foo-ed" + # * machine.do(event) ? vs machine.fooed() + # Actions are immediate-tense verbs: foo, connect def State(self, name, initial=False, **dot_attrs): s = _State(self, name, dot_attrs) @@ -260,6 +265,7 @@ class ConnectionMachine: waiting.upon(expire, goto=reconnect, color="blue") waiting.upon(stop, goto=cancel_timer) reconnecting.upon(d_callback, goto=reset_timer, color="blue") + reconnecting.upon(d_errback, goto=start_timer) reconnecting.upon(stop, goto=d_cancel) disconnecting.upon(onClose, goto=MC_stopped, color="orange") cancelling.upon(d_errback, goto=MC_stopped) From faab1e87d00f0af5b976a837cf1c9ed228befe18 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 28 Dec 2016 16:46:06 -0500 Subject: [PATCH 029/176] split _machine.py out --- src/wormhole/_c2.py | 163 +------------------------------------ src/wormhole/_machine.py | 169 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 162 deletions(-) create mode 100644 src/wormhole/_machine.py diff --git a/src/wormhole/_c2.py b/src/wormhole/_c2.py index b1eb720..d4c6708 100644 --- a/src/wormhole/_c2.py +++ b/src/wormhole/_c2.py @@ -1,167 +1,6 @@ - -class StateMachineError(Exception): - pass - -class _Transition: - def __init__(self, goto, color=None): - self._goto = goto - self._extra_dot_attrs = {} - if color: - self._extra_dot_attrs["color"] = color - self._extra_dot_attrs["fontcolor"] = color - def _dot_attrs(self): - return self._extra_dot_attrs - -class _State: - def __init__(self, m, name, extra_dot_attrs): - assert isinstance(m, Machine) - self.m = m - self._name = name - self._extra_dot_attrs = extra_dot_attrs - self.eventmap = {} - def upon(self, event, goto, color=None): - if event in self.eventmap: - raise StateMachineError("event already registered") - t = _Transition(goto, color=color) - self.eventmap[event] = t - def _dot_name(self): - return "S_"+self._name - def _dot_attrs(self): - attrs = {"label": self._name} - attrs.update(self._extra_dot_attrs) - return attrs - -class _Event: - def __init__(self, m, name): - assert isinstance(m, Machine) - self.m = m - self._name = name - def __call__(self): # *args, **kwargs - self.m._handle_event(self) - # return value? - def _dot_name(self): - return "E_"+self._name - def _dot_attrs(self): - return {"label": self._name} - -class _Action: - def __init__(self, m, f, extra_dot_attrs): - self.m = m - self.f = f - self._extra_dot_attrs = extra_dot_attrs - self.next_goto = None - self._name = f.__name__ - def goto(self, next_goto, color=None): - if self.next_goto: - raise StateMachineError("Action.goto() called twice") - self.next_goto = _Transition(next_goto, color=color) - def __call__(self): # *args, **kwargs ? - raise StateMachineError("don't call Actions directly") - def _dot_name(self): - return "A_"+self._name - def _dot_attrs(self): - attrs = {"shape": "box", "label": self._name} - attrs.update(self._extra_dot_attrs) - return attrs - -def format_attrs(**kwargs): - # return "", or "[attr=value attr=value]" - if not kwargs or all([not(v) for v in kwargs.values()]): - return "" - def escape(s): - return s.replace('\n', r'\n').replace('"', r'\"') - pieces = ['%s="%s"' % (k, escape(kwargs[k])) - for k in sorted(kwargs) - if kwargs[k]] - body = " ".join(pieces) - return "[%s]" % body - -class Machine: - def __init__(self): - self._initial_state = None - self._states = set() - self._events = set() - self._actions = set() - self._current_state = None - - def _maybe_start(self): - if self._current_state: - return - if not self._initial_state: - raise StateMachineError("no initial state") - self._current_state = self._initial_state - - def _handle_event(self, event): # other args? - self._maybe_start() - assert event in self._events - goto = self._current_state.eventmap.get(event) - if not goto: - raise StateMachineError("no transition for event %s from state %s" - % (event, self._current_state)) - # execute: ordering concerns here - while not isinstance(goto, _State): - assert isinstance(goto, _Action) - next_goto = goto.next_goto - goto.f() # args? - goto = next_goto - assert isinstance(goto, _State) - self._current_state = goto - - def _describe(self): - print "current state:", self._current_state - - def _dump_dot(self, f): - f.write("digraph {\n") - for s in sorted(self._states): - f.write(" %s %s\n" % (s._dot_name(), format_attrs(**s._dot_attrs()))) - f.write("\n") - for a in sorted(self._actions): - f.write(" %s %s\n" % (a._dot_name(), format_attrs(**a._dot_attrs()))) - f.write("\n") - for s in sorted(self._states): - for e in sorted(s.eventmap): - t = s.eventmap[e] - goto = t._goto - attrs = {"label": e._name} - attrs.update(t._dot_attrs()) - f.write(" %s -> %s %s\n" % (s._dot_name(), goto._dot_name(), - format_attrs(**attrs))) - f.write("\n") - for a in sorted(self._actions): - t = a.next_goto - f.write(" %s -> %s %s\n" % (a._dot_name(), t._goto._dot_name(), - format_attrs(**t._dot_attrs()))) - f.write("}\n") - - # all descriptions are from the state machine's point of view - # States are gerunds: Foo-ing - # Events are past-tense verbs: Foo-ed, as in "I have been Foo-ed" - # * machine.do(event) ? vs machine.fooed() - # Actions are immediate-tense verbs: foo, connect - - def State(self, name, initial=False, **dot_attrs): - s = _State(self, name, dot_attrs) - if initial: - if self._initial_state: - raise StateMachineError("duplicate initial state") - self._initial_state = s - self._states.add(s) - return s - - def Event(self, name): - e = _Event(self, name) - self._events.add(e) - return e - - def action(self, **dotattrs): - def wrap(f): - a = _Action(self, f, dotattrs) - self._actions.add(a) - return a - return wrap - from six.moves.urllib_parse import urlparse from twisted.internet import defer, reactor +from ._machine import Machine class ConnectionMachine: def __init__(self, ws_url): diff --git a/src/wormhole/_machine.py b/src/wormhole/_machine.py new file mode 100644 index 0000000..79ced56 --- /dev/null +++ b/src/wormhole/_machine.py @@ -0,0 +1,169 @@ + +class StateMachineError(Exception): + pass + +class _Transition: + def __init__(self, goto, color=None): + self._goto = goto + self._extra_dot_attrs = {} + if color: + self._extra_dot_attrs["color"] = color + self._extra_dot_attrs["fontcolor"] = color + def _dot_attrs(self): + return self._extra_dot_attrs + +class _State: + def __init__(self, m, name, extra_dot_attrs): + assert isinstance(m, Machine) + self.m = m + self._name = name + self._extra_dot_attrs = extra_dot_attrs + self.eventmap = {} + def upon(self, event, goto, color=None): + if event in self.eventmap: + raise StateMachineError("event already registered") + t = _Transition(goto, color=color) + self.eventmap[event] = t + def _dot_name(self): + return "S_"+self._name.replace(" ", "_") + def _dot_attrs(self): + attrs = {"label": self._name} + attrs.update(self._extra_dot_attrs) + return attrs + +class _Event: + def __init__(self, m, name): + assert isinstance(m, Machine) + self.m = m + self._name = name + def __call__(self): # *args, **kwargs + self.m._handle_event(self) + # return value? + def _dot_name(self): + return "E_"+self._name.replace(" ", "_") + def _dot_attrs(self): + return {"label": self._name} + +class _Action: + def __init__(self, m, f, extra_dot_attrs): + self.m = m + self.f = f + self._extra_dot_attrs = extra_dot_attrs + self.next_goto = None + self._name = f.__name__ + def goto(self, next_goto, color=None): + if self.next_goto: + raise StateMachineError("Action.goto() called twice") + self.next_goto = _Transition(next_goto, color=color) + def __call__(self): # *args, **kwargs ? + raise StateMachineError("don't call Actions directly") + def _dot_name(self): + return "A_"+self._name + def _dot_attrs(self): + attrs = {"shape": "box", "label": self._name} + attrs.update(self._extra_dot_attrs) + return attrs + +def format_attrs(**kwargs): + # return "", or "[attr=value attr=value]" + if not kwargs or all([not(v) for v in kwargs.values()]): + return "" + def escape(s): + return s.replace('\n', r'\n').replace('"', r'\"') + pieces = ['%s="%s"' % (k, escape(kwargs[k])) + for k in sorted(kwargs) + if kwargs[k]] + body = " ".join(pieces) + return "[%s]" % body + +class Machine: + def __init__(self): + self._initial_state = None + self._states = set() + self._events = set() + self._actions = set() + self._current_state = None + self._finalized = False + + def _maybe_finalize(self): + if self._finalized: + return + # do final consistency checks: are all events handled? + + def _maybe_start(self): + self._maybe_finalize() + if self._current_state: + return + if not self._initial_state: + raise StateMachineError("no initial state") + self._current_state = self._initial_state + + def _handle_event(self, event): # other args? + self._maybe_start() + assert event in self._events + goto = self._current_state.eventmap.get(event) + if not goto: + raise StateMachineError("no transition for event %s from state %s" + % (event, self._current_state)) + # execute: ordering concerns here + while not isinstance(goto, _State): + assert isinstance(goto, _Action) + next_goto = goto.next_goto + goto.f() # args? + goto = next_goto + assert isinstance(goto, _State) + self._current_state = goto + + def _describe(self): + print "current state:", self._current_state + + def _dump_dot(self, f): + self._maybe_finalize() + f.write("digraph {\n") + for s in sorted(self._states): + f.write(" %s %s\n" % (s._dot_name(), format_attrs(**s._dot_attrs()))) + f.write("\n") + for a in sorted(self._actions): + f.write(" %s %s\n" % (a._dot_name(), format_attrs(**a._dot_attrs()))) + f.write("\n") + for s in sorted(self._states): + for e in sorted(s.eventmap): + t = s.eventmap[e] + goto = t._goto + attrs = {"label": e._name} + attrs.update(t._dot_attrs()) + f.write(" %s -> %s %s\n" % (s._dot_name(), goto._dot_name(), + format_attrs(**attrs))) + f.write("\n") + for a in sorted(self._actions): + t = a.next_goto + f.write(" %s -> %s %s\n" % (a._dot_name(), t._goto._dot_name(), + format_attrs(**t._dot_attrs()))) + f.write("}\n") + + # all descriptions are from the state machine's point of view + # States are gerunds: Foo-ing + # Events are past-tense verbs: Foo-ed, as in "I have been Foo-ed" + # * machine.do(event) ? vs machine.fooed() + # Actions are immediate-tense verbs: foo, connect + + def State(self, name, initial=False, **dot_attrs): + s = _State(self, name, dot_attrs) + if initial: + if self._initial_state: + raise StateMachineError("duplicate initial state") + self._initial_state = s + self._states.add(s) + return s + + def Event(self, name): + e = _Event(self, name) + self._events.add(e) + return e + + def action(self, **dotattrs): + def wrap(f): + a = _Action(self, f, dotattrs) + self._actions.add(a) + return a + return wrap From 3bf762b4f76bbed605a578b070efd484dd3077cb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 29 Dec 2016 13:40:18 -0500 Subject: [PATCH 030/176] try coding top-level WormholeMachine --- src/wormhole/_c3.py | 76 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/wormhole/_c3.py diff --git a/src/wormhole/_c3.py b/src/wormhole/_c3.py new file mode 100644 index 0000000..98d64e2 --- /dev/null +++ b/src/wormhole/_c3.py @@ -0,0 +1,76 @@ +from ._machine import Machine + +class WormholeMachine: + m = Machine() + + know_nothing = m.State("know_nothing", initial=True) + know_code = m.State("know_code") + know_key = m.State("know_key", color="orange") + #verified_key = m.State("verified_key", color="green") + closed = m.State("closed") + + API_send = m.Event("API_send") + WM_set_code = m.Event("WM_set_code") + WM_rx_pake = m.Event("WM_rx_pake") + #WM_rx_msg = m.Event("WM_rx_msg") + close = m.Event("close") + + @m.action() + def set_code(self): + self._MM.set_nameplate() + self._build_pake() + self._MM.send(self._pake) + @m.action() + @m.outcome("pake ok") + @m.outcome("pake bad") + def compute_key(self): + self._key = self._computer_stuff() + if 1: + return "pake ok" + else: + return "pake bad" + @m.action() + def send_version(self): + self._MM.send(self._version) + @m.action() + @m.outcome("verify ok") + @m.outcome("verify bad") + def verify(self, msg, verify_ok, verify_bad): + try: + decrypted = decrypt(self._key, msg) + return verify_ok(decrypted) + except CryptoError: + return verify_bad() + @m.action() + def queue1(self, msg): + self._queue.append(msg) + @m.action() + def queue2(self, msg): + self._queue.append(msg) + @m.action() + def close_lonely(self): + self._MM.close("lonely") + @m.action() + def close_scary(self): + self._MM.close("scary") + + compute_key.upon("pake ok", goto=send_version) + compute_key.upon("pake bad", goto=close_scary) + know_nothing.upon(API_send, goto=queue1) + queue1.goto(know_nothing) + know_nothing.upon(WM_set_code, goto=set_code) + set_code.goto(know_code) + know_code.upon(API_send, goto=queue2) + queue2.goto(know_code) + know_code.upon(WM_rx_pake, goto=compute_key) + compute_key.goto(send_version) + send_version.goto(know_key) + know_code.upon(close, goto=close_lonely) + know_key.upon(close, goto=close_lonely) + close_lonely.goto(closed) + + +if __name__ == "__main__": + import sys + WM = WormholeMachine() + WM.m._dump_dot(sys.stdout) From 11a80f0018f547099fe00f33f7f7b5cec2297654 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 29 Dec 2016 21:29:55 -0500 Subject: [PATCH 031/176] moving to separate machine class --- docs/w4.dot | 2 +- src/wormhole/_connection.py | 210 +++++++++++++++++++++--------------- 2 files changed, 125 insertions(+), 87 deletions(-) diff --git a/docs/w4.dot b/docs/w4.dot index 7b4e75d..00ea37d 100644 --- a/docs/w4.dot +++ b/docs/w4.dot @@ -44,7 +44,7 @@ digraph { C_P_stop_timer -> C_P_stopped C_Pc2 [shape="box" label="ep.connect()" color="blue"] C_Pc2 -> C_Sc2 [color="blue"] - C_Sc2 [label="connecting" color="blue"] + C_Sc2 [label="reconnecting" color="blue"] C_Sc2 -> C_P_reset [label="d.callback" color="blue" fontcolor="blue"] C_P_reset [shape="box" label="reset\ntimer" color="blue"] C_P_reset -> C_S_negotiating [color="blue"] diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index a58238d..5e7f034 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -16,7 +16,7 @@ class WSClient(websocket.WebSocketClientProtocol): # this fires when the WebSocket is ready to go. No arguments print("onOpen", args) #self.wormhole_open = True - self.connection_machine.onOpen(self) + self.connection_machine.protocol_onOpen(self) #self.factory.d.callback(self) def onMessage(self, payload, isBinary): @@ -27,7 +27,7 @@ class WSClient(websocket.WebSocketClientProtocol): def onClose(self, wasClean, code, reason): print("onClose") - self.connection_machine.onClose(f=None) + self.connection_machine.protocol_onClose(wasClean, code, reason) #if self.wormhole_open: # self.wormhole._ws_closed(wasClean, code, reason) #else: @@ -51,29 +51,14 @@ class WSFactory(websocket.WebSocketClientFactory): # as long as its parent Wormhole does. @attrs -class WSRelayClient(object): - _wormhole = attrib() - _ws_url = attrib() - _reactor = attrib() - +class _WSRelayClient_Machine(object): + _c = attrib() m = MethodicalMachine() - ALLOW_CLOSE = True - - def __init__(self): - self._f = f = WSFactory(self._ws_url) - f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - f.connection_machine = self # calls onOpen and onClose - p = urlparse(self._ws_url) - self._ep = self._make_endpoint(p.hostname, p.port or 80) - self._connector = None - self._done_d = defer.Deferred() - def _make_endpoint(self, hostname, port): - return endpoints.HostnameEndpoint(self._reactor, hostname, port) @m.state(initial=True) def initial(self): pass @m.state() - def first_time_connecting(self): pass + def connecting(self): pass @m.state() def negotiating(self): pass @m.state(terminal=True) @@ -83,100 +68,150 @@ class WSRelayClient(object): @m.state() def waiting(self): pass @m.state() - def connecting(self): pass - if ALLOW_CLOSE: - @m.state() - def disconnecting(self): pass - @m.state() - def cancelling(self): pass - @m.state(terminal=True) - def closed(self): pass - + def reconnecting(self): pass + @m.state() + def disconnecting(self): pass + @m.state() + def cancelling(self): pass + @m.state(terminal=True) + def closed(self): pass @m.input() - def start(self): pass ; print("in start") + def start(self): pass ; print("input:start") @m.input() - def d_callback(self, p): pass ; print("in d_callback", p) + def d_callback(self): pass ; print("input:d_callback") @m.input() - def d_errback(self, f): pass ; print("in d_errback", f) + def d_errback(self): pass ; print("input:d_errback") @m.input() - - def d_cancel(self, f): pass # XXX remove f + def d_cancel(self): pass ; print("input:d_cancel") @m.input() - def onOpen(self, ws): pass ; print("in onOpen") + def onOpen(self): pass ; print("input:onOpen") @m.input() - def onClose(self, f): pass # XXX p.onClose does cm.onClose("made up failure") + def onClose(self): pass ; print("input:onClose") @m.input() def expire(self): pass - if ALLOW_CLOSE: - @m.input() - def stop(self, f): pass + @m.input() + def stop(self): pass + # outputs @m.output() def ep_connect(self): "ep.connect()" - print("ep_connect()") - self._d = self._ep.connect(self._f) - self._d.addCallbacks(self.d_callback, self.d_errback) + self._c.ep_connect() @m.output() - def add_connection(self, ws): - print("add_connection", ws) - self._connection = WSConnection(ws, self._wormhole.appid, - self._wormhole.side, self) - self._wormhole.add_connection(self._connection) + def reset_timer(self): + self._c.reset_timer() @m.output() - def M_lost(self, f): # XXX remove f - self._wormhole.M_lost(self._connection) - self._connection = None + def add_connection(self): + print("add_connection") + self._c.add_connection() @m.output() - def start_timer(self, f): # XXX remove f - print("start_timer") - self._t = self._reactor.callLater(3.0, self.expire) + def M_lost(self): + self._c.M_lost() @m.output() - def cancel_timer(self, f): # XXX remove f - print("cancel_timer") - self._t.cancel() - self._t = None + def start_timer(self): + self._c.start_timer() @m.output() - def dropConnection(self, f): # XXX remove f - print("dropConnection") - self._ws.dropConnection() + def cancel_timer(self): + self._c.cancel_timer() @m.output() - def notify_fail(self, f): - print("notify_fail", f.value) - self._done_d.errback(f) + def dropConnection(self): + self._c.dropConnection() + @m.output() + def notify_fail(self): + self._c.notify_fail() @m.output() def MC_stopped(self): - pass + self._c.MC_stopped() - initial.upon(start, enter=first_time_connecting, outputs=[ep_connect]) - first_time_connecting.upon(d_callback, enter=negotiating, outputs=[]) - first_time_connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) - first_time_connecting.upon(onClose, enter=failed, outputs=[notify_fail]) - if ALLOW_CLOSE: - first_time_connecting.upon(stop, enter=cancelling, - outputs=[d_cancel]) - cancelling.upon(d_errback, enter=closed, outputs=[]) + initial.upon(start, enter=connecting, outputs=[ep_connect]) + connecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) + connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) + connecting.upon(onClose, enter=failed, outputs=[notify_fail]) + connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) + cancelling.upon(d_errback, enter=closed, outputs=[]) negotiating.upon(onOpen, enter=open, outputs=[add_connection]) - if ALLOW_CLOSE: - negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) + negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) open.upon(onClose, enter=waiting, outputs=[M_lost, start_timer]) - if ALLOW_CLOSE: - open.upon(stop, enter=disconnecting, - outputs=[dropConnection, M_lost]) - connecting.upon(d_callback, enter=negotiating, outputs=[]) - connecting.upon(d_errback, enter=waiting, outputs=[start_timer]) - connecting.upon(onClose, enter=waiting, outputs=[start_timer]) - if ALLOW_CLOSE: - connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) + open.upon(stop, enter=disconnecting, outputs=[dropConnection, M_lost]) + reconnecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) + reconnecting.upon(d_errback, enter=waiting, outputs=[start_timer]) + reconnecting.upon(onClose, enter=waiting, outputs=[start_timer]) + reconnecting.upon(stop, enter=cancelling, outputs=[d_cancel]) + + waiting.upon(expire, enter=reconnecting, outputs=[ep_connect]) + waiting.upon(stop, enter=closed, outputs=[cancel_timer]) + disconnecting.upon(onClose, enter=closed, outputs=[MC_stopped]) + +@attrs +class WSRelayClient(object): + _wormhole = attrib() + _ws_url = attrib() + _reactor = attrib() + INITIAL_DELAY = 1.0 + + + def __init__(self): + self._m = _WSRelayClient_Machine(self) + self._f = f = WSFactory(self._ws_url) + f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + f.connection_machine = self # calls onOpen and onClose + p = urlparse(self._ws_url) + self._ep = self._make_endpoint(p.hostname, p.port or 80) + self._connector = None + self._done_d = defer.Deferred() + self._current_delay = self.INITIAL_DELAY + + def _make_endpoint(self, hostname, port): + return endpoints.HostnameEndpoint(self._reactor, hostname, port) + + # inputs from elsewhere + def d_callback(self, p): + self._p = p + self._m.d_callback() + def d_errback(self, f): + self._f = f + self._m.d_errback() + def protocol_onOpen(self, p): + self._m.onOpen() + def protocol_onClose(self, wasClean, code, reason): + self._m.onClose() + def C_stop(self): + self._m.stop() + def timer_expired(self): + self._m.expire() + + # outputs driven by the state machine + def ep_connect(self): + print("ep_connect()") + self._d = self._ep.connect(self._f) + self._d.addCallbacks(self.d_callback, self.d_errback) + def add_connection(self): + self._connection = WSConnection(ws, self._wormhole.appid, + self._wormhole.side, self) + self._wormhole.add_connection(self._connection) + def M_lost(self): + self._wormhole.M_lost(self._connection) + self._connection = None + def start_timer(self): + print("start_timer") + self._t = self._reactor.callLater(3.0, self.expire) + def cancel_timer(self): + print("cancel_timer") + self._t.cancel() + self._t = None + def dropConnection(self): + print("dropConnection") + self._ws.dropConnection() + def notify_fail(self): + print("notify_fail", self._f.value if self._f else None) + self._done_d.errback(self._f) + def MC_stopped(self): + pass - waiting.upon(expire, enter=connecting, outputs=[ep_connect]) - if ALLOW_CLOSE: - waiting.upon(stop, enter=closed, outputs=[cancel_timer]) - disconnecting.upon(onClose, enter=closed, outputs=[]) #MC_stopped def tryit(reactor): cm = WSRelayClient(None, "ws://127.0.0.1:4000/v1", reactor) @@ -447,9 +482,12 @@ class Wormhole: # connection is established, we'll send the new ones. self._outbound_messages = [] + # these methods are called from outside def start(self): self._relay_client.start() + # and these are the state-machine transition functions, which don't take + # args @m.state() def closed(initial=True): pass @m.state() From b934192f204d24b329e4006243e6439257e50ebd Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 30 Dec 2016 00:30:59 -0500 Subject: [PATCH 032/176] work on Mailbox machine --- docs/w4.dot | 6 +- src/wormhole/_connection.py | 19 ++--- src/wormhole/_mailbox.py | 148 ++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 src/wormhole/_mailbox.py diff --git a/docs/w4.dot b/docs/w4.dot index 00ea37d..5fd6c52 100644 --- a/docs/w4.dot +++ b/docs/w4.dot @@ -7,7 +7,9 @@ digraph { C_Pc1 [shape="box" label="ep.connect()" color="orange"] C_Pc1 -> C_Sc1 [color="orange"] C_Sc1 [label="connecting\n(1st time)" color="orange"] - C_Sc1 -> C_S_negotiating [label="d.callback" color="orange" fontcolor="orange"] + C_Sc1 -> C_P_reset [label="d.callback" color="orange" fontcolor="orange"] + C_P_reset [shape="box" label="reset\ntimer" color="orange"] + C_P_reset -> C_S_negotiating [color="orange"] C_Sc1 -> C_P_failed [label="d.errback" color="red"] C_Sc1 -> C_P_failed [label="p.onClose" color="red"] C_Sc1 -> C_P_cancel [label="C_stop()"] @@ -46,8 +48,6 @@ digraph { C_Pc2 -> C_Sc2 [color="blue"] C_Sc2 [label="reconnecting" color="blue"] C_Sc2 -> C_P_reset [label="d.callback" color="blue" fontcolor="blue"] - C_P_reset [shape="box" label="reset\ntimer" color="blue"] - C_P_reset -> C_S_negotiating [color="blue"] C_Sc2 -> C_P_wait [label="d.errback"] C_Sc2 -> C_P_cancel [label="C_stop()"] diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 5e7f034..ebc7209 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -43,13 +43,9 @@ class WSFactory(websocket.WebSocketClientFactory): #proto.wormhole_open = False return proto - # pip install (path to automat checkout)[visualize] # automat-visualize wormhole._connection -# We have one WSRelayClient for each wsurl we know about, and it lasts -# as long as its parent Wormhole does. - @attrs class _WSRelayClient_Machine(object): _c = attrib() @@ -102,9 +98,9 @@ class _WSRelayClient_Machine(object): def reset_timer(self): self._c.reset_timer() @m.output() - def add_connection(self): - print("add_connection") - self._c.add_connection() + def connection_established(self): + print("connection_established") + self._c.connection_established() @m.output() def M_lost(self): self._c.M_lost() @@ -131,7 +127,7 @@ class _WSRelayClient_Machine(object): connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) cancelling.upon(d_errback, enter=closed, outputs=[]) - negotiating.upon(onOpen, enter=open, outputs=[add_connection]) + negotiating.upon(onOpen, enter=open, outputs=[connection_established]) negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) @@ -146,9 +142,13 @@ class _WSRelayClient_Machine(object): waiting.upon(stop, enter=closed, outputs=[cancel_timer]) disconnecting.upon(onClose, enter=closed, outputs=[MC_stopped]) +# We have one WSRelayClient for each wsurl we know about, and it lasts +# as long as its parent Wormhole does. + @attrs class WSRelayClient(object): _wormhole = attrib() + _mailbox = attrib() _ws_url = attrib() _reactor = attrib() INITIAL_DELAY = 1.0 @@ -189,9 +189,10 @@ class WSRelayClient(object): print("ep_connect()") self._d = self._ep.connect(self._f) self._d.addCallbacks(self.d_callback, self.d_errback) - def add_connection(self): + def connection_established(self): self._connection = WSConnection(ws, self._wormhole.appid, self._wormhole.side, self) + self._mailbox.connected(ws) self._wormhole.add_connection(self._connection) def M_lost(self): self._wormhole.M_lost(self._connection) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py new file mode 100644 index 0000000..494d913 --- /dev/null +++ b/src/wormhole/_mailbox.py @@ -0,0 +1,148 @@ +from attr import attrs, attrib +from automat import MethodicalMachine + +@attrs +class _Mailbox_Machine(object): + _m = attrib() + m = MethodicalMachine() + + @m.state(initial=True) + def initial(self): pass + + @m.state() + def S1A(self): pass # know nothing, not connected + @m.state() + def S1B(self): pass # know nothing, yes connected + + @m.state() + def S2A(self): pass # not claimed, not connected + @m.state() + def S2B(self): pass # maybe claimed, yes connected + @m.state() + def S2C(self): pass # maybe claimed, not connected + + @m.state() + def S3A(self): pass # claimed, maybe opened, not connected + @m.state() + def S3B(self): pass # claimed, maybe opened, yes connected + + @m.state() + def S4A(self): pass # maybe released, maybe opened, not connected + @m.state() + def S4B(self): pass # maybe released, maybe opened, yes connected + + @m.state() + def S5A(self): pass # released, maybe open, not connected + @m.state() + def S5B(self): pass # released, maybe open, yes connected + + @m.state() + def SrcA(self): pass # waiting for release+close, not connected + @m.state() + def SrcB(self): pass # waiting for release+close, yes connected + @m.state() + def SrA(self): pass # waiting for release, not connected + @m.state() + def SrB(self): pass # waiting for release, yes connected + @m.state() + def ScA(self): pass # waiting for close, not connected + @m.state() + def ScB(self): pass # waiting for close, yes connected + @m.state() + def SsB(self): pass # closed, stopping + @m.state() + def Ss(self): pass # stopped + + + def connected(self, ws): + self._ws = ws + self.M_connected() + + @m.input() + def M_start_unconnected(self): pass + @m.input() + def M_start_connected(self): pass + @m.input() + def M_set_nameplate(self): pass + @m.input() + def M_connected(self): pass + @m.input() + def M_lost(self): pass + @m.input() + def M_send(self, msg): pass + @m.input() + def M_rx_claimed(self): pass + @m.input() + def M_rx_msg_from_me(self, msg): pass + @m.input() + def M_rx_msg_from_them(self, msg): pass + @m.input() + def M_rx_released(self): pass + @m.input() + def M_rx_closed(self): pass + @m.input() + def M_stopped(self): pass + + @m.output() + def tx_claim(self): pass + @m.output() + def tx_open(self): pass + @m.output() + def queue(self, msg): pass + @m.output() + def store_mailbox(self): pass # trouble(mb) + @m.output() + def tx_add(self, msg): pass + @m.output() + def tx_add_queued(self): pass + @m.output() + def tx_release(self): pass + @m.output() + def process_msg_from_them(self, msg): pass + @m.output() + def dequeue(self, msg): pass + + + initial.upon(M_start_connected, enter=S1A, outputs=[]) + initial.upon(M_start_unconnected, enter=S1B, outputs=[]) + S1A.upon(M_connected, enter=S1B, outputs=[]) + S1A.upon(M_set_nameplate, enter=S2A, outputs=[]) + S1B.upon(M_lost, enter=S1A, outputs=[]) + S1B.upon(M_set_nameplate, enter=S2B, outputs=[tx_claim]) + + S2A.upon(M_connected, enter=S2B, outputs=[tx_claim]) + #S2A.upon(M_close + S2A.upon(M_send, enter=S2A, outputs=[queue]) + S2B.upon(M_lost, enter=S2C, outputs=[]) + S2B.upon(M_send, enter=S2B, outputs=[queue]) + #S2B.upon(M_close + S2B.upon(M_rx_claimed, enter=S3B, outputs=[store_mailbox, tx_open, + tx_add_queued]) + S2C.upon(M_connected, enter=S2B, outputs=[tx_claim]) + S2C.upon(M_send, enter=S2C, outputs=[queue]) + + S3A.upon(M_connected, enter=S3B, outputs=[tx_open, tx_add_queued]) + S3A.upon(M_send, enter=S3A, outputs=[queue]) + S3B.upon(M_lost, enter=S3A, outputs=[]) + S3B.upon(M_rx_msg_from_them, enter=S4B, outputs=[#tx_release, # trouble + process_msg_from_them]) + S3B.upon(M_rx_msg_from_me, enter=S3B, outputs=[dequeue]) + S3B.upon(M_rx_claimed, enter=S3B, outputs=[]) + S3B.upon(M_send, enter=S3B, outputs=[queue, tx_add]) + + S4A.upon(M_connected, enter=S4B, outputs=[tx_open, tx_add_queued, tx_release]) + S4A.upon(M_send, enter=S4A, outputs=[queue]) + S4B.upon(M_lost, enter=S4A, outputs=[]) + S4B.upon(M_send, enter=S4B, outputs=[queue, tx_add]) + S4B.upon(M_rx_msg_from_them, enter=S4B, outputs=[process_msg_from_them]) + S4B.upon(M_rx_msg_from_me, enter=S4B, outputs=[dequeue]) + S4B.upon(M_rx_released, enter=S5B, outputs=[]) + + S5A.upon(M_connected, enter=S5B, outputs=[tx_open, tx_add_queued]) + S5A.upon(M_send, enter=S5A, outputs=[queue]) + S5B.upon(M_lost, enter=S5A, outputs=[]) + S5B.upon(M_send, enter=S5B, outputs=[queue, tx_add]) + S5B.upon(M_rx_msg_from_them, enter=S5B, outputs=[process_msg_from_them]) + S5B.upon(M_rx_msg_from_me, enter=S5B, outputs=[dequeue]) + + From 3af375b173f0eb7eb3358d552a9910e7b6b2b66e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 30 Dec 2016 01:18:20 -0500 Subject: [PATCH 033/176] finish Mailbox state machine, including close --- docs/w3.dot | 14 +++++--------- src/wormhole/_mailbox.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docs/w3.dot b/docs/w3.dot index 89a8bdf..7cb6d61 100644 --- a/docs/w3.dot +++ b/docs/w3.dot @@ -7,7 +7,6 @@ digraph { all dashed lines indicate M_close() pathways in from those states. Within this graph, all M_close() events leave the state unchanged. */ - {rank=same; MC_SrA MC_SrB} MC_SrA [label="SrA:\nwaiting for:\nrelease"] MC_SrA -> MC_Pr [label="M_connected()"] MC_Pr [shape="box" label="tx release" color="orange"] @@ -16,7 +15,6 @@ digraph { MC_SrB -> MC_SrA [label="M_lost()"] MC_SrB -> MC_P_stop [label="rx released" color="orange" fontcolor="orange"] - /*{rank=same; MC_ScA MC_ScB}*/ MC_ScA [label="ScA:\nwaiting for:\nclosed"] MC_ScA -> MC_Pc [label="M_connected()"] MC_Pc [shape="box" label="tx close" color="orange"] @@ -25,7 +23,6 @@ digraph { MC_ScB -> MC_ScA [label="M_lost()"] MC_ScB -> MC_P_stop [label="rx closed" color="orange" fontcolor="orange"] - {rank=same; MC_SrcA MC_SrcB} MC_SrcA [label="SrcA:\nwaiting for:\nrelease\nclose"] MC_SrcA -> MC_Prc [label="M_connected()"] MC_Prc [shape="box" label="tx release\ntx close" color="orange"] @@ -39,26 +36,26 @@ digraph { MC_P_stop [shape="box" label="C_stop()"] MC_P_stop -> MC_SsB - MC_SsB -> MC_Ss [label="MC_stopped()"] + MC_SsB -> MC_Ss [label="M_stopped()"] MC_SsB [label="SsB: closed\nstopping"] MC_Ss [label="Ss: closed" color="green"] + {rank=same; MC_S2A MC_S2B MC_S2C MC_S1A MC_S1B MC_S3A MC_S3B MC_S4A MC_S4B MC_S5A MC_S5B} MC_S1A [label="S1A" style="dashed"] MC_S1A -> MC_P_stop [style="dashed"] MC_S1B [label="S1B" color="orange" style="dashed"] MC_S1B -> MC_P_stop [style="dashed" color="orange"] - {rank=same; MC_S2A MC_S2B MC_S2C} + MC_S2C -> MC_S2A [style="invis"] MC_S2A [label="S2A" style="dashed"] MC_S2A -> MC_P_stop [style="dashed"] MC_S2C [label="S2C" style="dashed"] MC_S2C -> MC_SrA [style="dashed"] MC_S2B [label="S2B" color="orange" style="dashed"] - MC_S2B -> MC_SrB [color="orange" style="dashed"] + MC_S2B -> MC_Pr [color="orange" style="dashed"] - {rank=same; MC_title MC_S3A MC_S4A MC_S3B MC_S4B} MC_S3A [label="S3A" style="dashed"] MC_S3B [label="S3B" color="orange" style="dashed"] MC_S3A -> MC_SrcA [style="dashed"] @@ -69,10 +66,9 @@ digraph { MC_S4A -> MC_SrcA [style="dashed"] MC_S4B -> MC_Prc [color="orange" style="dashed"] - {rank=same; MC_S5A MC_S5B} MC_S5A [label="S5A" style="dashed"] MC_S5B [label="S5B" color="green" style="dashed"] MC_S5A -> MC_ScA [style="dashed"] - MC_S5B -> MC_Pc [color="green"] + MC_S5B -> MC_Pc [style="dashed" color="green"] } diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 494d913..ddf9595 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -81,6 +81,8 @@ class _Mailbox_Machine(object): @m.input() def M_rx_closed(self): pass @m.input() + def M_stop(self): pass + @m.input() def M_stopped(self): pass @m.output() @@ -98,51 +100,78 @@ class _Mailbox_Machine(object): @m.output() def tx_release(self): pass @m.output() + def tx_close(self): pass + @m.output() def process_msg_from_them(self, msg): pass @m.output() def dequeue(self, msg): pass + @m.output() + def C_stop(self): pass initial.upon(M_start_connected, enter=S1A, outputs=[]) initial.upon(M_start_unconnected, enter=S1B, outputs=[]) S1A.upon(M_connected, enter=S1B, outputs=[]) S1A.upon(M_set_nameplate, enter=S2A, outputs=[]) + S1A.upon(M_stop, enter=SsB, outputs=[C_stop]) S1B.upon(M_lost, enter=S1A, outputs=[]) S1B.upon(M_set_nameplate, enter=S2B, outputs=[tx_claim]) + S1B.upon(M_stop, enter=SsB, outputs=[C_stop]) S2A.upon(M_connected, enter=S2B, outputs=[tx_claim]) - #S2A.upon(M_close + S2A.upon(M_stop, enter=SsB, outputs=[C_stop]) S2A.upon(M_send, enter=S2A, outputs=[queue]) S2B.upon(M_lost, enter=S2C, outputs=[]) S2B.upon(M_send, enter=S2B, outputs=[queue]) - #S2B.upon(M_close + S2B.upon(M_stop, enter=SrB, outputs=[tx_release]) S2B.upon(M_rx_claimed, enter=S3B, outputs=[store_mailbox, tx_open, tx_add_queued]) S2C.upon(M_connected, enter=S2B, outputs=[tx_claim]) S2C.upon(M_send, enter=S2C, outputs=[queue]) + S2C.upon(M_stop, enter=SrA, outputs=[]) S3A.upon(M_connected, enter=S3B, outputs=[tx_open, tx_add_queued]) S3A.upon(M_send, enter=S3A, outputs=[queue]) + S3A.upon(M_stop, enter=SrcA, outputs=[]) S3B.upon(M_lost, enter=S3A, outputs=[]) S3B.upon(M_rx_msg_from_them, enter=S4B, outputs=[#tx_release, # trouble process_msg_from_them]) S3B.upon(M_rx_msg_from_me, enter=S3B, outputs=[dequeue]) S3B.upon(M_rx_claimed, enter=S3B, outputs=[]) S3B.upon(M_send, enter=S3B, outputs=[queue, tx_add]) + S3B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close]) S4A.upon(M_connected, enter=S4B, outputs=[tx_open, tx_add_queued, tx_release]) S4A.upon(M_send, enter=S4A, outputs=[queue]) + S4A.upon(M_stop, enter=SrcA, outputs=[]) S4B.upon(M_lost, enter=S4A, outputs=[]) S4B.upon(M_send, enter=S4B, outputs=[queue, tx_add]) S4B.upon(M_rx_msg_from_them, enter=S4B, outputs=[process_msg_from_them]) S4B.upon(M_rx_msg_from_me, enter=S4B, outputs=[dequeue]) S4B.upon(M_rx_released, enter=S5B, outputs=[]) + S4B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close]) S5A.upon(M_connected, enter=S5B, outputs=[tx_open, tx_add_queued]) S5A.upon(M_send, enter=S5A, outputs=[queue]) + S5A.upon(M_stop, enter=ScA, outputs=[]) S5B.upon(M_lost, enter=S5A, outputs=[]) S5B.upon(M_send, enter=S5B, outputs=[queue, tx_add]) S5B.upon(M_rx_msg_from_them, enter=S5B, outputs=[process_msg_from_them]) S5B.upon(M_rx_msg_from_me, enter=S5B, outputs=[dequeue]) + S5B.upon(M_stop, enter=ScB, outputs=[tx_close]) + SrcA.upon(M_connected, enter=SrcB, outputs=[tx_release, tx_close]) + SrcB.upon(M_lost, enter=SrcA, outputs=[]) + SrcB.upon(M_rx_closed, enter=SrB, outputs=[]) + SrcB.upon(M_rx_released, enter=ScB, outputs=[]) + + SrB.upon(M_lost, enter=SrA, outputs=[]) + SrA.upon(M_connected, enter=SrB, outputs=[tx_release]) + SrB.upon(M_rx_released, enter=SsB, outputs=[C_stop]) + + ScB.upon(M_lost, enter=ScA, outputs=[]) + ScB.upon(M_rx_closed, enter=SsB, outputs=[C_stop]) + ScA.upon(M_connected, enter=ScB, outputs=[tx_close]) + + SsB.upon(M_stopped, enter=Ss, outputs=[]) From 20b80be342f97f74fc666396bad0d60afb029e0f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 30 Dec 2016 01:27:03 -0500 Subject: [PATCH 034/176] remove stale machines --- docs/w.dot | 3 - src/wormhole/_connection.py | 174 +----------------------------------- src/wormhole/_mailbox.py | 3 +- 3 files changed, 4 insertions(+), 176 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index e62400e..b74cc29 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -200,10 +200,7 @@ digraph { WCM_S_known -> O_WM [style="invis"] O_WM [label="Wormhole\nMachine" style="dotted"] O_WM -> O_MM [style="dotted"] - O_WM -> O_MCM [style="dotted"] - O_MM -> O_MCM [style="dotted"] O_MM [label="Mailbox\nMachine" style="dotted"] - O_MCM [label="Mailbox\nClose\nMachine" style="dotted"] O_MM -> O_CM [style="dotted"] O_CM [label="Connection\nMachine" style="dotted"] } diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index ebc7209..808b70f 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -194,6 +194,7 @@ class WSRelayClient(object): self._wormhole.side, self) self._mailbox.connected(ws) self._wormhole.add_connection(self._connection) + self._ws_send_command("bind", appid=self._appid, side=self._side) def M_lost(self): self._wormhole.M_lost(self._connection) self._connection = None @@ -245,134 +246,8 @@ if __name__ == "__main__": from twisted.internet.task import react react(tryit) -# a new WSConnection is created each time the WSRelayClient gets through +# ??? a new WSConnection is created each time the WSRelayClient gets through # negotiation -@attrs -class WSConnection(object): - _ws = attrib() - _appid = attrib() - _side = attrib() - _wsrc = attrib() - m = MethodicalMachine() - - @m.state(initial=True) - def unbound(self): pass - @m.state() - def binding(self): pass - @m.state() - def neither(self): pass - @m.state() - def has_nameplate(self): pass - @m.state() - def has_mailbox(self): pass - @m.state() - def has_both(self): pass - @m.state() - def closing(self): pass - @m.state() - def closed(self): pass - - @m.input() - def bind(self): pass - @m.input() - def ack_bind(self): pass - @m.input() - def wsc_set_nameplate(self): pass - @m.input() - def wsc_set_mailbox(self, mailbox): pass - @m.input() - def wsc_release_nameplate(self): pass - @m.input() - def wsc_release_mailbox(self): pass - @m.input() - def ack_close(self): pass - - @m.output() - def send_bind(self): - self._ws_send_command("bind", appid=self._appid, side=self._side) - @m.output() - def notify_bound(self): - self._nameplate_machine.bound() - self._connection.make_listing_machine() - @m.output() - def m_set_mailbox(self, mailbox): - self._mailbox_machine.m_set_mailbox(mailbox) - @m.output() - def request_close(self): - self._wsrc.close() - @m.output() - def notify_close(self): - pass - - unbound.upon(bind, enter=binding, outputs=[send_bind]) - binding.upon(ack_bind, enter=neither, outputs=[notify_bound]) - neither.upon(wsc_set_nameplate, enter=has_nameplate, outputs=[]) - neither.upon(wsc_set_mailbox, enter=has_mailbox, outputs=[m_set_mailbox]) - has_nameplate.upon(wsc_set_mailbox, enter=has_both, outputs=[m_set_mailbox]) - has_nameplate.upon(wsc_release_nameplate, enter=closing, outputs=[request_close]) - has_mailbox.upon(wsc_set_nameplate, enter=has_both, outputs=[]) - has_mailbox.upon(wsc_release_mailbox, enter=closing, outputs=[request_close]) - has_both.upon(wsc_release_nameplate, enter=has_mailbox, outputs=[]) - has_both.upon(wsc_release_mailbox, enter=has_nameplate, outputs=[]) - closing.upon(ack_close, enter=closed, outputs=[]) - -class NameplateMachine(object): - m = MethodicalMachine() - - @m.state(initial=True) - def unclaimed(self): pass # but bound - @m.state() - def claiming(self): pass - @m.state() - def claimed(self): pass - @m.state() - def releasing(self): pass - @m.state(terminal=True) - def done(self): pass - - @m.input() - def learned_nameplate(self, nameplate): - """Call learned_nameplate() when you learn the nameplate: either - through allocation or code entry""" - pass - @m.input() - def rx_claimed(self, mailbox): pass # response("claimed") - @m.input() - def nm_release_nameplate(self): pass - @m.input() - def release_acked(self): pass # response("released") - - @m.output() - def send_claim(self, nameplate): - self._ws_send_command("claim", nameplate=nameplate) - @m.output() - def wsc_set_nameplate(self, mailbox): - self._connection_machine.wsc_set_nameplate() - @m.output() - def wsc_set_mailbox(self, mailbox): - self._connection_machine.wsc_set_mailbox() - @m.output() - def mm_set_mailbox(self, mailbox): - self._mm.mm_set_mailbox() - @m.output() - def send_release(self): - self._ws_send_command("release") - @m.output() - def wsc_release_nameplate(self): - # let someone know, when both the mailbox and the nameplate are - # released, the websocket can be closed, and we're done - self._wsc.wsc_release_nameplate() - - unclaimed.upon(learned_nameplate, enter=claiming, outputs=[send_claim]) - claiming.upon(rx_claimed, enter=claimed, outputs=[wsc_set_nameplate, - mm_set_mailbox, - wsc_set_mailbox]) - #claiming.upon(learned_nameplate, enter=claiming, outputs=[]) - claimed.upon(nm_release_nameplate, enter=releasing, outputs=[send_release]) - #claimed.upon(learned_nameplate, enter=claimed, outputs=[]) - #releasing.upon(release, enter=releasing, outputs=[]) - releasing.upon(release_acked, enter=done, outputs=[wsc_release_nameplate]) - #releasing.upon(learned_nameplate, enter=releasing, outputs=[]) class NameplateListingMachine(object): m = MethodicalMachine() @@ -427,51 +302,6 @@ class NameplateListingMachine(object): # nlm.list_nameplates().addCallback(display_completions) # c.register_dispatch("nameplates", nlm.response) -class MailboxMachine(object): - m = MethodicalMachine() - - @m.state() - def unknown(initial=True): pass - @m.state() - def mailbox_unused(): pass - @m.state() - def mailbox_used(): pass - - @m.input() - def mm_set_mailbox(self, mailbox): pass - @m.input() - def add_connection(self, connection): pass - @m.input() - def rx_message(self): pass - - @m.input() - def close(self): pass - - @m.output() - def open_mailbox(self): - self._mm.mm_set_mailbox(self._mailbox) - @m.output() - def nm_release_nameplate(self): - self._nm.nm_release_nameplate() - @m.output() - def wsc_release_mailbox(self): - self._wsc.wsc_release_mailbox() - @m.output() - def open_mailbox(self, mailbox): - self._ws_send_command("open", mailbox=mailbox) - - @m.output() - def close_mailbox(self, mood): - self._ws_send_command("close", mood=mood) - - unknown.upon(mm_set_mailbox, enter=mailbox_unused, outputs=[open_mailbox]) - mailbox_unused.upon(rx_message, enter=mailbox_used, - outputs=[nm_release_nameplate]) - #open.upon(message_pake, enter=key_established, outputs=[send_pake, - # send_version]) - #key_established.upon(message_version, enter=key_verified, outputs=[]) - #key_verified.upon(close, enter=closed, outputs=[wsc_release_mailbox]) - class Wormhole: m = MethodicalMachine() diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index ddf9595..c03e252 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -86,7 +86,8 @@ class _Mailbox_Machine(object): def M_stopped(self): pass @m.output() - def tx_claim(self): pass + def tx_claim(self): + self._c.send_command("claim", nameplate=self._nameplate) @m.output() def tx_open(self): pass @m.output() From cf981222c59f84e453a151eebce8e2154c1c1ea8 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 12 Jan 2017 14:34:54 -0800 Subject: [PATCH 035/176] think about "checkpointing" as a state with async exit when the checkpoint is finally written. Not sure this is the best idea. --- docs/w.dot | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/w.dot b/docs/w.dot index b74cc29..fa62850 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -15,10 +15,15 @@ digraph { WM_S_nothing -> WM_P_queue1 [label="API_send" style="dotted"] WM_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] WM_P_queue1 -> WM_S_nothing [style="dotted"] - WM_S_nothing -> WM_P_build_and_post_pake [label="WM_set_code()"] + WM_S_nothing -> WM_P_build_pake [label="WM_set_code()"] - WM_P_build_and_post_pake [label="M_set_nameplate()\nbuild_pake()\nM_send(pake)" shape="box"] - WM_P_build_and_post_pake -> WM_S_know_code + WM_P_build_pake [shape="box" label="build_pake()"] + WM_P_build_pake -> WM_S_save_pake + WM_S_save_pake [label="checkpoint"] + WM_S_save_pake -> WM_P_post_pake [label="saved"] + + WM_P_post_pake [label="M_set_nameplate()\nM_send(pake)" shape="box"] + WM_P_post_pake -> WM_S_know_code WM_S_know_code [label="know code\n"] WM_S_know_code -> WM_P_queue2 [label="API_send" style="dotted"] @@ -28,7 +33,9 @@ digraph { WM_S_know_code -> WM_P_mood_lonely [label="close"] WM_P_compute_key [label="compute_key()" shape="box"] - WM_P_compute_key -> WM_P_post_version [label="pake ok"] + WM_P_compute_key -> WM_P_save_key [label="pake ok"] + WM_P_save_key [label="checkpoint"] + WM_P_save_key -> WM_P_post_version [label="saved"] WM_P_compute_key -> WM_P_mood_scary [label="pake bad"] WM_P_mood_scary [shape="box" label="M_close()\nmood=scary"] From f6930a9bfcf4e1ea6b1456156fff1a3b335d87f0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 26 Jan 2017 17:22:28 -0800 Subject: [PATCH 036/176] more thoughts --- docs/w2a.dot | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/w3a.dot | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/w2a.dot create mode 100644 docs/w3a.dot diff --git a/docs/w2a.dot b/docs/w2a.dot new file mode 100644 index 0000000..7a845b9 --- /dev/null +++ b/docs/w2a.dot @@ -0,0 +1,80 @@ +digraph { + /* new idea */ + + M_title [label="Message\nMachine" style="dotted"] + + M_S1 [label="S1:\nknow nothing" color="orange"] + + M_S1 -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] + + M_S2 [label="S2:\nmaybe claimed" color="orange"] + /*M_S2 -> M_SrB [label="M_close()" style="dashed"] + M_SrB [label="SrB" style="dashed"]*/ + + M_P2_claim [shape="box" label="qtx claim" color="orange"] + M_P2_claim -> M_S2 [color="orange"] + M_S2 -> M_P2_queue [label="M_send(msg)" style="dotted"] + M_P2_queue [shape="box" label="queue" style="dotted"] + M_P2_queue -> M_S2 [style="dotted"] + + M_S2 -> M_P_open [label="rx claimed" color="orange" fontcolor="orange"] + M_P_open [shape="box" label="store mailbox\nqtx open\nqtx add(queued)" color="orange"] + M_P_open -> M_S3 [color="orange"] + + M_S3 [label="S3:\nclaimed\nmaybe open" color="orange"] + M_S3 -> M_S3 [label="rx claimed"] + M_S3 -> M_P3_send [label="M_send(msg)"] + M_P3_send [shape="box" label="queue\nqtx add(msg)"] + M_P3_send -> M_S3 + + M_S3 -> M_P3_process_ours [label="rx message(side=me)"] + M_P3_process_ours [shape="box" label="dequeue"] + M_P3_process_ours -> M_S3 + M_S3 -> M_P3_process_theirs1 [label="rx message(side!=me)" color="orange" fontcolor="orange"] + M_P3_process_theirs1 [shape="box" label="qtx release" color="orange"] + M_P3_process_theirs1 -> M_P3_process_theirs2 [color="orange"] + M_P3_process_theirs2 [shape="octagon" label="process message" color="orange"] + /* pay attention to the race here: this process_message() will + deliver msg_pake to the WormholeMachine, which will compute_key() and + M_send(version), and we're in between M_S2A (where M_send gets + queued) and M_S3A (where M_send gets sent and queued), and we're no + longer passing through the M_P3_open phase (which drains the queue). + So there's a real possibility of the outbound msg_version getting + dropped on the floor, or put in a queue but never delivered. */ + M_P3_process_theirs2 -> M_S4 [color="orange"] + + /*{rank=same; M_S4A M_P4_release M_S4 M_P4_process M_P4_send M_P4_queue}*/ + M_S4 [label="S4:\nmaybe released\nmaybe open" color="orange"] + M_S4 -> M_P4_send [label="M_send(msg)"] + M_P4_send [shape="box" label="queue\nqtx add(msg)"] + M_P4_send -> M_S4 + + M_S4 -> M_P4_process [label="rx message"] + M_P4_process [shape="octagon" label="process message"] + M_P4_process -> M_S4 + + M_S4 -> M_S5 [label="rx released" color="orange" fontcolor="orange"] + + seed [label="from Seed?"] + seed -> M_S5 + M_S5 [label="S5:\nreleased\nmaybe open" color="green"] + M_S5 -> M_process [label="rx message" color="green" fontcolor="green"] + M_process [shape="octagon" label="process message" color="green"] + M_process -> M_S5 [color="green"] + M_S5 -> M_P5_send [label="M_send(msg)" color="green" fontcolor="green"] + M_P5_send [shape="box" label="queue\nqtx add(msg)" color="green"] + M_P5_send -> M_S5 [color="green"] + /*M_S5 -> M_CcB_P_close [label="M_close()" style="dashed" color="orange" fontcolor="orange"] + M_CcB_P_close [label="qtx close" style="dashed" color="orange"] */ + + M_process [shape="octagon" label="process message"] + M_process_me [shape="box" label="dequeue"] + M_process -> M_process_me [label="side == me"] + M_process_them [shape="box" label="" style="dotted"] + M_process -> M_process_them [label="side != me"] + M_process_them -> M_process_pake [label="phase == pake"] + M_process_pake [shape="box" label="WM_rx_pake()"] + M_process_them -> M_process_other [label="phase in (version,numbered)"] + M_process_other [shape="box" label="WM_rx_msg()"] + +} diff --git a/docs/w3a.dot b/docs/w3a.dot new file mode 100644 index 0000000..5435b33 --- /dev/null +++ b/docs/w3a.dot @@ -0,0 +1,52 @@ +digraph { + /* M_close pathways */ + MC_title [label="Mailbox\nClose\nMachine" style="dotted"] + MC_title -> MC_S2 [style="invis"] + + /* All dashed states are from the main Mailbox Machine diagram, and + all dashed lines indicate M_close() pathways in from those states. + Within this graph, all M_close() events leave the state unchanged. */ + + MC_Pr [shape="box" label="qtx release" color="orange"] + MC_Pr -> MC_Sr [color="orange"] + MC_Sr [label="SrB:\nwaiting for:\nrelease" color="orange"] + MC_Sr -> MC_P_stop [label="rx released" color="orange" fontcolor="orange"] + + MC_Pc [shape="box" label="qtx close" color="orange"] + MC_Pc -> MC_Sc [color="orange"] + MC_Sc [label="ScB:\nwaiting for:\nclosed" color="orange"] + MC_Sc -> MC_P_stop [label="rx closed" color="orange" fontcolor="orange"] + + MC_Prc [shape="box" label="qtx release\nqtx close" color="orange"] + MC_Prc -> MC_Src [color="orange"] + MC_Src [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] + MC_Src -> MC_Sc [label="rx released" color="orange" fontcolor="orange"] + MC_Src -> MC_Sr [label="rx closed" color="orange" fontcolor="orange"] + + + MC_P_stop [shape="box" label="C_stop()"] + MC_P_stop -> MC_Ss + + MC_Ss -> MC_Ss [label="M_stopped()"] + MC_Ss [label="SsB: closed\nstopping"] + + MC_Ss [label="Ss: closed" color="green"] + + + {rank=same; MC_S2 MC_S1 MC_S3 MC_S4 MC_S5} + MC_S1 [label="S1" color="orange" style="dashed"] + MC_S1 -> MC_P_stop [style="dashed" color="orange"] + + MC_S2 [label="S2" color="orange" style="dashed"] + MC_S2 -> MC_Pr [color="orange" style="dashed"] + + MC_S3 [label="S3" color="orange" style="dashed"] + MC_S3 -> MC_Prc [color="orange" style="dashed"] + + MC_S4 [label="S4" color="orange" style="dashed"] + MC_S4 -> MC_Prc [color="orange" style="dashed"] + + MC_S5 [label="S5" color="green" style="dashed"] + MC_S5 -> MC_Pc [style="dashed" color="green"] + +} From 7f3e86acca0b18cd81667d7f8a2df8b8a2a78a9f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 12 Feb 2017 10:57:04 -0800 Subject: [PATCH 037/176] more fussing, split out M_S0 --- docs/w2.dot | 58 +++++++++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/w2.dot b/docs/w2.dot index 9f13362..cede4be 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -3,50 +3,56 @@ digraph { {rank=same; M_entry_whole_code M_title M_entry_allocation M_entry_interactive} M_entry_whole_code [label="whole\ncode"] - M_entry_whole_code -> M_S1A + M_entry_whole_code -> M_S0A M_title [label="Message\nMachine" style="dotted"] M_entry_whole_code -> M_title [style="invis"] M_entry_allocation [label="allocation" color="orange"] - M_entry_allocation -> M_S1B [label="already\nconnected" color="orange" fontcolor="orange"] + M_entry_allocation -> M_S0B [label="already\nconnected" color="orange" fontcolor="orange"] M_entry_interactive [label="interactive" color="orange"] - M_entry_interactive -> M_S1B [color="orange"] + M_entry_interactive -> M_S0B [color="orange"] - {rank=same; M_S1A M_S1B} - M_S1A [label="S1A:\nknow nothing"] - M_S1B [label="S1B:\nknow nothing\n(bound)" color="orange"] - M_S1A -> M_S1B [label="M_connected()"] - M_S1B -> M_S1A [label="M_lost()"] + {rank=same; M_S0A M_S0B} + M_S0A [label="S0A:\nknow nothing"] + M_S0B [label="S0B:\nknow nothing\n(bound)" color="orange"] + M_S0A -> M_S0B [label="M_connected()"] + M_S0B -> M_S0A [label="M_lost()"] - M_S1A -> M_S2A [label="M_set_nameplate()"] - M_S1B -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] + M_S0A -> M_S1A [label="M_set_nameplate()"] + M_S0B -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] - {rank=same; M_S2A M_S2B M_S2C M_P2_claim} - M_S2A [label="S2A:\nnot claimed"] - M_S2C [label="S2C:\nmaybe claimed"] - M_S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] - M_S2A -> M_P2_claim [label="M_connected()"] - M_S2A -> M_C_stop [label="M_close()" style="dashed"] + {rank=same; M_S1A M_C_stop M_P1A_queue} + M_S0B -> M_S2B [style="invis"] + M_S1A -> M_S2A [style="invis"] + M_S1A [label="S1A:\nnot claimed"] + M_S1A -> M_P2_claim [label="M_connected()"] + M_S1A -> M_C_stop [label="M_close()" style="dashed"] M_C_stop [shape="box" label="C_stop()" style="dashed"] + M_S1A -> M_P1A_queue [label="M_send(msg)" style="dotted"] + M_P1A_queue [shape="box" label="queue" style="dotted"] + M_P1A_queue -> M_S1A [style="dotted"] + + {rank=same; M_S2B M_S2A M_P2_claim} + M_S1A -> M_S2A [style="invis"] + M_S2A -> M_S3A [style="invis"] + M_S2A [label="S2A:\nmaybe claimed"] + M_S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] M_S2B -> M_SrB [label="M_close()" style="dashed"] M_SrB [label="SrB" style="dashed"] - M_S2C -> M_SrA [label="M_close()" style="dashed"] + M_S2A -> M_SrA [label="M_close()" style="dashed"] M_SrA [label="SrA" style="dashed"] - M_S2C -> M_P2_claim [label="M_connected()"] - M_S2B -> M_S2C [label="M_lost()"] + M_S2A -> M_P2_claim [label="M_connected()"] + M_S2B -> M_S2A [label="M_lost()"] M_P2_claim [shape="box" label="tx claim" color="orange"] M_P2_claim -> M_S2B [color="orange"] - M_S2A -> M_P2A_queue [label="M_send(msg)" style="dotted"] - M_P2A_queue [shape="box" label="queue" style="dotted"] - M_P2A_queue -> M_S2A [style="dotted"] - M_S2C -> M_P2C_queue [label="M_send(msg)" style="dotted"] + M_S2A -> M_P2C_queue [label="M_send(msg)" style="dotted"] M_P2C_queue [shape="box" label="queue" style="dotted"] - M_P2C_queue -> M_S2C [style="dotted"] + M_P2C_queue -> M_S2A [style="dotted"] M_S2B -> M_P2B_queue [label="M_send(msg)" style="dotted"] M_P2B_queue [shape="box" label="queue" style="dotted"] M_P2B_queue -> M_S2B [style="dotted"] - M_S2A -> M_S3A [label="(none)" style="invis"] + M_S1A -> M_S3A [label="(none)" style="invis"] M_S2B -> M_P_open [label="rx claimed" color="orange" fontcolor="orange"] M_P_open [shape="box" label="store mailbox\ntx open\ntx add(queued)" color="orange"] M_P_open -> M_S3B [color="orange"] @@ -76,7 +82,7 @@ digraph { M_P3_process_theirs2 [shape="octagon" label="process message" color="orange"] /* pay attention to the race here: this process_message() will deliver msg_pake to the WormholeMachine, which will compute_key() and - M_send(version), and we're in between M_S2A (where M_send gets + M_send(version), and we're in between M_S1A (where M_send gets queued) and M_S3A (where M_send gets sent and queued), and we're no longer passing through the M_P3_open phase (which drains the queue). So there's a real possibility of the outbound msg_version getting From 693e215d8b8678ae8ba052e78f7c957b756a684c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 13 Feb 2017 01:50:25 -0800 Subject: [PATCH 038/176] sketching out a journal-based demo app --- misc/demo-journal.py | 225 ++++++++++++++++++++++++++++++++++++++++ src/wormhole/journal.py | 23 ++++ 2 files changed, 248 insertions(+) create mode 100644 misc/demo-journal.py create mode 100644 src/wormhole/journal.py diff --git a/misc/demo-journal.py b/misc/demo-journal.py new file mode 100644 index 0000000..8211256 --- /dev/null +++ b/misc/demo-journal.py @@ -0,0 +1,225 @@ +import os, sys, json +from twisted.internet import task, defer, endpoints +from twisted.application import service, internet +from twisted.web import server, static, resource +from wormhole import journal + +class State(object): + @classmethod + def create_empty(klass): + self = klass() + # to avoid being tripped up by state-mutation side-effect bugs, we + # hold the serialized state in RAM, and re-deserialize it each time + # someone asks for a piece of it. + empty = {"version": 1, + "invitations": {}, # iid->invitation_state + "contacts": [], + } + self._bytes = json.dumps(empty).encode("utf-8") + return self + + @classmethod + def from_filename(klass, fn): + self = klass() + with open(fn, "rb") as f: + bytes = f.read() + self._bytes = bytes + # version check + data = self._as_data() + assert data["version"] == 1 + # schema check? + return self + + def save_to_filename(self, fn): + tmpfn = fn+".tmp" + with open(tmpfn, "wb") as f: + f.write(self._bytes) + os.rename(tmpfn, fn) + + def _as_data(self): + return json.loads(bytes.decode("utf-8")) + + @contextlib.contextmanager + def _mutate(self): + data = self._as_data() + yield data # mutable + self._bytes = json.dumps(data).encode("utf-8") + + def get_all_invitations(self): + return self._as_data()["invitations"] + def add_invitation(self, iid, invitation_state): + with self._mutate() as data: + data["invitations"][iid] = invitation_state + def update_invitation(self, iid, invitation_state): + with self._mutate() as data: + assert iid in data["invitations"] + data["invitations"][iid] = invitation_state + def remove_invitation(self, iid): + with self._mutate() as data: + del data["invitations"][iid] + + def add_contact(self, contact): + with self._mutate() as data: + data["contacts"].append(contact) + + + +class Root(resource.Resource): + pass + +class Status(resource.Resource): + def __init__(self, c): + resource.Resource.__init__(self) + self._call = c + def render_GET(self, req): + data = self._call() + req.setHeader(b"content-type", "text/plain") + return data + +class Action(resource.Resource): + def __init__(self, c): + resource.Resource.__init__(self) + self._call = c + def render_POST(self, req): + req.setHeader(b"content-type", "text/plain") + try: + args = json.load(req.content) + except ValueError: + req.setResponseCode(500) + return b"bad JSON" + data = self._call(args) + return data + +class Agent(service.MultiService): + def __init__(self, basedir, reactor): + service.MultiService.__init__(self) + self._basedir = basedir + self._reactor = reactor + + root = Root() + site = server.Site(root) + ep = endpoints.serverFromString(reactor, "tcp:8220") + internet.StreamServerEndpointService(ep, site).setServiceParent(self) + + self._jm = journal.JournalManager(self._save_state) + + root.putChild(b"", static.Data("root", "text/plain")) + root.putChild(b"list-invitations", Status(self._list_invitations)) + root.putChild(b"invite", Action(self._invite)) # {petname:} + root.putChild(b"accept", Action(self._accept)) # {petname:, code:} + + self._state_fn = os.path.join(self._basedir, "state.json") + self._state = State.from_filename(self._state_fn) + + self._wormholes = {} + for iid, invitation_state in self._state.get_all_invitations().items(): + def _dispatch(event, *args, **kwargs): + self._dispatch_wormhole_event(iid, event, *args, **kwargs) + w = wormhole.journaled_from_data(invitation_state["wormhole"], + reactor=self._reactor, + journal=self._jm, + event_handler=_dispatch) + self._wormholes[iid] = w + w.setServiceParent(self) + + + def _save_state(self): + self._state.save_to_filename(self._state_fn) + + def _list_invitations(self): + inv = self._state.get_all_invitations() + lines = ["%d: %s" % (iid, inv[iid]) for iid in sorted(inv)] + return b"\n".join(lines)+b"\n" + + def _invite(self, args): + print "invite", args + petname = args["petname"] + iid = random.randint(1,1000) + my_pubkey = random.randint(1,1000) + with self._jm.process(): + def _dispatch(event, *args, **kwargs): + self._dispatch_wormhole_event(iid, event, *args, **kwargs) + w = wormhole.journaled(reactor=self._reactor, + journal=self._jm, event_handler=_dispatch) + self._wormholes[iid] = w + w.setServiceParent(self) + w.get_code() # event_handler means code returns via callback + invitation_state = {"wormhole": w.to_data(), + "petname": petname, + "my_pubkey": my_pubkey, + } + self._state.add_invitation(iid, invitation_state) + return b"ok" + + def _accept(self, args): + print "accept", args + petname = args["petname"] + code = args["code"] + iid = random.randint(1,1000) + my_pubkey = random.randint(2,2000) + with self._jm.process(): + def _dispatch(event, *args, **kwargs): + self._dispatch_wormhole_event(iid, event, *args, **kwargs) + w = wormhole.wormhole(reactor=self._reactor, + event_dispatcher=_dispatch) + w.set_code(code) + md = {"my_pubkey": my_pubkey} + w.send(json.dumps(md).encode("utf-8")) + invitation_state = {"wormhole": w.to_data(), + "petname": petname, + "my_pubkey": my_pubkey, + } + self._state.add_invitation(iid, invitation_state) + return b"ok" + + def _dispatch_wormhole_event(self, iid, event, *args, **kwargs): + # we're already in a jm.process() context + invitation_state = self._state.get_all_invitations()[iid] + if event == "got-code": + (code,) = args + invitation_state["code"] = code + self._state.update_invitation(iid, invitation_state) + self._wormholes[iid].set_code(code) + # notify UI subscribers to update the display + elif event == "got-data": + (data,) = args + md = json.loads(data.decode("utf-8")) + contact = {"petname": invitation_state["petname"], + "my_pubkey": invitation_state["my_pubkey"], + "their_pubkey": md["my_pubkey"], + } + self._state.add_contact(contact) + self._wormholes[iid].close() + elif event == "closed": + self._wormholes[iid].disownServiceParent() + del self._wormholes[iid] + self._state.remove_invitation(iid) + + +def create(reactor, basedir): + os.mkdir(basedir) + s = State.create_empty() + s.save(os.path.join(basedir, "state.json")) + return defer.succeed(None) + +def run(reactor, basedir): + a = Agent(basedir, reactor) + a.startService() + print "agent listening on http://localhost:8220/" + d = defer.Deferred() + return d + + + +if __name__ == "__main__": + command = sys.argv[1] + basedir = sys.argv[2] + if command == "create": + task.react(create, (basedir,)) + elif command == "run": + task.react(run, (basedir,)) + else: + print "Unrecognized subcommand '%s'" % command + sys.exit(1) + + diff --git a/src/wormhole/journal.py b/src/wormhole/journal.py new file mode 100644 index 0000000..b3f9f08 --- /dev/null +++ b/src/wormhole/journal.py @@ -0,0 +1,23 @@ +import contextlib + +class JournalManager(object): + def __init__(self, save_checkpoint): + self._save_checkpoint = save_checkpoint + self._outbound_queue = [] + self._processing = False + + def queue_outbound(self, fn, *args, **kwargs): + assert self._processing + self._outbound_queue.append((fn, args, kwargs)) + + @contextlib.contextmanager + def process(self): + assert not self._processing + assert not self._outbound_queue + self._processing = True + yield # process inbound messages, change state, queue outbound + self._save_checkpoint() + for (fn, args, kwargs) in self._outbound_queue: + fn(*args, **kwargs) + self._outbound_queue[:] = [] + self._processing = False From 16c477424cfb83e49524848cc93d9cf0c4b91593 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 13 Feb 2017 23:12:57 -0800 Subject: [PATCH 039/176] more demo work --- docs/w2.dot | 16 +++--- docs/w3.dot | 13 +++-- misc/demo-journal.py | 107 +++++++++++++++++++++++++++------------ src/wormhole/_mailbox.py | 88 +++++++++++++++++++------------- src/wormhole/wormhole.py | 39 ++++++++++++++ 5 files changed, 183 insertions(+), 80 deletions(-) diff --git a/docs/w2.dot b/docs/w2.dot index cede4be..f56f3a8 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -32,17 +32,19 @@ digraph { M_P1A_queue -> M_S1A [style="dotted"] {rank=same; M_S2B M_S2A M_P2_claim} - M_S1A -> M_S2A [style="invis"] - M_S2A -> M_S3A [style="invis"] M_S2A [label="S2A:\nmaybe claimed"] M_S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] - M_S2B -> M_SrB [label="M_close()" style="dashed"] - M_SrB [label="SrB" style="dashed"] - M_S2A -> M_SrA [label="M_close()" style="dashed"] - M_SrA [label="SrA" style="dashed"] + #M_S2B -> M_SrB [label="M_close()" style="dashed"] + #M_SrB [label="SrB" style="dashed"] + #M_S2A -> M_SrA [label="M_close()" style="dashed"] + #M_SrA [label="SrA" style="dashed"] M_S2A -> M_P2_claim [label="M_connected()"] - M_S2B -> M_S2A [label="M_lost()"] + #M_S2B -> M_S2A [label="M_lost()"] # causes bad layout + M_S2B -> foo [label="M_lost()"] + foo [label="" style="dashed"] + foo -> M_S2A + M_P2_claim [shape="box" label="tx claim" color="orange"] M_P2_claim -> M_S2B [color="orange"] M_S2A -> M_P2C_queue [label="M_send(msg)" style="dotted"] diff --git a/docs/w3.dot b/docs/w3.dot index 7cb6d61..9da7688 100644 --- a/docs/w3.dot +++ b/docs/w3.dot @@ -41,18 +41,17 @@ digraph { MC_Ss [label="Ss: closed" color="green"] + MC_S0A [label="S0A" style="dashed"] + MC_S0A -> MC_P_stop [style="dashed"] + MC_S0B [label="S0B" style="dashed" color="orange"] + MC_S0B -> MC_P_stop [style="dashed" color="orange"] - {rank=same; MC_S2A MC_S2B MC_S2C MC_S1A MC_S1B MC_S3A MC_S3B MC_S4A MC_S4B MC_S5A MC_S5B} + {rank=same; MC_S2A MC_S2B MC_S3A MC_S3B MC_S4A MC_S4B MC_S5A MC_S5B} MC_S1A [label="S1A" style="dashed"] MC_S1A -> MC_P_stop [style="dashed"] - MC_S1B [label="S1B" color="orange" style="dashed"] - MC_S1B -> MC_P_stop [style="dashed" color="orange"] - MC_S2C -> MC_S2A [style="invis"] MC_S2A [label="S2A" style="dashed"] - MC_S2A -> MC_P_stop [style="dashed"] - MC_S2C [label="S2C" style="dashed"] - MC_S2C -> MC_SrA [style="dashed"] + MC_S2A -> MC_SrA [style="dashed"] MC_S2B [label="S2B" color="orange" style="dashed"] MC_S2B -> MC_Pr [color="orange" style="dashed"] diff --git a/misc/demo-journal.py b/misc/demo-journal.py index 8211256..ff76636 100644 --- a/misc/demo-journal.py +++ b/misc/demo-journal.py @@ -2,7 +2,15 @@ import os, sys, json from twisted.internet import task, defer, endpoints from twisted.application import service, internet from twisted.web import server, static, resource -from wormhole import journal +from wormhole import journal, wormhole + +# considerations for state management: +# * be somewhat principled about the data (e.g. have a schema) +# * discourage accidental schema changes +# * avoid surprise mutations by app code (don't hand out mutables) +# * discourage app from keeping state itself: make state object easy enough +# to use for everything. App should only hold objects that are active +# (Services, subscribers, etc). App must wire up these objects each time. class State(object): @classmethod @@ -118,7 +126,8 @@ class Agent(service.MultiService): w = wormhole.journaled_from_data(invitation_state["wormhole"], reactor=self._reactor, journal=self._jm, - event_handler=_dispatch) + event_handler=self, + event_handler_args=(iid,)) self._wormholes[iid] = w w.setServiceParent(self) @@ -134,13 +143,18 @@ class Agent(service.MultiService): def _invite(self, args): print "invite", args petname = args["petname"] + # it'd be better to use a unique object for the event_handler + # correlation, but we can't store them into the state database. I'm + # not 100% sure we need one for the database: maybe it should hold a + # list instead, and assign lookup keys at runtime. If they really + # need to be serializable, they should be allocated rather than + # random. iid = random.randint(1,1000) my_pubkey = random.randint(1,1000) with self._jm.process(): - def _dispatch(event, *args, **kwargs): - self._dispatch_wormhole_event(iid, event, *args, **kwargs) - w = wormhole.journaled(reactor=self._reactor, - journal=self._jm, event_handler=_dispatch) + w = wormhole.journaled(reactor=self._reactor, journal=self._jm, + event_handler=self, + event_handler_args=(iid,)) self._wormholes[iid] = w w.setServiceParent(self) w.get_code() # event_handler means code returns via callback @@ -158,10 +172,9 @@ class Agent(service.MultiService): iid = random.randint(1,1000) my_pubkey = random.randint(2,2000) with self._jm.process(): - def _dispatch(event, *args, **kwargs): - self._dispatch_wormhole_event(iid, event, *args, **kwargs) - w = wormhole.wormhole(reactor=self._reactor, - event_dispatcher=_dispatch) + w = wormhole.journaled(reactor=self._reactor, journal=self._jm, + event_dispatcher=self, + event_dispatcher_args=(iid,)) w.set_code(code) md = {"my_pubkey": my_pubkey} w.send(json.dumps(md).encode("utf-8")) @@ -172,29 +185,61 @@ class Agent(service.MultiService): self._state.add_invitation(iid, invitation_state) return b"ok" - def _dispatch_wormhole_event(self, iid, event, *args, **kwargs): + # dispatch options: + # * register one function, which takes (eventname, *args) + # * to handle multiple wormholes, app must give is a closure + # * register multiple functions (one per event type) + # * register an object, with well-known method names + # * extra: register args and/or kwargs with the callback + # + # events to dispatch: + # generated_code(code) + # got_verifier(verifier_bytes) + # verified() + # got_data(data_bytes) + # closed() + + def wormhole_dispatch_got_code(self, code, iid): # we're already in a jm.process() context invitation_state = self._state.get_all_invitations()[iid] - if event == "got-code": - (code,) = args - invitation_state["code"] = code - self._state.update_invitation(iid, invitation_state) - self._wormholes[iid].set_code(code) - # notify UI subscribers to update the display - elif event == "got-data": - (data,) = args - md = json.loads(data.decode("utf-8")) - contact = {"petname": invitation_state["petname"], - "my_pubkey": invitation_state["my_pubkey"], - "their_pubkey": md["my_pubkey"], - } - self._state.add_contact(contact) - self._wormholes[iid].close() - elif event == "closed": - self._wormholes[iid].disownServiceParent() - del self._wormholes[iid] - self._state.remove_invitation(iid) - + invitation_state["code"] = code + self._state.update_invitation(iid, invitation_state) + self._wormholes[iid].set_code(code) + # notify UI subscribers to update the display + + def wormhole_dispatch_got_verifier(self, verifier, iid): + pass + def wormhole_dispatch_verified(self, _, iid): + pass + + def wormhole_dispatch_got_data(self, data, iid): + invitation_state = self._state.get_all_invitations()[iid] + md = json.loads(data.decode("utf-8")) + contact = {"petname": invitation_state["petname"], + "my_pubkey": invitation_state["my_pubkey"], + "their_pubkey": md["my_pubkey"], + } + self._state.add_contact(contact) + self._wormholes[iid].close() # now waiting for "closed" + + def wormhole_dispatch_closed(self, _, iid): + self._wormholes[iid].disownServiceParent() + del self._wormholes[iid] + self._state.remove_invitation(iid) + + + def handle_app_event(self, args, ack_f): # sample function + # Imagine here that the app has received a message (not + # wormhole-related) from some other server, and needs to act on it. + # Also imagine that ack_f() is how we tell the sender that they can + # stop sending the message, or how we ask our poller/subscriber + # client to send a DELETE message. If the process dies before ack_f() + # delivers whatever it needs to deliver, then in the next launch, + # handle_app_event() will be called again. + stuff = parse(args) + with self._jm.process(): + update_my_state() + self._jm.queue_outbound(ack_f) def create(reactor, basedir): os.mkdir(basedir) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index c03e252..2c298b7 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -9,49 +9,66 @@ class _Mailbox_Machine(object): @m.state(initial=True) def initial(self): pass - @m.state() - def S1A(self): pass # know nothing, not connected - @m.state() - def S1B(self): pass # know nothing, yes connected + # all -A states: not connected + # all -B states: yes connected + # B states serialize as A, so they deserialize as unconnected + # S0: know nothing @m.state() - def S2A(self): pass # not claimed, not connected + def S0A(self): pass @m.state() - def S2B(self): pass # maybe claimed, yes connected - @m.state() - def S2C(self): pass # maybe claimed, not connected + def S0B(self): pass + # S1: nameplate known, not claimed @m.state() - def S3A(self): pass # claimed, maybe opened, not connected - @m.state() - def S3B(self): pass # claimed, maybe opened, yes connected + def S1A(self): pass + # S2: nameplate known, maybe claimed @m.state() - def S4A(self): pass # maybe released, maybe opened, not connected + def S2A(self): pass @m.state() - def S4B(self): pass # maybe released, maybe opened, yes connected + def S2B(self): pass + # S3: nameplate claimed, mailbox known, maybe open @m.state() - def S5A(self): pass # released, maybe open, not connected + def S3A(self): pass @m.state() - def S5B(self): pass # released, maybe open, yes connected + def S3B(self): pass + # S4: mailbox maybe open, nameplate maybe released + # We've definitely opened the mailbox at least once, but it must be + # re-opened with each connection, because open() is also subscribe() @m.state() - def SrcA(self): pass # waiting for release+close, not connected + def S4A(self): pass @m.state() - def SrcB(self): pass # waiting for release+close, yes connected + def S4B(self): pass + + # S5: mailbox maybe open, nameplate released @m.state() - def SrA(self): pass # waiting for release, not connected + def S5A(self): pass @m.state() - def SrB(self): pass # waiting for release, yes connected + def S5B(self): pass + + # Src: waiting for release+close @m.state() - def ScA(self): pass # waiting for close, not connected + def SrcA(self): pass @m.state() - def ScB(self): pass # waiting for close, yes connected + def SrcB(self): pass + # Sr: closed (or never opened), waiting for release @m.state() - def SsB(self): pass # closed, stopping + def SrA(self): pass @m.state() - def Ss(self): pass # stopped + def SrB(self): pass + # Sc: released (or never claimed), waiting for close + @m.state() + def ScA(self): pass + @m.state() + def ScB(self): pass + # Ss: closed and released, waiting for stop + @m.state() + def SsB(self): pass + @m.state() + def Ss(self): pass # terminal def connected(self, ws): @@ -110,26 +127,27 @@ class _Mailbox_Machine(object): def C_stop(self): pass - initial.upon(M_start_connected, enter=S1A, outputs=[]) - initial.upon(M_start_unconnected, enter=S1B, outputs=[]) - S1A.upon(M_connected, enter=S1B, outputs=[]) - S1A.upon(M_set_nameplate, enter=S2A, outputs=[]) - S1A.upon(M_stop, enter=SsB, outputs=[C_stop]) - S1B.upon(M_lost, enter=S1A, outputs=[]) - S1B.upon(M_set_nameplate, enter=S2B, outputs=[tx_claim]) - S1B.upon(M_stop, enter=SsB, outputs=[C_stop]) + initial.upon(M_start_unconnected, enter=S0A, outputs=[]) + initial.upon(M_start_connected, enter=S0B, outputs=[]) + S0A.upon(M_connected, enter=S0B, outputs=[]) + S0A.upon(M_set_nameplate, enter=S1A, outputs=[]) + S0A.upon(M_stop, enter=SsB, outputs=[C_stop]) + S0B.upon(M_lost, enter=S0A, outputs=[]) + S0B.upon(M_set_nameplate, enter=S2B, outputs=[tx_claim]) + S0B.upon(M_stop, enter=SsB, outputs=[C_stop]) + + S1A.upon(M_connected, enter=S2B, outputs=[tx_claim]) + S1A.upon(M_send, enter=S1A, outputs=[queue]) + S1A.upon(M_stop, enter=SrA, outputs=[]) S2A.upon(M_connected, enter=S2B, outputs=[tx_claim]) S2A.upon(M_stop, enter=SsB, outputs=[C_stop]) S2A.upon(M_send, enter=S2A, outputs=[queue]) - S2B.upon(M_lost, enter=S2C, outputs=[]) + S2B.upon(M_lost, enter=S2A, outputs=[]) S2B.upon(M_send, enter=S2B, outputs=[queue]) S2B.upon(M_stop, enter=SrB, outputs=[tx_release]) S2B.upon(M_rx_claimed, enter=S3B, outputs=[store_mailbox, tx_open, tx_add_queued]) - S2C.upon(M_connected, enter=S2B, outputs=[tx_claim]) - S2C.upon(M_send, enter=S2C, outputs=[queue]) - S2C.upon(M_stop, enter=SrA, outputs=[]) S3A.upon(M_connected, enter=S3B, outputs=[tx_open, tx_add_queued]) S3A.upon(M_send, enter=S3A, outputs=[queue]) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index ddcf290..4b9c5d9 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -919,3 +919,42 @@ def wormhole(appid, relay_url, reactor, tor_manager=None, timing=None, # timing = timing or DebugTiming() # w = _Wormhole.from_serialized(data, reactor, timing) # return w + + +# considerations for activity management: +# * websocket to server wants to be a t.a.i.ClientService +# * if Wormhole is a MultiService: +# * makes it easier to chain the ClientService to it +# * implies that nothing will happen before w.startService() +# * implies everything stops upon d=w.stopService() +# * if not: +# * + +class _JournaledWormhole(service.MultiService): + def __init__(self, reactor, journal_manager, event_dispatcher, + event_dispatcher_args=()): + pass + +class ImmediateJM(object): + def queue_outbound(self, fn, *args, **kwargs): + fn(*args, **kwargs) + @contextlib.contextmanager + def process(self): + yield + +class _Wormhole(_JournaledWormhole): + # send events to self, deliver them via Deferreds + def __init__(self, reactor): + _JournaledWormhole.__init__(self, reactor, ImmediateJM(), self) + +def wormhole(reactor): + w = _Wormhole(reactor) + w.startService() + return w + +def journaled_from_data(state, reactor, journal, + event_handler, event_handler_args=()): + pass + +def journaled(reactor, journal, event_handler, event_handler_args()): + pass From 40e0d6b66359ea8de61107ecdb6cd0c4f78ce3f3 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2017 12:11:17 -0800 Subject: [PATCH 040/176] more work, feels better now --- docs/machines.dot | 36 +++ docs/nameplates.dot | 47 ++++ docs/w2.dot | 20 +- src/wormhole/_connection.py | 371 +++------------------------ src/wormhole/_mailbox.py | 21 +- src/wormhole/_nameplate.py | 53 ++++ src/wormhole/_wormhole.py | 145 +++++++++++ src/wormhole/misc.py | 489 ++++++++++++++++++++++++++++++++++++ 8 files changed, 824 insertions(+), 358 deletions(-) create mode 100644 docs/machines.dot create mode 100644 docs/nameplates.dot create mode 100644 src/wormhole/_nameplate.py create mode 100644 src/wormhole/_wormhole.py create mode 100644 src/wormhole/misc.py diff --git a/docs/machines.dot b/docs/machines.dot new file mode 100644 index 0000000..b54a4e7 --- /dev/null +++ b/docs/machines.dot @@ -0,0 +1,36 @@ +digraph { + Wormhole [shape="box" label="Wormhole\n(manager)"] + Wormhole -> Mailbox [style="dashed" + label="M_set_nameplate()\nM_send()\nM_close()" + ] + Wormhole -> Mailbox + Mailbox -> Wormhole [style="dashed" + label="W_rx_pake()\nW_rx_msg()\nW_closed()" + ] + Mailbox [shape="box"] + Mailbox -> Connection [style="dashed" + label="C_tx_add()\nC_stop()" + ] + Mailbox -> Connection + Connection -> Mailbox [style="dashed" + label="M_connected()\nM_lost()\nM_rx_claimed()\nM_rx_message()\nM_rx_released()\nM_stopped()"] + + Connection -> websocket + + Nameplates [shape="box" label="Nameplate\nLister"] + Wormhole -> Nameplates [style="dashed" + label="NL_refresh_nameplates()" + ] + Nameplates -> Wormhole [style="dashed" + label="W_got_nameplates()" + ] + Connection -> Nameplates [style="dashed" + label="NL_connected()\nNL_lost()\nNL_rx_nameplates()" + ] + Nameplates -> Connection [style="dashed" + label="C_tx_list()" + ] + + + +} diff --git a/docs/nameplates.dot b/docs/nameplates.dot new file mode 100644 index 0000000..83bc86e --- /dev/null +++ b/docs/nameplates.dot @@ -0,0 +1,47 @@ +digraph { + /* + "connected": NL_connected + "rx": NL_rx_nameplates + "refresh": NL_refresh_nameplates + */ + {rank=same; NL_title NL_S0A NL_S0B} + NL_title [label="Nameplate\nLister" style="dotted"] + + NL_S0A [label="S0A:\nnot wanting\nunconnected"] + NL_S0B [label="S0B:\nnot wanting\nconnected" color="orange"] + + NL_S0A -> NL_S0B [label="connected"] + NL_S0B -> NL_S0A [label="lost"] + + NL_S0A -> NL_S1A [label="refresh"] + NL_S0B -> NL_P_tx [label="refresh" color="orange"] + + NL_S0A -> NL_P_tx [style="invis"] + + {rank=same; NL_S1A NL_P_tx NL_S1B NL_C_notify} + + NL_S1A [label="S1A:\nwant list\nunconnected"] + NL_S1B [label="S1B:\nwant list\nconnected" color="orange"] + + NL_S1A -> NL_P_tx [label="connected"] + NL_P_tx [shape="box" label="C.tx_list()" color="orange"] + NL_P_tx -> NL_S1B [color="orange"] + NL_S1B -> NL_S1A [label="lost"] + + NL_S1A -> foo [label="refresh"] + foo [label="" style="dashed"] + foo -> NL_S1A + + NL_S1B -> foo2 [label="refresh"] + foo2 [label="" style="dashed"] + foo2 -> NL_P_tx + + NL_S0B -> NL_C_notify [label="rx"] + NL_S1B -> NL_C_notify [label="rx"] + NL_C_notify [shape="box" label="W.got_nameplates()"] + NL_C_notify -> NL_S0B + + {rank=same; foo foo2 legend} + legend [shape="box" style="dotted" + label="connected: NL_connected()\nlost: NL_lost()\nrefresh: NL_refresh_nameplates()\nrx: NL_rx_nameplates()"] +} diff --git a/docs/w2.dot b/docs/w2.dot index f56f3a8..6b9d82c 100644 --- a/docs/w2.dot +++ b/docs/w2.dot @@ -1,11 +1,11 @@ digraph { /* new idea */ - {rank=same; M_entry_whole_code M_title M_entry_allocation M_entry_interactive} + {rank=same; M_title M_entry_whole_code M_entry_allocation M_entry_interactive} M_entry_whole_code [label="whole\ncode"] M_entry_whole_code -> M_S0A M_title [label="Message\nMachine" style="dotted"] - M_entry_whole_code -> M_title [style="invis"] + M_entry_allocation [label="allocation" color="orange"] M_entry_allocation -> M_S0B [label="already\nconnected" color="orange" fontcolor="orange"] M_entry_interactive [label="interactive" color="orange"] @@ -20,13 +20,11 @@ digraph { M_S0A -> M_S1A [label="M_set_nameplate()"] M_S0B -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] - {rank=same; M_S1A M_C_stop M_P1A_queue} + {rank=same; M_S1A M_P1A_queue} M_S0B -> M_S2B [style="invis"] M_S1A -> M_S2A [style="invis"] M_S1A [label="S1A:\nnot claimed"] M_S1A -> M_P2_claim [label="M_connected()"] - M_S1A -> M_C_stop [label="M_close()" style="dashed"] - M_C_stop [shape="box" label="C_stop()" style="dashed"] M_S1A -> M_P1A_queue [label="M_send(msg)" style="dotted"] M_P1A_queue [shape="box" label="queue" style="dotted"] M_P1A_queue -> M_S1A [style="dotted"] @@ -60,8 +58,8 @@ digraph { M_P_open -> M_S3B [color="orange"] {rank=same; M_S3A M_S3B M_P3_open M_P3_send} - M_S3A [label="S3A:\nclaimed\nmaybe open"] - M_S3B [label="S3B:\nclaimed\nmaybe open\n(bound)" color="orange"] + M_S3A [label="S3A:\nclaimed\nopened >=once"] + M_S3B [label="S3B:\nclaimed\nmaybe open now\n(bound)" color="orange"] M_S3A -> M_P3_open [label="M_connected()"] M_S3B -> M_S3A [label="M_lost()"] M_P3_open [shape="box" label="tx open\ntx add(queued)"] @@ -92,9 +90,9 @@ digraph { M_P3_process_theirs2 -> M_S4B [color="orange"] {rank=same; M_S4A M_P4_release M_S4B M_P4_process M_P4_send M_P4_queue} - M_S4A [label="S4A:\nmaybe released\nmaybe open\n"] + M_S4A [label="S4A:\nmaybe released\nopened >=once\n"] - M_S4B [label="S4B:\nmaybe released\nmaybe open\n(bound)" color="orange"] + M_S4B [label="S4B:\nmaybe released\nmaybe open now\n(bound)" color="orange"] M_S4A -> M_P4_release [label="M_connected()"] M_P4_release [shape="box" label="tx open\ntx add(queued)\ntx release"] M_S4B -> M_P4_send [label="M_send(msg)"] @@ -118,8 +116,8 @@ digraph { M_S4A -> seed [style="invis"] seed -> M_S5A {rank=same; seed M_S5A M_S5B M_P5_open M_process} - M_S5A [label="S5A:\nreleased\nmaybe open"] - M_S5B [label="S5B:\nreleased\nmaybe open\n(bound)" color="green"] + M_S5A [label="S5A:\nreleased\nopened >=once"] + M_S5B [label="S5B:\nreleased\nmaybe open now\n(bound)" color="green"] M_S5A -> M_P5_open [label="M_connected()"] M_P5_open [shape="box" label="tx open\ntx add(queued)"] M_P5_open -> M_S5B diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 808b70f..735e8d3 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -2,6 +2,7 @@ from six.moves.urllib_parse import urlparse from attr import attrs, attrib from twisted.internet import defer, endpoints #, error +from twisted.application import internet from autobahn.twisted import websocket from automat import MethodicalMachine @@ -16,6 +17,7 @@ class WSClient(websocket.WebSocketClientProtocol): # this fires when the WebSocket is ready to go. No arguments print("onOpen", args) #self.wormhole_open = True + # send BIND, since the MailboxMachine does not self.connection_machine.protocol_onOpen(self) #self.factory.d.callback(self) @@ -46,121 +48,49 @@ class WSFactory(websocket.WebSocketClientFactory): # pip install (path to automat checkout)[visualize] # automat-visualize wormhole._connection -@attrs -class _WSRelayClient_Machine(object): - _c = attrib() - m = MethodicalMachine() - - @m.state(initial=True) - def initial(self): pass - @m.state() - def connecting(self): pass - @m.state() - def negotiating(self): pass - @m.state(terminal=True) - def failed(self): pass - @m.state() - def open(self): pass - @m.state() - def waiting(self): pass - @m.state() - def reconnecting(self): pass - @m.state() - def disconnecting(self): pass - @m.state() - def cancelling(self): pass - @m.state(terminal=True) - def closed(self): pass - - @m.input() - def start(self): pass ; print("input:start") - @m.input() - def d_callback(self): pass ; print("input:d_callback") - @m.input() - def d_errback(self): pass ; print("input:d_errback") - @m.input() - def d_cancel(self): pass ; print("input:d_cancel") - @m.input() - def onOpen(self): pass ; print("input:onOpen") - @m.input() - def onClose(self): pass ; print("input:onClose") - @m.input() - def expire(self): pass - @m.input() - def stop(self): pass - - # outputs - @m.output() - def ep_connect(self): - "ep.connect()" - self._c.ep_connect() - @m.output() - def reset_timer(self): - self._c.reset_timer() - @m.output() - def connection_established(self): - print("connection_established") - self._c.connection_established() - @m.output() - def M_lost(self): - self._c.M_lost() - @m.output() - def start_timer(self): - self._c.start_timer() - @m.output() - def cancel_timer(self): - self._c.cancel_timer() - @m.output() - def dropConnection(self): - self._c.dropConnection() - @m.output() - def notify_fail(self): - self._c.notify_fail() - @m.output() - def MC_stopped(self): - self._c.MC_stopped() - - initial.upon(start, enter=connecting, outputs=[ep_connect]) - connecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) - connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) - connecting.upon(onClose, enter=failed, outputs=[notify_fail]) - connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) - cancelling.upon(d_errback, enter=closed, outputs=[]) - - negotiating.upon(onOpen, enter=open, outputs=[connection_established]) - negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) - negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) - - open.upon(onClose, enter=waiting, outputs=[M_lost, start_timer]) - open.upon(stop, enter=disconnecting, outputs=[dropConnection, M_lost]) - reconnecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) - reconnecting.upon(d_errback, enter=waiting, outputs=[start_timer]) - reconnecting.upon(onClose, enter=waiting, outputs=[start_timer]) - reconnecting.upon(stop, enter=cancelling, outputs=[d_cancel]) - - waiting.upon(expire, enter=reconnecting, outputs=[ep_connect]) - waiting.upon(stop, enter=closed, outputs=[cancel_timer]) - disconnecting.upon(onClose, enter=closed, outputs=[MC_stopped]) +class IRendezvousClient(Interface): + # must be an IService too + def set_dispatch(dispatcher): + """Assign a dispatcher object to this client. The following methods + will be called on this object when things happen: + * rx_welcome(welcome -> dict) + * rx_nameplates(nameplates -> list) # [{id: str,..}, ..] + * rx_allocated(nameplate -> str) + * rx_claimed(mailbox -> str) + * rx_released() + * rx_message(side -> str, phase -> str, body -> str, msg_id -> str) + * rx_closed() + * rx_pong(pong -> int) + """ + pass + def tx_list(): pass + def tx_allocate(): pass + def tx_claim(nameplate): pass + def tx_release(): pass + def tx_open(mailbox): pass + def tx_add(phase, body): pass + def tx_close(mood): pass + def tx_ping(ping): pass # We have one WSRelayClient for each wsurl we know about, and it lasts # as long as its parent Wormhole does. @attrs -class WSRelayClient(object): +class WSRelayClient(service.MultiService, object): + _journal = attrib() _wormhole = attrib() _mailbox = attrib() _ws_url = attrib() _reactor = attrib() - INITIAL_DELAY = 1.0 - def __init__(self): - self._m = _WSRelayClient_Machine(self) - self._f = f = WSFactory(self._ws_url) + f = WSFactory(self._ws_url) f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) f.connection_machine = self # calls onOpen and onClose p = urlparse(self._ws_url) - self._ep = self._make_endpoint(p.hostname, p.port or 80) + ep = self._make_endpoint(p.hostname, p.port or 80) + # default policy: 1s initial, random exponential backoff, max 60s + self._client_service = internet.ClientService(ep, f) self._connector = None self._done_d = defer.Deferred() self._current_delay = self.INITIAL_DELAY @@ -248,242 +178,3 @@ if __name__ == "__main__": # ??? a new WSConnection is created each time the WSRelayClient gets through # negotiation - -class NameplateListingMachine(object): - m = MethodicalMachine() - def __init__(self): - self._list_nameplate_waiters = [] - - # Ideally, each API request would spawn a new "list_nameplates" message - # to the server, so the response would be maximally fresh, but that would - # require correlating server request+response messages, and the protocol - # is intended to be less stateful than that. So we offer a weaker - # freshness property: if no server requests are in flight, then a new API - # request will provoke a new server request, and the result will be - # fresh. But if a server request is already in flight when a second API - # request arrives, both requests will be satisfied by the same response. - - @m.state(initial=True) - def idle(self): pass - @m.state() - def requesting(self): pass - - @m.input() - def list_nameplates(self): pass # returns Deferred - @m.input() - def response(self, message): pass - - @m.output() - def add_deferred(self): - d = defer.Deferred() - self._list_nameplate_waiters.append(d) - return d - @m.output() - def send_request(self): - self._connection.send_command("list") - @m.output() - def distribute_response(self, message): - nameplates = parse(message) - waiters = self._list_nameplate_waiters - self._list_nameplate_waiters = [] - for d in waiters: - d.callback(nameplates) - - idle.upon(list_nameplates, enter=requesting, - outputs=[add_deferred, send_request], - collector=lambda outs: outs[0]) - idle.upon(response, enter=idle, outputs=[]) - requesting.upon(list_nameplates, enter=requesting, - outputs=[add_deferred], - collector=lambda outs: outs[0]) - requesting.upon(response, enter=idle, outputs=[distribute_response]) - - # nlm._connection = c = Connection(ws) - # nlm.list_nameplates().addCallback(display_completions) - # c.register_dispatch("nameplates", nlm.response) - -class Wormhole: - m = MethodicalMachine() - - def __init__(self, ws_url, reactor): - self._relay_client = WSRelayClient(self, ws_url, reactor) - # This records all the messages we want the relay to have. Each time - # we establish a connection, we'll send them all (and the relay - # server will filter out duplicates). If we add any while a - # connection is established, we'll send the new ones. - self._outbound_messages = [] - - # these methods are called from outside - def start(self): - self._relay_client.start() - - # and these are the state-machine transition functions, which don't take - # args - @m.state() - def closed(initial=True): pass - @m.state() - def know_code_not_mailbox(): pass - @m.state() - def know_code_and_mailbox(): pass # no longer need nameplate - @m.state() - def waiting_first_msg(): pass # key is established, want any message - @m.state() - def processing_version(): pass - @m.state() - def processing_phase(): pass - @m.state() - def open(): pass # key is verified, can post app messages - @m.state(terminal=True) - def failed(): pass - - @m.input() - def deliver_message(self, message): pass - - def w_set_seed(self, code, mailbox): - """Call w_set_seed when we sprout a Wormhole Seed, which - contains both the code and the mailbox""" - self.w_set_code(code) - self.w_set_mailbox(mailbox) - - @m.input() - def w_set_code(self, code): - """Call w_set_code when you learn the code, probably because the user - typed it in.""" - @m.input() - def w_set_mailbox(self, mailbox): - """Call w_set_mailbox() when you learn the mailbox id, from the - response to claim_nameplate""" - pass - - - @m.input() - def rx_pake(self, pake): pass # reponse["message"][phase=pake] - - @m.input() - def rx_version(self, version): # response["message"][phase=version] - pass - @m.input() - def verify_good(self, verifier): pass - @m.input() - def verify_bad(self, f): pass - - @m.input() - def rx_phase(self, message): pass - @m.input() - def phase_good(self, message): pass - @m.input() - def phase_bad(self, f): pass - - @m.output() - def compute_and_post_pake(self, code): - self._code = code - self._pake = compute(code) - self._post(pake=self._pake) - self._ws_send_command("add", phase="pake", body=XXX(pake)) - @m.output() - def set_mailbox(self, mailbox): - self._mailbox = mailbox - @m.output() - def set_seed(self, code, mailbox): - self._code = code - self._mailbox = mailbox - - @m.output() - def process_version(self, version): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) - pass - - @m.output() - def notify_verified(self, verifier): - for d in self._verify_waiters: - d.callback(verifier) - @m.output() - def notify_failed(self, f): - for d in self._verify_waiters: - d.errback(f) - - @m.output() - def process_phase(self, message): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) - pass - - @m.output() - def post_inbound(self, message): - pass - - @m.output() - def deliver_message(self, message): - self._qc.deliver_message(message) - - @m.output() - def compute_key_and_post_version(self, pake): - self._key = x - self._verifier = x - plaintext = dict_to_bytes(self._my_versions) - phase = "version" - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) - self._msg_send(phase, encrypted) - - closed.upon(w_set_code, enter=know_code_not_mailbox, - outputs=[compute_and_post_pake]) - know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, - outputs=[set_mailbox]) - know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg, - outputs=[compute_key_and_post_version]) - waiting_first_msg.upon(rx_version, enter=processing_version, - outputs=[process_version]) - processing_version.upon(verify_good, enter=open, outputs=[notify_verified]) - processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed]) - open.upon(rx_phase, enter=processing_phase, outputs=[process_phase]) - processing_phase.upon(phase_good, enter=open, outputs=[post_inbound]) - processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed]) - -class QueueConnect: - m = MethodicalMachine() - def __init__(self): - self._outbound_messages = [] - self._connection = None - @m.state() - def disconnected(): pass - @m.state() - def connected(): pass - - @m.input() - def deliver_message(self, message): pass - @m.input() - def connect(self, connection): pass - @m.input() - def disconnect(self): pass - - @m.output() - def remember_connection(self, connection): - self._connection = connection - @m.output() - def forget_connection(self): - self._connection = None - @m.output() - def queue_message(self, message): - self._outbound_messages.append(message) - @m.output() - def send_message(self, message): - self._connection.send(message) - @m.output() - def send_queued_messages(self, connection): - for m in self._outbound_messages: - connection.send(m) - - disconnected.upon(deliver_message, enter=disconnected, outputs=[queue_message]) - disconnected.upon(connect, enter=connected, outputs=[remember_connection, - send_queued_messages]) - connected.upon(deliver_message, enter=connected, - outputs=[queue_message, send_message]) - connected.upon(disconnect, enter=disconnected, outputs=[forget_connection]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 2c298b7..f67bb5a 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -3,6 +3,7 @@ from automat import MethodicalMachine @attrs class _Mailbox_Machine(object): + _connection_machine = attrib() _m = attrib() m = MethodicalMachine() @@ -120,12 +121,17 @@ class _Mailbox_Machine(object): @m.output() def tx_close(self): pass @m.output() + def process_first_msg_from_them(self, msg): + self.tx_release() + self.process_msg_from_them(msg) + @m.output() def process_msg_from_them(self, msg): pass @m.output() def dequeue(self, msg): pass @m.output() def C_stop(self): pass - + @m.output() + def WM_stopped(self): pass initial.upon(M_start_unconnected, enter=S0A, outputs=[]) initial.upon(M_start_connected, enter=S0B, outputs=[]) @@ -138,10 +144,10 @@ class _Mailbox_Machine(object): S1A.upon(M_connected, enter=S2B, outputs=[tx_claim]) S1A.upon(M_send, enter=S1A, outputs=[queue]) - S1A.upon(M_stop, enter=SrA, outputs=[]) + S1A.upon(M_stop, enter=SsB, outputs=[C_stop]) S2A.upon(M_connected, enter=S2B, outputs=[tx_claim]) - S2A.upon(M_stop, enter=SsB, outputs=[C_stop]) + S2A.upon(M_stop, enter=SrA, outputs=[]) S2A.upon(M_send, enter=S2A, outputs=[queue]) S2B.upon(M_lost, enter=S2A, outputs=[]) S2B.upon(M_send, enter=S2B, outputs=[queue]) @@ -153,14 +159,15 @@ class _Mailbox_Machine(object): S3A.upon(M_send, enter=S3A, outputs=[queue]) S3A.upon(M_stop, enter=SrcA, outputs=[]) S3B.upon(M_lost, enter=S3A, outputs=[]) - S3B.upon(M_rx_msg_from_them, enter=S4B, outputs=[#tx_release, # trouble - process_msg_from_them]) + S3B.upon(M_rx_msg_from_them, enter=S4B, + outputs=[process_first_msg_from_them]) S3B.upon(M_rx_msg_from_me, enter=S3B, outputs=[dequeue]) S3B.upon(M_rx_claimed, enter=S3B, outputs=[]) S3B.upon(M_send, enter=S3B, outputs=[queue, tx_add]) S3B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close]) - S4A.upon(M_connected, enter=S4B, outputs=[tx_open, tx_add_queued, tx_release]) + S4A.upon(M_connected, enter=S4B, + outputs=[tx_open, tx_add_queued, tx_release]) S4A.upon(M_send, enter=S4A, outputs=[queue]) S4A.upon(M_stop, enter=SrcA, outputs=[]) S4B.upon(M_lost, enter=S4A, outputs=[]) @@ -192,5 +199,5 @@ class _Mailbox_Machine(object): ScB.upon(M_rx_closed, enter=SsB, outputs=[C_stop]) ScA.upon(M_connected, enter=ScB, outputs=[tx_close]) - SsB.upon(M_stopped, enter=Ss, outputs=[]) + SsB.upon(M_stopped, enter=Ss, outputs=[WM_stopped]) diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py new file mode 100644 index 0000000..8014cfd --- /dev/null +++ b/src/wormhole/_nameplate.py @@ -0,0 +1,53 @@ + +class NameplateListingMachine(object): + m = MethodicalMachine() + def __init__(self): + self._list_nameplate_waiters = [] + + # Ideally, each API request would spawn a new "list_nameplates" message + # to the server, so the response would be maximally fresh, but that would + # require correlating server request+response messages, and the protocol + # is intended to be less stateful than that. So we offer a weaker + # freshness property: if no server requests are in flight, then a new API + # request will provoke a new server request, and the result will be + # fresh. But if a server request is already in flight when a second API + # request arrives, both requests will be satisfied by the same response. + + @m.state(initial=True) + def idle(self): pass + @m.state() + def requesting(self): pass + + @m.input() + def list_nameplates(self): pass # returns Deferred + @m.input() + def response(self, message): pass + + @m.output() + def add_deferred(self): + d = defer.Deferred() + self._list_nameplate_waiters.append(d) + return d + @m.output() + def send_request(self): + self._connection.send_command("list") + @m.output() + def distribute_response(self, message): + nameplates = parse(message) + waiters = self._list_nameplate_waiters + self._list_nameplate_waiters = [] + for d in waiters: + d.callback(nameplates) + + idle.upon(list_nameplates, enter=requesting, + outputs=[add_deferred, send_request], + collector=lambda outs: outs[0]) + idle.upon(response, enter=idle, outputs=[]) + requesting.upon(list_nameplates, enter=requesting, + outputs=[add_deferred], + collector=lambda outs: outs[0]) + requesting.upon(response, enter=idle, outputs=[distribute_response]) + + # nlm._connection = c = Connection(ws) + # nlm.list_nameplates().addCallback(display_completions) + # c.register_dispatch("nameplates", nlm.response) diff --git a/src/wormhole/_wormhole.py b/src/wormhole/_wormhole.py new file mode 100644 index 0000000..697fab3 --- /dev/null +++ b/src/wormhole/_wormhole.py @@ -0,0 +1,145 @@ + +class Wormhole: + m = MethodicalMachine() + + def __init__(self, ws_url, reactor): + self._relay_client = WSRelayClient(self, ws_url, reactor) + # This records all the messages we want the relay to have. Each time + # we establish a connection, we'll send them all (and the relay + # server will filter out duplicates). If we add any while a + # connection is established, we'll send the new ones. + self._outbound_messages = [] + + # these methods are called from outside + def start(self): + self._relay_client.start() + + # and these are the state-machine transition functions, which don't take + # args + @m.state() + def closed(initial=True): pass + @m.state() + def know_code_not_mailbox(): pass + @m.state() + def know_code_and_mailbox(): pass # no longer need nameplate + @m.state() + def waiting_first_msg(): pass # key is established, want any message + @m.state() + def processing_version(): pass + @m.state() + def processing_phase(): pass + @m.state() + def open(): pass # key is verified, can post app messages + @m.state(terminal=True) + def failed(): pass + + @m.input() + def deliver_message(self, message): pass + + def w_set_seed(self, code, mailbox): + """Call w_set_seed when we sprout a Wormhole Seed, which + contains both the code and the mailbox""" + self.w_set_code(code) + self.w_set_mailbox(mailbox) + + @m.input() + def w_set_code(self, code): + """Call w_set_code when you learn the code, probably because the user + typed it in.""" + @m.input() + def w_set_mailbox(self, mailbox): + """Call w_set_mailbox() when you learn the mailbox id, from the + response to claim_nameplate""" + pass + + + @m.input() + def rx_pake(self, pake): pass # reponse["message"][phase=pake] + + @m.input() + def rx_version(self, version): # response["message"][phase=version] + pass + @m.input() + def verify_good(self, verifier): pass + @m.input() + def verify_bad(self, f): pass + + @m.input() + def rx_phase(self, message): pass + @m.input() + def phase_good(self, message): pass + @m.input() + def phase_bad(self, f): pass + + @m.output() + def compute_and_post_pake(self, code): + self._code = code + self._pake = compute(code) + self._post(pake=self._pake) + self._ws_send_command("add", phase="pake", body=XXX(pake)) + @m.output() + def set_mailbox(self, mailbox): + self._mailbox = mailbox + @m.output() + def set_seed(self, code, mailbox): + self._code = code + self._mailbox = mailbox + + @m.output() + def process_version(self, version): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass + + @m.output() + def notify_verified(self, verifier): + for d in self._verify_waiters: + d.callback(verifier) + @m.output() + def notify_failed(self, f): + for d in self._verify_waiters: + d.errback(f) + + @m.output() + def process_phase(self, message): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass + + @m.output() + def post_inbound(self, message): + pass + + @m.output() + def deliver_message(self, message): + self._qc.deliver_message(message) + + @m.output() + def compute_key_and_post_version(self, pake): + self._key = x + self._verifier = x + plaintext = dict_to_bytes(self._my_versions) + phase = "version" + data_key = self._derive_phase_key(self._side, phase) + encrypted = self._encrypt_data(data_key, plaintext) + self._msg_send(phase, encrypted) + + closed.upon(w_set_code, enter=know_code_not_mailbox, + outputs=[compute_and_post_pake]) + know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, + outputs=[set_mailbox]) + know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg, + outputs=[compute_key_and_post_version]) + waiting_first_msg.upon(rx_version, enter=processing_version, + outputs=[process_version]) + processing_version.upon(verify_good, enter=open, outputs=[notify_verified]) + processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed]) + open.upon(rx_phase, enter=processing_phase, outputs=[process_phase]) + processing_phase.upon(phase_good, enter=open, outputs=[post_inbound]) + processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed]) diff --git a/src/wormhole/misc.py b/src/wormhole/misc.py new file mode 100644 index 0000000..808b70f --- /dev/null +++ b/src/wormhole/misc.py @@ -0,0 +1,489 @@ + +from six.moves.urllib_parse import urlparse +from attr import attrs, attrib +from twisted.internet import defer, endpoints #, error +from autobahn.twisted import websocket +from automat import MethodicalMachine + +class WSClient(websocket.WebSocketClientProtocol): + def onConnect(self, response): + # this fires during WebSocket negotiation, and isn't very useful + # unless you want to modify the protocol settings + print("onConnect", response) + #self.connection_machine.onConnect(self) + + def onOpen(self, *args): + # this fires when the WebSocket is ready to go. No arguments + print("onOpen", args) + #self.wormhole_open = True + self.connection_machine.protocol_onOpen(self) + #self.factory.d.callback(self) + + def onMessage(self, payload, isBinary): + print("onMessage") + return + assert not isBinary + self.wormhole._ws_dispatch_response(payload) + + def onClose(self, wasClean, code, reason): + print("onClose") + self.connection_machine.protocol_onClose(wasClean, code, reason) + #if self.wormhole_open: + # self.wormhole._ws_closed(wasClean, code, reason) + #else: + # # we closed before establishing a connection (onConnect) or + # # finishing WebSocket negotiation (onOpen): errback + # self.factory.d.errback(error.ConnectError(reason)) + +class WSFactory(websocket.WebSocketClientFactory): + protocol = WSClient + def buildProtocol(self, addr): + proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) + proto.connection_machine = self.connection_machine + #proto.wormhole_open = False + return proto + +# pip install (path to automat checkout)[visualize] +# automat-visualize wormhole._connection + +@attrs +class _WSRelayClient_Machine(object): + _c = attrib() + m = MethodicalMachine() + + @m.state(initial=True) + def initial(self): pass + @m.state() + def connecting(self): pass + @m.state() + def negotiating(self): pass + @m.state(terminal=True) + def failed(self): pass + @m.state() + def open(self): pass + @m.state() + def waiting(self): pass + @m.state() + def reconnecting(self): pass + @m.state() + def disconnecting(self): pass + @m.state() + def cancelling(self): pass + @m.state(terminal=True) + def closed(self): pass + + @m.input() + def start(self): pass ; print("input:start") + @m.input() + def d_callback(self): pass ; print("input:d_callback") + @m.input() + def d_errback(self): pass ; print("input:d_errback") + @m.input() + def d_cancel(self): pass ; print("input:d_cancel") + @m.input() + def onOpen(self): pass ; print("input:onOpen") + @m.input() + def onClose(self): pass ; print("input:onClose") + @m.input() + def expire(self): pass + @m.input() + def stop(self): pass + + # outputs + @m.output() + def ep_connect(self): + "ep.connect()" + self._c.ep_connect() + @m.output() + def reset_timer(self): + self._c.reset_timer() + @m.output() + def connection_established(self): + print("connection_established") + self._c.connection_established() + @m.output() + def M_lost(self): + self._c.M_lost() + @m.output() + def start_timer(self): + self._c.start_timer() + @m.output() + def cancel_timer(self): + self._c.cancel_timer() + @m.output() + def dropConnection(self): + self._c.dropConnection() + @m.output() + def notify_fail(self): + self._c.notify_fail() + @m.output() + def MC_stopped(self): + self._c.MC_stopped() + + initial.upon(start, enter=connecting, outputs=[ep_connect]) + connecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) + connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) + connecting.upon(onClose, enter=failed, outputs=[notify_fail]) + connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) + cancelling.upon(d_errback, enter=closed, outputs=[]) + + negotiating.upon(onOpen, enter=open, outputs=[connection_established]) + negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) + negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) + + open.upon(onClose, enter=waiting, outputs=[M_lost, start_timer]) + open.upon(stop, enter=disconnecting, outputs=[dropConnection, M_lost]) + reconnecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) + reconnecting.upon(d_errback, enter=waiting, outputs=[start_timer]) + reconnecting.upon(onClose, enter=waiting, outputs=[start_timer]) + reconnecting.upon(stop, enter=cancelling, outputs=[d_cancel]) + + waiting.upon(expire, enter=reconnecting, outputs=[ep_connect]) + waiting.upon(stop, enter=closed, outputs=[cancel_timer]) + disconnecting.upon(onClose, enter=closed, outputs=[MC_stopped]) + +# We have one WSRelayClient for each wsurl we know about, and it lasts +# as long as its parent Wormhole does. + +@attrs +class WSRelayClient(object): + _wormhole = attrib() + _mailbox = attrib() + _ws_url = attrib() + _reactor = attrib() + INITIAL_DELAY = 1.0 + + + def __init__(self): + self._m = _WSRelayClient_Machine(self) + self._f = f = WSFactory(self._ws_url) + f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + f.connection_machine = self # calls onOpen and onClose + p = urlparse(self._ws_url) + self._ep = self._make_endpoint(p.hostname, p.port or 80) + self._connector = None + self._done_d = defer.Deferred() + self._current_delay = self.INITIAL_DELAY + + def _make_endpoint(self, hostname, port): + return endpoints.HostnameEndpoint(self._reactor, hostname, port) + + # inputs from elsewhere + def d_callback(self, p): + self._p = p + self._m.d_callback() + def d_errback(self, f): + self._f = f + self._m.d_errback() + def protocol_onOpen(self, p): + self._m.onOpen() + def protocol_onClose(self, wasClean, code, reason): + self._m.onClose() + def C_stop(self): + self._m.stop() + def timer_expired(self): + self._m.expire() + + # outputs driven by the state machine + def ep_connect(self): + print("ep_connect()") + self._d = self._ep.connect(self._f) + self._d.addCallbacks(self.d_callback, self.d_errback) + def connection_established(self): + self._connection = WSConnection(ws, self._wormhole.appid, + self._wormhole.side, self) + self._mailbox.connected(ws) + self._wormhole.add_connection(self._connection) + self._ws_send_command("bind", appid=self._appid, side=self._side) + def M_lost(self): + self._wormhole.M_lost(self._connection) + self._connection = None + def start_timer(self): + print("start_timer") + self._t = self._reactor.callLater(3.0, self.expire) + def cancel_timer(self): + print("cancel_timer") + self._t.cancel() + self._t = None + def dropConnection(self): + print("dropConnection") + self._ws.dropConnection() + def notify_fail(self): + print("notify_fail", self._f.value if self._f else None) + self._done_d.errback(self._f) + def MC_stopped(self): + pass + + +def tryit(reactor): + cm = WSRelayClient(None, "ws://127.0.0.1:4000/v1", reactor) + print("_ConnectionMachine created") + print("start:", cm.start()) + print("waiting on _done_d to finish") + return cm._done_d + +# http://autobahn-python.readthedocs.io/en/latest/websocket/programming.html +# observed sequence of events: +# success: d_callback, onConnect(response), onOpen(), onMessage() +# negotifail (non-websocket): d_callback, onClose() +# noconnect: d_errback + +def tryws(reactor): + ws_url = "ws://127.0.0.1:40001/v1" + f = WSFactory(ws_url) + p = urlparse(ws_url) + ep = endpoints.HostnameEndpoint(reactor, p.hostname, p.port or 80) + d = ep.connect(f) + def _good(p): print("_good", p) + def _bad(f): print("_bad", f) + d.addCallbacks(_good, _bad) + return defer.Deferred() + +if __name__ == "__main__": + import sys + from twisted.python import log + log.startLogging(sys.stdout) + from twisted.internet.task import react + react(tryit) + +# ??? a new WSConnection is created each time the WSRelayClient gets through +# negotiation + +class NameplateListingMachine(object): + m = MethodicalMachine() + def __init__(self): + self._list_nameplate_waiters = [] + + # Ideally, each API request would spawn a new "list_nameplates" message + # to the server, so the response would be maximally fresh, but that would + # require correlating server request+response messages, and the protocol + # is intended to be less stateful than that. So we offer a weaker + # freshness property: if no server requests are in flight, then a new API + # request will provoke a new server request, and the result will be + # fresh. But if a server request is already in flight when a second API + # request arrives, both requests will be satisfied by the same response. + + @m.state(initial=True) + def idle(self): pass + @m.state() + def requesting(self): pass + + @m.input() + def list_nameplates(self): pass # returns Deferred + @m.input() + def response(self, message): pass + + @m.output() + def add_deferred(self): + d = defer.Deferred() + self._list_nameplate_waiters.append(d) + return d + @m.output() + def send_request(self): + self._connection.send_command("list") + @m.output() + def distribute_response(self, message): + nameplates = parse(message) + waiters = self._list_nameplate_waiters + self._list_nameplate_waiters = [] + for d in waiters: + d.callback(nameplates) + + idle.upon(list_nameplates, enter=requesting, + outputs=[add_deferred, send_request], + collector=lambda outs: outs[0]) + idle.upon(response, enter=idle, outputs=[]) + requesting.upon(list_nameplates, enter=requesting, + outputs=[add_deferred], + collector=lambda outs: outs[0]) + requesting.upon(response, enter=idle, outputs=[distribute_response]) + + # nlm._connection = c = Connection(ws) + # nlm.list_nameplates().addCallback(display_completions) + # c.register_dispatch("nameplates", nlm.response) + +class Wormhole: + m = MethodicalMachine() + + def __init__(self, ws_url, reactor): + self._relay_client = WSRelayClient(self, ws_url, reactor) + # This records all the messages we want the relay to have. Each time + # we establish a connection, we'll send them all (and the relay + # server will filter out duplicates). If we add any while a + # connection is established, we'll send the new ones. + self._outbound_messages = [] + + # these methods are called from outside + def start(self): + self._relay_client.start() + + # and these are the state-machine transition functions, which don't take + # args + @m.state() + def closed(initial=True): pass + @m.state() + def know_code_not_mailbox(): pass + @m.state() + def know_code_and_mailbox(): pass # no longer need nameplate + @m.state() + def waiting_first_msg(): pass # key is established, want any message + @m.state() + def processing_version(): pass + @m.state() + def processing_phase(): pass + @m.state() + def open(): pass # key is verified, can post app messages + @m.state(terminal=True) + def failed(): pass + + @m.input() + def deliver_message(self, message): pass + + def w_set_seed(self, code, mailbox): + """Call w_set_seed when we sprout a Wormhole Seed, which + contains both the code and the mailbox""" + self.w_set_code(code) + self.w_set_mailbox(mailbox) + + @m.input() + def w_set_code(self, code): + """Call w_set_code when you learn the code, probably because the user + typed it in.""" + @m.input() + def w_set_mailbox(self, mailbox): + """Call w_set_mailbox() when you learn the mailbox id, from the + response to claim_nameplate""" + pass + + + @m.input() + def rx_pake(self, pake): pass # reponse["message"][phase=pake] + + @m.input() + def rx_version(self, version): # response["message"][phase=version] + pass + @m.input() + def verify_good(self, verifier): pass + @m.input() + def verify_bad(self, f): pass + + @m.input() + def rx_phase(self, message): pass + @m.input() + def phase_good(self, message): pass + @m.input() + def phase_bad(self, f): pass + + @m.output() + def compute_and_post_pake(self, code): + self._code = code + self._pake = compute(code) + self._post(pake=self._pake) + self._ws_send_command("add", phase="pake", body=XXX(pake)) + @m.output() + def set_mailbox(self, mailbox): + self._mailbox = mailbox + @m.output() + def set_seed(self, code, mailbox): + self._code = code + self._mailbox = mailbox + + @m.output() + def process_version(self, version): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass + + @m.output() + def notify_verified(self, verifier): + for d in self._verify_waiters: + d.callback(verifier) + @m.output() + def notify_failed(self, f): + for d in self._verify_waiters: + d.errback(f) + + @m.output() + def process_phase(self, message): # response["message"][phase=version] + their_verifier = com + if OK: + self.verify_good(verifier) + else: + self.verify_bad(f) + pass + + @m.output() + def post_inbound(self, message): + pass + + @m.output() + def deliver_message(self, message): + self._qc.deliver_message(message) + + @m.output() + def compute_key_and_post_version(self, pake): + self._key = x + self._verifier = x + plaintext = dict_to_bytes(self._my_versions) + phase = "version" + data_key = self._derive_phase_key(self._side, phase) + encrypted = self._encrypt_data(data_key, plaintext) + self._msg_send(phase, encrypted) + + closed.upon(w_set_code, enter=know_code_not_mailbox, + outputs=[compute_and_post_pake]) + know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, + outputs=[set_mailbox]) + know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg, + outputs=[compute_key_and_post_version]) + waiting_first_msg.upon(rx_version, enter=processing_version, + outputs=[process_version]) + processing_version.upon(verify_good, enter=open, outputs=[notify_verified]) + processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed]) + open.upon(rx_phase, enter=processing_phase, outputs=[process_phase]) + processing_phase.upon(phase_good, enter=open, outputs=[post_inbound]) + processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed]) + +class QueueConnect: + m = MethodicalMachine() + def __init__(self): + self._outbound_messages = [] + self._connection = None + @m.state() + def disconnected(): pass + @m.state() + def connected(): pass + + @m.input() + def deliver_message(self, message): pass + @m.input() + def connect(self, connection): pass + @m.input() + def disconnect(self): pass + + @m.output() + def remember_connection(self, connection): + self._connection = connection + @m.output() + def forget_connection(self): + self._connection = None + @m.output() + def queue_message(self, message): + self._outbound_messages.append(message) + @m.output() + def send_message(self, message): + self._connection.send(message) + @m.output() + def send_queued_messages(self, connection): + for m in self._outbound_messages: + connection.send(m) + + disconnected.upon(deliver_message, enter=disconnected, outputs=[queue_message]) + disconnected.upon(connect, enter=connected, outputs=[remember_connection, + send_queued_messages]) + connected.upon(deliver_message, enter=connected, + outputs=[queue_message, send_message]) + connected.upon(disconnect, enter=disconnected, outputs=[forget_connection]) From c9f3abe703ccd6df0958339b945004bec84687df Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2017 12:19:39 -0800 Subject: [PATCH 041/176] rename .dot files, remove obsolete ones --- docs/{w4.dot => _connection.dot} | 0 docs/{events.dot => _events.dot} | 0 docs/{states-code.dot => _states-code.dot} | 0 docs/code.dot | 41 +++++++ docs/{w2.dot => mailbox.dot} | 0 docs/{w3.dot => mailbox_close.dot} | 0 docs/w.dot | 127 --------------------- docs/w2a.dot | 80 ------------- docs/w3a.dot | 52 --------- docs/wormhole.dot | 82 +++++++++++++ 10 files changed, 123 insertions(+), 259 deletions(-) rename docs/{w4.dot => _connection.dot} (100%) rename docs/{events.dot => _events.dot} (100%) rename docs/{states-code.dot => _states-code.dot} (100%) create mode 100644 docs/code.dot rename docs/{w2.dot => mailbox.dot} (100%) rename docs/{w3.dot => mailbox_close.dot} (100%) delete mode 100644 docs/w2a.dot delete mode 100644 docs/w3a.dot create mode 100644 docs/wormhole.dot diff --git a/docs/w4.dot b/docs/_connection.dot similarity index 100% rename from docs/w4.dot rename to docs/_connection.dot diff --git a/docs/events.dot b/docs/_events.dot similarity index 100% rename from docs/events.dot rename to docs/_events.dot diff --git a/docs/states-code.dot b/docs/_states-code.dot similarity index 100% rename from docs/states-code.dot rename to docs/_states-code.dot diff --git a/docs/code.dot b/docs/code.dot new file mode 100644 index 0000000..991036f --- /dev/null +++ b/docs/code.dot @@ -0,0 +1,41 @@ +digraph { + + WCM_start [label="Wormhole Code\nMachine" style="dotted"] + WCM_start -> WCM_S_unknown [style="invis"] + WCM_S_unknown [label="unknown"] + WCM_S_unknown -> WCM_P_set_code [label="set"] + WCM_P_set_code [shape="box" label="WM_set_code()"] + WCM_P_set_code -> WCM_S_known + WCM_S_known [label="known" color="green"] + + WCM_S_unknown -> WCM_P_list_nameplates [label="input"] + WCM_S_typing_nameplate [label="typing\nnameplate"] + + WCM_S_typing_nameplate -> WCM_P_nameplate_completion [label=""] + WCM_P_nameplate_completion [shape="box" label="completion?"] + WCM_P_nameplate_completion -> WCM_P_list_nameplates + WCM_P_list_nameplates [shape="box" label="NLM_update_nameplates()"] + WCM_P_list_nameplates -> WCM_S_typing_nameplate + + WCM_S_typing_nameplate -> WCM_P_got_nameplates [label="C_rx_nameplates()"] + WCM_P_got_nameplates [shape="box" label="stash nameplates\nfor completion"] + WCM_P_got_nameplates -> WCM_S_typing_nameplate + WCM_S_typing_nameplate -> WCM_P_finish_nameplate [label="finished\nnameplate"] + WCM_P_finish_nameplate [shape="box" label="lookup wordlist\nfor completion"] + WCM_P_finish_nameplate -> WCM_S_typing_code + WCM_S_typing_code [label="typing\ncode"] + WCM_S_typing_code -> WCM_P_code_completion [label=""] + WCM_P_code_completion [shape="box" label="completion"] + WCM_P_code_completion -> WCM_S_typing_code + + WCM_S_typing_code -> WCM_P_set_code [label="finished\ncode"] + + WCM_S_unknown -> WCM_P_allocate [label="allocate"] + WCM_P_allocate [shape="box" label="C_allocate_nameplate()"] + WCM_P_allocate -> WCM_S_allocate_waiting + WCM_S_allocate_waiting [label="waiting"] + WCM_S_allocate_waiting -> WCM_P_allocate_generate [label="WCM_rx_allocation()"] + WCM_P_allocate_generate [shape="box" label="generate\nrandom code"] + WCM_P_allocate_generate -> WCM_P_set_code + +} diff --git a/docs/w2.dot b/docs/mailbox.dot similarity index 100% rename from docs/w2.dot rename to docs/mailbox.dot diff --git a/docs/w3.dot b/docs/mailbox_close.dot similarity index 100% rename from docs/w3.dot rename to docs/mailbox_close.dot diff --git a/docs/w.dot b/docs/w.dot index fa62850..244ee2d 100644 --- a/docs/w.dot +++ b/docs/w.dot @@ -1,124 +1,4 @@ digraph { - - /* could shave a RTT by committing to the nameplate early, before - finishing the rest of the code input. While the user is still - typing/completing the code, we claim the nameplate, open the mailbox, - and retrieve the peer's PAKE message. Then as soon as the user - finishes entering the code, we build our own PAKE message, send PAKE, - compute the key, send VERSION. Starting from the Return, this saves - two round trips. OTOH it adds consequences to hitting Tab. */ - - WM_start [label="Wormhole\nMachine" style="dotted"] - WM_start -> WM_S_nothing [style="invis"] - - WM_S_nothing [label="know\nnothing"] - WM_S_nothing -> WM_P_queue1 [label="API_send" style="dotted"] - WM_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] - WM_P_queue1 -> WM_S_nothing [style="dotted"] - WM_S_nothing -> WM_P_build_pake [label="WM_set_code()"] - - WM_P_build_pake [shape="box" label="build_pake()"] - WM_P_build_pake -> WM_S_save_pake - WM_S_save_pake [label="checkpoint"] - WM_S_save_pake -> WM_P_post_pake [label="saved"] - - WM_P_post_pake [label="M_set_nameplate()\nM_send(pake)" shape="box"] - WM_P_post_pake -> WM_S_know_code - - WM_S_know_code [label="know code\n"] - WM_S_know_code -> WM_P_queue2 [label="API_send" style="dotted"] - WM_P_queue2 [shape="box" style="dotted" label="queue\noutbound msg"] - WM_P_queue2 -> WM_S_know_code [style="dotted"] - WM_S_know_code -> WM_P_compute_key [label="WM_rx_pake"] - WM_S_know_code -> WM_P_mood_lonely [label="close"] - - WM_P_compute_key [label="compute_key()" shape="box"] - WM_P_compute_key -> WM_P_save_key [label="pake ok"] - WM_P_save_key [label="checkpoint"] - WM_P_save_key -> WM_P_post_version [label="saved"] - WM_P_compute_key -> WM_P_mood_scary [label="pake bad"] - - WM_P_mood_scary [shape="box" label="M_close()\nmood=scary"] - WM_P_mood_scary -> WM_P_notify_failure - - WM_P_notify_failure [shape="box" label="notify_failure()" color="red"] - WM_P_notify_failure -> WM_S_closed - - WM_P_post_version [label="M_send(version)\nnotify_verifier()" shape="box"] - WM_P_post_version -> WM_S_know_key - - WM_S_know_key [label="know_key\nunverified" color="orange"] - WM_S_know_key -> WM_P_queue3 [label="API_send" style="dotted"] - WM_P_queue3 [shape="box" style="dotted" label="queue\noutbound msg"] - WM_P_queue3 -> WM_S_know_key [style="dotted"] - WM_S_know_key -> WM_P_verify [label="WM_rx_msg()"] /* version or phase */ - WM_S_know_key -> WM_P_mood_lonely [label="close"] /* more like impatient */ - - WM_P_verify [label="verify(msg)" shape="box"] - WM_P_verify -> WM_P_accept_msg [label="verify good"] - WM_P_verify -> WM_P_mood_scary [label="verify bad"] - - WM_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] - WM_P_accept_msg -> WM_P_send_queued - - WM_P_send_queued [shape="box" label="M_send()\nqueued"] - WM_P_send_queued -> WM_S_verified_key - - WM_S_verified_key [color="green"] - WM_S_verified_key -> WM_P_verify [label="WM_rx_msg()"] /* probably phase */ - WM_S_verified_key -> WM_P_mood_happy [label="close"] - WM_S_verified_key -> WM_P_send [label="API_send"] - - WM_P_mood_happy [shape="box" label="M_close()\nmood=happy"] - WM_P_mood_happy -> WM_S_closed - - WM_P_mood_lonely [shape="box" label="M_close()\nmood=lonely"] - WM_P_mood_lonely -> WM_S_closed - - WM_P_send [shape="box" label="M_send(msg)"] - WM_P_send -> WM_S_verified_key - - WM_S_closed [label="closed"] - - - WCM_start [label="Wormhole Code\nMachine" style="dotted"] - WCM_start -> WCM_S_unknown [style="invis"] - WCM_S_unknown [label="unknown"] - WCM_S_unknown -> WCM_P_set_code [label="set"] - WCM_P_set_code [shape="box" label="WM_set_code()"] - WCM_P_set_code -> WCM_S_known - WCM_S_known [label="known" color="green"] - - WCM_S_unknown -> WCM_P_list_nameplates [label="input"] - WCM_S_typing_nameplate [label="typing\nnameplate"] - - WCM_S_typing_nameplate -> WCM_P_nameplate_completion [label=""] - WCM_P_nameplate_completion [shape="box" label="completion?"] - WCM_P_nameplate_completion -> WCM_P_list_nameplates - WCM_P_list_nameplates [shape="box" label="NLM_update_nameplates()"] - WCM_P_list_nameplates -> WCM_S_typing_nameplate - - WCM_S_typing_nameplate -> WCM_P_got_nameplates [label="C_rx_nameplates()"] - WCM_P_got_nameplates [shape="box" label="stash nameplates\nfor completion"] - WCM_P_got_nameplates -> WCM_S_typing_nameplate - WCM_S_typing_nameplate -> WCM_P_finish_nameplate [label="finished\nnameplate"] - WCM_P_finish_nameplate [shape="box" label="lookup wordlist\nfor completion"] - WCM_P_finish_nameplate -> WCM_S_typing_code - WCM_S_typing_code [label="typing\ncode"] - WCM_S_typing_code -> WCM_P_code_completion [label=""] - WCM_P_code_completion [shape="box" label="completion"] - WCM_P_code_completion -> WCM_S_typing_code - - WCM_S_typing_code -> WCM_P_set_code [label="finished\ncode"] - - WCM_S_unknown -> WCM_P_allocate [label="allocate"] - WCM_P_allocate [shape="box" label="C_allocate_nameplate()"] - WCM_P_allocate -> WCM_S_allocate_waiting - WCM_S_allocate_waiting [label="waiting"] - WCM_S_allocate_waiting -> WCM_P_allocate_generate [label="WCM_rx_allocation()"] - WCM_P_allocate_generate [shape="box" label="generate\nrandom code"] - WCM_P_allocate_generate -> WCM_P_set_code - /* NM_start [label="Nameplate\nMachine" style="dotted"] @@ -203,11 +83,4 @@ digraph { P2_P_send_pakev2 -> P2_P_process_v2 [label="rx pake_v2"] P2_P_process_v2 [shape="box" label="process v2"] */ - - WCM_S_known -> O_WM [style="invis"] - O_WM [label="Wormhole\nMachine" style="dotted"] - O_WM -> O_MM [style="dotted"] - O_MM [label="Mailbox\nMachine" style="dotted"] - O_MM -> O_CM [style="dotted"] - O_CM [label="Connection\nMachine" style="dotted"] } diff --git a/docs/w2a.dot b/docs/w2a.dot deleted file mode 100644 index 7a845b9..0000000 --- a/docs/w2a.dot +++ /dev/null @@ -1,80 +0,0 @@ -digraph { - /* new idea */ - - M_title [label="Message\nMachine" style="dotted"] - - M_S1 [label="S1:\nknow nothing" color="orange"] - - M_S1 -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] - - M_S2 [label="S2:\nmaybe claimed" color="orange"] - /*M_S2 -> M_SrB [label="M_close()" style="dashed"] - M_SrB [label="SrB" style="dashed"]*/ - - M_P2_claim [shape="box" label="qtx claim" color="orange"] - M_P2_claim -> M_S2 [color="orange"] - M_S2 -> M_P2_queue [label="M_send(msg)" style="dotted"] - M_P2_queue [shape="box" label="queue" style="dotted"] - M_P2_queue -> M_S2 [style="dotted"] - - M_S2 -> M_P_open [label="rx claimed" color="orange" fontcolor="orange"] - M_P_open [shape="box" label="store mailbox\nqtx open\nqtx add(queued)" color="orange"] - M_P_open -> M_S3 [color="orange"] - - M_S3 [label="S3:\nclaimed\nmaybe open" color="orange"] - M_S3 -> M_S3 [label="rx claimed"] - M_S3 -> M_P3_send [label="M_send(msg)"] - M_P3_send [shape="box" label="queue\nqtx add(msg)"] - M_P3_send -> M_S3 - - M_S3 -> M_P3_process_ours [label="rx message(side=me)"] - M_P3_process_ours [shape="box" label="dequeue"] - M_P3_process_ours -> M_S3 - M_S3 -> M_P3_process_theirs1 [label="rx message(side!=me)" color="orange" fontcolor="orange"] - M_P3_process_theirs1 [shape="box" label="qtx release" color="orange"] - M_P3_process_theirs1 -> M_P3_process_theirs2 [color="orange"] - M_P3_process_theirs2 [shape="octagon" label="process message" color="orange"] - /* pay attention to the race here: this process_message() will - deliver msg_pake to the WormholeMachine, which will compute_key() and - M_send(version), and we're in between M_S2A (where M_send gets - queued) and M_S3A (where M_send gets sent and queued), and we're no - longer passing through the M_P3_open phase (which drains the queue). - So there's a real possibility of the outbound msg_version getting - dropped on the floor, or put in a queue but never delivered. */ - M_P3_process_theirs2 -> M_S4 [color="orange"] - - /*{rank=same; M_S4A M_P4_release M_S4 M_P4_process M_P4_send M_P4_queue}*/ - M_S4 [label="S4:\nmaybe released\nmaybe open" color="orange"] - M_S4 -> M_P4_send [label="M_send(msg)"] - M_P4_send [shape="box" label="queue\nqtx add(msg)"] - M_P4_send -> M_S4 - - M_S4 -> M_P4_process [label="rx message"] - M_P4_process [shape="octagon" label="process message"] - M_P4_process -> M_S4 - - M_S4 -> M_S5 [label="rx released" color="orange" fontcolor="orange"] - - seed [label="from Seed?"] - seed -> M_S5 - M_S5 [label="S5:\nreleased\nmaybe open" color="green"] - M_S5 -> M_process [label="rx message" color="green" fontcolor="green"] - M_process [shape="octagon" label="process message" color="green"] - M_process -> M_S5 [color="green"] - M_S5 -> M_P5_send [label="M_send(msg)" color="green" fontcolor="green"] - M_P5_send [shape="box" label="queue\nqtx add(msg)" color="green"] - M_P5_send -> M_S5 [color="green"] - /*M_S5 -> M_CcB_P_close [label="M_close()" style="dashed" color="orange" fontcolor="orange"] - M_CcB_P_close [label="qtx close" style="dashed" color="orange"] */ - - M_process [shape="octagon" label="process message"] - M_process_me [shape="box" label="dequeue"] - M_process -> M_process_me [label="side == me"] - M_process_them [shape="box" label="" style="dotted"] - M_process -> M_process_them [label="side != me"] - M_process_them -> M_process_pake [label="phase == pake"] - M_process_pake [shape="box" label="WM_rx_pake()"] - M_process_them -> M_process_other [label="phase in (version,numbered)"] - M_process_other [shape="box" label="WM_rx_msg()"] - -} diff --git a/docs/w3a.dot b/docs/w3a.dot deleted file mode 100644 index 5435b33..0000000 --- a/docs/w3a.dot +++ /dev/null @@ -1,52 +0,0 @@ -digraph { - /* M_close pathways */ - MC_title [label="Mailbox\nClose\nMachine" style="dotted"] - MC_title -> MC_S2 [style="invis"] - - /* All dashed states are from the main Mailbox Machine diagram, and - all dashed lines indicate M_close() pathways in from those states. - Within this graph, all M_close() events leave the state unchanged. */ - - MC_Pr [shape="box" label="qtx release" color="orange"] - MC_Pr -> MC_Sr [color="orange"] - MC_Sr [label="SrB:\nwaiting for:\nrelease" color="orange"] - MC_Sr -> MC_P_stop [label="rx released" color="orange" fontcolor="orange"] - - MC_Pc [shape="box" label="qtx close" color="orange"] - MC_Pc -> MC_Sc [color="orange"] - MC_Sc [label="ScB:\nwaiting for:\nclosed" color="orange"] - MC_Sc -> MC_P_stop [label="rx closed" color="orange" fontcolor="orange"] - - MC_Prc [shape="box" label="qtx release\nqtx close" color="orange"] - MC_Prc -> MC_Src [color="orange"] - MC_Src [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] - MC_Src -> MC_Sc [label="rx released" color="orange" fontcolor="orange"] - MC_Src -> MC_Sr [label="rx closed" color="orange" fontcolor="orange"] - - - MC_P_stop [shape="box" label="C_stop()"] - MC_P_stop -> MC_Ss - - MC_Ss -> MC_Ss [label="M_stopped()"] - MC_Ss [label="SsB: closed\nstopping"] - - MC_Ss [label="Ss: closed" color="green"] - - - {rank=same; MC_S2 MC_S1 MC_S3 MC_S4 MC_S5} - MC_S1 [label="S1" color="orange" style="dashed"] - MC_S1 -> MC_P_stop [style="dashed" color="orange"] - - MC_S2 [label="S2" color="orange" style="dashed"] - MC_S2 -> MC_Pr [color="orange" style="dashed"] - - MC_S3 [label="S3" color="orange" style="dashed"] - MC_S3 -> MC_Prc [color="orange" style="dashed"] - - MC_S4 [label="S4" color="orange" style="dashed"] - MC_S4 -> MC_Prc [color="orange" style="dashed"] - - MC_S5 [label="S5" color="green" style="dashed"] - MC_S5 -> MC_Pc [style="dashed" color="green"] - -} diff --git a/docs/wormhole.dot b/docs/wormhole.dot new file mode 100644 index 0000000..8c55ad1 --- /dev/null +++ b/docs/wormhole.dot @@ -0,0 +1,82 @@ +digraph { + + /* could shave a RTT by committing to the nameplate early, before + finishing the rest of the code input. While the user is still + typing/completing the code, we claim the nameplate, open the mailbox, + and retrieve the peer's PAKE message. Then as soon as the user + finishes entering the code, we build our own PAKE message, send PAKE, + compute the key, send VERSION. Starting from the Return, this saves + two round trips. OTOH it adds consequences to hitting Tab. */ + + WM_start [label="Wormhole\nMachine" style="dotted"] + WM_start -> WM_S_nothing [style="invis"] + + WM_S_nothing [label="know\nnothing"] + WM_S_nothing -> WM_P_queue1 [label="API_send" style="dotted"] + WM_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] + WM_P_queue1 -> WM_S_nothing [style="dotted"] + WM_S_nothing -> WM_P_build_pake [label="WM_set_code()"] + + WM_P_build_pake [shape="box" label="build_pake()"] + WM_P_build_pake -> WM_S_save_pake + WM_S_save_pake [label="checkpoint"] + WM_S_save_pake -> WM_P_post_pake [label="saved"] + + WM_P_post_pake [label="M_set_nameplate()\nM_send(pake)" shape="box"] + WM_P_post_pake -> WM_S_know_code + + WM_S_know_code [label="know code\n"] + WM_S_know_code -> WM_P_queue2 [label="API_send" style="dotted"] + WM_P_queue2 [shape="box" style="dotted" label="queue\noutbound msg"] + WM_P_queue2 -> WM_S_know_code [style="dotted"] + WM_S_know_code -> WM_P_compute_key [label="WM_rx_pake"] + WM_S_know_code -> WM_P_mood_lonely [label="close"] + + WM_P_compute_key [label="compute_key()" shape="box"] + WM_P_compute_key -> WM_P_save_key [label="pake ok"] + WM_P_save_key [label="checkpoint"] + WM_P_save_key -> WM_P_post_version [label="saved"] + WM_P_compute_key -> WM_P_mood_scary [label="pake bad"] + + WM_P_mood_scary [shape="box" label="M_close()\nmood=scary"] + WM_P_mood_scary -> WM_P_notify_failure + + WM_P_notify_failure [shape="box" label="notify_failure()" color="red"] + WM_P_notify_failure -> WM_S_closed + + WM_P_post_version [label="M_send(version)\nnotify_verifier()" shape="box"] + WM_P_post_version -> WM_S_know_key + + WM_S_know_key [label="know_key\nunverified" color="orange"] + WM_S_know_key -> WM_P_queue3 [label="API_send" style="dotted"] + WM_P_queue3 [shape="box" style="dotted" label="queue\noutbound msg"] + WM_P_queue3 -> WM_S_know_key [style="dotted"] + WM_S_know_key -> WM_P_verify [label="WM_rx_msg()"] /* version or phase */ + WM_S_know_key -> WM_P_mood_lonely [label="close"] /* more like impatient */ + + WM_P_verify [label="verify(msg)" shape="box"] + WM_P_verify -> WM_P_accept_msg [label="verify good"] + WM_P_verify -> WM_P_mood_scary [label="verify bad"] + + WM_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] + WM_P_accept_msg -> WM_P_send_queued + + WM_P_send_queued [shape="box" label="M_send()\nqueued"] + WM_P_send_queued -> WM_S_verified_key + + WM_S_verified_key [color="green"] + WM_S_verified_key -> WM_P_verify [label="WM_rx_msg()"] /* probably phase */ + WM_S_verified_key -> WM_P_mood_happy [label="close"] + WM_S_verified_key -> WM_P_send [label="API_send"] + + WM_P_mood_happy [shape="box" label="M_close()\nmood=happy"] + WM_P_mood_happy -> WM_S_closed + + WM_P_mood_lonely [shape="box" label="M_close()\nmood=lonely"] + WM_P_mood_lonely -> WM_S_closed + + WM_P_send [shape="box" label="M_send(msg)"] + WM_P_send -> WM_S_verified_key + + WM_S_closed [label="closed"] +} From 7cc50e970108ca983400eb114ea3e17e38d7efcb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2017 13:32:56 -0800 Subject: [PATCH 042/176] .gitignore: ignore some generated images --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c4cf83b..046e229 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ target/ /misc/node_modules/ /docs/events.png /docs/states-code.png +/docs/*.png +/.automat_visualize/ From 44cc1399c49d64c1760943d27abf30b8a8cce674 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2017 16:11:26 -0800 Subject: [PATCH 043/176] make mailbox/mailbox_close/machines consistent finally get mailbox.png layout good enough --- docs/machines.dot | 50 ++++---- docs/mailbox.dot | 252 +++++++++++++++++++++-------------------- docs/mailbox_close.dot | 104 ++++++++--------- 3 files changed, 211 insertions(+), 195 deletions(-) diff --git a/docs/machines.dot b/docs/machines.dot index b54a4e7..1c2272a 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -1,34 +1,40 @@ digraph { - Wormhole [shape="box" label="Wormhole\n(manager)"] - Wormhole -> Mailbox [style="dashed" - label="M_set_nameplate()\nM_send()\nM_close()" - ] - Wormhole -> Mailbox - Mailbox -> Wormhole [style="dashed" - label="W_rx_pake()\nW_rx_msg()\nW_closed()" - ] - Mailbox [shape="box"] - Mailbox -> Connection [style="dashed" - label="C_tx_add()\nC_stop()" - ] - Mailbox -> Connection - Connection -> Mailbox [style="dashed" - label="M_connected()\nM_lost()\nM_rx_claimed()\nM_rx_message()\nM_rx_released()\nM_stopped()"] - - Connection -> websocket + Wormhole [shape="box" label="Wormhole\n(manager)" + color="blue" fontcolor="blue"] + Mailbox [shape="box" color="blue" fontcolor="blue"] + Connection [shape="oval" color="blue" fontcolor="blue"] + websocket [shape="oval" color="blue" fontcolor="blue"] + Nameplates [shape="box" label="Nameplate\nLister" + color="blue" fontcolor="blue"] + + Connection -> websocket [color="blue"] + + Wormhole -> Mailbox [style="dashed" + label="set_nameplate\nadd_message\nclose" + ] + Wormhole -> Mailbox [color="blue"] + Mailbox -> Wormhole [style="dashed" + label="got_message\nclosed" + ] + Mailbox -> Connection [style="dashed" + label="tx_claim\ntx_open\ntx_add\ntx_release\ntx_close\nstop" + ] + Mailbox -> Connection [color="blue"] + Connection -> Mailbox [style="dashed" + label="connected\nlost\nrx_claimed\nrx_message\nrx_released\nrx_closed\nstopped"] - Nameplates [shape="box" label="Nameplate\nLister"] Wormhole -> Nameplates [style="dashed" - label="NL_refresh_nameplates()" + label="refresh_nameplates" ] + Wormhole -> Nameplates [color="blue"] Nameplates -> Wormhole [style="dashed" - label="W_got_nameplates()" + label="got_nameplates" ] Connection -> Nameplates [style="dashed" - label="NL_connected()\nNL_lost()\nNL_rx_nameplates()" + label="connected\nlost\nrx_nameplates" ] Nameplates -> Connection [style="dashed" - label="C_tx_list()" + label="tx_list" ] diff --git a/docs/mailbox.dot b/docs/mailbox.dot index 6b9d82c..65fb17d 100644 --- a/docs/mailbox.dot +++ b/docs/mailbox.dot @@ -1,147 +1,157 @@ digraph { /* new idea */ - {rank=same; M_title M_entry_whole_code M_entry_allocation M_entry_interactive} - M_entry_whole_code [label="whole\ncode"] - M_entry_whole_code -> M_S0A - M_title [label="Message\nMachine" style="dotted"] + {rank=same; title entry_whole_code entry_allocation entry_interactive} + entry_whole_code [label="whole\ncode"] + entry_whole_code -> S0A + title [label="Message\nMachine" style="dotted"] - M_entry_allocation [label="allocation" color="orange"] - M_entry_allocation -> M_S0B [label="already\nconnected" color="orange" fontcolor="orange"] - M_entry_interactive [label="interactive" color="orange"] - M_entry_interactive -> M_S0B [color="orange"] + entry_allocation [label="allocation" color="orange"] + entry_allocation -> S0B [label="(already\nconnected)" + color="orange" fontcolor="orange"] + entry_interactive [label="interactive" color="orange"] + entry_interactive -> S0B [color="orange"] - {rank=same; M_S0A M_S0B} - M_S0A [label="S0A:\nknow nothing"] - M_S0B [label="S0B:\nknow nothing\n(bound)" color="orange"] - M_S0A -> M_S0B [label="M_connected()"] - M_S0B -> M_S0A [label="M_lost()"] + {rank=same; S0A P0_connected S0B} + S0A [label="S0A:\nknow nothing"] + S0B [label="S0B:\nknow nothing\n(bound)" color="orange"] + S0A -> P0_connected [label="connected"] + P0_connected [label="(nothing)" shape="box" style="dashed"] + P0_connected -> S0B + S0B -> S0A [label="lost"] - M_S0A -> M_S1A [label="M_set_nameplate()"] - M_S0B -> M_P2_claim [label="M_set_nameplate()" color="orange" fontcolor="orange"] + S0A -> S1A [label="set_nameplate"] + S0B -> P2_connected [label="set_nameplate" color="orange" fontcolor="orange"] + P0A_queue [shape="box" label="queue" style="dotted"] + S0A -> P0A_queue [label="add_message" style="dotted"] + P0A_queue -> S0A [style="dotted"] - {rank=same; M_S1A M_P1A_queue} - M_S0B -> M_S2B [style="invis"] - M_S1A -> M_S2A [style="invis"] - M_S1A [label="S1A:\nnot claimed"] - M_S1A -> M_P2_claim [label="M_connected()"] - M_S1A -> M_P1A_queue [label="M_send(msg)" style="dotted"] - M_P1A_queue [shape="box" label="queue" style="dotted"] - M_P1A_queue -> M_S1A [style="dotted"] + {rank=same; S1A P1A_queue} + S1A [label="S1A:\nnot claimed"] + S1A -> P2_connected [label="connected"] + S1A -> P1A_queue [label="add_message" style="dotted"] + P1A_queue [shape="box" label="queue" style="dotted"] + P1A_queue -> S1A [style="dotted"] - {rank=same; M_S2B M_S2A M_P2_claim} - M_S2A [label="S2A:\nmaybe claimed"] - M_S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] - #M_S2B -> M_SrB [label="M_close()" style="dashed"] - #M_SrB [label="SrB" style="dashed"] - #M_S2A -> M_SrA [label="M_close()" style="dashed"] - #M_SrA [label="SrA" style="dashed"] + {rank=same; S2A P2_connected S2B} + S2A [label="S2A:\nmaybe claimed"] + S2A -> P2_connected [label="connected"] + P2_connected [shape="box" label="C.tx_claim" color="orange"] + P2_connected -> S2B [color="orange"] + S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] + #S2B -> SrB [label="close()" style="dashed"] + #SrB [label="SrB" style="dashed"] + #S2A -> SrA [label="close()" style="dashed"] + #SrA [label="SrA" style="dashed"] - M_S2A -> M_P2_claim [label="M_connected()"] - #M_S2B -> M_S2A [label="M_lost()"] # causes bad layout - M_S2B -> foo [label="M_lost()"] + #S2B -> S2A [label="lost"] # causes bad layout + S2B -> foo [label="lost"] foo [label="" style="dashed"] - foo -> M_S2A + foo -> S2A - M_P2_claim [shape="box" label="tx claim" color="orange"] - M_P2_claim -> M_S2B [color="orange"] - M_S2A -> M_P2C_queue [label="M_send(msg)" style="dotted"] - M_P2C_queue [shape="box" label="queue" style="dotted"] - M_P2C_queue -> M_S2A [style="dotted"] - M_S2B -> M_P2B_queue [label="M_send(msg)" style="dotted"] - M_P2B_queue [shape="box" label="queue" style="dotted"] - M_P2B_queue -> M_S2B [style="dotted"] + S2A -> P2C_queue [label="add_message" style="dotted"] + P2C_queue [shape="box" label="queue" style="dotted"] + P2C_queue -> S2A [style="dotted"] + S2B -> P2B_queue [label="add_message" style="dotted"] + P2B_queue [shape="box" label="queue" style="dotted"] + P2B_queue -> S2B [style="dotted"] - M_S1A -> M_S3A [label="(none)" style="invis"] - M_S2B -> M_P_open [label="rx claimed" color="orange" fontcolor="orange"] - M_P_open [shape="box" label="store mailbox\ntx open\ntx add(queued)" color="orange"] - M_P_open -> M_S3B [color="orange"] + S1A -> S3A [label="(none)" style="invis"] + S2B -> P_open [label="rx_claimed" color="orange" fontcolor="orange"] + P_open [shape="box" label="store mailbox\nC.tx_open\nC.tx_add(queued)" color="orange"] + P_open -> S3B [color="orange"] - {rank=same; M_S3A M_S3B M_P3_open M_P3_send} - M_S3A [label="S3A:\nclaimed\nopened >=once"] - M_S3B [label="S3B:\nclaimed\nmaybe open now\n(bound)" color="orange"] - M_S3A -> M_P3_open [label="M_connected()"] - M_S3B -> M_S3A [label="M_lost()"] - M_P3_open [shape="box" label="tx open\ntx add(queued)"] - M_P3_open -> M_S3B - M_S3B -> M_S3B [label="rx claimed"] - M_S3B -> M_P3_send [label="M_send(msg)"] - M_P3_send [shape="box" label="queue\ntx add(msg)"] - M_P3_send -> M_S3B - M_S3A -> M_P3_queue [label="M_send(msg)" style="dotted"] - M_P3_queue [shape="box" label="queue" style="dotted"] - M_P3_queue -> M_S3A [style="dotted"] + subgraph {rank=same; S3A S3B P3_connected} + S3A [label="S3A:\nclaimed\nopened >=once"] + S3B [label="S3B:\nclaimed\nmaybe open now\n(bound)" color="orange"] + S3A -> P3_connected [label="connected"] + S3B -> S3A [label="lost"] - M_S3A -> M_S4A [label="(none)" style="invis"] - M_S3B -> M_P3_process_ours [label="rx message(side=me)"] - M_P3_process_ours [shape="box" label="dequeue"] - M_P3_process_ours -> M_S3B - M_S3B -> M_P3_process_theirs1 [label="rx message(side!=me)" color="orange" fontcolor="orange"] - M_P3_process_theirs1 [shape="box" label="tx release" color="orange"] - M_P3_process_theirs1 -> M_P3_process_theirs2 [color="orange"] - M_P3_process_theirs2 [shape="octagon" label="process message" color="orange"] + P3_connected [shape="box" label="C.tx_open\nC.tx_add(queued)"] + P3_connected -> S3B + + S3A -> P3_queue [label="add_message" style="dotted"] + P3_queue [shape="box" label="queue" style="dotted"] + P3_queue -> S3A [style="dotted"] + + S3B -> S3B [label="rx_claimed"] + + S3B -> P3_send [label="add_message"] + P3_send [shape="box" label="queue\nC.tx_add(msg)"] + P3_send -> S3B + + S3A -> S4A [label="(none)" style="invis"] + S3B -> P3_process_ours [label="rx_message\n(ours)"] + P3_process_ours [shape="box" label="dequeue"] + P3_process_ours -> S3B + S3B -> P3_process_theirs [label="rx_message\n(theirs)" + color="orange" fontcolor="orange"] + P3_process_theirs [shape="box" label="C.tx_release\nW.got_message" + color="orange"] /* pay attention to the race here: this process_message() will deliver msg_pake to the WormholeMachine, which will compute_key() and - M_send(version), and we're in between M_S1A (where M_send gets - queued) and M_S3A (where M_send gets sent and queued), and we're no - longer passing through the M_P3_open phase (which drains the queue). + send(version), and we're in between S1A (where send gets + queued) and S3A (where send gets sent and queued), and we're no + longer passing through the P3_connected phase (which drains the queue). So there's a real possibility of the outbound msg_version getting dropped on the floor, or put in a queue but never delivered. */ - M_P3_process_theirs2 -> M_S4B [color="orange"] + P3_process_theirs -> S4B [color="orange"] - {rank=same; M_S4A M_P4_release M_S4B M_P4_process M_P4_send M_P4_queue} - M_S4A [label="S4A:\nmaybe released\nopened >=once\n"] + subgraph {rank=same; S4A P4_connected S4B} + S4A [label="S4A:\nmaybe released\nopened >=once\n"] - M_S4B [label="S4B:\nmaybe released\nmaybe open now\n(bound)" color="orange"] - M_S4A -> M_P4_release [label="M_connected()"] - M_P4_release [shape="box" label="tx open\ntx add(queued)\ntx release"] - M_S4B -> M_P4_send [label="M_send(msg)"] - M_P4_send [shape="box" label="queue\ntx add(msg)"] - M_P4_send -> M_S4B - M_S4A -> M_P4_queue [label="M_send(msg)" style="dotted"] - M_P4_queue [shape="box" label="queue" style="dotted"] - M_P4_queue -> M_S4A [style="dotted"] + S4B [label="S4B:\nmaybe released\nmaybe open now\n(bound)" color="orange"] + S4A -> P4_connected [label="connected"] + P4_connected [shape="box" label="C.tx_open\nC.tx_add(queued)\nC.tx_release"] + S4B -> P4_send [label="add_message"] + P4_send [shape="box" label="queue\nC.tx_add(msg)"] + P4_send -> S4B + S4A -> P4_queue [label="add_message" style="dotted"] + P4_queue [shape="box" label="queue" style="dotted"] + P4_queue -> S4A [style="dotted"] - M_P4_release -> M_S4B - M_S4B -> M_S4A [label="M_lost()"] - M_S4B -> M_P4_process [label="rx message"] - M_P4_process [shape="octagon" label="process message"] - M_P4_process -> M_S4B + P4_connected -> S4B + S4B -> S4A [label="lost"] + S4B -> P4_process_ours [label="rx_message\n(ours)"] + P4_process_ours [shape="box" label="dequeue"] + P4_process_ours -> S4B + S4B -> P4_process_theirs [label="rx_message\n(theirs)"] + P4_process_theirs [shape="box" label="W.got_message"] + P4_process_theirs -> S4B - M_S4A -> M_S5A [label="(none)" style="invis"] - M_S4B -> M_S5B [label="rx released" color="orange" fontcolor="orange"] + S4A -> S5A [label="(none)" style="invis"] + S4B -> S5B [label="rx released" color="orange" fontcolor="orange"] - seed [label="from Seed?"] - M_S3A -> seed [style="invis"] - M_S4A -> seed [style="invis"] - seed -> M_S5A - {rank=same; seed M_S5A M_S5B M_P5_open M_process} - M_S5A [label="S5A:\nreleased\nopened >=once"] - M_S5B [label="S5B:\nreleased\nmaybe open now\n(bound)" color="green"] - M_S5A -> M_P5_open [label="M_connected()"] - M_P5_open [shape="box" label="tx open\ntx add(queued)"] - M_P5_open -> M_S5B - M_S5B -> M_S5A [label="M_lost()"] - M_S5B -> M_process [label="rx message" color="green" fontcolor="green"] - M_process [shape="octagon" label="process message" color="green"] - M_process -> M_S5B [color="green"] - M_S5B -> M_P5_send [label="M_send(msg)" color="green" fontcolor="green"] - M_P5_send [shape="box" label="queue\ntx add(msg)" color="green"] - M_P5_send -> M_S5B [color="green"] - M_S5A -> M_P5_queue [label="M_send(msg)" style="dotted"] - M_P5_queue [shape="box" label="queue" style="dotted"] - M_P5_queue -> M_S5A [style="dotted"] - M_S5B -> M_CcB_P_close [label="M_close()" style="dashed" color="orange" fontcolor="orange"] - M_CcB_P_close [label="tx close" style="dashed" color="orange"] + P4_queue -> S5A [style="invis"] + subgraph {S5A P5_connected S5B} + {rank=same; S5A P5_connected S5B} - M_process [shape="octagon" label="process message"] - M_process_me [shape="box" label="dequeue"] - M_process -> M_process_me [label="side == me"] - M_process_them [shape="box" label="" style="dotted"] - M_process -> M_process_them [label="side != me"] - M_process_them -> M_process_pake [label="phase == pake"] - M_process_pake [shape="box" label="WM_rx_pake()"] - M_process_them -> M_process_other [label="phase in (version,numbered)"] - M_process_other [shape="box" label="WM_rx_msg()"] + S5A [label="S5A:\nreleased\nopened >=once"] + S5A -> P5_connected [label="connected"] + P5_connected [shape="box" label="C.tx_open\nC.tx_add(queued)"] + + S5B -> P5_send [label="add_message" color="green" fontcolor="green"] + P5_send [shape="box" label="queue\nC.tx_add(msg)" color="green"] + P5_send -> S5B [color="green"] + S5A -> P5_queue [label="add_message" style="dotted"] + P5_queue [shape="box" label="queue" style="dotted"] + P5_queue -> S5A [style="dotted"] + + P5_connected -> S5B + S5B [label="S5B:\nreleased\nmaybe open now\n(bound)" color="green"] + S5B -> S5A [label="lost"] + + S5B -> P5_process_ours [label="rx_message\n(ours)"] + P5_process_ours [shape="box" label="dequeue"] + P5_process_ours -> S5B + S5B -> P5_process_theirs [label="rx_message\n(theirs)"] + P5_process_theirs [shape="box" label="W.got_message"] + P5_process_theirs -> S5B + + foo5 [label="" style="invis"] + S5A -> foo5 [style="invis"] + foo5 -> P5_close [style="invis"] + S5B -> P5_close [label="close" style="dashed" color="orange" fontcolor="orange"] + P5_close [shape="box" label="tx_close" style="dashed" color="orange"] } diff --git a/docs/mailbox_close.dot b/docs/mailbox_close.dot index 9da7688..7e521a1 100644 --- a/docs/mailbox_close.dot +++ b/docs/mailbox_close.dot @@ -1,73 +1,73 @@ digraph { /* M_close pathways */ - MC_title [label="Mailbox\nClose\nMachine" style="dotted"] - MC_title -> MC_S2B [style="invis"] + title [label="Mailbox\nClose\nMachine" style="dotted"] + title -> S2B [style="invis"] /* All dashed states are from the main Mailbox Machine diagram, and all dashed lines indicate M_close() pathways in from those states. Within this graph, all M_close() events leave the state unchanged. */ - MC_SrA [label="SrA:\nwaiting for:\nrelease"] - MC_SrA -> MC_Pr [label="M_connected()"] - MC_Pr [shape="box" label="tx release" color="orange"] - MC_Pr -> MC_SrB [color="orange"] - MC_SrB [label="SrB:\nwaiting for:\nrelease" color="orange"] - MC_SrB -> MC_SrA [label="M_lost()"] - MC_SrB -> MC_P_stop [label="rx released" color="orange" fontcolor="orange"] + SrA [label="SrA:\nwaiting for:\nrelease"] + SrA -> Pr [label="connected"] + Pr [shape="box" label="C.tx_release" color="orange"] + Pr -> SrB [color="orange"] + SrB [label="SrB:\nwaiting for:\nrelease" color="orange"] + SrB -> SrA [label="lost"] + SrB -> P_stop [label="rx_released" color="orange" fontcolor="orange"] - MC_ScA [label="ScA:\nwaiting for:\nclosed"] - MC_ScA -> MC_Pc [label="M_connected()"] - MC_Pc [shape="box" label="tx close" color="orange"] - MC_Pc -> MC_ScB [color="orange"] - MC_ScB [label="ScB:\nwaiting for:\nclosed" color="orange"] - MC_ScB -> MC_ScA [label="M_lost()"] - MC_ScB -> MC_P_stop [label="rx closed" color="orange" fontcolor="orange"] + ScA [label="ScA:\nwaiting for:\nclosed"] + ScA -> Pc [label="connected"] + Pc [shape="box" label="C.tx_close" color="orange"] + Pc -> ScB [color="orange"] + ScB [label="ScB:\nwaiting for:\nclosed" color="orange"] + ScB -> ScA [label="lost"] + ScB -> P_stop [label="rx_closed" color="orange" fontcolor="orange"] - MC_SrcA [label="SrcA:\nwaiting for:\nrelease\nclose"] - MC_SrcA -> MC_Prc [label="M_connected()"] - MC_Prc [shape="box" label="tx release\ntx close" color="orange"] - MC_Prc -> MC_SrcB [color="orange"] - MC_SrcB [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] - MC_SrcB -> MC_SrcA [label="M_lost()"] - MC_SrcB -> MC_ScB [label="rx released" color="orange" fontcolor="orange"] - MC_SrcB -> MC_SrB [label="rx closed" color="orange" fontcolor="orange"] + SrcA [label="SrcA:\nwaiting for:\nrelease\nclose"] + SrcA -> Prc [label="connected"] + Prc [shape="box" label="C.tx_release\nC.tx_close" color="orange"] + Prc -> SrcB [color="orange"] + SrcB [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] + SrcB -> SrcA [label="lost"] + SrcB -> ScB [label="rx_released" color="orange" fontcolor="orange"] + SrcB -> SrB [label="rx_closed" color="orange" fontcolor="orange"] - MC_P_stop [shape="box" label="C_stop()"] - MC_P_stop -> MC_SsB + P_stop [shape="box" label="C.stop"] + P_stop -> SsB - MC_SsB -> MC_Ss [label="M_stopped()"] - MC_SsB [label="SsB: closed\nstopping"] + SsB -> Ss [label="stopped"] + SsB [label="SsB: closed\nstopping"] - MC_Ss [label="Ss: closed" color="green"] + Ss [label="Ss: closed" color="green"] - MC_S0A [label="S0A" style="dashed"] - MC_S0A -> MC_P_stop [style="dashed"] - MC_S0B [label="S0B" style="dashed" color="orange"] - MC_S0B -> MC_P_stop [style="dashed" color="orange"] + S0A [label="S0A" style="dashed"] + S0A -> P_stop [style="dashed"] + S0B [label="S0B" style="dashed" color="orange"] + S0B -> P_stop [style="dashed" color="orange"] - {rank=same; MC_S2A MC_S2B MC_S3A MC_S3B MC_S4A MC_S4B MC_S5A MC_S5B} - MC_S1A [label="S1A" style="dashed"] - MC_S1A -> MC_P_stop [style="dashed"] + {rank=same; S2A S2B S3A S3B S4A S4B S5A S5B} + S1A [label="S1A" style="dashed"] + S1A -> P_stop [style="dashed"] - MC_S2A [label="S2A" style="dashed"] - MC_S2A -> MC_SrA [style="dashed"] - MC_S2B [label="S2B" color="orange" style="dashed"] - MC_S2B -> MC_Pr [color="orange" style="dashed"] + S2A [label="S2A" style="dashed"] + S2A -> SrA [label="stop" style="dashed"] + S2B [label="S2B" color="orange" style="dashed"] + S2B -> Pr [color="orange" style="dashed"] - MC_S3A [label="S3A" style="dashed"] - MC_S3B [label="S3B" color="orange" style="dashed"] - MC_S3A -> MC_SrcA [style="dashed"] - MC_S3B -> MC_Prc [color="orange" style="dashed"] + S3A [label="S3A" style="dashed"] + S3B [label="S3B" color="orange" style="dashed"] + S3A -> SrcA [style="dashed"] + S3B -> Prc [color="orange" style="dashed"] - MC_S4A [label="S4A" style="dashed"] - MC_S4B [label="S4B" color="orange" style="dashed"] - MC_S4A -> MC_SrcA [style="dashed"] - MC_S4B -> MC_Prc [color="orange" style="dashed"] + S4A [label="S4A" style="dashed"] + S4B [label="S4B" color="orange" style="dashed"] + S4A -> SrcA [style="dashed"] + S4B -> Prc [color="orange" style="dashed"] - MC_S5A [label="S5A" style="dashed"] - MC_S5B [label="S5B" color="green" style="dashed"] - MC_S5A -> MC_ScA [style="dashed"] - MC_S5B -> MC_Pc [style="dashed" color="green"] + S5A [label="S5A" style="dashed"] + S5B [label="S5B" color="green" style="dashed"] + S5A -> ScA [style="dashed"] + S5B -> Pc [style="dashed" color="green"] } From 2fc5af7bd0e5e8beb51de29fd46c18244f1dce29 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2017 16:17:56 -0800 Subject: [PATCH 044/176] nameplates.dot: done --- docs/nameplates.dot | 55 +++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/docs/nameplates.dot b/docs/nameplates.dot index 83bc86e..5ed4e66 100644 --- a/docs/nameplates.dot +++ b/docs/nameplates.dot @@ -1,47 +1,42 @@ digraph { - /* - "connected": NL_connected - "rx": NL_rx_nameplates - "refresh": NL_refresh_nameplates - */ - {rank=same; NL_title NL_S0A NL_S0B} - NL_title [label="Nameplate\nLister" style="dotted"] + {rank=same; title S0A S0B} + title [label="Nameplate\nLister" style="dotted"] - NL_S0A [label="S0A:\nnot wanting\nunconnected"] - NL_S0B [label="S0B:\nnot wanting\nconnected" color="orange"] + S0A [label="S0A:\nnot wanting\nunconnected"] + S0B [label="S0B:\nnot wanting\nconnected" color="orange"] - NL_S0A -> NL_S0B [label="connected"] - NL_S0B -> NL_S0A [label="lost"] + S0A -> S0B [label="connected"] + S0B -> S0A [label="lost"] - NL_S0A -> NL_S1A [label="refresh"] - NL_S0B -> NL_P_tx [label="refresh" color="orange"] + S0A -> S1A [label="refresh"] + S0B -> P_tx [label="refresh" color="orange" fontcolor="orange"] - NL_S0A -> NL_P_tx [style="invis"] + S0A -> P_tx [style="invis"] - {rank=same; NL_S1A NL_P_tx NL_S1B NL_C_notify} + {rank=same; S1A P_tx S1B P_notify} - NL_S1A [label="S1A:\nwant list\nunconnected"] - NL_S1B [label="S1B:\nwant list\nconnected" color="orange"] + S1A [label="S1A:\nwant list\nunconnected"] + S1B [label="S1B:\nwant list\nconnected" color="orange"] - NL_S1A -> NL_P_tx [label="connected"] - NL_P_tx [shape="box" label="C.tx_list()" color="orange"] - NL_P_tx -> NL_S1B [color="orange"] - NL_S1B -> NL_S1A [label="lost"] + S1A -> P_tx [label="connected"] + P_tx [shape="box" label="C.tx_list()" color="orange"] + P_tx -> S1B [color="orange"] + S1B -> S1A [label="lost"] - NL_S1A -> foo [label="refresh"] + S1A -> foo [label="refresh"] foo [label="" style="dashed"] - foo -> NL_S1A + foo -> S1A - NL_S1B -> foo2 [label="refresh"] + S1B -> foo2 [label="refresh"] foo2 [label="" style="dashed"] - foo2 -> NL_P_tx + foo2 -> P_tx - NL_S0B -> NL_C_notify [label="rx"] - NL_S1B -> NL_C_notify [label="rx"] - NL_C_notify [shape="box" label="W.got_nameplates()"] - NL_C_notify -> NL_S0B + S0B -> P_notify [label="rx"] + S1B -> P_notify [label="rx" color="orange" fontcolor="orange"] + P_notify [shape="box" label="W.got_nameplates()"] + P_notify -> S0B {rank=same; foo foo2 legend} legend [shape="box" style="dotted" - label="connected: NL_connected()\nlost: NL_lost()\nrefresh: NL_refresh_nameplates()\nrx: NL_rx_nameplates()"] + label="refresh: NL.refresh_nameplates()\nrx: NL.got_nameplates()"] } From 50050dc140f586e35809b0350ebf6b545b7e515a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Feb 2017 17:46:28 -0800 Subject: [PATCH 045/176] finish wormhole.dot --- docs/machines.dot | 19 ++++++-- docs/wormhole.dot | 115 ++++++++++++++++++++++++---------------------- 2 files changed, 75 insertions(+), 59 deletions(-) diff --git a/docs/machines.dot b/docs/machines.dot index 1c2272a..d0eff5e 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -1,25 +1,34 @@ digraph { + App [shape="box" color="blue" fontcolor="blue"] Wormhole [shape="box" label="Wormhole\n(manager)" color="blue" fontcolor="blue"] Mailbox [shape="box" color="blue" fontcolor="blue"] - Connection [shape="oval" color="blue" fontcolor="blue"] + Connection [label="Rendezvous\nConnector" + shape="oval" color="blue" fontcolor="blue"] websocket [shape="oval" color="blue" fontcolor="blue"] Nameplates [shape="box" label="Nameplate\nLister" - color="blue" fontcolor="blue"] + color="blue" fontcolor="blue" + ] Connection -> websocket [color="blue"] + App -> Wormhole [style="dashed" label="set_code\nsend\nclose\n(once)"] + App -> Wormhole [color="blue"] + Wormhole -> App [style="dashed" label="got_verifier\nreceived\nclosed\n(once)"] + + Wormhole -> Connection [color="blue"] + Wormhole -> Mailbox [style="dashed" - label="set_nameplate\nadd_message\nclose" + label="set_nameplate\nadd_message\nclose\n(once)" ] Wormhole -> Mailbox [color="blue"] Mailbox -> Wormhole [style="dashed" - label="got_message\nclosed" + label="got_message\nclosed\n(once)" ] + Mailbox -> Connection [style="dashed" label="tx_claim\ntx_open\ntx_add\ntx_release\ntx_close\nstop" ] - Mailbox -> Connection [color="blue"] Connection -> Mailbox [style="dashed" label="connected\nlost\nrx_claimed\nrx_message\nrx_released\nrx_closed\nstopped"] diff --git a/docs/wormhole.dot b/docs/wormhole.dot index 8c55ad1..83744dd 100644 --- a/docs/wormhole.dot +++ b/docs/wormhole.dot @@ -8,75 +8,82 @@ digraph { compute the key, send VERSION. Starting from the Return, this saves two round trips. OTOH it adds consequences to hitting Tab. */ - WM_start [label="Wormhole\nMachine" style="dotted"] - WM_start -> WM_S_nothing [style="invis"] + start [label="Wormhole\nMachine" style="dotted"] - WM_S_nothing [label="know\nnothing"] - WM_S_nothing -> WM_P_queue1 [label="API_send" style="dotted"] - WM_P_queue1 [shape="box" style="dotted" label="queue\noutbound msg"] - WM_P_queue1 -> WM_S_nothing [style="dotted"] - WM_S_nothing -> WM_P_build_pake [label="WM_set_code()"] + S0 [label="S0: know\nnothing"] + S0 -> P0_queue [label="send" style="dotted"] + P0_queue [shape="box" style="dotted" label="queue"] + P0_queue -> S0 [style="dotted"] + S0 -> P0_build [label="set_code"] - WM_P_build_pake [shape="box" label="build_pake()"] - WM_P_build_pake -> WM_S_save_pake - WM_S_save_pake [label="checkpoint"] - WM_S_save_pake -> WM_P_post_pake [label="saved"] + P0_build [shape="box" label="build_pake\nM.set_nameplate\nM.add_message(pake)"] + P0_build -> S1 + S1 [label="S1: know\ncode"] + S1 -> P1_queue [label="send" style="dotted"] + P1_queue [shape="box" style="dotted" label="queue"] + P1_queue -> S1 [style="dotted"] - WM_P_post_pake [label="M_set_nameplate()\nM_send(pake)" shape="box"] - WM_P_post_pake -> WM_S_know_code + /* the Mailbox will deliver each message exactly once, but doesn't + guarantee ordering: if Alice starts the process, then disconnects, + then Bob starts (reading PAKE, sending both his PAKE and his VERSION + phase), then Alice will see both PAKE and VERSION on her next + connect, and might get the VERSION first. - WM_S_know_code [label="know code\n"] - WM_S_know_code -> WM_P_queue2 [label="API_send" style="dotted"] - WM_P_queue2 [shape="box" style="dotted" label="queue\noutbound msg"] - WM_P_queue2 -> WM_S_know_code [style="dotted"] - WM_S_know_code -> WM_P_compute_key [label="WM_rx_pake"] - WM_S_know_code -> WM_P_mood_lonely [label="close"] + The Wormhole will queue inbound messages that it isn't ready for. The + wormhole shim that lets applications do w.get(phase=) must do + something similar, queueing inbound messages until it sees one for + the phase it currently cares about.*/ - WM_P_compute_key [label="compute_key()" shape="box"] - WM_P_compute_key -> WM_P_save_key [label="pake ok"] - WM_P_save_key [label="checkpoint"] - WM_P_save_key -> WM_P_post_version [label="saved"] - WM_P_compute_key -> WM_P_mood_scary [label="pake bad"] + S1 -> P_mood_scary [label="got_message(pake)\npake bad"] + S1 -> P1_compute [label="got_message(pake)\npake good"] + S1 -> P1_queue_inbound [label="got_message(other)"] + P1_queue_inbound [shape="box" style="dotted" label="queue\ninbound"] + P1_queue_inbound -> S1 + S1 -> P_mood_lonely [label="close"] - WM_P_mood_scary [shape="box" label="M_close()\nmood=scary"] - WM_P_mood_scary -> WM_P_notify_failure + P1_compute [label="compute_key\nM.add_message(version)\nA.got_verifier\nschedule process inbound queue?" shape="box"] + P1_compute -> S2 - WM_P_notify_failure [shape="box" label="notify_failure()" color="red"] - WM_P_notify_failure -> WM_S_closed + P_mood_scary [shape="box" label="M.close\nmood=scary"] + P_mood_scary -> P_notify_failure - WM_P_post_version [label="M_send(version)\nnotify_verifier()" shape="box"] - WM_P_post_version -> WM_S_know_key + P_notify_failure [shape="box" label="(record failure)" color="red"] + P_notify_failure -> S_closing - WM_S_know_key [label="know_key\nunverified" color="orange"] - WM_S_know_key -> WM_P_queue3 [label="API_send" style="dotted"] - WM_P_queue3 [shape="box" style="dotted" label="queue\noutbound msg"] - WM_P_queue3 -> WM_S_know_key [style="dotted"] - WM_S_know_key -> WM_P_verify [label="WM_rx_msg()"] /* version or phase */ - WM_S_know_key -> WM_P_mood_lonely [label="close"] /* more like impatient */ + S2 [label="S2: know_key\n(unverified)" color="orange"] + S2 -> P_queue3 [label="send" style="dotted"] + P_queue3 [shape="box" style="dotted" label="queue"] + P_queue3 -> S2 [style="dotted"] + S2 -> P_verify [label="got_message"] /* version or phase */ + S2 -> P_mood_lonely [label="close"] /* more like impatient */ - WM_P_verify [label="verify(msg)" shape="box"] - WM_P_verify -> WM_P_accept_msg [label="verify good"] - WM_P_verify -> WM_P_mood_scary [label="verify bad"] + P_verify [label="verify(msg)" shape="box"] + P_verify -> P_accept_msg [label="(good)"] + P_verify -> P_mood_scary [label="(bad)"] - WM_P_accept_msg [label="deliver\ninbound\nmsg()" shape="box"] - WM_P_accept_msg -> WM_P_send_queued + P_accept_msg [label="A.received(msg)\nencrypt queued\nM.add_message(queued)" + shape="box"] + P_accept_msg -> S3 - WM_P_send_queued [shape="box" label="M_send()\nqueued"] - WM_P_send_queued -> WM_S_verified_key + S3 [label="S3: know_key\n(verified)" color="green"] + S3 -> P_verify [label="got_message"] /* probably phase */ + S3 -> P_mood_happy [label="close"] + S3 -> P_send [label="send"] - WM_S_verified_key [color="green"] - WM_S_verified_key -> WM_P_verify [label="WM_rx_msg()"] /* probably phase */ - WM_S_verified_key -> WM_P_mood_happy [label="close"] - WM_S_verified_key -> WM_P_send [label="API_send"] + P_mood_happy [shape="box" label="M.close\nmood=happy"] + P_mood_happy -> S_closing - WM_P_mood_happy [shape="box" label="M_close()\nmood=happy"] - WM_P_mood_happy -> WM_S_closed + P_mood_lonely [shape="box" label="M.close\nmood=lonely"] + P_mood_lonely -> S_closing - WM_P_mood_lonely [shape="box" label="M_close()\nmood=lonely"] - WM_P_mood_lonely -> WM_S_closed + P_send [shape="box" label="encrypt\nM.add_message(msg)"] + P_send -> S3 - WM_P_send [shape="box" label="M_send(msg)"] - WM_P_send -> WM_S_verified_key + S_closing [label="closing"] + S_closing -> P_closed [label="closed"] + S_closing -> S_closing [label="got_message"] - WM_S_closed [label="closed"] + P_closed [shape="box" label="A.closed"] + P_closed -> S_closed + S_closed [label="closed"] } From c050d067532b62f2eb69bca01cca9abf56e35581 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Feb 2017 11:26:11 -0800 Subject: [PATCH 046/176] update code.dot --- docs/code.dot | 71 ++++++++++++++++++++++++++--------------------- docs/machines.dot | 24 +++++++++++----- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/docs/code.dot b/docs/code.dot index 991036f..429aa74 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -1,41 +1,48 @@ digraph { - WCM_start [label="Wormhole Code\nMachine" style="dotted"] - WCM_start -> WCM_S_unknown [style="invis"] - WCM_S_unknown [label="unknown"] - WCM_S_unknown -> WCM_P_set_code [label="set"] - WCM_P_set_code [shape="box" label="WM_set_code()"] - WCM_P_set_code -> WCM_S_known - WCM_S_known [label="known" color="green"] + start [label="Wormhole Code\nMachine" style="dotted"] + {rank=same; start S0} + {rank=same; P_list_nameplates P_allocate} + {rank=same; S1 S2} + {rank=same; S3 P_allocate_generate} + start -> S0 [style="invis"] + S0 [label="S0:\nunknown"] + S0 -> P_set_code [label="set"] + P_set_code [shape="box" label="W.set_code"] + P_set_code -> S_known + S_known [label="known" color="green"] - WCM_S_unknown -> WCM_P_list_nameplates [label="input"] - WCM_S_typing_nameplate [label="typing\nnameplate"] + S0 -> P_list_nameplates [label="input"] + S2 [label="S2: typing\nnameplate"] - WCM_S_typing_nameplate -> WCM_P_nameplate_completion [label=""] - WCM_P_nameplate_completion [shape="box" label="completion?"] - WCM_P_nameplate_completion -> WCM_P_list_nameplates - WCM_P_list_nameplates [shape="box" label="NLM_update_nameplates()"] - WCM_P_list_nameplates -> WCM_S_typing_nameplate + S2 -> P_nameplate_completion [label=""] + P_nameplate_completion [shape="box" label="do completion"] + P_nameplate_completion -> P_list_nameplates + P_list_nameplates [shape="box" label="NL.refresh_nameplates"] + P_list_nameplates -> S2 - WCM_S_typing_nameplate -> WCM_P_got_nameplates [label="C_rx_nameplates()"] - WCM_P_got_nameplates [shape="box" label="stash nameplates\nfor completion"] - WCM_P_got_nameplates -> WCM_S_typing_nameplate - WCM_S_typing_nameplate -> WCM_P_finish_nameplate [label="finished\nnameplate"] - WCM_P_finish_nameplate [shape="box" label="lookup wordlist\nfor completion"] - WCM_P_finish_nameplate -> WCM_S_typing_code - WCM_S_typing_code [label="typing\ncode"] - WCM_S_typing_code -> WCM_P_code_completion [label=""] - WCM_P_code_completion [shape="box" label="completion"] - WCM_P_code_completion -> WCM_S_typing_code + S2 -> P_got_nameplates [label="got_nameplates"] + P_got_nameplates [shape="box" label="stash nameplates\nfor completion"] + P_got_nameplates -> S2 + S2 -> P_finish_nameplate [label="" + color="orange" + fontcolor="orange"] + P_finish_nameplate [shape="box" label="lookup wordlist\nfor completion"] + P_finish_nameplate -> S3 + S3 [label="S3: typing\ncode"] + S3 -> P_code_completion [label=""] + P_code_completion [shape="box" label="do completion"] + P_code_completion -> S3 - WCM_S_typing_code -> WCM_P_set_code [label="finished\ncode"] + S3 -> P_set_code [label="" + color="orange" fontcolor="orange"] - WCM_S_unknown -> WCM_P_allocate [label="allocate"] - WCM_P_allocate [shape="box" label="C_allocate_nameplate()"] - WCM_P_allocate -> WCM_S_allocate_waiting - WCM_S_allocate_waiting [label="waiting"] - WCM_S_allocate_waiting -> WCM_P_allocate_generate [label="WCM_rx_allocation()"] - WCM_P_allocate_generate [shape="box" label="generate\nrandom code"] - WCM_P_allocate_generate -> WCM_P_set_code + S0 -> P_allocate [label="allocate"] + P_allocate [shape="box" label="C.tx_allocate"] + P_allocate -> S1 + S1 [label="S1:\nallocating"] + S1 -> P_allocate_generate [label="rx_allocated"] + P_allocate_generate [shape="box" label="generate\nrandom code"] + P_allocate_generate -> P_set_code } diff --git a/docs/machines.dot b/docs/machines.dot index d0eff5e..89dc10e 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -6,9 +6,11 @@ digraph { Connection [label="Rendezvous\nConnector" shape="oval" color="blue" fontcolor="blue"] websocket [shape="oval" color="blue" fontcolor="blue"] + Code [shape="box" label="Code" color="blue" fontcolor="blue"] Nameplates [shape="box" label="Nameplate\nLister" color="blue" fontcolor="blue" ] + {rank=same; Nameplates Code} Connection -> websocket [color="blue"] @@ -32,13 +34,6 @@ digraph { Connection -> Mailbox [style="dashed" label="connected\nlost\nrx_claimed\nrx_message\nrx_released\nrx_closed\nstopped"] - Wormhole -> Nameplates [style="dashed" - label="refresh_nameplates" - ] - Wormhole -> Nameplates [color="blue"] - Nameplates -> Wormhole [style="dashed" - label="got_nameplates" - ] Connection -> Nameplates [style="dashed" label="connected\nlost\nrx_nameplates" ] @@ -46,6 +41,21 @@ digraph { label="tx_list" ] + Wormhole -> Code [color="blue"] + Code -> Connection [style="dashed" + label="tx_allocate" + ] + Connection -> Code [style="dashed" + label="rx_allocated"] + Nameplates -> Code [style="dashed" + label="got_nameplates" + ] + Code -> Nameplates [color="blue"] + Code -> Nameplates [style="dashed" + label="refresh_nameplates" + ] + + } From a67564833549536663a85b20fa2ed58d865972b0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Feb 2017 11:31:49 -0800 Subject: [PATCH 047/176] code.dot: better names --- docs/code.dot | 58 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/docs/code.dot b/docs/code.dot index 429aa74..f19c956 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -2,47 +2,45 @@ digraph { start [label="Wormhole Code\nMachine" style="dotted"] {rank=same; start S0} - {rank=same; P_list_nameplates P_allocate} + {rank=same; P0_list_nameplates P0_allocate} {rank=same; S1 S2} - {rank=same; S3 P_allocate_generate} + {rank=same; S3 P1_generate} start -> S0 [style="invis"] S0 [label="S0:\nunknown"] - S0 -> P_set_code [label="set"] - P_set_code [shape="box" label="W.set_code"] - P_set_code -> S_known - S_known [label="known" color="green"] + S0 -> P0_set_code [label="set"] + P0_set_code [shape="box" label="W.set_code"] + P0_set_code -> S4 + S4 [label="S4: known" color="green"] - S0 -> P_list_nameplates [label="input"] + S0 -> P0_list_nameplates [label="input"] S2 [label="S2: typing\nnameplate"] - S2 -> P_nameplate_completion [label=""] - P_nameplate_completion [shape="box" label="do completion"] - P_nameplate_completion -> P_list_nameplates - P_list_nameplates [shape="box" label="NL.refresh_nameplates"] - P_list_nameplates -> S2 + S2 -> P2_completion [label=""] + P2_completion [shape="box" label="do completion"] + P2_completion -> P0_list_nameplates + P0_list_nameplates [shape="box" label="NL.refresh_nameplates"] + P0_list_nameplates -> S2 - S2 -> P_got_nameplates [label="got_nameplates"] - P_got_nameplates [shape="box" label="stash nameplates\nfor completion"] - P_got_nameplates -> S2 - S2 -> P_finish_nameplate [label="" - color="orange" - fontcolor="orange"] - P_finish_nameplate [shape="box" label="lookup wordlist\nfor completion"] - P_finish_nameplate -> S3 + S2 -> P2_got_nameplates [label="got_nameplates"] + P2_got_nameplates [shape="box" label="stash nameplates\nfor completion"] + P2_got_nameplates -> S2 + S2 -> P2_finish [label="" color="orange" fontcolor="orange"] + P2_finish [shape="box" label="lookup wordlist\nfor completion"] + P2_finish -> S3 S3 [label="S3: typing\ncode"] - S3 -> P_code_completion [label=""] - P_code_completion [shape="box" label="do completion"] - P_code_completion -> S3 + S3 -> P3_completion [label=""] + P3_completion [shape="box" label="do completion"] + P3_completion -> S3 - S3 -> P_set_code [label="" + S3 -> P0_set_code [label="" color="orange" fontcolor="orange"] - S0 -> P_allocate [label="allocate"] - P_allocate [shape="box" label="C.tx_allocate"] - P_allocate -> S1 + S0 -> P0_allocate [label="allocate"] + P0_allocate [shape="box" label="C.tx_allocate"] + P0_allocate -> S1 S1 [label="S1:\nallocating"] - S1 -> P_allocate_generate [label="rx_allocated"] - P_allocate_generate [shape="box" label="generate\nrandom code"] - P_allocate_generate -> P_set_code + S1 -> P1_generate [label="rx_allocated"] + P1_generate [shape="box" label="generate\nrandom code"] + P1_generate -> P0_set_code } From e85309a7844d2a06c883a32d3c3ae21f125d0796 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Feb 2017 12:27:15 -0800 Subject: [PATCH 048/176] split out receive/send machines --- docs/key.dot | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/receive.dot | 49 +++++++++++++++++++++++++++++ docs/send.dot | 16 ++++++++++ 3 files changed, 147 insertions(+) create mode 100644 docs/key.dot create mode 100644 docs/receive.dot create mode 100644 docs/send.dot diff --git a/docs/key.dot b/docs/key.dot new file mode 100644 index 0000000..b342475 --- /dev/null +++ b/docs/key.dot @@ -0,0 +1,82 @@ +digraph { + + /* could shave a RTT by committing to the nameplate early, before + finishing the rest of the code input. While the user is still + typing/completing the code, we claim the nameplate, open the mailbox, + and retrieve the peer's PAKE message. Then as soon as the user + finishes entering the code, we build our own PAKE message, send PAKE, + compute the key, send VERSION. Starting from the Return, this saves + two round trips. OTOH it adds consequences to hitting Tab. */ + + start [label="Key\nMachine" style="dotted"] + + S0 [label="S0: know\nnothing"] + S0 -> P0_build [label="set_code"] + + P0_build [shape="box" label="build_pake\nM.set_nameplate\nM.add_message(pake)"] + P0_build -> S1 + S1 [label="S1: know\ncode"] + + /* the Mailbox will deliver each message exactly once, but doesn't + guarantee ordering: if Alice starts the process, then disconnects, + then Bob starts (reading PAKE, sending both his PAKE and his VERSION + phase), then Alice will see both PAKE and VERSION on her next + connect, and might get the VERSION first. + + The Wormhole will queue inbound messages that it isn't ready for. The + wormhole shim that lets applications do w.get(phase=) must do + something similar, queueing inbound messages until it sees one for + the phase it currently cares about.*/ + + S1 -> P_mood_scary [label="got_message(pake)\npake bad"] + S1 -> P1_compute [label="got_message(pake)\npake good"] + S1 -> P1_queue_inbound [label="got_message(data)" style="dashed"] + P1_queue_inbound [shape="box" style="dotted" label="queue"] + P1_queue_inbound -> S1 [style="dashed"] + S1 -> P1_queue_version [label="got_message(version)"] + P1_queue_version [label="queue\nversion??"] + P1_queue_version -> S1 + S1 -> P_mood_lonely [label="close"] + + P1_compute [label="compute_key\nM.add_message(version)\nA.got_verifier" shape="box"] + P1_compute -> S2 + + P_mood_scary [shape="box" label="M.close\nmood=scary"] + P_mood_scary -> P_notify_failure + + P_notify_failure [shape="box" label="(record failure)" color="red"] + P_notify_failure -> S_closing + + S2 [label="S2: know_key\n(unverified)" color="orange"] + S2 -> P2_queue_inbound [label="got_message(data)" style="dashed"] + P2_queue_inbound [shape="box" style="dotted" label="queue"] + P2_queue_inbound -> S2 + S2 -> P2_verified [label="got_message(version)\ngood"] + S2 -> P_mood_scary [label="got_message(version)\nbad"] + S2 -> P_mood_lonely [label="close"] /* more like impatient */ + + P2_verified [label="D.got_message(queued)\nA.received(msg)\nencrypt queued\nM.add_message(queued)" + shape="box"] + P2_verified -> S3 + + S3 [label="S3: know_key\n(verified)" color="green"] + S3 -> P3_accept [label="got_message(data)"] /* probably phase */ + S3 -> P_mood_happy [label="close"] + S3 -> P_notify_failure [label="scary"] + + P3_accept [shape="box" label="decrypt\nR.got_message(good,bad)"] + P3_accept -> S3 + P_mood_happy [shape="box" label="M.close\nmood=happy"] + P_mood_happy -> S_closing + + P_mood_lonely [shape="box" label="M.close\nmood=lonely"] + P_mood_lonely -> S_closing + + S_closing [label="closing"] + S_closing -> P_closed [label="closed"] + S_closing -> S_closing [label="got_message"] + + P_closed [shape="box" label="A.closed"] + P_closed -> S_closed + S_closed [label="closed"] +} diff --git a/docs/receive.dot b/docs/receive.dot new file mode 100644 index 0000000..1f26604 --- /dev/null +++ b/docs/receive.dot @@ -0,0 +1,49 @@ +digraph { + + /* could shave a RTT by committing to the nameplate early, before + finishing the rest of the code input. While the user is still + typing/completing the code, we claim the nameplate, open the mailbox, + and retrieve the peer's PAKE message. Then as soon as the user + finishes entering the code, we build our own PAKE message, send PAKE, + compute the key, send VERSION. Starting from the Return, this saves + two round trips. OTOH it adds consequences to hitting Tab. */ + + start [label="Receive\nMachine" style="dotted"] + + S0 [label="S0: unknown\ncode"] + S0 -> P0_got_key [label="got_verified_key"] + + P0_got_key [shape="box" label="record key"] + P0_got_key -> S1 + + S0 -> P_mood_lonely [label="close"] + + S1 [label="S1: verified\nkey" color="green"] + + S1 -> P_mood_scary [label="got_message(bad)"] + S1 -> P1_accept_msg [label="got_message(good)"] + S1 -> P_mood_happy [label="close"] + + P1_accept_msg [label="A.received(msg)" shape="box"] + P1_accept_msg -> S1 + + P_mood_scary [shape="box" label="K.scary"] + P_mood_scary -> S_closed + + P_notify_failure [shape="box" label="(record failure)" color="red"] + P_notify_failure -> S_closing + + P_mood_happy [shape="box" label="M.close\nmood=happy"] + P_mood_happy -> S_closing + + P_mood_lonely [shape="box" label="M.close\nmood=lonely"] + P_mood_lonely -> S_closing + + S_closing [label="closing"] + S_closing -> P_closed [label="closed"] + S_closing -> S_closing [label="got_message"] + + P_closed [shape="box" label="A.closed"] + P_closed -> S_closed + S_closed [label="closed"] +} diff --git a/docs/send.dot b/docs/send.dot new file mode 100644 index 0000000..6f4ad6b --- /dev/null +++ b/docs/send.dot @@ -0,0 +1,16 @@ +digraph { + start [label="Send\nMachine" style="dotted"] + + S0 [label="S0: unknown\nkey"] + S0 -> P0_queue [label="send" style="dashed"] + P0_queue [shape="box" label="queue" style="dashed"] + P0_queue -> S0 [style="dashed"] + S0 -> P0_got_key [label="set_verified_key"] + + P0_got_key [shape="box" label="drain queue:\n[encrypt\n M.add_message]"] + P0_got_key -> S1 + S1 [label="S1: verified\nkey"] + S1 -> P1_send [label="send"] + P1_send [shape="box" label="encrypt\nM.add_message"] + P1_send -> S1 +} From f3b1e847e9e9f47a52a0512a6a479fa2709fc91e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 21 Feb 2017 17:56:32 -0800 Subject: [PATCH 049/176] fix everything: should now be consistent and correct Start with machines.dot, which gives the override. Then traverse downwards from wormhole.dot . --- docs/code.dot | 4 +-- docs/key.dot | 57 +++++------------------------ docs/machines.dot | 43 +++++++++++++++------- docs/mailbox.dot | 9 ++--- docs/order.dot | 35 ++++++++++++++++++ docs/receive.dot | 48 ++++++++++--------------- docs/send.dot | 11 +++--- docs/wormhole.dot | 92 ++++++++++++++++------------------------------- 8 files changed, 138 insertions(+), 161 deletions(-) create mode 100644 docs/order.dot diff --git a/docs/code.dot b/docs/code.dot index f19c956..864d757 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -24,7 +24,7 @@ digraph { S2 -> P2_got_nameplates [label="got_nameplates"] P2_got_nameplates [shape="box" label="stash nameplates\nfor completion"] P2_got_nameplates -> S2 - S2 -> P2_finish [label="" color="orange" fontcolor="orange"] + S2 -> P2_finish [label="" color="orange" fontcolor="orange"] P2_finish [shape="box" label="lookup wordlist\nfor completion"] P2_finish -> S3 S3 [label="S3: typing\ncode"] @@ -33,7 +33,7 @@ digraph { P3_completion -> S3 S3 -> P0_set_code [label="" - color="orange" fontcolor="orange"] + color="orange" fontcolor="orange"] S0 -> P0_allocate [label="allocate"] P0_allocate [shape="box" label="C.tx_allocate"] diff --git a/docs/key.dot b/docs/key.dot index b342475..de75c2b 100644 --- a/docs/key.dot +++ b/docs/key.dot @@ -13,7 +13,7 @@ digraph { S0 [label="S0: know\nnothing"] S0 -> P0_build [label="set_code"] - P0_build [shape="box" label="build_pake\nM.set_nameplate\nM.add_message(pake)"] + P0_build [shape="box" label="build_pake\nM.add_message(pake)"] P0_build -> S1 S1 [label="S1: know\ncode"] @@ -28,55 +28,16 @@ digraph { something similar, queueing inbound messages until it sees one for the phase it currently cares about.*/ - S1 -> P_mood_scary [label="got_message(pake)\npake bad"] - S1 -> P1_compute [label="got_message(pake)\npake good"] - S1 -> P1_queue_inbound [label="got_message(data)" style="dashed"] - P1_queue_inbound [shape="box" style="dotted" label="queue"] - P1_queue_inbound -> S1 [style="dashed"] - S1 -> P1_queue_version [label="got_message(version)"] - P1_queue_version [label="queue\nversion??"] - P1_queue_version -> S1 - S1 -> P_mood_lonely [label="close"] + S1 -> P_mood_scary [label="got_pake\npake bad"] + P_mood_scary [shape="box" color="red" label="W.scared"] + P_mood_scary -> S3 [color="red"] + S3 [label="S3:\nscared" color="red"] + S1 -> P1_compute [label="got_pake\npake good"] + #S1 -> P_mood_lonely [label="close"] - P1_compute [label="compute_key\nM.add_message(version)\nA.got_verifier" shape="box"] + P1_compute [label="compute_key\nM.add_message(version)\nW.got_verifier\nR.got_key" shape="box"] P1_compute -> S2 - P_mood_scary [shape="box" label="M.close\nmood=scary"] - P_mood_scary -> P_notify_failure + S2 [label="S2: know_key" color="green"] - P_notify_failure [shape="box" label="(record failure)" color="red"] - P_notify_failure -> S_closing - - S2 [label="S2: know_key\n(unverified)" color="orange"] - S2 -> P2_queue_inbound [label="got_message(data)" style="dashed"] - P2_queue_inbound [shape="box" style="dotted" label="queue"] - P2_queue_inbound -> S2 - S2 -> P2_verified [label="got_message(version)\ngood"] - S2 -> P_mood_scary [label="got_message(version)\nbad"] - S2 -> P_mood_lonely [label="close"] /* more like impatient */ - - P2_verified [label="D.got_message(queued)\nA.received(msg)\nencrypt queued\nM.add_message(queued)" - shape="box"] - P2_verified -> S3 - - S3 [label="S3: know_key\n(verified)" color="green"] - S3 -> P3_accept [label="got_message(data)"] /* probably phase */ - S3 -> P_mood_happy [label="close"] - S3 -> P_notify_failure [label="scary"] - - P3_accept [shape="box" label="decrypt\nR.got_message(good,bad)"] - P3_accept -> S3 - P_mood_happy [shape="box" label="M.close\nmood=happy"] - P_mood_happy -> S_closing - - P_mood_lonely [shape="box" label="M.close\nmood=lonely"] - P_mood_lonely -> S_closing - - S_closing [label="closing"] - S_closing -> P_closed [label="closed"] - S_closing -> S_closing [label="got_message"] - - P_closed [shape="box" label="A.closed"] - P_closed -> S_closed - S_closed [label="closed"] } diff --git a/docs/machines.dot b/docs/machines.dot index 89dc10e..7e2ecd3 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -4,29 +4,46 @@ digraph { color="blue" fontcolor="blue"] Mailbox [shape="box" color="blue" fontcolor="blue"] Connection [label="Rendezvous\nConnector" - shape="oval" color="blue" fontcolor="blue"] - websocket [shape="oval" color="blue" fontcolor="blue"] + shape="box" color="blue" fontcolor="blue"] + websocket [color="blue" fontcolor="blue"] + Order [shape="box" label="Ordering" color="blue" fontcolor="blue"] + Key [shape="box" label="Key" color="blue" fontcolor="blue"] + Send [shape="box" label="Send" color="blue" fontcolor="blue"] + Receive [shape="box" label="Receive" color="blue" fontcolor="blue"] Code [shape="box" label="Code" color="blue" fontcolor="blue"] Nameplates [shape="box" label="Nameplate\nLister" color="blue" fontcolor="blue" ] - {rank=same; Nameplates Code} Connection -> websocket [color="blue"] + #Connection -> Order [color="blue"] App -> Wormhole [style="dashed" label="set_code\nsend\nclose\n(once)"] - App -> Wormhole [color="blue"] + #App -> Wormhole [color="blue"] Wormhole -> App [style="dashed" label="got_verifier\nreceived\nclosed\n(once)"] - Wormhole -> Connection [color="blue"] + #Wormhole -> Connection [color="blue"] + + Wormhole -> Send [style="dashed" label="send"] Wormhole -> Mailbox [style="dashed" - label="set_nameplate\nadd_message\nclose\n(once)" - ] - Wormhole -> Mailbox [color="blue"] - Mailbox -> Wormhole [style="dashed" - label="got_message\nclosed\n(once)" + label="set_nameplate\nclose\n(once)" ] + #Wormhole -> Mailbox [color="blue"] + Mailbox -> Wormhole [style="dashed" label="closed\n(once)"] + Mailbox -> Order [style="dashed" label="got_message (once)"] + Wormhole -> Key [style="dashed" label="set_code"] + Key -> Wormhole [style="dashed" label="got_verifier\nscared"] + Order -> Key [style="dashed" label="got_pake"] + Order -> Receive [style="dashed" label="got_message"] + #Wormhole -> Key [color="blue"] + Key -> Mailbox [style="dashed" label="add_message (pake)\nadd_message (version)"] + Receive -> Send [style="dashed" label="got_verified_key"] + Send -> Mailbox [style="dashed" label="add_message (phase)"] + + Key -> Receive [style="dashed" label="got_key"] + Receive -> Wormhole [style="dashed" + label="happy\nscared\ngot_message"] Mailbox -> Connection [style="dashed" label="tx_claim\ntx_open\ntx_add\ntx_release\ntx_close\nstop" @@ -41,7 +58,7 @@ digraph { label="tx_list" ] - Wormhole -> Code [color="blue"] + #Wormhole -> Code [color="blue"] Code -> Connection [style="dashed" label="tx_allocate" ] @@ -50,10 +67,12 @@ digraph { Nameplates -> Code [style="dashed" label="got_nameplates" ] - Code -> Nameplates [color="blue"] + #Code -> Nameplates [color="blue"] Code -> Nameplates [style="dashed" label="refresh_nameplates" ] + Code -> Wormhole [style="dashed" + label="set_code"] diff --git a/docs/mailbox.dot b/docs/mailbox.dot index 65fb17d..51afafc 100644 --- a/docs/mailbox.dot +++ b/docs/mailbox.dot @@ -86,8 +86,9 @@ digraph { P3_process_ours -> S3B S3B -> P3_process_theirs [label="rx_message\n(theirs)" color="orange" fontcolor="orange"] - P3_process_theirs [shape="box" label="C.tx_release\nW.got_message" - color="orange"] + P3_process_theirs [shape="box" color="orange" + label="C.tx_release\nO.got_message if new\nrecord" + ] /* pay attention to the race here: this process_message() will deliver msg_pake to the WormholeMachine, which will compute_key() and send(version), and we're in between S1A (where send gets @@ -116,7 +117,7 @@ digraph { P4_process_ours [shape="box" label="dequeue"] P4_process_ours -> S4B S4B -> P4_process_theirs [label="rx_message\n(theirs)"] - P4_process_theirs [shape="box" label="W.got_message"] + P4_process_theirs [shape="box" label="O.got_message if new\nrecord"] P4_process_theirs -> S4B S4A -> S5A [label="(none)" style="invis"] @@ -145,7 +146,7 @@ digraph { P5_process_ours [shape="box" label="dequeue"] P5_process_ours -> S5B S5B -> P5_process_theirs [label="rx_message\n(theirs)"] - P5_process_theirs [shape="box" label="W.got_message"] + P5_process_theirs [shape="box" label="O.got_message if new\nrecord"] P5_process_theirs -> S5B foo5 [label="" style="invis"] diff --git a/docs/order.dot b/docs/order.dot new file mode 100644 index 0000000..202bc10 --- /dev/null +++ b/docs/order.dot @@ -0,0 +1,35 @@ +digraph { + start [label="Order\nMachine" style="dotted"] + /* our goal: deliver PAKE before anything else */ + + {rank=same; S0 P0_other} + {rank=same; S1 P1_other} + + S0 [label="S0: no pake" color="orange"] + S1 [label="S1: yes pake" color="green"] + S0 -> P0_pake [label="got_pake" + color="orange" fontcolor="orange"] + P0_pake [shape="box" color="orange" + label="K.got_pake\ndrain queue:\n[R.got_message]" + ] + P0_pake -> S1 [color="orange"] + S0 -> P0_other [label="got_version\ngot_phase" style="dotted"] + P0_other [shape="box" label="queue" style="dotted"] + P0_other -> S0 [style="dotted"] + + S1 -> P1_other [label="got_version\ngot_phase"] + P1_other [shape="box" label="R.got_message"] + P1_other -> S1 + + + /* the Mailbox will deliver each message exactly once, but doesn't + guarantee ordering: if Alice starts the process, then disconnects, + then Bob starts (reading PAKE, sending both his PAKE and his VERSION + phase), then Alice will see both PAKE and VERSION on her next + connect, and might get the VERSION first. + + The Wormhole will queue inbound messages that it isn't ready for. The + wormhole shim that lets applications do w.get(phase=) must do + something similar, queueing inbound messages until it sees one for + the phase it currently cares about.*/ +} diff --git a/docs/receive.dot b/docs/receive.dot index 1f26604..3761350 100644 --- a/docs/receive.dot +++ b/docs/receive.dot @@ -10,40 +10,30 @@ digraph { start [label="Receive\nMachine" style="dotted"] - S0 [label="S0: unknown\ncode"] - S0 -> P0_got_key [label="got_verified_key"] + S0 [label="S0:\nunknown key" color="orange"] + S0 -> P0_got_key [label="got_key" color="orange"] - P0_got_key [shape="box" label="record key"] - P0_got_key -> S1 + P0_got_key [shape="box" label="record key" color="orange"] + P0_got_key -> S1 [color="orange"] - S0 -> P_mood_lonely [label="close"] + S1 [label="S1:\nunverified key" color="orange"] + S1 -> P_mood_scary [label="got_message\n(bad)"] + S1 -> P1_accept_msg [label="got_message\n(good)" color="orange"] + P1_accept_msg [shape="box" label="S.got_verified_key\nW.happy\nW.got_message" + color="orange"] + P1_accept_msg -> S2 [color="orange"] - S1 [label="S1: verified\nkey" color="green"] + S2 [label="S2:\nverified key" color="green"] - S1 -> P_mood_scary [label="got_message(bad)"] - S1 -> P1_accept_msg [label="got_message(good)"] - S1 -> P_mood_happy [label="close"] + S2 -> P2_accept_msg [label="got_message\n(good)" color="orange"] + S2 -> P_mood_scary [label="got_message(bad)"] - P1_accept_msg [label="A.received(msg)" shape="box"] - P1_accept_msg -> S1 + P2_accept_msg [label="W.got_message" shape="box" color="orange"] + P2_accept_msg -> S2 [color="orange"] - P_mood_scary [shape="box" label="K.scary"] - P_mood_scary -> S_closed + P_mood_scary [shape="box" label="W.scared" color="red"] + P_mood_scary -> S3 [color="red"] - P_notify_failure [shape="box" label="(record failure)" color="red"] - P_notify_failure -> S_closing - - P_mood_happy [shape="box" label="M.close\nmood=happy"] - P_mood_happy -> S_closing - - P_mood_lonely [shape="box" label="M.close\nmood=lonely"] - P_mood_lonely -> S_closing - - S_closing [label="closing"] - S_closing -> P_closed [label="closed"] - S_closing -> S_closing [label="got_message"] - - P_closed [shape="box" label="A.closed"] - P_closed -> S_closed - S_closed [label="closed"] + S3 [label="S3:\nscared" color="red"] + S3 -> S3 [label="got_message"] } diff --git a/docs/send.dot b/docs/send.dot index 6f4ad6b..91ed067 100644 --- a/docs/send.dot +++ b/docs/send.dot @@ -1,11 +1,14 @@ digraph { start [label="Send\nMachine" style="dotted"] + {rank=same; S0 P0_queue} + {rank=same; S1 P1_send} + S0 [label="S0: unknown\nkey"] - S0 -> P0_queue [label="send" style="dashed"] - P0_queue [shape="box" label="queue" style="dashed"] - P0_queue -> S0 [style="dashed"] - S0 -> P0_got_key [label="set_verified_key"] + S0 -> P0_queue [label="send" style="dotted"] + P0_queue [shape="box" label="queue" style="dotted"] + P0_queue -> S0 [style="dotted"] + S0 -> P0_got_key [label="got_verified_key"] P0_got_key [shape="box" label="drain queue:\n[encrypt\n M.add_message]"] P0_got_key -> S1 diff --git a/docs/wormhole.dot b/docs/wormhole.dot index 83744dd..317ef59 100644 --- a/docs/wormhole.dot +++ b/docs/wormhole.dot @@ -10,80 +10,48 @@ digraph { start [label="Wormhole\nMachine" style="dotted"] - S0 [label="S0: know\nnothing"] - S0 -> P0_queue [label="send" style="dotted"] - P0_queue [shape="box" style="dotted" label="queue"] - P0_queue -> S0 [style="dotted"] + {rank=same; P0_code S0} + P0_code [shape="box" style="dashed" + label="Code.input\n or Code.allocate\n or Code.set"] + P0_code -> S0 + S0 [label="S0: empty"] S0 -> P0_build [label="set_code"] - P0_build [shape="box" label="build_pake\nM.set_nameplate\nM.add_message(pake)"] + P0_build [shape="box" label="M.set_nameplate\nK.set_code"] P0_build -> S1 - S1 [label="S1: know\ncode"] - S1 -> P1_queue [label="send" style="dotted"] - P1_queue [shape="box" style="dotted" label="queue"] - P1_queue -> S1 [style="dotted"] + S1 [label="S1: lonely" color="orange"] - /* the Mailbox will deliver each message exactly once, but doesn't - guarantee ordering: if Alice starts the process, then disconnects, - then Bob starts (reading PAKE, sending both his PAKE and his VERSION - phase), then Alice will see both PAKE and VERSION on her next - connect, and might get the VERSION first. + S1 -> S2 [label="happy"] - The Wormhole will queue inbound messages that it isn't ready for. The - wormhole shim that lets applications do w.get(phase=) must do - something similar, queueing inbound messages until it sees one for - the phase it currently cares about.*/ + S1 -> P_close_scary [label="scared" color="red"] + S1 -> P_close_lonely [label="close"] + P_close_lonely [shape="box" label="M.close(lonely)"] + P_close_lonely -> S_closing - S1 -> P_mood_scary [label="got_message(pake)\npake bad"] - S1 -> P1_compute [label="got_message(pake)\npake good"] - S1 -> P1_queue_inbound [label="got_message(other)"] - P1_queue_inbound [shape="box" style="dotted" label="queue\ninbound"] - P1_queue_inbound -> S1 - S1 -> P_mood_lonely [label="close"] + P_close_scary [shape="box" label="M.close(scary)" color="red"] + P_close_scary -> S_closing [color="red"] - P1_compute [label="compute_key\nM.add_message(version)\nA.got_verifier\nschedule process inbound queue?" shape="box"] - P1_compute -> S2 + S2 [label="S2: happy" color="green"] + S2 -> P2_close [label="close"] + P2_close [shape="box" label="M.close(happy)"] + P2_close -> S_closing - P_mood_scary [shape="box" label="M.close\nmood=scary"] - P_mood_scary -> P_notify_failure - - P_notify_failure [shape="box" label="(record failure)" color="red"] - P_notify_failure -> S_closing - - S2 [label="S2: know_key\n(unverified)" color="orange"] - S2 -> P_queue3 [label="send" style="dotted"] - P_queue3 [shape="box" style="dotted" label="queue"] - P_queue3 -> S2 [style="dotted"] - S2 -> P_verify [label="got_message"] /* version or phase */ - S2 -> P_mood_lonely [label="close"] /* more like impatient */ - - P_verify [label="verify(msg)" shape="box"] - P_verify -> P_accept_msg [label="(good)"] - P_verify -> P_mood_scary [label="(bad)"] - - P_accept_msg [label="A.received(msg)\nencrypt queued\nM.add_message(queued)" - shape="box"] - P_accept_msg -> S3 - - S3 [label="S3: know_key\n(verified)" color="green"] - S3 -> P_verify [label="got_message"] /* probably phase */ - S3 -> P_mood_happy [label="close"] - S3 -> P_send [label="send"] - - P_mood_happy [shape="box" label="M.close\nmood=happy"] - P_mood_happy -> S_closing - - P_mood_lonely [shape="box" label="M.close\nmood=lonely"] - P_mood_lonely -> S_closing - - P_send [shape="box" label="encrypt\nM.add_message(msg)"] - P_send -> S3 + S2 -> P2_got_message [label="got_message"] + P2_got_message [shape="box" label="A.received"] + P2_got_message -> S2 S_closing [label="closing"] S_closing -> P_closed [label="closed"] - S_closing -> S_closing [label="got_message"] + S_closing -> S_closing [label="got_message\nhappy\nscared\nclose"] - P_closed [shape="box" label="A.closed"] + P_closed [shape="box" label="A.closed(reason)"] P_closed -> S_closed S_closed [label="closed"] + + {rank=same; Other S_closed} + Other [shape="box" style="dashed" + label="send -> S.send\ngot_verifier -> A.got_verifier" + ] + + } From a3ec344eb8ae103787600da0170e5835422bac5e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 21 Feb 2017 18:15:47 -0800 Subject: [PATCH 050/176] clean up machine names/initials C: Code RC: Rendezvous Connector R: Receive --- docs/code.dot | 2 +- docs/machines.dot | 2 +- docs/mailbox.dot | 18 +++++++++--------- docs/nameplates.dot | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/code.dot b/docs/code.dot index 864d757..c5ada30 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -36,7 +36,7 @@ digraph { color="orange" fontcolor="orange"] S0 -> P0_allocate [label="allocate"] - P0_allocate [shape="box" label="C.tx_allocate"] + P0_allocate [shape="box" label="RC.tx_allocate"] P0_allocate -> S1 S1 [label="S1:\nallocating"] S1 -> P1_generate [label="rx_allocated"] diff --git a/docs/machines.dot b/docs/machines.dot index 7e2ecd3..a1df2e7 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -4,7 +4,7 @@ digraph { color="blue" fontcolor="blue"] Mailbox [shape="box" color="blue" fontcolor="blue"] Connection [label="Rendezvous\nConnector" - shape="box" color="blue" fontcolor="blue"] + shape="oval" color="blue" fontcolor="blue"] websocket [color="blue" fontcolor="blue"] Order [shape="box" label="Ordering" color="blue" fontcolor="blue"] Key [shape="box" label="Key" color="blue" fontcolor="blue"] diff --git a/docs/mailbox.dot b/docs/mailbox.dot index 51afafc..2f64062 100644 --- a/docs/mailbox.dot +++ b/docs/mailbox.dot @@ -36,7 +36,7 @@ digraph { {rank=same; S2A P2_connected S2B} S2A [label="S2A:\nmaybe claimed"] S2A -> P2_connected [label="connected"] - P2_connected [shape="box" label="C.tx_claim" color="orange"] + P2_connected [shape="box" label="RC.tx_claim" color="orange"] P2_connected -> S2B [color="orange"] S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] #S2B -> SrB [label="close()" style="dashed"] @@ -58,7 +58,7 @@ digraph { S1A -> S3A [label="(none)" style="invis"] S2B -> P_open [label="rx_claimed" color="orange" fontcolor="orange"] - P_open [shape="box" label="store mailbox\nC.tx_open\nC.tx_add(queued)" color="orange"] + P_open [shape="box" label="store mailbox\nRC.tx_open\nRC.tx_add(queued)" color="orange"] P_open -> S3B [color="orange"] subgraph {rank=same; S3A S3B P3_connected} @@ -67,7 +67,7 @@ digraph { S3A -> P3_connected [label="connected"] S3B -> S3A [label="lost"] - P3_connected [shape="box" label="C.tx_open\nC.tx_add(queued)"] + P3_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)"] P3_connected -> S3B S3A -> P3_queue [label="add_message" style="dotted"] @@ -77,7 +77,7 @@ digraph { S3B -> S3B [label="rx_claimed"] S3B -> P3_send [label="add_message"] - P3_send [shape="box" label="queue\nC.tx_add(msg)"] + P3_send [shape="box" label="queue\nRC.tx_add(msg)"] P3_send -> S3B S3A -> S4A [label="(none)" style="invis"] @@ -87,7 +87,7 @@ digraph { S3B -> P3_process_theirs [label="rx_message\n(theirs)" color="orange" fontcolor="orange"] P3_process_theirs [shape="box" color="orange" - label="C.tx_release\nO.got_message if new\nrecord" + label="RC.tx_release\nO.got_message if new\nrecord" ] /* pay attention to the race here: this process_message() will deliver msg_pake to the WormholeMachine, which will compute_key() and @@ -103,9 +103,9 @@ digraph { S4B [label="S4B:\nmaybe released\nmaybe open now\n(bound)" color="orange"] S4A -> P4_connected [label="connected"] - P4_connected [shape="box" label="C.tx_open\nC.tx_add(queued)\nC.tx_release"] + P4_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)\nRC.tx_release"] S4B -> P4_send [label="add_message"] - P4_send [shape="box" label="queue\nC.tx_add(msg)"] + P4_send [shape="box" label="queue\nRC.tx_add(msg)"] P4_send -> S4B S4A -> P4_queue [label="add_message" style="dotted"] P4_queue [shape="box" label="queue" style="dotted"] @@ -129,10 +129,10 @@ digraph { S5A [label="S5A:\nreleased\nopened >=once"] S5A -> P5_connected [label="connected"] - P5_connected [shape="box" label="C.tx_open\nC.tx_add(queued)"] + P5_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)"] S5B -> P5_send [label="add_message" color="green" fontcolor="green"] - P5_send [shape="box" label="queue\nC.tx_add(msg)" color="green"] + P5_send [shape="box" label="queue\nRC.tx_add(msg)" color="green"] P5_send -> S5B [color="green"] S5A -> P5_queue [label="add_message" style="dotted"] P5_queue [shape="box" label="queue" style="dotted"] diff --git a/docs/nameplates.dot b/docs/nameplates.dot index 5ed4e66..8bc165d 100644 --- a/docs/nameplates.dot +++ b/docs/nameplates.dot @@ -19,7 +19,7 @@ digraph { S1B [label="S1B:\nwant list\nconnected" color="orange"] S1A -> P_tx [label="connected"] - P_tx [shape="box" label="C.tx_list()" color="orange"] + P_tx [shape="box" label="RC.tx_list()" color="orange"] P_tx -> S1B [color="orange"] S1B -> S1A [label="lost"] @@ -33,7 +33,7 @@ digraph { S0B -> P_notify [label="rx"] S1B -> P_notify [label="rx" color="orange" fontcolor="orange"] - P_notify [shape="box" label="W.got_nameplates()"] + P_notify [shape="box" label="C.got_nameplates()"] P_notify -> S0B {rank=same; foo foo2 legend} From b179e66d08ad4f28bff13599e276d01e47ad88a4 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 21 Feb 2017 18:46:06 -0800 Subject: [PATCH 051/176] start on machine implementation: _key.py and _send.py --- src/wormhole/_key.py | 71 +++++++++++++++++++++++++++++++++++++++++++ src/wormhole/_send.py | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/wormhole/_key.py create mode 100644 src/wormhole/_send.py diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py new file mode 100644 index 0000000..6a03970 --- /dev/null +++ b/src/wormhole/_key.py @@ -0,0 +1,71 @@ +from automat import MethodicalMachine +from spake2 import SPAKE2_Symmetric +from hkdf import Hkdf +from nacl.secret import SecretBox +from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes) + +def HKDF(skm, outlen, salt=None, CTXinfo=b""): + return Hkdf(salt, skm).expand(CTXinfo, outlen) + +def derive_key(key, purpose, length=SecretBox.KEY_SIZE): + if not isinstance(key, type(b"")): raise TypeError(type(key)) + if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) + if not isinstance(length, int): raise TypeError(type(length)) + return HKDF(key, length, CTXinfo=purpose) + +class KeyMachine(object): + m = MethodicalMachine() + def __init__(self, wormhole, timing): + self._wormhole = wormhole + self._timing = timing + def set_mailbox(self, mailbox): + self._mailbox = mailbox + def set_receive(self, receive): + self._receive = receive + + @m.state(initial=True) + def S0_know_nothing(self): pass + @m.state() + def S1_know_code(self): pass + @m.state() + def S2_know_key(self): pass + @m.state() + def S3_scared(self): pass + + def got_pake(self, payload): + if "pake_v1" in payload: + self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) + else: + self.got_pake_bad() + + @m.input() + def set_code(self, code): pass + @m.input() + def got_pake_good(self, msg2): pass + @m.input() + def got_pake_bad(self): pass + + @m.output() + def build_pake(self, code): + with self._timing.add("pake1", waiting="crypto"): + self._sp = SPAKE2_Symmetric(to_bytes(code), + idSymmetric=to_bytes(self._appid)) + msg1 = self._sp.start() + self._mailbox.add_message("pake", {"pake_v1": bytes_to_hexstr(msg1)}) + + @m.output() + def scared(self): + self._wormhole.scared() + @m.output() + def compute_key(self, msg2): + assert isinstance(msg2, type(b"")) + with self._timing.add("pake2", waiting="crypto"): + key = self._sp.finish(msg2) + self._my_versions = {} + self._mailbox.add_message("version", self._my_versions) + self._wormhole.got_verifier(derive_key(key, b"wormhole:verifier")) + self._receive.got_key(key) + + S0_know_nothing.upon(set_code, enter=S1_know_code, outputs=[build_pake]) + S1_know_code.upon(got_pake_good, enter=S2_know_key, outputs=[compute_key]) + S1_know_code.upon(got_pake_bad, enter=S3_scared, outputs=[scared]) diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py new file mode 100644 index 0000000..8d2ac0f --- /dev/null +++ b/src/wormhole/_send.py @@ -0,0 +1,62 @@ +from automat import MethodicalMachine +from spake2 import SPAKE2_Symmetric +from hkdf import Hkdf +from nacl.secret import SecretBox +from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes) + +def HKDF(skm, outlen, salt=None, CTXinfo=b""): + return Hkdf(salt, skm).expand(CTXinfo, outlen) + +def derive_key(key, purpose, length=SecretBox.KEY_SIZE): + if not isinstance(key, type(b"")): raise TypeError(type(key)) + if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) + if not isinstance(length, int): raise TypeError(type(length)) + return HKDF(key, length, CTXinfo=purpose) + +class SendMachine(object): + m = MethodicalMachine() + def __init__(self, timing): + self._timing = timing + def set_mailbox(self, mailbox): + self._mailbox = mailbox + + @m.state(initial=True) + def S0_no_key(self): pass + @m.state() + def S1_verified_key(self): pass + + def got_pake(self, payload): + if "pake_v1" in payload: + self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) + else: + self.got_pake_bad() + + @m.input() + def got_verified_key(self, key): pass + @m.input() + def send(self, phase, payload): pass + + @m.output() + def queue(self, phase, payload): + self._queue.append((phase, payload)) + @m.output() + def record_key(self, key): + self._key = key + @m.output() + def drain(self, key): + del key + for (phase, payload) in self._queue: + self._encrypt_and_send(phase, payload) + @m.output() + def deliver(self, phase, payload): + self._encrypt_and_send(phase, payload) + + def _encrypt_and_send(self, phase, payload): + data_key = self._derive_phase_key(self._side, phase) + encrypted = self._encrypt_data(data_key, plaintext) + self._mailbox.add_message(phase, encrypted) + + S0_no_key.upon(send, enter=S0_no_key, outputs=[queue]) + S0_no_key.upon(got_verified_key, enter=S1_verified_key, + outputs=[record_key, drain]) + S1_verified_key.upon(send, enter=S1_verified_key, outputs=[deliver]) From 21cb62a4cff1ad8036da4c197a5a6eeb56116c81 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 11:25:37 -0800 Subject: [PATCH 052/176] move _c2.py out of the way, I might want it later --- src/wormhole/_c2.py => _c2.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/wormhole/_c2.py => _c2.py (100%) diff --git a/src/wormhole/_c2.py b/_c2.py similarity index 100% rename from src/wormhole/_c2.py rename to _c2.py From 9ae8091ec321bbc066b6e0ea4f82f87c58baefbb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 11:25:57 -0800 Subject: [PATCH 053/176] delete old files --- src/wormhole/_c3.py | 76 ------------------ src/wormhole/_machine.py | 169 --------------------------------------- 2 files changed, 245 deletions(-) delete mode 100644 src/wormhole/_c3.py delete mode 100644 src/wormhole/_machine.py diff --git a/src/wormhole/_c3.py b/src/wormhole/_c3.py deleted file mode 100644 index 98d64e2..0000000 --- a/src/wormhole/_c3.py +++ /dev/null @@ -1,76 +0,0 @@ -from ._machine import Machine - -class WormholeMachine: - m = Machine() - - know_nothing = m.State("know_nothing", initial=True) - know_code = m.State("know_code") - know_key = m.State("know_key", color="orange") - #verified_key = m.State("verified_key", color="green") - closed = m.State("closed") - - API_send = m.Event("API_send") - WM_set_code = m.Event("WM_set_code") - WM_rx_pake = m.Event("WM_rx_pake") - #WM_rx_msg = m.Event("WM_rx_msg") - close = m.Event("close") - - @m.action() - def set_code(self): - self._MM.set_nameplate() - self._build_pake() - self._MM.send(self._pake) - @m.action() - @m.outcome("pake ok") - @m.outcome("pake bad") - def compute_key(self): - self._key = self._computer_stuff() - if 1: - return "pake ok" - else: - return "pake bad" - @m.action() - def send_version(self): - self._MM.send(self._version) - @m.action() - @m.outcome("verify ok") - @m.outcome("verify bad") - def verify(self, msg, verify_ok, verify_bad): - try: - decrypted = decrypt(self._key, msg) - return verify_ok(decrypted) - except CryptoError: - return verify_bad() - @m.action() - def queue1(self, msg): - self._queue.append(msg) - @m.action() - def queue2(self, msg): - self._queue.append(msg) - @m.action() - def close_lonely(self): - self._MM.close("lonely") - @m.action() - def close_scary(self): - self._MM.close("scary") - - compute_key.upon("pake ok", goto=send_version) - compute_key.upon("pake bad", goto=close_scary) - know_nothing.upon(API_send, goto=queue1) - queue1.goto(know_nothing) - know_nothing.upon(WM_set_code, goto=set_code) - set_code.goto(know_code) - know_code.upon(API_send, goto=queue2) - queue2.goto(know_code) - know_code.upon(WM_rx_pake, goto=compute_key) - compute_key.goto(send_version) - send_version.goto(know_key) - know_code.upon(close, goto=close_lonely) - know_key.upon(close, goto=close_lonely) - close_lonely.goto(closed) - - -if __name__ == "__main__": - import sys - WM = WormholeMachine() - WM.m._dump_dot(sys.stdout) diff --git a/src/wormhole/_machine.py b/src/wormhole/_machine.py deleted file mode 100644 index 79ced56..0000000 --- a/src/wormhole/_machine.py +++ /dev/null @@ -1,169 +0,0 @@ - -class StateMachineError(Exception): - pass - -class _Transition: - def __init__(self, goto, color=None): - self._goto = goto - self._extra_dot_attrs = {} - if color: - self._extra_dot_attrs["color"] = color - self._extra_dot_attrs["fontcolor"] = color - def _dot_attrs(self): - return self._extra_dot_attrs - -class _State: - def __init__(self, m, name, extra_dot_attrs): - assert isinstance(m, Machine) - self.m = m - self._name = name - self._extra_dot_attrs = extra_dot_attrs - self.eventmap = {} - def upon(self, event, goto, color=None): - if event in self.eventmap: - raise StateMachineError("event already registered") - t = _Transition(goto, color=color) - self.eventmap[event] = t - def _dot_name(self): - return "S_"+self._name.replace(" ", "_") - def _dot_attrs(self): - attrs = {"label": self._name} - attrs.update(self._extra_dot_attrs) - return attrs - -class _Event: - def __init__(self, m, name): - assert isinstance(m, Machine) - self.m = m - self._name = name - def __call__(self): # *args, **kwargs - self.m._handle_event(self) - # return value? - def _dot_name(self): - return "E_"+self._name.replace(" ", "_") - def _dot_attrs(self): - return {"label": self._name} - -class _Action: - def __init__(self, m, f, extra_dot_attrs): - self.m = m - self.f = f - self._extra_dot_attrs = extra_dot_attrs - self.next_goto = None - self._name = f.__name__ - def goto(self, next_goto, color=None): - if self.next_goto: - raise StateMachineError("Action.goto() called twice") - self.next_goto = _Transition(next_goto, color=color) - def __call__(self): # *args, **kwargs ? - raise StateMachineError("don't call Actions directly") - def _dot_name(self): - return "A_"+self._name - def _dot_attrs(self): - attrs = {"shape": "box", "label": self._name} - attrs.update(self._extra_dot_attrs) - return attrs - -def format_attrs(**kwargs): - # return "", or "[attr=value attr=value]" - if not kwargs or all([not(v) for v in kwargs.values()]): - return "" - def escape(s): - return s.replace('\n', r'\n').replace('"', r'\"') - pieces = ['%s="%s"' % (k, escape(kwargs[k])) - for k in sorted(kwargs) - if kwargs[k]] - body = " ".join(pieces) - return "[%s]" % body - -class Machine: - def __init__(self): - self._initial_state = None - self._states = set() - self._events = set() - self._actions = set() - self._current_state = None - self._finalized = False - - def _maybe_finalize(self): - if self._finalized: - return - # do final consistency checks: are all events handled? - - def _maybe_start(self): - self._maybe_finalize() - if self._current_state: - return - if not self._initial_state: - raise StateMachineError("no initial state") - self._current_state = self._initial_state - - def _handle_event(self, event): # other args? - self._maybe_start() - assert event in self._events - goto = self._current_state.eventmap.get(event) - if not goto: - raise StateMachineError("no transition for event %s from state %s" - % (event, self._current_state)) - # execute: ordering concerns here - while not isinstance(goto, _State): - assert isinstance(goto, _Action) - next_goto = goto.next_goto - goto.f() # args? - goto = next_goto - assert isinstance(goto, _State) - self._current_state = goto - - def _describe(self): - print "current state:", self._current_state - - def _dump_dot(self, f): - self._maybe_finalize() - f.write("digraph {\n") - for s in sorted(self._states): - f.write(" %s %s\n" % (s._dot_name(), format_attrs(**s._dot_attrs()))) - f.write("\n") - for a in sorted(self._actions): - f.write(" %s %s\n" % (a._dot_name(), format_attrs(**a._dot_attrs()))) - f.write("\n") - for s in sorted(self._states): - for e in sorted(s.eventmap): - t = s.eventmap[e] - goto = t._goto - attrs = {"label": e._name} - attrs.update(t._dot_attrs()) - f.write(" %s -> %s %s\n" % (s._dot_name(), goto._dot_name(), - format_attrs(**attrs))) - f.write("\n") - for a in sorted(self._actions): - t = a.next_goto - f.write(" %s -> %s %s\n" % (a._dot_name(), t._goto._dot_name(), - format_attrs(**t._dot_attrs()))) - f.write("}\n") - - # all descriptions are from the state machine's point of view - # States are gerunds: Foo-ing - # Events are past-tense verbs: Foo-ed, as in "I have been Foo-ed" - # * machine.do(event) ? vs machine.fooed() - # Actions are immediate-tense verbs: foo, connect - - def State(self, name, initial=False, **dot_attrs): - s = _State(self, name, dot_attrs) - if initial: - if self._initial_state: - raise StateMachineError("duplicate initial state") - self._initial_state = s - self._states.add(s) - return s - - def Event(self, name): - e = _Event(self, name) - self._events.add(e) - return e - - def action(self, **dotattrs): - def wrap(f): - a = _Action(self, f, dotattrs) - self._actions.add(a) - return a - return wrap From 80661392b69e5fb5151629be9b258a5efb44e349 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 11:26:11 -0800 Subject: [PATCH 054/176] build out all state machines still early: automat is happy (they're syntactically valid), but the Outputs are not implemented, and there are plenty of type mismatches --- docs/machines.dot | 2 + docs/nameplates.dot | 2 +- docs/wormhole.dot | 2 + src/wormhole/_code.py | 112 +++++++++++++++ src/wormhole/_connection.py | 4 +- src/wormhole/_interfaces.py | 20 +++ src/wormhole/_key.py | 53 +++++-- src/wormhole/_mailbox.py | 270 +++++++++++++++++++++++------------- src/wormhole/_nameplate.py | 72 ++++++---- src/wormhole/_order.py | 55 ++++++++ src/wormhole/_receive.py | 72 ++++++++++ src/wormhole/_rendezvous.py | 39 ++++++ src/wormhole/_send.py | 31 ++--- src/wormhole/_wormhole.py | 219 +++++++++++++++-------------- 14 files changed, 682 insertions(+), 271 deletions(-) create mode 100644 src/wormhole/_code.py create mode 100644 src/wormhole/_interfaces.py create mode 100644 src/wormhole/_order.py create mode 100644 src/wormhole/_receive.py create mode 100644 src/wormhole/_rendezvous.py diff --git a/docs/machines.dot b/docs/machines.dot index a1df2e7..b87655a 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -73,6 +73,8 @@ digraph { ] Code -> Wormhole [style="dashed" label="set_code"] + App -> Code [style="dashed" + label="allocate\ninput\nset"] diff --git a/docs/nameplates.dot b/docs/nameplates.dot index 8bc165d..77b7e1e 100644 --- a/docs/nameplates.dot +++ b/docs/nameplates.dot @@ -38,5 +38,5 @@ digraph { {rank=same; foo foo2 legend} legend [shape="box" style="dotted" - label="refresh: NL.refresh_nameplates()\nrx: NL.got_nameplates()"] + label="refresh: NL.refresh_nameplates()\nrx: NL.rx_nameplates()"] } diff --git a/docs/wormhole.dot b/docs/wormhole.dot index 317ef59..2ce81a3 100644 --- a/docs/wormhole.dot +++ b/docs/wormhole.dot @@ -40,6 +40,8 @@ digraph { P2_got_message [shape="box" label="A.received"] P2_got_message -> S2 + S2 -> P_close_scary [label="scared" color="red"] + S_closing [label="closing"] S_closing -> P_closed [label="closed"] S_closing -> S_closing [label="got_message\nhappy\nscared\nclose"] diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py new file mode 100644 index 0000000..2ad6f9f --- /dev/null +++ b/src/wormhole/_code.py @@ -0,0 +1,112 @@ +import os +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces +from .wordlist import (byte_to_even_word, byte_to_odd_word, + #even_words_lowercase, odd_words_lowercase, + ) + +def make_code(nameplate, code_length): + assert isinstance(nameplate, type("")), type(nameplate) + words = [] + for i in range(code_length): + # we start with an "odd word" + if i % 2 == 0: + words.append(byte_to_odd_word[os.urandom(1)].lower()) + else: + words.append(byte_to_even_word[os.urandom(1)].lower()) + return "%s-%s" % (nameplate, "-".join(words)) + +@implementer(_interfaces.ICode) +class Code(object): + m = MethodicalMachine() + def __init__(self, code_length, timing): + self._code_length = code_length + self._timing = timing + def wire(self, wormhole, rendezvous_connector, nameplate_lister): + self._W = _interfaces.IWormhole(wormhole) + self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) + self._NL = _interfaces.INameplateListing(nameplate_lister) + + @m.state(initial=True) + def S0_unknown(self): pass + @m.state() + def S1_allocating(self): pass + @m.state() + def S2_typing_nameplate(self): pass + @m.state() + def S3_typing_code(self): pass + @m.state() + def S4_known(self): pass + + # from App + @m.input() + def allocate(self): pass + @m.input() + def input(self): pass + @m.input() + def set(self, code): pass + + # from RendezvousConnector + @m.input() + def rx_allocated(self, nameplate): pass + + # from NameplateLister + @m.input() + def got_nameplates(self, nameplates): pass + + # from stdin/readline/??? + @m.input() + def tab(self): pass + @m.input() + def hyphen(self): pass + @m.input() + def RETURN(self, code): pass + + @m.output() + def NL_refresh_nameplates(self): + self._NL.refresh_nameplates() + @m.output() + def RC_tx_allocate(self): + self._RC.tx_allocate() + @m.output() + def do_completion_nameplates(self): + pass + @m.output() + def stash_nameplates(self, nameplates): + self._known_nameplates = nameplates + pass + @m.output() + def lookup_wordlist(self): + pass + @m.output() + def do_completion_code(self): + pass + @m.output() + def generate_and_set(self, nameplate): + self._code = make_code(nameplate, self._code_length) + self._W_set_code() + + @m.output() + def W_set_code(self, code): + self._code = code + self._W_set_code() + + def _W_set_code(self): + self._W.set_code(self._code) + + S0_unknown.upon(allocate, enter=S1_allocating, outputs=[RC_tx_allocate]) + S1_allocating.upon(rx_allocated, enter=S4_known, outputs=[generate_and_set]) + + S0_unknown.upon(set, enter=S4_known, outputs=[W_set_code]) + + S0_unknown.upon(input, enter=S2_typing_nameplate, + outputs=[NL_refresh_nameplates]) + S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate, + outputs=[do_completion_nameplates]) + S2_typing_nameplate.upon(got_nameplates, enter=S2_typing_nameplate, + outputs=[stash_nameplates]) + S2_typing_nameplate.upon(hyphen, enter=S3_typing_code, + outputs=[lookup_wordlist]) + S3_typing_code.upon(tab, enter=S3_typing_code, outputs=[do_completion_code]) + S3_typing_code.upon(RETURN, enter=S4_known, outputs=[W_set_code]) diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py index 735e8d3..5086571 100644 --- a/src/wormhole/_connection.py +++ b/src/wormhole/_connection.py @@ -1,8 +1,8 @@ - +from zope.interface import Interface from six.moves.urllib_parse import urlparse from attr import attrs, attrib from twisted.internet import defer, endpoints #, error -from twisted.application import internet +from twisted.application import internet, service from autobahn.twisted import websocket from automat import MethodicalMachine diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py new file mode 100644 index 0000000..647dd2b --- /dev/null +++ b/src/wormhole/_interfaces.py @@ -0,0 +1,20 @@ +from zope.interface import Interface + +class IWormhole(Interface): + pass +class IMailbox(Interface): + pass +class ISend(Interface): + pass +class IOrder(Interface): + pass +class IKey(Interface): + pass +class IReceive(Interface): + pass +class IRendezvousConnector(Interface): + pass +class INameplateListing(Interface): + pass +class ICode(Interface): + pass diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 6a03970..7ce979e 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -1,8 +1,15 @@ -from automat import MethodicalMachine +from hashlib import sha256 +from zope.interface import implementer from spake2 import SPAKE2_Symmetric from hkdf import Hkdf from nacl.secret import SecretBox +from nacl.exceptions import CryptoError +from automat import MethodicalMachine from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes) +from . import _interfaces +CryptoError +__all__ = ["derive_key", "derive_phase_key", "CryptoError", + "Key"] def HKDF(skm, outlen, salt=None, CTXinfo=b""): return Hkdf(salt, skm).expand(CTXinfo, outlen) @@ -13,15 +20,33 @@ def derive_key(key, purpose, length=SecretBox.KEY_SIZE): if not isinstance(length, int): raise TypeError(type(length)) return HKDF(key, length, CTXinfo=purpose) -class KeyMachine(object): +def derive_phase_key(side, phase): + assert isinstance(side, type("")), type(side) + assert isinstance(phase, type("")), type(phase) + side_bytes = side.encode("ascii") + phase_bytes = phase.encode("ascii") + purpose = (b"wormhole:phase:" + + sha256(side_bytes).digest() + + sha256(phase_bytes).digest()) + return derive_key(purpose) + +def decrypt_data(key, encrypted): + assert isinstance(key, type(b"")), type(key) + assert isinstance(encrypted, type(b"")), type(encrypted) + assert len(key) == SecretBox.KEY_SIZE, len(key) + box = SecretBox(key) + data = box.decrypt(encrypted) + return data + +@implementer(_interfaces.IKey) +class Key(object): m = MethodicalMachine() - def __init__(self, wormhole, timing): - self._wormhole = wormhole + def __init__(self, timing): self._timing = timing - def set_mailbox(self, mailbox): - self._mailbox = mailbox - def set_receive(self, receive): - self._receive = receive + def wire(self, wormhole, mailbox, receive): + self._W = _interfaces.IWormhole(wormhole) + self._M = _interfaces.IMailbox(mailbox) + self._R = _interfaces.IReceive(receive) @m.state(initial=True) def S0_know_nothing(self): pass @@ -29,7 +54,7 @@ class KeyMachine(object): def S1_know_code(self): pass @m.state() def S2_know_key(self): pass - @m.state() + @m.state(terminal=True) def S3_scared(self): pass def got_pake(self, payload): @@ -51,20 +76,20 @@ class KeyMachine(object): self._sp = SPAKE2_Symmetric(to_bytes(code), idSymmetric=to_bytes(self._appid)) msg1 = self._sp.start() - self._mailbox.add_message("pake", {"pake_v1": bytes_to_hexstr(msg1)}) + self._M.add_message("pake", {"pake_v1": bytes_to_hexstr(msg1)}) @m.output() def scared(self): - self._wormhole.scared() + self._W.scared() @m.output() def compute_key(self, msg2): assert isinstance(msg2, type(b"")) with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) self._my_versions = {} - self._mailbox.add_message("version", self._my_versions) - self._wormhole.got_verifier(derive_key(key, b"wormhole:verifier")) - self._receive.got_key(key) + self._M.add_message("version", self._my_versions) + self._W.got_verifier(derive_key(key, b"wormhole:verifier")) + self._R.got_key(key) S0_know_nothing.upon(set_code, enter=S1_know_code, outputs=[build_pake]) S1_know_code.upon(got_pake_good, enter=S2_know_key, outputs=[compute_key]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index f67bb5a..376393c 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -1,12 +1,21 @@ -from attr import attrs, attrib +from zope.interface import implementer from automat import MethodicalMachine +from . import _interfaces -@attrs -class _Mailbox_Machine(object): - _connection_machine = attrib() - _m = attrib() +@implementer(_interfaces.IMailbox) +class Mailbox(object): m = MethodicalMachine() + def __init__(self, side): + self._side = side + self._mood = None + self._nameplate = None + + def wire(self, wormhole, rendezvous_connector, ordering): + self._W = _interfaces.IWormhole(wormhole) + self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) + self._O = _interfaces.IOrder(ordering) + @m.state(initial=True) def initial(self): pass @@ -68,136 +77,201 @@ class _Mailbox_Machine(object): # Ss: closed and released, waiting for stop @m.state() def SsB(self): pass - @m.state() - def Ss(self): pass # terminal + @m.state(terminal=True) + def Ss(self): pass - def connected(self, ws): - self._ws = ws - self.M_connected() - @m.input() - def M_start_unconnected(self): pass + def start_unconnected(self): pass @m.input() - def M_start_connected(self): pass + def start_connected(self): pass + + # from Wormhole @m.input() - def M_set_nameplate(self): pass + def set_nameplate(self, nameplate): pass @m.input() - def M_connected(self): pass + def close(self, mood): pass + + # from RendezvousConnector @m.input() - def M_lost(self): pass + def connected(self): pass @m.input() - def M_send(self, msg): pass + def lost(self): pass + @m.input() - def M_rx_claimed(self): pass + def rx_claimed(self, mailbox): pass + + def rx_message(self, side, phase, msg): + if side == self._side: + self.rx_message_ours(phase, msg) + else: + self.rx_message_theirs(phase, msg) @m.input() - def M_rx_msg_from_me(self, msg): pass + def rx_message_ours(self, phase, msg): pass @m.input() - def M_rx_msg_from_them(self, msg): pass + def rx_message_theirs(self, phase, msg): pass @m.input() - def M_rx_released(self): pass + def rx_released(self): pass @m.input() - def M_rx_closed(self): pass + def rx_closed(self): pass @m.input() - def M_stop(self): pass + def stopped(self): pass + + # from Send or Key @m.input() - def M_stopped(self): pass + def add_message(self, phase, msg): pass + @m.output() - def tx_claim(self): - self._c.send_command("claim", nameplate=self._nameplate) + def record_nameplate(self, nameplate): + self._nameplate = nameplate @m.output() - def tx_open(self): pass + def record_nameplate_and_RC_tx_claim(self, nameplate): + self._nameplate = nameplate + self._RX.tx_claim(self._nameplate) @m.output() - def queue(self, msg): pass + def RC_tx_claim(self): + # when invoked via M.connected(), we must use the stored nameplate + self._RC.tx_claim(self._nameplate) @m.output() - def store_mailbox(self): pass # trouble(mb) + def RC_tx_open(self): + assert self._mailbox + self._RC.tx_open(self._mailbox) @m.output() - def tx_add(self, msg): pass + def queue(self, phase, msg): + self._pending_outbound[phase] = msg @m.output() - def tx_add_queued(self): pass + def store_mailbox_and_RC_tx_open_and_drain(self, mailbox): + self._mailbox = mailbox + self._RC.tx_open(mailbox) + self._drain() @m.output() - def tx_release(self): pass + def drain(self): + self._drain() + def _drain(self): + for phase, msg in self._pending_outbound.items(): + self._RC.tx_add(phase, msg) @m.output() - def tx_close(self): pass + def RC_tx_add(self, phase, msg): + self._RC.tx_add(phase, msg) @m.output() - def process_first_msg_from_them(self, msg): - self.tx_release() - self.process_msg_from_them(msg) + def RC_tx_release(self): + self._RC.tx_release() @m.output() - def process_msg_from_them(self, msg): pass + def RC_tx_release_and_accept(self, phase, msg): + self._RC.tx_release() + self._accept(phase, msg) @m.output() - def dequeue(self, msg): pass + def record_mood_and_RC_tx_release(self, mood): + self._mood = mood + self._RC.tx_release() @m.output() - def C_stop(self): pass + def record_mood_and_RC_tx_release_and_RC_tx_close(self, mood): + self._mood = mood + self._RC.tx_release() + self._RC.tx_close(self._mood) @m.output() - def WM_stopped(self): pass + def RC_tx_close(self): + assert self._mood + self._RC.tx_close(self._mood) + @m.output() + def record_mood_and_RC_tx_close(self, mood): + self._mood = mood + self._RC.tx_close(self._mood) + @m.output() + def accept(self, phase, msg): + self._accept(phase, msg) + def _accept(self, phase, msg): + if phase not in self._processed: + self._O.got_message(phase, msg) + self._processed.add(phase) + @m.output() + def dequeue(self, phase, msg): + self._pending_outbound.pop(phase) + @m.output() + def record_mood(self, mood): + self._mood = mood + @m.output() + def record_mood_and_RC_stop(self, mood): + self._mood = mood + self._RC_stop() + @m.output() + def RC_stop(self): + self._RC_stop() + @m.output() + def W_closed(self): + self._W.closed() - initial.upon(M_start_unconnected, enter=S0A, outputs=[]) - initial.upon(M_start_connected, enter=S0B, outputs=[]) - S0A.upon(M_connected, enter=S0B, outputs=[]) - S0A.upon(M_set_nameplate, enter=S1A, outputs=[]) - S0A.upon(M_stop, enter=SsB, outputs=[C_stop]) - S0B.upon(M_lost, enter=S0A, outputs=[]) - S0B.upon(M_set_nameplate, enter=S2B, outputs=[tx_claim]) - S0B.upon(M_stop, enter=SsB, outputs=[C_stop]) + initial.upon(start_unconnected, enter=S0A, outputs=[]) + initial.upon(start_connected, enter=S0B, outputs=[]) + S0A.upon(connected, enter=S0B, outputs=[]) + S0A.upon(set_nameplate, enter=S1A, outputs=[record_nameplate]) + S0A.upon(add_message, enter=S0A, outputs=[queue]) + S0B.upon(lost, enter=S0A, outputs=[]) + S0B.upon(set_nameplate, enter=S2B, outputs=[record_nameplate_and_RC_tx_claim]) + S0B.upon(add_message, enter=S0B, outputs=[queue]) - S1A.upon(M_connected, enter=S2B, outputs=[tx_claim]) - S1A.upon(M_send, enter=S1A, outputs=[queue]) - S1A.upon(M_stop, enter=SsB, outputs=[C_stop]) + S1A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) + S1A.upon(add_message, enter=S1A, outputs=[queue]) - S2A.upon(M_connected, enter=S2B, outputs=[tx_claim]) - S2A.upon(M_stop, enter=SrA, outputs=[]) - S2A.upon(M_send, enter=S2A, outputs=[queue]) - S2B.upon(M_lost, enter=S2A, outputs=[]) - S2B.upon(M_send, enter=S2B, outputs=[queue]) - S2B.upon(M_stop, enter=SrB, outputs=[tx_release]) - S2B.upon(M_rx_claimed, enter=S3B, outputs=[store_mailbox, tx_open, - tx_add_queued]) + S2A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) + S2A.upon(add_message, enter=S2A, outputs=[queue]) + S2B.upon(lost, enter=S2A, outputs=[]) + S2B.upon(add_message, enter=S2B, outputs=[queue]) + S2B.upon(rx_claimed, enter=S3B, + outputs=[store_mailbox_and_RC_tx_open_and_drain]) - S3A.upon(M_connected, enter=S3B, outputs=[tx_open, tx_add_queued]) - S3A.upon(M_send, enter=S3A, outputs=[queue]) - S3A.upon(M_stop, enter=SrcA, outputs=[]) - S3B.upon(M_lost, enter=S3A, outputs=[]) - S3B.upon(M_rx_msg_from_them, enter=S4B, - outputs=[process_first_msg_from_them]) - S3B.upon(M_rx_msg_from_me, enter=S3B, outputs=[dequeue]) - S3B.upon(M_rx_claimed, enter=S3B, outputs=[]) - S3B.upon(M_send, enter=S3B, outputs=[queue, tx_add]) - S3B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close]) + S3A.upon(connected, enter=S3B, outputs=[RC_tx_open, drain]) + S3A.upon(add_message, enter=S3A, outputs=[queue]) + S3B.upon(lost, enter=S3A, outputs=[]) + S3B.upon(rx_message_theirs, enter=S4B, outputs=[RC_tx_release_and_accept]) + S3B.upon(rx_message_ours, enter=S3B, outputs=[dequeue]) + S3B.upon(rx_claimed, enter=S3B, outputs=[]) + S3B.upon(add_message, enter=S3B, outputs=[queue, RC_tx_add]) - S4A.upon(M_connected, enter=S4B, - outputs=[tx_open, tx_add_queued, tx_release]) - S4A.upon(M_send, enter=S4A, outputs=[queue]) - S4A.upon(M_stop, enter=SrcA, outputs=[]) - S4B.upon(M_lost, enter=S4A, outputs=[]) - S4B.upon(M_send, enter=S4B, outputs=[queue, tx_add]) - S4B.upon(M_rx_msg_from_them, enter=S4B, outputs=[process_msg_from_them]) - S4B.upon(M_rx_msg_from_me, enter=S4B, outputs=[dequeue]) - S4B.upon(M_rx_released, enter=S5B, outputs=[]) - S4B.upon(M_stop, enter=SrcB, outputs=[tx_release, tx_close]) + S4A.upon(connected, enter=S4B, + outputs=[RC_tx_open, drain, RC_tx_release]) + S4A.upon(add_message, enter=S4A, outputs=[queue]) + S4B.upon(lost, enter=S4A, outputs=[]) + S4B.upon(add_message, enter=S4B, outputs=[queue, RC_tx_add]) + S4B.upon(rx_message_theirs, enter=S4B, outputs=[accept]) + S4B.upon(rx_message_ours, enter=S4B, outputs=[dequeue]) + S4B.upon(rx_released, enter=S5B, outputs=[]) - S5A.upon(M_connected, enter=S5B, outputs=[tx_open, tx_add_queued]) - S5A.upon(M_send, enter=S5A, outputs=[queue]) - S5A.upon(M_stop, enter=ScA, outputs=[]) - S5B.upon(M_lost, enter=S5A, outputs=[]) - S5B.upon(M_send, enter=S5B, outputs=[queue, tx_add]) - S5B.upon(M_rx_msg_from_them, enter=S5B, outputs=[process_msg_from_them]) - S5B.upon(M_rx_msg_from_me, enter=S5B, outputs=[dequeue]) - S5B.upon(M_stop, enter=ScB, outputs=[tx_close]) + S5A.upon(connected, enter=S5B, outputs=[RC_tx_open, drain]) + S5A.upon(add_message, enter=S5A, outputs=[queue]) + S5B.upon(lost, enter=S5A, outputs=[]) + S5B.upon(add_message, enter=S5B, outputs=[queue, RC_tx_add]) + S5B.upon(rx_message_theirs, enter=S5B, outputs=[accept]) + S5B.upon(rx_message_ours, enter=S5B, outputs=[dequeue]) - SrcA.upon(M_connected, enter=SrcB, outputs=[tx_release, tx_close]) - SrcB.upon(M_lost, enter=SrcA, outputs=[]) - SrcB.upon(M_rx_closed, enter=SrB, outputs=[]) - SrcB.upon(M_rx_released, enter=ScB, outputs=[]) + if True: + S0A.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop]) + S0B.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop]) + S1A.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop]) + S2A.upon(close, enter=SrA, outputs=[record_mood]) + S2B.upon(close, enter=SrB, outputs=[record_mood_and_RC_tx_release]) + S3A.upon(close, enter=SrcA, outputs=[record_mood]) + S3B.upon(close, enter=SrcB, + outputs=[record_mood_and_RC_tx_release_and_RC_tx_close]) + S4A.upon(close, enter=SrcA, outputs=[record_mood]) + S4B.upon(close, enter=SrcB, + outputs=[record_mood_and_RC_tx_release_and_RC_tx_close]) + S5A.upon(close, enter=ScA, outputs=[record_mood]) + S5B.upon(close, enter=ScB, outputs=[record_mood_and_RC_tx_close]) - SrB.upon(M_lost, enter=SrA, outputs=[]) - SrA.upon(M_connected, enter=SrB, outputs=[tx_release]) - SrB.upon(M_rx_released, enter=SsB, outputs=[C_stop]) + SrcA.upon(connected, enter=SrcB, outputs=[RC_tx_release, RC_tx_close]) + SrcB.upon(lost, enter=SrcA, outputs=[]) + SrcB.upon(rx_closed, enter=SrB, outputs=[]) + SrcB.upon(rx_released, enter=ScB, outputs=[]) - ScB.upon(M_lost, enter=ScA, outputs=[]) - ScB.upon(M_rx_closed, enter=SsB, outputs=[C_stop]) - ScA.upon(M_connected, enter=ScB, outputs=[tx_close]) + SrB.upon(lost, enter=SrA, outputs=[]) + SrA.upon(connected, enter=SrB, outputs=[RC_tx_release]) + SrB.upon(rx_released, enter=SsB, outputs=[RC_stop]) - SsB.upon(M_stopped, enter=Ss, outputs=[WM_stopped]) + ScB.upon(lost, enter=ScA, outputs=[]) + ScB.upon(rx_closed, enter=SsB, outputs=[RC_stop]) + ScA.upon(connected, enter=ScB, outputs=[RC_tx_close]) + + SsB.upon(stopped, enter=Ss, outputs=[W_closed]) diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 8014cfd..b6c1ed9 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -1,8 +1,15 @@ +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces -class NameplateListingMachine(object): +@implementer(_interfaces.INameplateListing) +class NameplateListing(object): m = MethodicalMachine() def __init__(self): - self._list_nameplate_waiters = [] + pass + def wire(self, rendezvous_connector, code): + self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) + self._C = _interfaces.ICode(code) # Ideally, each API request would spawn a new "list_nameplates" message # to the server, so the response would be maximally fresh, but that would @@ -14,40 +21,45 @@ class NameplateListingMachine(object): # request arrives, both requests will be satisfied by the same response. @m.state(initial=True) - def idle(self): pass + def S0A_idle_disconnected(self): pass @m.state() - def requesting(self): pass + def S1A_wanting_disconnected(self): pass + @m.state() + def S0B_idle_connected(self): pass + @m.state() + def S1B_wanting_connected(self): pass @m.input() - def list_nameplates(self): pass # returns Deferred + def connected(self): pass @m.input() - def response(self, message): pass + def lost(self): pass + @m.input() + def refresh_nameplates(self): pass + @m.input() + def rx_nameplates(self, message): pass @m.output() - def add_deferred(self): - d = defer.Deferred() - self._list_nameplate_waiters.append(d) - return d + def RC_tx_list(self): + self._RC.tx_list() @m.output() - def send_request(self): - self._connection.send_command("list") - @m.output() - def distribute_response(self, message): - nameplates = parse(message) - waiters = self._list_nameplate_waiters - self._list_nameplate_waiters = [] - for d in waiters: - d.callback(nameplates) + def C_got_nameplates(self, message): + self._C.got_nameplates(message["nameplates"]) - idle.upon(list_nameplates, enter=requesting, - outputs=[add_deferred, send_request], - collector=lambda outs: outs[0]) - idle.upon(response, enter=idle, outputs=[]) - requesting.upon(list_nameplates, enter=requesting, - outputs=[add_deferred], - collector=lambda outs: outs[0]) - requesting.upon(response, enter=idle, outputs=[distribute_response]) + S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) + S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) - # nlm._connection = c = Connection(ws) - # nlm.list_nameplates().addCallback(display_completions) - # c.register_dispatch("nameplates", nlm.response) + S0A_idle_disconnected.upon(refresh_nameplates, + enter=S1A_wanting_disconnected, outputs=[]) + S1A_wanting_disconnected.upon(refresh_nameplates, + enter=S1A_wanting_disconnected, outputs=[]) + S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, + outputs=[RC_tx_list]) + S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + outputs=[RC_tx_list]) + S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, + outputs=[C_got_nameplates]) + S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) + S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + outputs=[RC_tx_list]) + S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, + outputs=[C_got_nameplates]) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py new file mode 100644 index 0000000..f914130 --- /dev/null +++ b/src/wormhole/_order.py @@ -0,0 +1,55 @@ +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces + +@implementer(_interfaces.IOrder) +class Order(object): + m = MethodicalMachine() + def __init__(self, side, timing): + self._side = side + self._timing = timing + self._key = None + self._queue = [] + def wire(self, key, receive): + self._K = _interfaces.IKey(key) + self._R = _interfaces.IReceive(receive) + + @m.state(initial=True) + def S0_no_pake(self): pass + @m.state(terminal=True) + def S1_yes_pake(self): pass + + def got_message(self, phase, payload): + if phase == "pake": + self.got_pake(phase, payload) + else: + self.got_non_pake(phase, payload) + + @m.input() + def got_pake(self, phase, payload): pass + @m.input() + def got_non_pake(self, phase, payload): pass + + @m.output() + def queue(self, phase, payload): + self._queue.append((phase, payload)) + @m.output() + def notify_key(self, phase, payload): + self._K.got_pake(payload) + @m.output() + def drain(self, phase, payload): + del phase + del payload + for (phase, payload) in self._queue: + self._deliver(phase, payload) + self._queue[:] = [] + @m.output() + def deliver(self, phase, payload): + self._deliver(phase, payload) + + def _deliver(self, phase, payload): + self._R.got_message(phase, payload) + + S0_no_pake.upon(got_non_pake, enter=S0_no_pake, outputs=[queue]) + S0_no_pake.upon(got_pake, enter=S1_yes_pake, outputs=[notify_key, drain]) + S1_yes_pake.upon(got_non_pake, enter=S1_yes_pake, outputs=[deliver]) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py new file mode 100644 index 0000000..a31c4f4 --- /dev/null +++ b/src/wormhole/_receive.py @@ -0,0 +1,72 @@ +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces +from ._key import derive_phase_key, decrypt_data, CryptoError + +@implementer(_interfaces.IReceive) +class Receive(object): + m = MethodicalMachine() + def __init__(self, side, timing): + self._side = side + self._timing = timing + self._key = None + def wire(self, wormhole, key, send): + self._W = _interfaces.IWormhole(wormhole) + self._K = _interfaces.IKey(key) + self._S = _interfaces.ISend(send) + + @m.state(initial=True) + def S0_unknown_key(self): pass + @m.state() + def S1_unverified_key(self): pass + @m.state() + def S2_verified_key(self): pass + @m.state(terminal=True) + def S3_scared(self): pass + + def got_message(self, phase, payload): + assert self._key + data_key = derive_phase_key(self._side, phase) + try: + plaintext = decrypt_data(data_key, body) + except CryptoError: + self.got_message_bad() + return + self.got_message_good(phase, plaintext) + + @m.input() + def got_key(self, key): pass + @m.input() + def got_message_good(self, phase, plaintext): pass + @m.input() + def got_message_bad(self): pass + + @m.output() + def record_key(self, key): + self._key = key + @m.output() + def S_got_verified_key(self, phase, plaintext): + assert self._key + self._S.got_verified_key(self._key) + @m.output() + def W_happy(self, phase, plaintext): + self._W.happy() + @m.output() + def W_got_message(self, phase, plaintext): + self._W.got_message(phase, plaintext) + @m.output() + def W_scared(self): + self._W.scared() + + S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key]) + S1_unverified_key.upon(got_message_good, enter=S2_verified_key, + outputs=[S_got_verified_key, W_happy, W_got_message]) + S1_unverified_key.upon(got_message_bad, enter=S3_scared, + outputs=[W_scared]) + S2_verified_key.upon(got_message_bad, enter=S3_scared, + outputs=[W_scared]) + S2_verified_key.upon(got_message_good, enter=S2_verified_key, + outputs=[W_got_message]) + S3_scared.upon(got_message_good, enter=S3_scared, outputs=[]) + S3_scared.upon(got_message_bad, enter=S3_scared, outputs=[]) + diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py new file mode 100644 index 0000000..ca8a0f7 --- /dev/null +++ b/src/wormhole/_rendezvous.py @@ -0,0 +1,39 @@ +from zope.interface import implementer +from twisted.application import service +from . import _interfaces + +@implementer(_interfaces.IRendezvousConnector) +class RendezvousConnector(service.MultiService, object): + def __init__(self, journal, timing): + self._journal = journal + self._timing = timing + + def wire(self, mailbox, code, nameplate_lister): + self._M = _interfaces.IMailbox(mailbox) + self._C = _interfaces.ICode(code) + self._NL = _interfaces.INameplateListing(nameplate_lister) + + + # from Mailbox + def tx_claim(self): + pass + def tx_open(self): + pass + def tx_add(self, x): + pass + def tx_release(self): + pass + def tx_close(self, mood): + pass + def stop(self): + pass + + # from NameplateLister + def tx_list(self): + pass + + # from Code + def tx_allocate(self): + pass + + # record, message, payload, packet, bundle, ciphertext, plaintext diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 8d2ac0f..8bfa791 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -1,28 +1,20 @@ +from zope.interface import implementer from automat import MethodicalMachine -from spake2 import SPAKE2_Symmetric -from hkdf import Hkdf -from nacl.secret import SecretBox -from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes) +from . import _interfaces +from .util import hexstr_to_bytes -def HKDF(skm, outlen, salt=None, CTXinfo=b""): - return Hkdf(salt, skm).expand(CTXinfo, outlen) - -def derive_key(key, purpose, length=SecretBox.KEY_SIZE): - if not isinstance(key, type(b"")): raise TypeError(type(key)) - if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) - if not isinstance(length, int): raise TypeError(type(length)) - return HKDF(key, length, CTXinfo=purpose) - -class SendMachine(object): +@implementer(_interfaces.ISend) +class Send(object): m = MethodicalMachine() - def __init__(self, timing): + def __init__(self, side, timing): + self._side = side self._timing = timing - def set_mailbox(self, mailbox): - self._mailbox = mailbox + def wire(self, mailbox): + self._M = _interfaces.IMailbox(mailbox) @m.state(initial=True) def S0_no_key(self): pass - @m.state() + @m.state(terminal=True) def S1_verified_key(self): pass def got_pake(self, payload): @@ -47,6 +39,7 @@ class SendMachine(object): del key for (phase, payload) in self._queue: self._encrypt_and_send(phase, payload) + self._queue[:] = [] @m.output() def deliver(self, phase, payload): self._encrypt_and_send(phase, payload) @@ -54,7 +47,7 @@ class SendMachine(object): def _encrypt_and_send(self, phase, payload): data_key = self._derive_phase_key(self._side, phase) encrypted = self._encrypt_data(data_key, plaintext) - self._mailbox.add_message(phase, encrypted) + self._M.add_message(phase, encrypted) S0_no_key.upon(send, enter=S0_no_key, outputs=[queue]) S0_no_key.upon(got_verified_key, enter=S1_verified_key, diff --git a/src/wormhole/_wormhole.py b/src/wormhole/_wormhole.py index 697fab3..3c626dd 100644 --- a/src/wormhole/_wormhole.py +++ b/src/wormhole/_wormhole.py @@ -1,14 +1,39 @@ +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces +from ._mailbox import Mailbox +from ._send import Send +from ._order import Order +from ._key import Key +from ._receive import Receive +from ._rendezvous import RendezvousConnector +from ._nameplate import NameplateListing +from ._code import Code +@implementer(_interfaces.IWormhole) class Wormhole: m = MethodicalMachine() - def __init__(self, ws_url, reactor): - self._relay_client = WSRelayClient(self, ws_url, reactor) - # This records all the messages we want the relay to have. Each time - # we establish a connection, we'll send them all (and the relay - # server will filter out duplicates). If we add any while a - # connection is established, we'll send the new ones. - self._outbound_messages = [] + def __init__(self, side, reactor, timing): + self._reactor = reactor + + self._M = Mailbox(side) + self._S = Send(side, timing) + self._O = Order(side, timing) + self._K = Key(timing) + self._R = Receive(side, timing) + self._RC = RendezvousConnector(side, timing, reactor) + self._NL = NameplateListing() + self._C = Code(timing) + + self._M.wire(self, self._RC, self._O) + self._S.wire(self._M) + self._O.wire(self._K, self._R) + self._K.wire(self, self._M, self._R) + self._R.wire(self, self._K, self._S) + self._RC.wire(self._M, self._C, self._NL) + self._NL.wire(self._RC, self._C) + self._C.wire(self, self._RC, self._NL) # these methods are called from outside def start(self): @@ -16,130 +41,110 @@ class Wormhole: # and these are the state-machine transition functions, which don't take # args + @m.state(initial=True) + def S0_empty(self): pass @m.state() - def closed(initial=True): pass + def S1_lonely(self): pass @m.state() - def know_code_not_mailbox(): pass + def S2_happy(self): pass @m.state() - def know_code_and_mailbox(): pass # no longer need nameplate - @m.state() - def waiting_first_msg(): pass # key is established, want any message - @m.state() - def processing_version(): pass - @m.state() - def processing_phase(): pass - @m.state() - def open(): pass # key is verified, can post app messages + def S3_closing(self): pass @m.state(terminal=True) - def failed(): pass + def S4_closed(self): pass + # from the Application, or some sort of top-level shim @m.input() - def deliver_message(self, message): pass + def send(self, phase, message): pass + @m.input() + def close(self): pass - def w_set_seed(self, code, mailbox): - """Call w_set_seed when we sprout a Wormhole Seed, which - contains both the code and the mailbox""" - self.w_set_code(code) - self.w_set_mailbox(mailbox) + # from Code (which may be provoked by the Application) + @m.input() + def set_code(self, code): pass + # Key sends (got_verifier, scared) + # Receive sends (got_message, happy, scared) @m.input() - def w_set_code(self, code): - """Call w_set_code when you learn the code, probably because the user - typed it in.""" + def happy(self): pass @m.input() - def w_set_mailbox(self, mailbox): - """Call w_set_mailbox() when you learn the mailbox id, from the - response to claim_nameplate""" - pass + def scared(self): pass + def got_message(self, phase, plaintext): + if phase == "version": + self.got_version(plaintext) + else: + self.got_phase(phase, plaintext) + @m.input() + def got_version(self, version): pass + @m.input() + def got_phase(self, phase, plaintext): pass + @m.input() + def got_verifier(self, verifier): pass + # Mailbox sends closed + @m.input() + def closed(self): pass - @m.input() - def rx_pake(self, pake): pass # reponse["message"][phase=pake] - - @m.input() - def rx_version(self, version): # response["message"][phase=version] - pass - @m.input() - def verify_good(self, verifier): pass - @m.input() - def verify_bad(self, f): pass - - @m.input() - def rx_phase(self, message): pass - @m.input() - def phase_good(self, message): pass - @m.input() - def phase_bad(self, f): pass @m.output() - def compute_and_post_pake(self, code): - self._code = code - self._pake = compute(code) - self._post(pake=self._pake) - self._ws_send_command("add", phase="pake", body=XXX(pake)) - @m.output() - def set_mailbox(self, mailbox): - self._mailbox = mailbox - @m.output() - def set_seed(self, code, mailbox): - self._code = code - self._mailbox = mailbox - + def got_code(self, code): + nameplate = code.split("-")[0] + self._M.set_nameplate(nameplate) + self._K.set_code(code) @m.output() def process_version(self, version): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) pass @m.output() - def notify_verified(self, verifier): - for d in self._verify_waiters: - d.callback(verifier) - @m.output() - def notify_failed(self, f): - for d in self._verify_waiters: - d.errback(f) + def S_send(self, phase, message): + self._S.send(phase, message) @m.output() - def process_phase(self, message): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) - pass + def close_scared(self): + self._M.close("scary") + @m.output() + def close_lonely(self): + self._M.close("lonely") + @m.output() + def close_happy(self): + self._M.close("happy") @m.output() - def post_inbound(self, message): - pass + def A_received(self, phase, plaintext): + self._A.received(phase, plaintext) + @m.output() + def A_got_verifier(self, verifier): + self._A.got_verifier(verifier) @m.output() - def deliver_message(self, message): - self._qc.deliver_message(message) + def A_closed(self): + result = "???" + self._A.closed(result) - @m.output() - def compute_key_and_post_version(self, pake): - self._key = x - self._verifier = x - plaintext = dict_to_bytes(self._my_versions) - phase = "version" - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) - self._msg_send(phase, encrypted) + S0_empty.upon(send, enter=S0_empty, outputs=[S_send]) + S0_empty.upon(set_code, enter=S1_lonely, outputs=[got_code]) + S1_lonely.upon(happy, enter=S2_happy, outputs=[]) + S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) + S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) + S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) + S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[A_got_verifier]) + S2_happy.upon(got_phase, enter=S2_happy, outputs=[A_received]) + S2_happy.upon(got_version, enter=S2_happy, outputs=[process_version]) + S2_happy.upon(scared, enter=S3_closing, outputs=[close_scared]) + S2_happy.upon(close, enter=S3_closing, outputs=[close_happy]) + S2_happy.upon(send, enter=S2_happy, outputs=[S_send]) + + S3_closing.upon(got_phase, enter=S3_closing, outputs=[]) + S3_closing.upon(got_version, enter=S3_closing, outputs=[]) + S3_closing.upon(happy, enter=S3_closing, outputs=[]) + S3_closing.upon(scared, enter=S3_closing, outputs=[]) + S3_closing.upon(close, enter=S3_closing, outputs=[]) + S3_closing.upon(send, enter=S3_closing, outputs=[]) + S3_closing.upon(closed, enter=S4_closed, outputs=[A_closed]) + + S4_closed.upon(got_phase, enter=S4_closed, outputs=[]) + S4_closed.upon(got_version, enter=S4_closed, outputs=[]) + S4_closed.upon(happy, enter=S4_closed, outputs=[]) + S4_closed.upon(scared, enter=S4_closed, outputs=[]) + S4_closed.upon(close, enter=S4_closed, outputs=[]) + S4_closed.upon(send, enter=S4_closed, outputs=[]) - closed.upon(w_set_code, enter=know_code_not_mailbox, - outputs=[compute_and_post_pake]) - know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, - outputs=[set_mailbox]) - know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg, - outputs=[compute_key_and_post_version]) - waiting_first_msg.upon(rx_version, enter=processing_version, - outputs=[process_version]) - processing_version.upon(verify_good, enter=open, outputs=[notify_verified]) - processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed]) - open.upon(rx_phase, enter=processing_phase, outputs=[process_phase]) - processing_phase.upon(phase_good, enter=open, outputs=[post_inbound]) - processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed]) From 3101ca51dbdb2f6ddbbc46ebf03541c85342143d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 11:28:49 -0800 Subject: [PATCH 055/176] name cleanup --- src/wormhole/_code.py | 2 +- src/wormhole/_interfaces.py | 2 +- src/wormhole/_nameplate.py | 2 +- src/wormhole/_rendezvous.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 2ad6f9f..2cc80e0 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -26,7 +26,7 @@ class Code(object): def wire(self, wormhole, rendezvous_connector, nameplate_lister): self._W = _interfaces.IWormhole(wormhole) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) - self._NL = _interfaces.INameplateListing(nameplate_lister) + self._NL = _interfaces.INameplateLister(nameplate_lister) @m.state(initial=True) def S0_unknown(self): pass diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index 647dd2b..c095498 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -14,7 +14,7 @@ class IReceive(Interface): pass class IRendezvousConnector(Interface): pass -class INameplateListing(Interface): +class INameplateLister(Interface): pass class ICode(Interface): pass diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index b6c1ed9..719f354 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -2,7 +2,7 @@ from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces -@implementer(_interfaces.INameplateListing) +@implementer(_interfaces.INameplateLister) class NameplateListing(object): m = MethodicalMachine() def __init__(self): diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index ca8a0f7..e43ef06 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -11,7 +11,7 @@ class RendezvousConnector(service.MultiService, object): def wire(self, mailbox, code, nameplate_lister): self._M = _interfaces.IMailbox(mailbox) self._C = _interfaces.ICode(code) - self._NL = _interfaces.INameplateListing(nameplate_lister) + self._NL = _interfaces.INameplateLister(nameplate_lister) # from Mailbox From a2ed35ceb8cb97234b3e437b5b257f38110b11b7 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 12:51:53 -0800 Subject: [PATCH 056/176] remove old files, lots of type work --- docs/machines.dot | 6 +- src/wormhole/_connection.py | 180 ------- src/wormhole/_interfaces.py | 6 + src/wormhole/_key.py | 19 +- src/wormhole/_mailbox.py | 48 +- src/wormhole/_order.py | 38 +- src/wormhole/_receive.py | 6 +- src/wormhole/_rendezvous.py | 197 +++++++- src/wormhole/_send.py | 32 +- src/wormhole/_wormhole.py | 18 +- src/wormhole/journal.py | 16 +- src/wormhole/wormhole.py | 920 +----------------------------------- 12 files changed, 302 insertions(+), 1184 deletions(-) delete mode 100644 src/wormhole/_connection.py diff --git a/docs/machines.dot b/docs/machines.dot index b87655a..7be9edc 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -18,11 +18,13 @@ digraph { Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] - App -> Wormhole [style="dashed" label="set_code\nsend\nclose\n(once)"] + App -> Wormhole [style="dashed" label="start\nset_code\nsend\nclose\n(once)"] #App -> Wormhole [color="blue"] Wormhole -> App [style="dashed" label="got_verifier\nreceived\nclosed\n(once)"] #Wormhole -> Connection [color="blue"] + Wormhole -> Connection [style="dashed" label="start"] + Connection -> Wormhole [style="dashed" label="rx_welcome"] Wormhole -> Send [style="dashed" label="send"] @@ -75,8 +77,6 @@ digraph { label="set_code"] App -> Code [style="dashed" label="allocate\ninput\nset"] - - } diff --git a/src/wormhole/_connection.py b/src/wormhole/_connection.py deleted file mode 100644 index 5086571..0000000 --- a/src/wormhole/_connection.py +++ /dev/null @@ -1,180 +0,0 @@ -from zope.interface import Interface -from six.moves.urllib_parse import urlparse -from attr import attrs, attrib -from twisted.internet import defer, endpoints #, error -from twisted.application import internet, service -from autobahn.twisted import websocket -from automat import MethodicalMachine - -class WSClient(websocket.WebSocketClientProtocol): - def onConnect(self, response): - # this fires during WebSocket negotiation, and isn't very useful - # unless you want to modify the protocol settings - print("onConnect", response) - #self.connection_machine.onConnect(self) - - def onOpen(self, *args): - # this fires when the WebSocket is ready to go. No arguments - print("onOpen", args) - #self.wormhole_open = True - # send BIND, since the MailboxMachine does not - self.connection_machine.protocol_onOpen(self) - #self.factory.d.callback(self) - - def onMessage(self, payload, isBinary): - print("onMessage") - return - assert not isBinary - self.wormhole._ws_dispatch_response(payload) - - def onClose(self, wasClean, code, reason): - print("onClose") - self.connection_machine.protocol_onClose(wasClean, code, reason) - #if self.wormhole_open: - # self.wormhole._ws_closed(wasClean, code, reason) - #else: - # # we closed before establishing a connection (onConnect) or - # # finishing WebSocket negotiation (onOpen): errback - # self.factory.d.errback(error.ConnectError(reason)) - -class WSFactory(websocket.WebSocketClientFactory): - protocol = WSClient - def buildProtocol(self, addr): - proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) - proto.connection_machine = self.connection_machine - #proto.wormhole_open = False - return proto - -# pip install (path to automat checkout)[visualize] -# automat-visualize wormhole._connection - -class IRendezvousClient(Interface): - # must be an IService too - def set_dispatch(dispatcher): - """Assign a dispatcher object to this client. The following methods - will be called on this object when things happen: - * rx_welcome(welcome -> dict) - * rx_nameplates(nameplates -> list) # [{id: str,..}, ..] - * rx_allocated(nameplate -> str) - * rx_claimed(mailbox -> str) - * rx_released() - * rx_message(side -> str, phase -> str, body -> str, msg_id -> str) - * rx_closed() - * rx_pong(pong -> int) - """ - pass - def tx_list(): pass - def tx_allocate(): pass - def tx_claim(nameplate): pass - def tx_release(): pass - def tx_open(mailbox): pass - def tx_add(phase, body): pass - def tx_close(mood): pass - def tx_ping(ping): pass - -# We have one WSRelayClient for each wsurl we know about, and it lasts -# as long as its parent Wormhole does. - -@attrs -class WSRelayClient(service.MultiService, object): - _journal = attrib() - _wormhole = attrib() - _mailbox = attrib() - _ws_url = attrib() - _reactor = attrib() - - def __init__(self): - f = WSFactory(self._ws_url) - f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - f.connection_machine = self # calls onOpen and onClose - p = urlparse(self._ws_url) - ep = self._make_endpoint(p.hostname, p.port or 80) - # default policy: 1s initial, random exponential backoff, max 60s - self._client_service = internet.ClientService(ep, f) - self._connector = None - self._done_d = defer.Deferred() - self._current_delay = self.INITIAL_DELAY - - def _make_endpoint(self, hostname, port): - return endpoints.HostnameEndpoint(self._reactor, hostname, port) - - # inputs from elsewhere - def d_callback(self, p): - self._p = p - self._m.d_callback() - def d_errback(self, f): - self._f = f - self._m.d_errback() - def protocol_onOpen(self, p): - self._m.onOpen() - def protocol_onClose(self, wasClean, code, reason): - self._m.onClose() - def C_stop(self): - self._m.stop() - def timer_expired(self): - self._m.expire() - - # outputs driven by the state machine - def ep_connect(self): - print("ep_connect()") - self._d = self._ep.connect(self._f) - self._d.addCallbacks(self.d_callback, self.d_errback) - def connection_established(self): - self._connection = WSConnection(ws, self._wormhole.appid, - self._wormhole.side, self) - self._mailbox.connected(ws) - self._wormhole.add_connection(self._connection) - self._ws_send_command("bind", appid=self._appid, side=self._side) - def M_lost(self): - self._wormhole.M_lost(self._connection) - self._connection = None - def start_timer(self): - print("start_timer") - self._t = self._reactor.callLater(3.0, self.expire) - def cancel_timer(self): - print("cancel_timer") - self._t.cancel() - self._t = None - def dropConnection(self): - print("dropConnection") - self._ws.dropConnection() - def notify_fail(self): - print("notify_fail", self._f.value if self._f else None) - self._done_d.errback(self._f) - def MC_stopped(self): - pass - - -def tryit(reactor): - cm = WSRelayClient(None, "ws://127.0.0.1:4000/v1", reactor) - print("_ConnectionMachine created") - print("start:", cm.start()) - print("waiting on _done_d to finish") - return cm._done_d - -# http://autobahn-python.readthedocs.io/en/latest/websocket/programming.html -# observed sequence of events: -# success: d_callback, onConnect(response), onOpen(), onMessage() -# negotifail (non-websocket): d_callback, onClose() -# noconnect: d_errback - -def tryws(reactor): - ws_url = "ws://127.0.0.1:40001/v1" - f = WSFactory(ws_url) - p = urlparse(ws_url) - ep = endpoints.HostnameEndpoint(reactor, p.hostname, p.port or 80) - d = ep.connect(f) - def _good(p): print("_good", p) - def _bad(f): print("_bad", f) - d.addCallbacks(_good, _bad) - return defer.Deferred() - -if __name__ == "__main__": - import sys - from twisted.python import log - log.startLogging(sys.stdout) - from twisted.internet.task import react - react(tryit) - -# ??? a new WSConnection is created each time the WSRelayClient gets through -# negotiation diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index c095498..67bec5e 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -18,3 +18,9 @@ class INameplateLister(Interface): pass class ICode(Interface): pass + +class ITiming(Interface): + pass + +class IJournal(Interface): # TODO: this needs to be public + pass diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 7ce979e..aa0863c 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -4,8 +4,10 @@ from spake2 import SPAKE2_Symmetric from hkdf import Hkdf from nacl.secret import SecretBox from nacl.exceptions import CryptoError +from nacl import utils from automat import MethodicalMachine -from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes) +from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes, + bytes_to_dict, dict_to_bytes) from . import _interfaces CryptoError __all__ = ["derive_key", "derive_phase_key", "CryptoError", @@ -38,6 +40,14 @@ def decrypt_data(key, encrypted): data = box.decrypt(encrypted) return data +def encrypt_data(key, plaintext): + assert isinstance(key, type(b"")), type(key) + assert isinstance(plaintext, type(b"")), type(plaintext) + assert len(key) == SecretBox.KEY_SIZE, len(key) + box = SecretBox(key) + nonce = utils.random(SecretBox.NONCE_SIZE) + return box.encrypt(plaintext, nonce) + @implementer(_interfaces.IKey) class Key(object): m = MethodicalMachine() @@ -57,7 +67,9 @@ class Key(object): @m.state(terminal=True) def S3_scared(self): pass - def got_pake(self, payload): + def got_pake(self, body): + assert isinstance(body, type(b"")), type(body) + payload = bytes_to_dict(body) if "pake_v1" in payload: self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) else: @@ -76,7 +88,8 @@ class Key(object): self._sp = SPAKE2_Symmetric(to_bytes(code), idSymmetric=to_bytes(self._appid)) msg1 = self._sp.start() - self._M.add_message("pake", {"pake_v1": bytes_to_hexstr(msg1)}) + body = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg1)}) + self._M.add_message("pake", body) @m.output() def scared(self): diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 376393c..51c58cb 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -10,6 +10,9 @@ class Mailbox(object): self._side = side self._mood = None self._nameplate = None + self._mailbox = None + self._pending_outbound = {} + self._processed = set() def wire(self, wormhole, rendezvous_connector, ordering): self._W = _interfaces.IWormhole(wormhole) @@ -101,15 +104,18 @@ class Mailbox(object): @m.input() def rx_claimed(self, mailbox): pass - def rx_message(self, side, phase, msg): + def rx_message(self, side, phase, body): + assert isinstance(side, type("")), type(side) + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) if side == self._side: - self.rx_message_ours(phase, msg) + self.rx_message_ours(phase, body) else: - self.rx_message_theirs(phase, msg) + self.rx_message_theirs(phase, body) @m.input() - def rx_message_ours(self, phase, msg): pass + def rx_message_ours(self, phase, body): pass @m.input() - def rx_message_theirs(self, phase, msg): pass + def rx_message_theirs(self, phase, body): pass @m.input() def rx_released(self): pass @m.input() @@ -119,7 +125,7 @@ class Mailbox(object): # from Send or Key @m.input() - def add_message(self, phase, msg): pass + def add_message(self, phase, body): pass @m.output() @@ -138,8 +144,10 @@ class Mailbox(object): assert self._mailbox self._RC.tx_open(self._mailbox) @m.output() - def queue(self, phase, msg): - self._pending_outbound[phase] = msg + def queue(self, phase, body): + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) + self._pending_outbound[phase] = body @m.output() def store_mailbox_and_RC_tx_open_and_drain(self, mailbox): self._mailbox = mailbox @@ -149,18 +157,20 @@ class Mailbox(object): def drain(self): self._drain() def _drain(self): - for phase, msg in self._pending_outbound.items(): - self._RC.tx_add(phase, msg) + for phase, body in self._pending_outbound.items(): + self._RC.tx_add(phase, body) @m.output() - def RC_tx_add(self, phase, msg): - self._RC.tx_add(phase, msg) + def RC_tx_add(self, phase, body): + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) + self._RC.tx_add(phase, body) @m.output() def RC_tx_release(self): self._RC.tx_release() @m.output() - def RC_tx_release_and_accept(self, phase, msg): + def RC_tx_release_and_accept(self, phase, body): self._RC.tx_release() - self._accept(phase, msg) + self._accept(phase, body) @m.output() def record_mood_and_RC_tx_release(self, mood): self._mood = mood @@ -179,14 +189,14 @@ class Mailbox(object): self._mood = mood self._RC.tx_close(self._mood) @m.output() - def accept(self, phase, msg): - self._accept(phase, msg) - def _accept(self, phase, msg): + def accept(self, phase, body): + self._accept(phase, body) + def _accept(self, phase, body): if phase not in self._processed: - self._O.got_message(phase, msg) + self._O.got_message(phase, body) self._processed.add(phase) @m.output() - def dequeue(self, phase, msg): + def dequeue(self, phase, body): self._pending_outbound.pop(phase) @m.output() def record_mood(self, mood): diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index f914130..d803fca 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -19,36 +19,40 @@ class Order(object): @m.state(terminal=True) def S1_yes_pake(self): pass - def got_message(self, phase, payload): + def got_message(self, phase, body): + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) if phase == "pake": - self.got_pake(phase, payload) + self.got_pake(phase, body) else: - self.got_non_pake(phase, payload) + self.got_non_pake(phase, body) @m.input() - def got_pake(self, phase, payload): pass + def got_pake(self, phase, body): pass @m.input() - def got_non_pake(self, phase, payload): pass + def got_non_pake(self, phase, body): pass @m.output() - def queue(self, phase, payload): - self._queue.append((phase, payload)) + def queue(self, phase, body): + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) + self._queue.append((phase, body)) @m.output() - def notify_key(self, phase, payload): - self._K.got_pake(payload) + def notify_key(self, phase, body): + self._K.got_pake(body) @m.output() - def drain(self, phase, payload): + def drain(self, phase, body): del phase - del payload - for (phase, payload) in self._queue: - self._deliver(phase, payload) + del body + for (phase, body) in self._queue: + self._deliver(phase, body) self._queue[:] = [] @m.output() - def deliver(self, phase, payload): - self._deliver(phase, payload) + def deliver(self, phase, body): + self._deliver(phase, body) - def _deliver(self, phase, payload): - self._R.got_message(phase, payload) + def _deliver(self, phase, body): + self._R.got_message(phase, body) S0_no_pake.upon(got_non_pake, enter=S0_no_pake, outputs=[queue]) S0_no_pake.upon(got_pake, enter=S1_yes_pake, outputs=[notify_key, drain]) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index a31c4f4..b15cbf2 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -24,7 +24,9 @@ class Receive(object): @m.state(terminal=True) def S3_scared(self): pass - def got_message(self, phase, payload): + def got_message(self, phase, body): + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) assert self._key data_key = derive_phase_key(self._side, phase) try: @@ -53,6 +55,8 @@ class Receive(object): self._W.happy() @m.output() def W_got_message(self, phase, plaintext): + assert isinstance(phase, type("")), type(phase) + assert isinstance(plaintext, type(b"")), type(plaintext) self._W.got_message(phase, plaintext) @m.output() def W_scared(self): diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index e43ef06..8eb52d4 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -1,39 +1,200 @@ +import os +from six.moves.urllib_parse import urlparse +from attr import attrs, attrib +from attr.validators import provides, instance_of from zope.interface import implementer -from twisted.application import service +from twisted.python import log +from twisted.internet import defer, endpoints +from twisted.application import internet +from autobahn.twisted import websocket from . import _interfaces +from .util import (bytes_to_hexstr, hexstr_to_bytes, + bytes_to_dict, dict_to_bytes) +class WSClient(websocket.WebSocketClientProtocol): + def onConnect(self, response): + # this fires during WebSocket negotiation, and isn't very useful + # unless you want to modify the protocol settings + #print("onConnect", response) + pass + + def onOpen(self, *args): + # this fires when the WebSocket is ready to go. No arguments + #print("onOpen", args) + #self.wormhole_open = True + self._RC.ws_open(self) + + def onMessage(self, payload, isBinary): + #print("onMessage") + assert not isBinary + self._RC.ws_message(payload) + + def onClose(self, wasClean, code, reason): + #print("onClose") + self._RC.ws_close(wasClean, code, reason) + #if self.wormhole_open: + # self.wormhole._ws_closed(wasClean, code, reason) + #else: + # # we closed before establishing a connection (onConnect) or + # # finishing WebSocket negotiation (onOpen): errback + # self.factory.d.errback(error.ConnectError(reason)) + +class WSFactory(websocket.WebSocketClientFactory): + protocol = WSClient + def __init__(self, RC, *args, **kwargs): + websocket.WebSocketClientFactory.__init__(self, *args, **kwargs) + self._RC = RC + + def buildProtocol(self, addr): + proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) + proto._RC = self._RC + #proto.wormhole_open = False + return proto + +@attrs @implementer(_interfaces.IRendezvousConnector) -class RendezvousConnector(service.MultiService, object): - def __init__(self, journal, timing): - self._journal = journal - self._timing = timing +class RendezvousConnector(object): + _url = attrib(instance_of(type(u""))) + _appid = attrib(instance_of(type(u""))) + _side = attrib(instance_of(type(u""))) + _reactor = attrib() + _journal = attrib(provides(_interfaces.IJournal)) + _timing = attrib(provides(_interfaces.ITiming)) - def wire(self, mailbox, code, nameplate_lister): + def __init__(self): + self._ws = None + f = WSFactory(self, self._url) + f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) + p = urlparse(self._url) + ep = self._make_endpoint(p.hostname, p.port or 80) + self._connector = internet.ClientService(ep, f) + + def _make_endpoint(self, hostname, port): + # TODO: Tor goes here + return endpoints.HostnameEndpoint(self._reactor, hostname, port) + + def wire(self, wormhole, mailbox, code, nameplate_lister): + self._W = _interfaces.IWormhole(wormhole) self._M = _interfaces.IMailbox(mailbox) self._C = _interfaces.ICode(code) self._NL = _interfaces.INameplateLister(nameplate_lister) + # from Wormhole + def start(self): + self._connector.startService() # from Mailbox - def tx_claim(self): - pass - def tx_open(self): - pass - def tx_add(self, x): - pass + def tx_claim(self, nameplate): + self._tx("claim", nameplate=nameplate) + + def tx_open(self, mailbox): + self._tx("open", mailbox=mailbox) + + def tx_add(self, phase, body): + assert isinstance(phase, type("")), type(phase) + assert isinstance(body, type(b"")), type(body) + self._tx("add", phase=phase, body=bytes_to_hexstr(body)) + def tx_release(self): - pass + self._tx("release") + def tx_close(self, mood): - pass + self._tx("close", mood=mood) + def stop(self): - pass + d = defer.maybeDeferred(self._connector.stopService) + d.addErrback(log.err) # TODO: deliver error upstairs? + d.addBoth(self._stopped) + # from NameplateLister def tx_list(self): - pass + self._tx("list") # from Code def tx_allocate(self): + self._tx("allocate") + + # from our WSClient (the WebSocket protocol) + def ws_open(self, proto): + self._ws = proto + self._tx("bind", appid=self._appid, side=self._side) + self._M.connected() + self._NL.connected() + + def ws_message(self, payload): + msg = bytes_to_dict(payload) + if self.DEBUG and msg["type"]!="ack": print("DIS", msg["type"], msg) + self._timing.add("ws_receive", _side=self._side, message=msg) + mtype = msg["type"] + meth = getattr(self, "_response_handle_"+mtype, None) + if not meth: + # make tests fail, but real application will ignore it + log.err(ValueError("Unknown inbound message type %r" % (msg,))) + return + return meth(msg) + + def ws_close(self, wasClean, code, reason): + self._ws = None + self._M.lost() + self._NL.lost() + + # internal + def _stopped(self, res): + self._M.stopped() + + def _tx(self, mtype, **kwargs): + assert self._ws + # msgid is used by misc/dump-timing.py to correlate our sends with + # their receives, and vice versa. They are also correlated with the + # ACKs we get back from the server (which we otherwise ignore). There + # are so few messages, 16 bits is enough to be mostly-unique. + if self.DEBUG: print("SEND", mtype) + kwargs["id"] = bytes_to_hexstr(os.urandom(2)) + kwargs["type"] = mtype + payload = dict_to_bytes(kwargs) + self._timing.add("ws_send", _side=self._side, **kwargs) + self._ws.sendMessage(payload, False) + + def _response_handle_allocated(self, msg): + nameplate = msg["nameplate"] + assert isinstance(nameplate, type("")), type(nameplate) + self._C.rx_allocated(nameplate) + + def _response_handle_nameplates(self, msg): + nameplates = msg["nameplates"] + assert isinstance(nameplates, list), type(nameplates) + nids = [] + for n in nameplates: + assert isinstance(n, dict), type(n) + nameplate_id = n["id"] + assert isinstance(nameplate_id, type("")), type(nameplate_id) + nids.append(nameplate_id) + self._NL.rx_nameplates(nids) + + def _response_handle_ack(self, msg): pass - - # record, message, payload, packet, bundle, ciphertext, plaintext + + def _response_handle_welcome(self, msg): + self._W.rx_welcome(msg["welcome"]) + + def _response_handle_claimed(self, msg): + mailbox = msg["mailbox"] + assert isinstance(mailbox, type("")), type(mailbox) + self._M.rx_claimed(mailbox) + + def _response_handle_message(self, msg): + side = msg["side"] + phase = msg["phase"] + assert isinstance(phase, type("")), type(phase) + body = hexstr_to_bytes(msg["body"]) # bytes + self._M.rx_message(side, phase, body) + + def _response_handle_released(self, msg): + self._M.rx_released() + + def _response_handle_closed(self, msg): + self._M.rx_closed() + + + # record, message, payload, packet, bundle, ciphertext, plaintext diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 8bfa791..b717d14 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -1,7 +1,7 @@ from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces -from .util import hexstr_to_bytes +from ._key import derive_phase_key, encrypt_data @implementer(_interfaces.ISend) class Send(object): @@ -17,36 +17,34 @@ class Send(object): @m.state(terminal=True) def S1_verified_key(self): pass - def got_pake(self, payload): - if "pake_v1" in payload: - self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) - else: - self.got_pake_bad() - @m.input() def got_verified_key(self, key): pass @m.input() - def send(self, phase, payload): pass + def send(self, phase, plaintext): pass @m.output() - def queue(self, phase, payload): - self._queue.append((phase, payload)) + def queue(self, phase, plaintext): + assert isinstance(phase, type("")), type(phase) + assert isinstance(plaintext, type(b"")), type(plaintext) + self._queue.append((phase, plaintext)) @m.output() def record_key(self, key): self._key = key @m.output() def drain(self, key): del key - for (phase, payload) in self._queue: - self._encrypt_and_send(phase, payload) + for (phase, plaintext) in self._queue: + self._encrypt_and_send(phase, plaintext) self._queue[:] = [] @m.output() - def deliver(self, phase, payload): - self._encrypt_and_send(phase, payload) + def deliver(self, phase, plaintext): + assert isinstance(phase, type("")), type(phase) + assert isinstance(plaintext, type(b"")), type(plaintext) + self._encrypt_and_send(phase, plaintext) - def _encrypt_and_send(self, phase, payload): - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) + def _encrypt_and_send(self, phase, plaintext): + data_key = derive_phase_key(self._side, phase) + encrypted = encrypt_data(data_key, plaintext) self._M.add_message(phase, encrypted) S0_no_key.upon(send, enter=S0_no_key, outputs=[queue]) diff --git a/src/wormhole/_wormhole.py b/src/wormhole/_wormhole.py index 3c626dd..11a250f 100644 --- a/src/wormhole/_wormhole.py +++ b/src/wormhole/_wormhole.py @@ -9,6 +9,7 @@ from ._receive import Receive from ._rendezvous import RendezvousConnector from ._nameplate import NameplateListing from ._code import Code +from .util import bytes_to_dict @implementer(_interfaces.IWormhole) class Wormhole: @@ -31,13 +32,13 @@ class Wormhole: self._O.wire(self._K, self._R) self._K.wire(self, self._M, self._R) self._R.wire(self, self._K, self._S) - self._RC.wire(self._M, self._C, self._NL) + self._RC.wire(self, self._M, self._C, self._NL) self._NL.wire(self._RC, self._C) self._C.wire(self, self._RC, self._NL) # these methods are called from outside def start(self): - self._relay_client.start() + self._RC.start() # and these are the state-machine transition functions, which don't take # args @@ -54,7 +55,7 @@ class Wormhole: # from the Application, or some sort of top-level shim @m.input() - def send(self, phase, message): pass + def send(self, phase, plaintext): pass @m.input() def close(self): pass @@ -69,6 +70,8 @@ class Wormhole: @m.input() def scared(self): pass def got_message(self, phase, plaintext): + assert isinstance(phase, type("")), type(phase) + assert isinstance(plaintext, type(b"")), type(plaintext) if phase == "version": self.got_version(plaintext) else: @@ -91,12 +94,13 @@ class Wormhole: self._M.set_nameplate(nameplate) self._K.set_code(code) @m.output() - def process_version(self, version): # response["message"][phase=version] - pass + def process_version(self, plaintext): + self._their_versions = bytes_to_dict(plaintext) + # ignored for now @m.output() - def S_send(self, phase, message): - self._S.send(phase, message) + def S_send(self, phase, plaintext): + self._S.send(phase, plaintext) @m.output() def close_scared(self): diff --git a/src/wormhole/journal.py b/src/wormhole/journal.py index b3f9f08..69f0bf5 100644 --- a/src/wormhole/journal.py +++ b/src/wormhole/journal.py @@ -1,6 +1,9 @@ +from zope.interface import implementer import contextlib +from _interfaces import IJournal -class JournalManager(object): +@implementer(IJournal) +class Journal(object): def __init__(self, save_checkpoint): self._save_checkpoint = save_checkpoint self._outbound_queue = [] @@ -8,7 +11,7 @@ class JournalManager(object): def queue_outbound(self, fn, *args, **kwargs): assert self._processing - self._outbound_queue.append((fn, args, kwargs)) + self._outbound_queue.append((fn, args, kwargs) @contextlib.contextmanager def process(self): @@ -21,3 +24,12 @@ class JournalManager(object): fn(*args, **kwargs) self._outbound_queue[:] = [] self._processing = False + + +@implementer(IJournal) +class ImmediateJournal(object): + def queue_outbound(self, fn, *args, **kwargs): + fn(*args, **kwargs) + @contextlib.contextmanager + def process(self): + yield diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 4b9c5d9..863d638 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -1,912 +1,5 @@ from __future__ import print_function, absolute_import, unicode_literals -import os, sys, re -from six.moves.urllib_parse import urlparse -from twisted.internet import defer, endpoints #, error -from twisted.internet.threads import deferToThread, blockingCallFromThread -from twisted.internet.defer import inlineCallbacks, returnValue -from twisted.python import log, failure -from nacl.secret import SecretBox -from nacl.exceptions import CryptoError -from nacl import utils -from spake2 import SPAKE2_Symmetric -from hashlib import sha256 -from . import __version__ -from . import codes -#from .errors import ServerError, Timeout -from .errors import (WrongPasswordError, InternalError, WelcomeError, - WormholeClosedError, KeyFormatError) -from .timing import DebugTiming -from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes, - dict_to_bytes, bytes_to_dict) -from hkdf import Hkdf - -def HKDF(skm, outlen, salt=None, CTXinfo=b""): - return Hkdf(salt, skm).expand(CTXinfo, outlen) - -CONFMSG_NONCE_LENGTH = 128//8 -CONFMSG_MAC_LENGTH = 256//8 -def make_confmsg(confkey, nonce): - return nonce+HKDF(confkey, CONFMSG_MAC_LENGTH, nonce) - - -# We send the following messages through the relay server to the far side (by -# sending "add" commands to the server, and getting "message" responses): -# -# phase=setup: -# * unauthenticated version strings (but why?) -# * early warmup for connection hints ("I can do tor, spin up HS") -# * wordlist l10n identifier -# phase=pake: just the SPAKE2 'start' message (binary) -# phase=version: version data, key verification (HKDF(key, nonce)+nonce) -# phase=1,2,3,..: application messages - -class _GetCode: - def __init__(self, code_length, send_command, timing): - self._code_length = code_length - self._send_command = send_command - self._timing = timing - self._allocated_d = defer.Deferred() - - @inlineCallbacks - def go(self): - with self._timing.add("allocate"): - self._send_command("allocate") - nameplate_id = yield self._allocated_d - code = codes.make_code(nameplate_id, self._code_length) - assert isinstance(code, type("")), type(code) - returnValue(code) - - def _response_handle_allocated(self, msg): - nid = msg["nameplate"] - assert isinstance(nid, type("")), type(nid) - self._allocated_d.callback(nid) - -class _InputCode: - def __init__(self, reactor, prompt, code_length, send_command, timing, - stderr): - self._reactor = reactor - self._prompt = prompt - self._code_length = code_length - self._send_command = send_command - self._timing = timing - self._stderr = stderr - - @inlineCallbacks - def _list(self): - self._lister_d = defer.Deferred() - self._send_command("list") - nameplates = yield self._lister_d - self._lister_d = None - returnValue(nameplates) - - def _list_blocking(self): - return blockingCallFromThread(self._reactor, self._list) - - @inlineCallbacks - def go(self): - # fetch the list of nameplates ahead of time, to give us a chance to - # discover the welcome message (and warn the user about an obsolete - # client) - # - # TODO: send the request early, show the prompt right away, hide the - # latency in the user's indecision and slow typing. If we're lucky - # the answer will come back before they hit TAB. - - initial_nameplate_ids = yield self._list() - with self._timing.add("input code", waiting="user"): - t = self._reactor.addSystemEventTrigger("before", "shutdown", - self._warn_readline) - res = yield deferToThread(codes.input_code_with_completion, - self._prompt, - initial_nameplate_ids, - self._list_blocking, - self._code_length) - (code, used_completion) = res - self._reactor.removeSystemEventTrigger(t) - if not used_completion: - self._remind_about_tab() - returnValue(code) - - def _response_handle_nameplates(self, msg): - nameplates = msg["nameplates"] - assert isinstance(nameplates, list), type(nameplates) - nids = [] - for n in nameplates: - assert isinstance(n, dict), type(n) - nameplate_id = n["id"] - assert isinstance(nameplate_id, type("")), type(nameplate_id) - nids.append(nameplate_id) - self._lister_d.callback(nids) - - def _warn_readline(self): - # When our process receives a SIGINT, Twisted's SIGINT handler will - # stop the reactor and wait for all threads to terminate before the - # process exits. However, if we were waiting for - # input_code_with_completion() when SIGINT happened, the readline - # thread will be blocked waiting for something on stdin. Trick the - # user into satisfying the blocking read so we can exit. - print("\nCommand interrupted: please press Return to quit", - file=sys.stderr) - - # Other potential approaches to this problem: - # * hard-terminate our process with os._exit(1), but make sure the - # tty gets reset to a normal mode ("cooked"?) first, so that the - # next shell command the user types is echoed correctly - # * track down the thread (t.p.threadable.getThreadID from inside the - # thread), get a cffi binding to pthread_kill, deliver SIGINT to it - # * allocate a pty pair (pty.openpty), replace sys.stdin with the - # slave, build a pty bridge that copies bytes (and other PTY - # things) from the real stdin to the master, then close the slave - # at shutdown, so readline sees EOF - # * write tab-completion and basic editing (TTY raw mode, - # backspace-is-erase) without readline, probably with curses or - # twisted.conch.insults - # * write a separate program to get codes (maybe just "wormhole - # --internal-get-code"), run it as a subprocess, let it inherit - # stdin/stdout, send it SIGINT when we receive SIGINT ourselves. It - # needs an RPC mechanism (over some extra file descriptors) to ask - # us to fetch the current nameplate_id list. - # - # Note that hard-terminating our process with os.kill(os.getpid(), - # signal.SIGKILL), or SIGTERM, doesn't seem to work: the thread - # doesn't see the signal, and we must still wait for stdin to make - # readline finish. - - def _remind_about_tab(self): - print(" (note: you can use to complete words)", file=self._stderr) - -class _WelcomeHandler: - def __init__(self, url, current_version, signal_error): - self._ws_url = url - self._version_warning_displayed = False - self._current_version = current_version - self._signal_error = signal_error - - 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._ws_url, motd_formatted), file=sys.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") - -# states for nameplates, mailboxes, and the websocket connection -(CLOSED, OPENING, OPEN, CLOSING) = ("closed", "opening", "open", "closing") - -class _Wormhole: - DEBUG = False - - def __init__(self, appid, relay_url, reactor, tor_manager, timing, stderr): - self._appid = appid - self._ws_url = relay_url - self._reactor = reactor - self._tor_manager = tor_manager - self._timing = timing - self._stderr = stderr - - self._welcomer = _WelcomeHandler(self._ws_url, __version__, - self._signal_error) - self._side = bytes_to_hexstr(os.urandom(5)) - self._connection_state = CLOSED - self._connection_waiters = [] - self._ws_t = None - self._started_get_code = False - self._get_code = None - self._started_input_code = False - self._input_code_waiter = None - self._code = None - self._nameplate_id = None - self._nameplate_state = CLOSED - self._mailbox_id = None - self._mailbox_state = CLOSED - self._flag_need_nameplate = True - self._flag_need_to_see_mailbox_used = True - self._flag_need_to_build_msg1 = True - self._flag_need_to_send_PAKE = True - self._establish_key_called = False - self._key_waiter = None - self._key = None - - self._version_message = None - self._version_checked = False - self._get_verifier_called = False - self._verifier = None # bytes - self._verify_result = None # bytes or a Failure - self._verifier_waiter = None - - self._my_versions = {} # sent - self._their_versions = {} # received - - self._close_called = False # the close() API has been called - self._closing = False # we've started shutdown - self._disconnect_waiter = defer.Deferred() - self._error = None - - self._next_send_phase = 0 - # send() queues plaintext here, waiting for a connection and the key - self._plaintext_to_send = [] # (phase, plaintext) - self._sent_phases = set() # to detect double-send - - self._next_receive_phase = 0 - self._receive_waiters = {} # phase -> Deferred - self._received_messages = {} # phase -> plaintext - - # API METHODS for applications to call - - # You must use at least one of these entry points, to establish the - # wormhole code. Other APIs will stall or be queued until we have one. - - # entry point 1: generate a new code. returns a Deferred - def get_code(self, code_length=2): # XX rename to allocate_code()? create_? - return self._API_get_code(code_length) - - # entry point 2: interactively type in a code, with completion. returns - # Deferred - def input_code(self, prompt="Enter wormhole code: ", code_length=2): - return self._API_input_code(prompt, code_length) - - # entry point 3: paste in a fully-formed code. No return value. - def set_code(self, code): - self._API_set_code(code) - - # todo: restore-saved-state entry points - - def establish_key(self): - """ - returns a Deferred that fires when we've established the shared key. - When successful, the Deferred fires with a simple `True`, otherwise - it fails. - """ - return self._API_establish_key() - - def verify(self): - """Returns a Deferred that fires when we've heard back from the other - side, and have confirmed that they used the right wormhole code. When - successful, the Deferred fires with a "verifier" (a bytestring) which - can be compared out-of-band before making additional API calls. If - they used the wrong wormhole code, the Deferred errbacks with - WrongPasswordError. - """ - return self._API_verify() - - def send(self, outbound_data): - return self._API_send(outbound_data) - - def get(self): - return self._API_get() - - def derive_key(self, purpose, length): - """Derive a new key from the established wormhole channel for some - other purpose. This is a deterministic randomized function of the - session key and the 'purpose' string (unicode/py3-string). This - cannot be called until verify() or get() has fired. - """ - return self._API_derive_key(purpose, length) - - def close(self, res=None): - """Collapse the wormhole, freeing up server resources and flushing - all pending messages. Returns a Deferred that fires when everything - is done. It fires with any argument close() was given, to enable use - as a d.addBoth() handler: - - w = wormhole(...) - d = w.get() - .. - d.addBoth(w.close) - return d - - Another reasonable approach is to use inlineCallbacks: - - @inlineCallbacks - def pair(self, code): - w = wormhole(...) - try: - them = yield w.get() - finally: - yield w.close() - """ - return self._API_close(res) - - # INTERNAL METHODS beyond here - - def _start(self): - d = self._connect() # causes stuff to happen - d.addErrback(log.err) - return d # fires when connection is established, if you care - - - - def _make_endpoint(self, hostname, port): - if self._tor_manager: - return self._tor_manager.get_endpoint_for(hostname, port) - # note: HostnameEndpoints have a default 30s timeout - return endpoints.HostnameEndpoint(self._reactor, hostname, port) - - def _connect(self): - # TODO: if we lose the connection, make a new one, re-establish the - # state - assert self._side - self._connection_state = OPENING - self._ws_t = self._timing.add("open websocket") - p = urlparse(self._ws_url) - f = WSFactory(self._ws_url) - f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - f.wormhole = self - f.d = defer.Deferred() - # TODO: if hostname="localhost", I get three factories starting - # and stopping (maybe 127.0.0.1, ::1, and something else?), and - # an error in the factory is masked. - ep = self._make_endpoint(p.hostname, p.port or 80) - # .connect errbacks if the TCP connection fails - d = ep.connect(f) - d.addCallback(self._event_connected) - # f.d is errbacked if WebSocket negotiation fails, and the WebSocket - # drops any data sent before onOpen() fires, so we must wait for it - d.addCallback(lambda _: f.d) - d.addCallback(self._event_ws_opened) - return d - - def _event_connected(self, ws): - self._ws = ws - if self._ws_t: - self._ws_t.finish() - - def _event_ws_opened(self, _): - self._connection_state = OPEN - if self._closing: - return self._maybe_finished_closing() - self._ws_send_command("bind", appid=self._appid, side=self._side) - self._maybe_claim_nameplate() - self._maybe_send_pake() - waiters, self._connection_waiters = self._connection_waiters, [] - for d in waiters: - d.callback(None) - - def _when_connected(self): - if self._connection_state == OPEN: - return defer.succeed(None) - d = defer.Deferred() - self._connection_waiters.append(d) - return d - - def _ws_send_command(self, mtype, **kwargs): - # msgid is used by misc/dump-timing.py to correlate our sends with - # their receives, and vice versa. They are also correlated with the - # ACKs we get back from the server (which we otherwise ignore). There - # are so few messages, 16 bits is enough to be mostly-unique. - if self.DEBUG: print("SEND", mtype) - kwargs["id"] = bytes_to_hexstr(os.urandom(2)) - kwargs["type"] = mtype - payload = dict_to_bytes(kwargs) - self._timing.add("ws_send", _side=self._side, **kwargs) - self._ws.sendMessage(payload, False) - - def _ws_dispatch_response(self, payload): - msg = bytes_to_dict(payload) - if self.DEBUG and msg["type"]!="ack": print("DIS", msg["type"], msg) - self._timing.add("ws_receive", _side=self._side, message=msg) - mtype = msg["type"] - meth = getattr(self, "_response_handle_"+mtype, None) - if not meth: - # make tests fail, but real application will ignore it - log.err(ValueError("Unknown inbound message type %r" % (msg,))) - return - return meth(msg) - - def _response_handle_ack(self, msg): - pass - - def _response_handle_welcome(self, msg): - self._welcomer.handle_welcome(msg["welcome"]) - - # entry point 1: generate a new code - @inlineCallbacks - def _API_get_code(self, code_length): - if self._code is not None: raise InternalError - if self._started_get_code: raise InternalError - self._started_get_code = True - with self._timing.add("API get_code"): - yield self._when_connected() - gc = _GetCode(code_length, self._ws_send_command, self._timing) - self._get_code = gc - self._response_handle_allocated = gc._response_handle_allocated - # TODO: signal_error - code = yield gc.go() - self._get_code = None - self._nameplate_state = OPEN - self._event_learned_code(code) - returnValue(code) - - # entry point 2: interactively type in a code, with completion - @inlineCallbacks - def _API_input_code(self, prompt, code_length): - if self._code is not None: raise InternalError - if self._started_input_code: raise InternalError - self._started_input_code = True - with self._timing.add("API input_code"): - yield self._when_connected() - ic = _InputCode(self._reactor, prompt, code_length, - self._ws_send_command, self._timing, self._stderr) - self._response_handle_nameplates = ic._response_handle_nameplates - # we reveal the Deferred we're waiting on, so _signal_error can - # wake us up if something goes wrong (like a welcome error) - self._input_code_waiter = ic.go() - code = yield self._input_code_waiter - self._input_code_waiter = None - self._event_learned_code(code) - returnValue(None) - - # entry point 3: paste in a fully-formed code - def _API_set_code(self, code): - self._timing.add("API set_code") - if not isinstance(code, type(u"")): - raise TypeError("Unexpected code type '{}'".format(type(code))) - if self._code is not None: - raise InternalError - self._event_learned_code(code) - - # TODO: entry point 4: restore pre-contact saved state (we haven't heard - # from the peer yet, so we still need the nameplate) - - # TODO: entry point 5: restore post-contact saved state (so we don't need - # or use the nameplate, only the mailbox) - def _restore_post_contact_state(self, state): - # ... - self._flag_need_nameplate = False - #self._mailbox_id = X(state) - self._event_learned_mailbox() - - def _event_learned_code(self, code): - self._timing.add("code established") - # bail out early if the password contains spaces... - # this should raise a useful error - if ' ' in code: - raise KeyFormatError("code (%s) contains spaces." % code) - self._code = code - mo = re.search(r'^(\d+)-', code) - if not mo: - raise ValueError("code (%s) must start with NN-" % code) - nid = mo.group(1) - assert isinstance(nid, type("")), type(nid) - self._nameplate_id = nid - # fire more events - self._maybe_build_msg1() - self._event_learned_nameplate() - - def _maybe_build_msg1(self): - if not (self._code and self._flag_need_to_build_msg1): - return - with self._timing.add("pake1", waiting="crypto"): - self._sp = SPAKE2_Symmetric(to_bytes(self._code), - idSymmetric=to_bytes(self._appid)) - self._msg1 = self._sp.start() - self._flag_need_to_build_msg1 = False - self._event_built_msg1() - - def _event_built_msg1(self): - self._maybe_send_pake() - - # every _maybe_X starts with a set of conditions - # for each such condition Y, every _event_Y must call _maybe_X - - def _event_learned_nameplate(self): - self._maybe_claim_nameplate() - - def _maybe_claim_nameplate(self): - if not (self._nameplate_id and self._connection_state == OPEN): - return - self._ws_send_command("claim", nameplate=self._nameplate_id) - self._nameplate_state = OPEN - - def _response_handle_claimed(self, msg): - mailbox_id = msg["mailbox"] - assert isinstance(mailbox_id, type("")), type(mailbox_id) - self._mailbox_id = mailbox_id - self._event_learned_mailbox() - - def _event_learned_mailbox(self): - if not self._mailbox_id: raise InternalError - assert self._mailbox_state == CLOSED, self._mailbox_state - if self._closing: - return - self._ws_send_command("open", mailbox=self._mailbox_id) - self._mailbox_state = OPEN - # causes old messages to be sent now, and subscribes to new messages - self._maybe_send_pake() - self._maybe_send_phase_messages() - - def _maybe_send_pake(self): - # TODO: deal with reentrant call - if not (self._connection_state == OPEN - and self._mailbox_state == OPEN - and self._flag_need_to_send_PAKE): - return - body = {"pake_v1": bytes_to_hexstr(self._msg1)} - payload = dict_to_bytes(body) - self._msg_send("pake", payload) - self._flag_need_to_send_PAKE = False - - def _event_received_pake(self, pake_msg): - payload = bytes_to_dict(pake_msg) - msg2 = hexstr_to_bytes(payload["pake_v1"]) - with self._timing.add("pake2", waiting="crypto"): - self._key = self._sp.finish(msg2) - self._event_established_key() - - def _event_established_key(self): - self._timing.add("key established") - self._maybe_notify_key() - - # both sides send different (random) version messages - self._send_version_message() - - verifier = self._derive_key(b"wormhole:verifier") - self._event_computed_verifier(verifier) - - self._maybe_check_version() - self._maybe_send_phase_messages() - - def _API_establish_key(self): - if self._error: return defer.fail(self._error) - if self._establish_key_called: raise InternalError - self._establish_key_called = True - if self._key is not None: - return defer.succeed(True) - self._key_waiter = defer.Deferred() - return self._key_waiter - - def _maybe_notify_key(self): - if self._key is None: - return - if self._error: - result = failure.Failure(self._error) - else: - result = True - if self._key_waiter and not self._key_waiter.called: - self._key_waiter.callback(result) - - def _send_version_message(self): - # this is encrypted like a normal phase message, and includes a - # dictionary of version flags to let the other Wormhole know what - # we're capable of (for future expansion) - plaintext = dict_to_bytes(self._my_versions) - phase = "version" - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) - self._msg_send(phase, encrypted) - - def _API_verify(self): - if self._error: return defer.fail(self._error) - if self._get_verifier_called: raise InternalError - self._get_verifier_called = True - if self._verify_result: - return defer.succeed(self._verify_result) # bytes or Failure - self._verifier_waiter = defer.Deferred() - return self._verifier_waiter - - def _event_computed_verifier(self, verifier): - self._verifier = verifier - self._maybe_notify_verify() - - def _maybe_notify_verify(self): - if not (self._verifier and self._version_checked): - return - if self._error: - self._verify_result = failure.Failure(self._error) - else: - self._verify_result = self._verifier - if self._verifier_waiter and not self._verifier_waiter.called: - self._verifier_waiter.callback(self._verify_result) - - def _event_received_version(self, side, body): - # We ought to have the master key by now, because sensible peers - # should always send "pake" before sending "version". It might be - # nice to relax this requirement, which means storing the received - # version message, and having _event_established_key call - # _check_version() - self._version_message = (side, body) - self._maybe_check_version() - - def _maybe_check_version(self): - if not (self._key and self._version_message): - return - if self._version_checked: - return - self._version_checked = True - - side, body = self._version_message - data_key = self._derive_phase_key(side, "version") - try: - plaintext = self._decrypt_data(data_key, body) - except CryptoError: - # this makes all API calls fail - if self.DEBUG: print("CONFIRM FAILED") - self._signal_error(WrongPasswordError(), "scary") - return - msg = bytes_to_dict(plaintext) - self._version_received(msg) - - self._maybe_notify_verify() - - def _version_received(self, msg): - self._their_versions = msg - - def _API_send(self, outbound_data): - if self._error: raise self._error - if not isinstance(outbound_data, type(b"")): - raise TypeError(type(outbound_data)) - phase = self._next_send_phase - self._next_send_phase += 1 - self._plaintext_to_send.append( (phase, outbound_data) ) - with self._timing.add("API send", phase=phase): - self._maybe_send_phase_messages() - - def _derive_phase_key(self, side, phase): - assert isinstance(side, type("")), type(side) - assert isinstance(phase, type("")), type(phase) - side_bytes = side.encode("ascii") - phase_bytes = phase.encode("ascii") - purpose = (b"wormhole:phase:" - + sha256(side_bytes).digest() - + sha256(phase_bytes).digest()) - return self._derive_key(purpose) - - def _maybe_send_phase_messages(self): - # TODO: deal with reentrant call - if not (self._connection_state == OPEN - and self._mailbox_state == OPEN - and self._key): - return - plaintexts = self._plaintext_to_send - self._plaintext_to_send = [] - for pm in plaintexts: - (phase_int, plaintext) = pm - assert isinstance(phase_int, int), type(phase_int) - phase = "%d" % phase_int - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) - self._msg_send(phase, encrypted) - - def _encrypt_data(self, key, data): - # Without predefined roles, we can't derive predictably unique keys - # for each side, so we use the same key for both. We use random - # nonces to keep the messages distinct, and we automatically ignore - # reflections. - # TODO: HKDF(side, nonce, key) ?? include 'side' to prevent - # reflections, since we no longer compare messages - assert isinstance(key, type(b"")), type(key) - assert isinstance(data, type(b"")), type(data) - assert len(key) == SecretBox.KEY_SIZE, len(key) - box = SecretBox(key) - nonce = utils.random(SecretBox.NONCE_SIZE) - return box.encrypt(data, nonce) - - def _msg_send(self, phase, body): - if phase in self._sent_phases: raise InternalError - assert self._mailbox_state == OPEN, self._mailbox_state - self._sent_phases.add(phase) - # TODO: retry on failure, with exponential backoff. We're guarding - # against the rendezvous server being temporarily offline. - self._timing.add("add", phase=phase) - self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) - - def _event_mailbox_used(self): - if self.DEBUG: print("_event_mailbox_used") - if self._flag_need_to_see_mailbox_used: - self._maybe_release_nameplate() - self._flag_need_to_see_mailbox_used = False - - def _API_derive_key(self, purpose, length): - if self._error: raise self._error - if self._key is None: - raise InternalError # call derive_key after get_verifier() or get() - if not isinstance(purpose, type("")): raise TypeError(type(purpose)) - return self._derive_key(to_bytes(purpose), length) - - def _derive_key(self, purpose, length=SecretBox.KEY_SIZE): - if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) - if self._key is None: - raise InternalError # call derive_key after get_verifier() or get() - return HKDF(self._key, length, CTXinfo=purpose) - - def _response_handle_message(self, msg): - side = msg["side"] - phase = msg["phase"] - assert isinstance(phase, type("")), type(phase) - body = hexstr_to_bytes(msg["body"]) - if side == self._side: - return - self._event_received_peer_message(side, phase, body) - - def _event_received_peer_message(self, side, phase, body): - # any message in the mailbox means we no longer need the nameplate - self._event_mailbox_used() - - if self._closing: - log.msg("received peer message while closing '%s'" % phase) - if phase in self._received_messages: - log.msg("ignoring duplicate peer message '%s'" % phase) - return - - if phase == "pake": - self._received_messages["pake"] = body - return self._event_received_pake(body) - if phase == "version": - self._received_messages["version"] = body - return self._event_received_version(side, body) - if re.search(r'^\d+$', phase): - return self._event_received_phase_message(side, phase, body) - # ignore unrecognized phases, for forwards-compatibility - log.msg("received unknown phase '%s'" % phase) - - def _event_received_phase_message(self, side, phase, body): - # It's a numbered phase message, aimed at the application above us. - # Decrypt and deliver upstairs, notifying anyone waiting on it - try: - data_key = self._derive_phase_key(side, phase) - plaintext = self._decrypt_data(data_key, body) - except CryptoError: - e = WrongPasswordError() - self._signal_error(e, "scary") # flunk all other API calls - # make tests fail, if they aren't explicitly catching it - if self.DEBUG: print("CryptoError in msg received") - log.err(e) - if self.DEBUG: print(" did log.err", e) - return # ignore this message - self._received_messages[phase] = plaintext - if phase in self._receive_waiters: - d = self._receive_waiters.pop(phase) - d.callback(plaintext) - - def _decrypt_data(self, key, encrypted): - assert isinstance(key, type(b"")), type(key) - assert isinstance(encrypted, type(b"")), type(encrypted) - assert len(key) == SecretBox.KEY_SIZE, len(key) - box = SecretBox(key) - data = box.decrypt(encrypted) - return data - - def _API_get(self): - if self._error: return defer.fail(self._error) - phase = "%d" % self._next_receive_phase - self._next_receive_phase += 1 - with self._timing.add("API get", phase=phase): - if phase in self._received_messages: - return defer.succeed(self._received_messages[phase]) - d = self._receive_waiters[phase] = defer.Deferred() - return d - - def _signal_error(self, error, mood): - if self.DEBUG: print("_signal_error", error, mood) - if self._error: - return - self._maybe_close(error, mood) - if self.DEBUG: print("_signal_error done") - - @inlineCallbacks - def _API_close(self, res, mood="happy"): - if self.DEBUG: print("close") - if self._close_called: raise InternalError - self._close_called = True - self._maybe_close(WormholeClosedError(), mood) - if self.DEBUG: print("waiting for disconnect") - yield self._disconnect_waiter - returnValue(res) - - def _maybe_close(self, error, mood): - if self._closing: - return - - # ordering constraints: - # * must wait for nameplate/mailbox acks before closing the websocket - # * must mark APIs for failure before errbacking Deferreds - # * since we give up control - # * must mark self._closing before errbacking Deferreds - # * since caller may call close() when we give up control - # * and close() will reenter _maybe_close - - self._error = error # causes new API calls to fail - - # since we're about to give up control by errbacking any API - # Deferreds, set self._closing, to make sure that a new call to - # close() isn't going to confuse anything - self._closing = True - - # now errback all API deferreds except close(): get_code, - # input_code, verify, get - if self._input_code_waiter and not self._input_code_waiter.called: - self._input_code_waiter.errback(error) - for d in self._connection_waiters: # input_code, get_code (early) - if self.DEBUG: print("EB cw") - d.errback(error) - if self._get_code: # get_code (late) - if self.DEBUG: print("EB gc") - self._get_code._allocated_d.errback(error) - if self._verifier_waiter and not self._verifier_waiter.called: - if self.DEBUG: print("EB VW") - self._verifier_waiter.errback(error) - if self._key_waiter and not self._key_waiter.called: - if self.DEBUG: print("EB KW") - self._key_waiter.errback(error) - for d in self._receive_waiters.values(): - if self.DEBUG: print("EB RW") - d.errback(error) - # Release nameplate and close mailbox, if either was claimed/open. - # Since _closing is True when both ACKs come back, the handlers will - # close the websocket. When *that* finishes, _disconnect_waiter() - # will fire. - self._maybe_release_nameplate() - self._maybe_close_mailbox(mood) - # In the off chance we got closed before we even claimed the - # nameplate, give _maybe_finished_closing a chance to run now. - self._maybe_finished_closing() - - def _maybe_release_nameplate(self): - if self.DEBUG: print("_maybe_release_nameplate", self._nameplate_state) - if self._nameplate_state == OPEN: - if self.DEBUG: print(" sending release") - self._ws_send_command("release") - self._nameplate_state = CLOSING - - def _response_handle_released(self, msg): - self._nameplate_state = CLOSED - self._maybe_finished_closing() - - def _maybe_close_mailbox(self, mood): - if self.DEBUG: print("_maybe_close_mailbox", self._mailbox_state) - if self._mailbox_state == OPEN: - if self.DEBUG: print(" sending close") - self._ws_send_command("close", mood=mood) - self._mailbox_state = CLOSING - - def _response_handle_closed(self, msg): - self._mailbox_state = CLOSED - self._maybe_finished_closing() - - def _maybe_finished_closing(self): - if self.DEBUG: print("_maybe_finished_closing", self._closing, self._nameplate_state, self._mailbox_state, self._connection_state) - if not self._closing: - return - if (self._nameplate_state == CLOSED - and self._mailbox_state == CLOSED - and self._connection_state == OPEN): - self._connection_state = CLOSING - self._drop_connection() - - def _drop_connection(self): - # separate method so it can be overridden by tests - self._ws.transport.loseConnection() # probably flushes output - # calls _ws_closed() when done - - def _ws_closed(self, wasClean, code, reason): - # For now (until we add reconnection), losing the websocket means - # losing everything. Make all API callers fail. Help someone waiting - # in close() to finish - self._connection_state = CLOSED - self._disconnect_waiter.callback(None) - self._maybe_finished_closing() - - # what needs to happen when _ws_closed() happens unexpectedly - # * errback all API deferreds - # * maybe: cause new API calls to fail - # * obviously can't release nameplate or close mailbox - # * can't re-close websocket - # * close(wait=True) callers should fire right away +from .journal import ImmediateJournal def wormhole(appid, relay_url, reactor, tor_manager=None, timing=None, stderr=sys.stderr): @@ -935,17 +28,10 @@ class _JournaledWormhole(service.MultiService): event_dispatcher_args=()): pass -class ImmediateJM(object): - def queue_outbound(self, fn, *args, **kwargs): - fn(*args, **kwargs) - @contextlib.contextmanager - def process(self): - yield - class _Wormhole(_JournaledWormhole): # send events to self, deliver them via Deferreds def __init__(self, reactor): - _JournaledWormhole.__init__(self, reactor, ImmediateJM(), self) + _JournaledWormhole.__init__(self, reactor, ImmediateJournal(), self) def wormhole(reactor): w = _Wormhole(reactor) @@ -956,5 +42,5 @@ def journaled_from_data(state, reactor, journal, event_handler, event_handler_args=()): pass -def journaled(reactor, journal, event_handler, event_handler_args()): +def journaled(reactor, journal, event_handler, event_handler_args=()): pass From 92f2b89d3e9da6b3b59203002b4ff9a74432b1fa Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 13:44:56 -0800 Subject: [PATCH 057/176] journal: fix syntax --- src/wormhole/journal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wormhole/journal.py b/src/wormhole/journal.py index 69f0bf5..7d640ea 100644 --- a/src/wormhole/journal.py +++ b/src/wormhole/journal.py @@ -11,7 +11,7 @@ class Journal(object): def queue_outbound(self, fn, *args, **kwargs): assert self._processing - self._outbound_queue.append((fn, args, kwargs) + self._outbound_queue.append((fn, args, kwargs)) @contextlib.contextmanager def process(self): From 20814a65f419a117e0b845e543044443c95c7a0f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 13:45:18 -0800 Subject: [PATCH 058/176] rename Wormhole (machine) to Boss, leave room for higher-level thing --- docs/{wormhole.dot => boss.dot} | 2 +- docs/code.dot | 2 +- docs/machines.dot | 34 ++++++++++++------------- docs/mailbox_close.dot | 4 ++- docs/receive.dot | 6 ++--- src/wormhole/{_wormhole.py => _boss.py} | 4 +-- src/wormhole/_interfaces.py | 2 +- src/wormhole/_key.py | 16 +++++++----- src/wormhole/_mailbox.py | 8 +++--- src/wormhole/_receive.py | 18 +++++++------ src/wormhole/_rendezvous.py | 8 +++--- src/wormhole/_send.py | 2 ++ 12 files changed, 57 insertions(+), 49 deletions(-) rename docs/{wormhole.dot => boss.dot} (97%) rename src/wormhole/{_wormhole.py => _boss.py} (98%) diff --git a/docs/wormhole.dot b/docs/boss.dot similarity index 97% rename from docs/wormhole.dot rename to docs/boss.dot index 2ce81a3..b2a40f5 100644 --- a/docs/wormhole.dot +++ b/docs/boss.dot @@ -8,7 +8,7 @@ digraph { compute the key, send VERSION. Starting from the Return, this saves two round trips. OTOH it adds consequences to hitting Tab. */ - start [label="Wormhole\nMachine" style="dotted"] + start [label="Boss\n(manager)" style="dotted"] {rank=same; P0_code S0} P0_code [shape="box" style="dashed" diff --git a/docs/code.dot b/docs/code.dot index c5ada30..5f63fc1 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -8,7 +8,7 @@ digraph { start -> S0 [style="invis"] S0 [label="S0:\nunknown"] S0 -> P0_set_code [label="set"] - P0_set_code [shape="box" label="W.set_code"] + P0_set_code [shape="box" label="B.set_code"] P0_set_code -> S4 S4 [label="S4: known" color="green"] diff --git a/docs/machines.dot b/docs/machines.dot index 7be9edc..9d22173 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -1,6 +1,6 @@ digraph { App [shape="box" color="blue" fontcolor="blue"] - Wormhole [shape="box" label="Wormhole\n(manager)" + Boss [shape="box" label="Boss\n(manager)" color="blue" fontcolor="blue"] Mailbox [shape="box" color="blue" fontcolor="blue"] Connection [label="Rendezvous\nConnector" @@ -18,33 +18,33 @@ digraph { Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] - App -> Wormhole [style="dashed" label="start\nset_code\nsend\nclose\n(once)"] - #App -> Wormhole [color="blue"] - Wormhole -> App [style="dashed" label="got_verifier\nreceived\nclosed\n(once)"] + App -> Boss [style="dashed" label="start\nset_code\nsend\nclose\n(once)"] + #App -> Boss [color="blue"] + Boss -> App [style="dashed" label="got_verifier\nreceived\nclosed\n(once)"] - #Wormhole -> Connection [color="blue"] - Wormhole -> Connection [style="dashed" label="start"] - Connection -> Wormhole [style="dashed" label="rx_welcome"] + #Boss -> Connection [color="blue"] + Boss -> Connection [style="dashed" label="start"] + Connection -> Boss [style="dashed" label="rx_welcome"] - Wormhole -> Send [style="dashed" label="send"] + Boss -> Send [style="dashed" label="send"] - Wormhole -> Mailbox [style="dashed" + Boss -> Mailbox [style="dashed" label="set_nameplate\nclose\n(once)" ] - #Wormhole -> Mailbox [color="blue"] - Mailbox -> Wormhole [style="dashed" label="closed\n(once)"] + #Boss -> Mailbox [color="blue"] + Mailbox -> Boss [style="dashed" label="closed\n(once)"] Mailbox -> Order [style="dashed" label="got_message (once)"] - Wormhole -> Key [style="dashed" label="set_code"] - Key -> Wormhole [style="dashed" label="got_verifier\nscared"] + Boss -> Key [style="dashed" label="set_code"] + Key -> Boss [style="dashed" label="got_verifier\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] - #Wormhole -> Key [color="blue"] + #Boss -> Key [color="blue"] Key -> Mailbox [style="dashed" label="add_message (pake)\nadd_message (version)"] Receive -> Send [style="dashed" label="got_verified_key"] Send -> Mailbox [style="dashed" label="add_message (phase)"] Key -> Receive [style="dashed" label="got_key"] - Receive -> Wormhole [style="dashed" + Receive -> Boss [style="dashed" label="happy\nscared\ngot_message"] Mailbox -> Connection [style="dashed" @@ -60,7 +60,7 @@ digraph { label="tx_list" ] - #Wormhole -> Code [color="blue"] + #Boss -> Code [color="blue"] Code -> Connection [style="dashed" label="tx_allocate" ] @@ -73,7 +73,7 @@ digraph { Code -> Nameplates [style="dashed" label="refresh_nameplates" ] - Code -> Wormhole [style="dashed" + Code -> Boss [style="dashed" label="set_code"] App -> Code [style="dashed" label="allocate\ninput\nset"] diff --git a/docs/mailbox_close.dot b/docs/mailbox_close.dot index 7e521a1..6765d84 100644 --- a/docs/mailbox_close.dot +++ b/docs/mailbox_close.dot @@ -36,8 +36,10 @@ digraph { P_stop [shape="box" label="C.stop"] P_stop -> SsB - SsB -> Ss [label="stopped"] SsB [label="SsB: closed\nstopping"] + SsB -> Pss [label="stopped"] + Pss [shape="box" label="B.closed"] + Pss -> Ss Ss [label="Ss: closed" color="green"] diff --git a/docs/receive.dot b/docs/receive.dot index 3761350..3fe136d 100644 --- a/docs/receive.dot +++ b/docs/receive.dot @@ -19,7 +19,7 @@ digraph { S1 [label="S1:\nunverified key" color="orange"] S1 -> P_mood_scary [label="got_message\n(bad)"] S1 -> P1_accept_msg [label="got_message\n(good)" color="orange"] - P1_accept_msg [shape="box" label="S.got_verified_key\nW.happy\nW.got_message" + P1_accept_msg [shape="box" label="S.got_verified_key\nB.happy\nB.got_message" color="orange"] P1_accept_msg -> S2 [color="orange"] @@ -28,10 +28,10 @@ digraph { S2 -> P2_accept_msg [label="got_message\n(good)" color="orange"] S2 -> P_mood_scary [label="got_message(bad)"] - P2_accept_msg [label="W.got_message" shape="box" color="orange"] + P2_accept_msg [label="B.got_message" shape="box" color="orange"] P2_accept_msg -> S2 [color="orange"] - P_mood_scary [shape="box" label="W.scared" color="red"] + P_mood_scary [shape="box" label="B.scared" color="red"] P_mood_scary -> S3 [color="red"] S3 [label="S3:\nscared" color="red"] diff --git a/src/wormhole/_wormhole.py b/src/wormhole/_boss.py similarity index 98% rename from src/wormhole/_wormhole.py rename to src/wormhole/_boss.py index 11a250f..db5ead0 100644 --- a/src/wormhole/_wormhole.py +++ b/src/wormhole/_boss.py @@ -11,8 +11,8 @@ from ._nameplate import NameplateListing from ._code import Code from .util import bytes_to_dict -@implementer(_interfaces.IWormhole) -class Wormhole: +@implementer(_interfaces.IBoss) +class Boss: m = MethodicalMachine() def __init__(self, side, reactor, timing): diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index 67bec5e..b8abf49 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -1,6 +1,6 @@ from zope.interface import Interface -class IWormhole(Interface): +class IBoss(Interface): pass class IMailbox(Interface): pass diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index aa0863c..760d679 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -53,8 +53,8 @@ class Key(object): m = MethodicalMachine() def __init__(self, timing): self._timing = timing - def wire(self, wormhole, mailbox, receive): - self._W = _interfaces.IWormhole(wormhole) + def wire(self, boss, mailbox, receive): + self._B = _interfaces.IBoss(boss) self._M = _interfaces.IMailbox(mailbox) self._R = _interfaces.IReceive(receive) @@ -67,6 +67,11 @@ class Key(object): @m.state(terminal=True) def S3_scared(self): pass + # from Boss + @m.input() + def set_code(self, code): pass + + # from Ordering def got_pake(self, body): assert isinstance(body, type(b"")), type(body) payload = bytes_to_dict(body) @@ -74,9 +79,6 @@ class Key(object): self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) else: self.got_pake_bad() - - @m.input() - def set_code(self, code): pass @m.input() def got_pake_good(self, msg2): pass @m.input() @@ -93,7 +95,7 @@ class Key(object): @m.output() def scared(self): - self._W.scared() + self._B.scared() @m.output() def compute_key(self, msg2): assert isinstance(msg2, type(b"")) @@ -101,7 +103,7 @@ class Key(object): key = self._sp.finish(msg2) self._my_versions = {} self._M.add_message("version", self._my_versions) - self._W.got_verifier(derive_key(key, b"wormhole:verifier")) + self._B.got_verifier(derive_key(key, b"wormhole:verifier")) self._R.got_key(key) S0_know_nothing.upon(set_code, enter=S1_know_code, outputs=[build_pake]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 51c58cb..93b5ba5 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -14,8 +14,8 @@ class Mailbox(object): self._pending_outbound = {} self._processed = set() - def wire(self, wormhole, rendezvous_connector, ordering): - self._W = _interfaces.IWormhole(wormhole) + def wire(self, boss, rendezvous_connector, ordering): + self._B = _interfaces.IBoss(boss) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._O = _interfaces.IOrder(ordering) @@ -89,7 +89,7 @@ class Mailbox(object): @m.input() def start_connected(self): pass - # from Wormhole + # from Boss @m.input() def set_nameplate(self, nameplate): pass @m.input() @@ -210,7 +210,7 @@ class Mailbox(object): self._RC_stop() @m.output() def W_closed(self): - self._W.closed() + self._B.closed() initial.upon(start_unconnected, enter=S0A, outputs=[]) initial.upon(start_connected, enter=S0B, outputs=[]) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index b15cbf2..5d9e803 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -10,8 +10,8 @@ class Receive(object): self._side = side self._timing = timing self._key = None - def wire(self, wormhole, key, send): - self._W = _interfaces.IWormhole(wormhole) + def wire(self, boss, key, send): + self._B = _interfaces.IBoss(boss) self._K = _interfaces.IKey(key) self._S = _interfaces.ISend(send) @@ -24,6 +24,7 @@ class Receive(object): @m.state(terminal=True) def S3_scared(self): pass + # from Ordering def got_message(self, phase, body): assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) @@ -35,14 +36,15 @@ class Receive(object): self.got_message_bad() return self.got_message_good(phase, plaintext) - - @m.input() - def got_key(self, key): pass @m.input() def got_message_good(self, phase, plaintext): pass @m.input() def got_message_bad(self): pass + # from Key + @m.input() + def got_key(self, key): pass + @m.output() def record_key(self, key): self._key = key @@ -52,15 +54,15 @@ class Receive(object): self._S.got_verified_key(self._key) @m.output() def W_happy(self, phase, plaintext): - self._W.happy() + self._B.happy() @m.output() def W_got_message(self, phase, plaintext): assert isinstance(phase, type("")), type(phase) assert isinstance(plaintext, type(b"")), type(plaintext) - self._W.got_message(phase, plaintext) + self._B.got_message(phase, plaintext) @m.output() def W_scared(self): - self._W.scared() + self._B.scared() S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key]) S1_unverified_key.upon(got_message_good, enter=S2_verified_key, diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 8eb52d4..fac6fe9 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -73,13 +73,13 @@ class RendezvousConnector(object): # TODO: Tor goes here return endpoints.HostnameEndpoint(self._reactor, hostname, port) - def wire(self, wormhole, mailbox, code, nameplate_lister): - self._W = _interfaces.IWormhole(wormhole) + def wire(self, boss, mailbox, code, nameplate_lister): + self._B = _interfaces.IBoss(boss) self._M = _interfaces.IMailbox(mailbox) self._C = _interfaces.ICode(code) self._NL = _interfaces.INameplateLister(nameplate_lister) - # from Wormhole + # from Boss def start(self): self._connector.startService() @@ -176,7 +176,7 @@ class RendezvousConnector(object): pass def _response_handle_welcome(self, msg): - self._W.rx_welcome(msg["welcome"]) + self._B.rx_welcome(msg["welcome"]) def _response_handle_claimed(self, msg): mailbox = msg["mailbox"] diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index b717d14..3d6b1e1 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -17,8 +17,10 @@ class Send(object): @m.state(terminal=True) def S1_verified_key(self): pass + # from Receive @m.input() def got_verified_key(self, key): pass + # from Boss @m.input() def send(self, phase, plaintext): pass From d4bedeafbfbd929e712d71d96fceb489b3bc13a0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 16:56:39 -0800 Subject: [PATCH 059/176] general fixes --- docs/boss.dot | 6 +++--- docs/code.dot | 10 +++++----- docs/key.dot | 2 +- docs/machines.dot | 16 ++++++++-------- docs/mailbox.dot | 27 +++++++++++++++++++++++++++ src/wormhole/_boss.py | 33 ++++++++++++++++++++++++++------- src/wormhole/_code.py | 35 +++++++++++++++++++++-------------- src/wormhole/_key.py | 11 +++++++---- src/wormhole/_mailbox.py | 7 +++++-- src/wormhole/_order.py | 10 +++++++--- src/wormhole/_receive.py | 11 ++++++++--- src/wormhole/_rendezvous.py | 10 +++++----- src/wormhole/wormhole.py | 2 +- 13 files changed, 124 insertions(+), 56 deletions(-) diff --git a/docs/boss.dot b/docs/boss.dot index b2a40f5..03062d3 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -12,12 +12,12 @@ digraph { {rank=same; P0_code S0} P0_code [shape="box" style="dashed" - label="Code.input\n or Code.allocate\n or Code.set"] + label="input -> Code.input\n or allocate -> Code.allocate\n or set_code -> Code.set_code"] P0_code -> S0 S0 [label="S0: empty"] S0 -> P0_build [label="set_code"] - P0_build [shape="box" label="M.set_nameplate\nK.set_code"] + P0_build [shape="box" label="W.got_code\nM.set_nameplate\nK.got_code"] P0_build -> S1 S1 [label="S1: lonely" color="orange"] @@ -52,7 +52,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="send -> S.send\ngot_verifier -> A.got_verifier" + label="send -> S.send\ngot_verifier -> A.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/docs/code.dot b/docs/code.dot index 5f63fc1..1a67209 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -7,9 +7,9 @@ digraph { {rank=same; S3 P1_generate} start -> S0 [style="invis"] S0 [label="S0:\nunknown"] - S0 -> P0_set_code [label="set"] - P0_set_code [shape="box" label="B.set_code"] - P0_set_code -> S4 + S0 -> P0_got_code [label="set"] + P0_got_code [shape="box" label="B.got_code"] + P0_got_code -> S4 S4 [label="S4: known" color="green"] S0 -> P0_list_nameplates [label="input"] @@ -32,7 +32,7 @@ digraph { P3_completion [shape="box" label="do completion"] P3_completion -> S3 - S3 -> P0_set_code [label="" + S3 -> P0_got_code [label="" color="orange" fontcolor="orange"] S0 -> P0_allocate [label="allocate"] @@ -41,6 +41,6 @@ digraph { S1 [label="S1:\nallocating"] S1 -> P1_generate [label="rx_allocated"] P1_generate [shape="box" label="generate\nrandom code"] - P1_generate -> P0_set_code + P1_generate -> P0_got_code } diff --git a/docs/key.dot b/docs/key.dot index de75c2b..5e4e23c 100644 --- a/docs/key.dot +++ b/docs/key.dot @@ -11,7 +11,7 @@ digraph { start [label="Key\nMachine" style="dotted"] S0 [label="S0: know\nnothing"] - S0 -> P0_build [label="set_code"] + S0 -> P0_build [label="got_code"] P0_build [shape="box" label="build_pake\nM.add_message(pake)"] P0_build -> S1 diff --git a/docs/machines.dot b/docs/machines.dot index 9d22173..ee5a7a3 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -1,5 +1,5 @@ digraph { - App [shape="box" color="blue" fontcolor="blue"] + Wormhole [shape="oval" color="blue" fontcolor="blue"] Boss [shape="box" label="Boss\n(manager)" color="blue" fontcolor="blue"] Mailbox [shape="box" color="blue" fontcolor="blue"] @@ -18,9 +18,9 @@ digraph { Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] - App -> Boss [style="dashed" label="start\nset_code\nsend\nclose\n(once)"] - #App -> Boss [color="blue"] - Boss -> App [style="dashed" label="got_verifier\nreceived\nclosed\n(once)"] + Wormhole -> Boss [style="dashed" label="allocate\ninput\nset_code\nsend\nclose\n(once)"] + #Wormhole -> Boss [color="blue"] + Boss -> Wormhole [style="dashed" label="got_code\ngot_verifier\nreceived\nclosed\n(once)"] #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] @@ -34,7 +34,7 @@ digraph { #Boss -> Mailbox [color="blue"] Mailbox -> Boss [style="dashed" label="closed\n(once)"] Mailbox -> Order [style="dashed" label="got_message (once)"] - Boss -> Key [style="dashed" label="set_code"] + Boss -> Key [style="dashed" label="got_code"] Key -> Boss [style="dashed" label="got_verifier\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] @@ -73,10 +73,10 @@ digraph { Code -> Nameplates [style="dashed" label="refresh_nameplates" ] + Boss -> Code [style="dashed" + label="allocate\ninput\nset_code"] Code -> Boss [style="dashed" - label="set_code"] - App -> Code [style="dashed" - label="allocate\ninput\nset"] + label="got_code"] } diff --git a/docs/mailbox.dot b/docs/mailbox.dot index 2f64062..1353a14 100644 --- a/docs/mailbox.dot +++ b/docs/mailbox.dot @@ -156,3 +156,30 @@ digraph { P5_close [shape="box" label="tx_close" style="dashed" color="orange"] } + +/* + +Can this be split into one machine for the Nameplate, and a second for the +Mailbox? + +Nameplate: + +* 0: know nothing (connected, not connected) +* 1: know nameplate, never claimed, need to claim +* 2: maybe claimed, need to claim +* 3: definitely claimed, need to claim +* 4: definitely claimed, need to release +* 5: maybe released +* 6: definitely released + +Mailbox: +* 0: unknown +* 1: know mailbox, need open, not open +* 2: know mailbox, need open, maybe open +* 3: definitely open, need open +* 4: need closed, maybe open +* 5: need closed, maybe closed ? +* 6: definitely closed + + +*/ \ No newline at end of file diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index db5ead0..bb15e82 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -53,15 +53,34 @@ class Boss: @m.state(terminal=True) def S4_closed(self): pass - # from the Application, or some sort of top-level shim + # from the Wormhole + + # input/allocate/set_code are regular methods, not state-transition + # inputs. We expect them to be called just after initialization, while + # we're in the S0_empty state. You must call exactly one of them, and the + # call must happen while we're in S0_empty, which makes them good + # candiates for being a proper @m.input, but set_code() will immediately + # (reentrantly) cause self.got_code() to be fired, which is messy. These + # are all passthroughs to the Code machine, so one alternative would be + # to have Wormhole call Code.{input,allocate,set_code} instead, but that + # would require the Wormhole to be aware of Code (whereas right now + # Wormhole only knows about this Boss instance, and everything else is + # hidden away). + def input(self, stdio): + self._C.input(stdio) + def allocate(self, code_length): + self._C.allocate(code_length) + def set_code(self, code): + self._C.set_code(code) + @m.input() def send(self, phase, plaintext): pass @m.input() def close(self): pass - # from Code (which may be provoked by the Application) + # from Code (provoked by input/allocate/set_code) @m.input() - def set_code(self, code): pass + def got_code(self, code): pass # Key sends (got_verifier, scared) # Receive sends (got_message, happy, scared) @@ -77,7 +96,7 @@ class Boss: else: self.got_phase(phase, plaintext) @m.input() - def got_version(self, version): pass + def got_version(self, plaintext): pass @m.input() def got_phase(self, phase, plaintext): pass @m.input() @@ -89,10 +108,10 @@ class Boss: @m.output() - def got_code(self, code): + def do_got_code(self, code): nameplate = code.split("-")[0] self._M.set_nameplate(nameplate) - self._K.set_code(code) + self._K.got_code(code) @m.output() def process_version(self, plaintext): self._their_versions = bytes_to_dict(plaintext) @@ -125,7 +144,7 @@ class Boss: self._A.closed(result) S0_empty.upon(send, enter=S0_empty, outputs=[S_send]) - S0_empty.upon(set_code, enter=S1_lonely, outputs=[got_code]) + S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code]) S1_lonely.upon(happy, enter=S2_happy, outputs=[]) S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 2cc80e0..efad69d 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -1,5 +1,7 @@ import os from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides from automat import MethodicalMachine from . import _interfaces from .wordlist import (byte_to_even_word, byte_to_odd_word, @@ -17,12 +19,12 @@ def make_code(nameplate, code_length): words.append(byte_to_even_word[os.urandom(1)].lower()) return "%s-%s" % (nameplate, "-".join(words)) +@attrs @implementer(_interfaces.ICode) class Code(object): + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, code_length, timing): - self._code_length = code_length - self._timing = timing + def wire(self, wormhole, rendezvous_connector, nameplate_lister): self._W = _interfaces.IWormhole(wormhole) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) @@ -41,9 +43,9 @@ class Code(object): # from App @m.input() - def allocate(self): pass + def allocate(self, code_length): pass @m.input() - def input(self): pass + def input(self, stdio): pass @m.input() def set(self, code): pass @@ -67,7 +69,12 @@ class Code(object): def NL_refresh_nameplates(self): self._NL.refresh_nameplates() @m.output() - def RC_tx_allocate(self): + def start_input_and_NL_refresh_nameplates(self, stdio): + self._stdio = stdio + self._NL.refresh_nameplates() + @m.output() + def RC_tx_allocate(self, code_length): + self._code_length = code_length self._RC.tx_allocate() @m.output() def do_completion_nameplates(self): @@ -85,23 +92,23 @@ class Code(object): @m.output() def generate_and_set(self, nameplate): self._code = make_code(nameplate, self._code_length) - self._W_set_code() + self._W_got_code() @m.output() - def W_set_code(self, code): + def W_got_code(self, code): self._code = code - self._W_set_code() + self._W_got_code() - def _W_set_code(self): - self._W.set_code(self._code) + def _W_got_code(self): + self._W.got_code(self._code) S0_unknown.upon(allocate, enter=S1_allocating, outputs=[RC_tx_allocate]) S1_allocating.upon(rx_allocated, enter=S4_known, outputs=[generate_and_set]) - S0_unknown.upon(set, enter=S4_known, outputs=[W_set_code]) + S0_unknown.upon(set, enter=S4_known, outputs=[W_got_code]) S0_unknown.upon(input, enter=S2_typing_nameplate, - outputs=[NL_refresh_nameplates]) + outputs=[start_input_and_NL_refresh_nameplates]) S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate, outputs=[do_completion_nameplates]) S2_typing_nameplate.upon(got_nameplates, enter=S2_typing_nameplate, @@ -109,4 +116,4 @@ class Code(object): S2_typing_nameplate.upon(hyphen, enter=S3_typing_code, outputs=[lookup_wordlist]) S3_typing_code.upon(tab, enter=S3_typing_code, outputs=[do_completion_code]) - S3_typing_code.upon(RETURN, enter=S4_known, outputs=[W_set_code]) + S3_typing_code.upon(RETURN, enter=S4_known, outputs=[W_got_code]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 760d679..edbcda7 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -1,5 +1,7 @@ from hashlib import sha256 from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides from spake2 import SPAKE2_Symmetric from hkdf import Hkdf from nacl.secret import SecretBox @@ -48,11 +50,12 @@ def encrypt_data(key, plaintext): nonce = utils.random(SecretBox.NONCE_SIZE) return box.encrypt(plaintext, nonce) +@attrs @implementer(_interfaces.IKey) class Key(object): + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, timing): - self._timing = timing + def wire(self, boss, mailbox, receive): self._B = _interfaces.IBoss(boss) self._M = _interfaces.IMailbox(mailbox) @@ -69,7 +72,7 @@ class Key(object): # from Boss @m.input() - def set_code(self, code): pass + def got_code(self, code): pass # from Ordering def got_pake(self, body): @@ -106,6 +109,6 @@ class Key(object): self._B.got_verifier(derive_key(key, b"wormhole:verifier")) self._R.got_key(key) - S0_know_nothing.upon(set_code, enter=S1_know_code, outputs=[build_pake]) + S0_know_nothing.upon(got_code, enter=S1_know_code, outputs=[build_pake]) S1_know_code.upon(got_pake_good, enter=S2_know_key, outputs=[compute_key]) S1_know_code.upon(got_pake_bad, enter=S3_scared, outputs=[scared]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 93b5ba5..5469806 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -1,13 +1,16 @@ from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import instance_of from automat import MethodicalMachine from . import _interfaces +@attrs @implementer(_interfaces.IMailbox) class Mailbox(object): + _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() - def __init__(self, side): - self._side = side + def __init__(self): self._mood = None self._nameplate = None self._mailbox = None diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index d803fca..73934e8 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -1,13 +1,17 @@ from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides, instance_of from automat import MethodicalMachine from . import _interfaces +@attrs @implementer(_interfaces.IOrder) class Order(object): + _side = attrib(validator=instance_of(type(u""))) + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, side, timing): - self._side = side - self._timing = timing + + def __init__(self): self._key = None self._queue = [] def wire(self, key, receive): diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 5d9e803..8fec9a8 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -1,15 +1,20 @@ from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides, instance_of from automat import MethodicalMachine from . import _interfaces from ._key import derive_phase_key, decrypt_data, CryptoError +@attrs @implementer(_interfaces.IReceive) class Receive(object): + _side = attrib(validator=instance_of(type(u""))) + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, side, timing): - self._side = side - self._timing = timing + + def __init__(self): self._key = None + def wire(self, boss, key, send): self._B = _interfaces.IBoss(boss) self._K = _interfaces.IKey(key) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index fac6fe9..52a32fe 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -54,12 +54,12 @@ class WSFactory(websocket.WebSocketClientFactory): @attrs @implementer(_interfaces.IRendezvousConnector) class RendezvousConnector(object): - _url = attrib(instance_of(type(u""))) - _appid = attrib(instance_of(type(u""))) - _side = attrib(instance_of(type(u""))) + _url = attrib(validator=instance_of(type(u""))) + _appid = attrib(validator=instance_of(type(u""))) + _side = attrib(validator=instance_of(type(u""))) _reactor = attrib() - _journal = attrib(provides(_interfaces.IJournal)) - _timing = attrib(provides(_interfaces.ITiming)) + _journal = attrib(validator=provides(_interfaces.IJournal)) + _timing = attrib(validator=provides(_interfaces.ITiming)) def __init__(self): self._ws = None diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 863d638..ec6b8e1 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -23,7 +23,7 @@ def wormhole(appid, relay_url, reactor, tor_manager=None, timing=None, # * if not: # * -class _JournaledWormhole(service.MultiService): +class _JournaledWormhole(object): def __init__(self, reactor, journal_manager, event_dispatcher, event_dispatcher_args=()): pass From 825370fdd2b2a3edef22afc3da13b1c062057301 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 17:02:01 -0800 Subject: [PATCH 060/176] cleanups, remove misc.py --- src/wormhole/misc.py | 489 --------------------------------------- src/wormhole/wormhole.py | 4 +- 2 files changed, 3 insertions(+), 490 deletions(-) delete mode 100644 src/wormhole/misc.py diff --git a/src/wormhole/misc.py b/src/wormhole/misc.py deleted file mode 100644 index 808b70f..0000000 --- a/src/wormhole/misc.py +++ /dev/null @@ -1,489 +0,0 @@ - -from six.moves.urllib_parse import urlparse -from attr import attrs, attrib -from twisted.internet import defer, endpoints #, error -from autobahn.twisted import websocket -from automat import MethodicalMachine - -class WSClient(websocket.WebSocketClientProtocol): - def onConnect(self, response): - # this fires during WebSocket negotiation, and isn't very useful - # unless you want to modify the protocol settings - print("onConnect", response) - #self.connection_machine.onConnect(self) - - def onOpen(self, *args): - # this fires when the WebSocket is ready to go. No arguments - print("onOpen", args) - #self.wormhole_open = True - self.connection_machine.protocol_onOpen(self) - #self.factory.d.callback(self) - - def onMessage(self, payload, isBinary): - print("onMessage") - return - assert not isBinary - self.wormhole._ws_dispatch_response(payload) - - def onClose(self, wasClean, code, reason): - print("onClose") - self.connection_machine.protocol_onClose(wasClean, code, reason) - #if self.wormhole_open: - # self.wormhole._ws_closed(wasClean, code, reason) - #else: - # # we closed before establishing a connection (onConnect) or - # # finishing WebSocket negotiation (onOpen): errback - # self.factory.d.errback(error.ConnectError(reason)) - -class WSFactory(websocket.WebSocketClientFactory): - protocol = WSClient - def buildProtocol(self, addr): - proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) - proto.connection_machine = self.connection_machine - #proto.wormhole_open = False - return proto - -# pip install (path to automat checkout)[visualize] -# automat-visualize wormhole._connection - -@attrs -class _WSRelayClient_Machine(object): - _c = attrib() - m = MethodicalMachine() - - @m.state(initial=True) - def initial(self): pass - @m.state() - def connecting(self): pass - @m.state() - def negotiating(self): pass - @m.state(terminal=True) - def failed(self): pass - @m.state() - def open(self): pass - @m.state() - def waiting(self): pass - @m.state() - def reconnecting(self): pass - @m.state() - def disconnecting(self): pass - @m.state() - def cancelling(self): pass - @m.state(terminal=True) - def closed(self): pass - - @m.input() - def start(self): pass ; print("input:start") - @m.input() - def d_callback(self): pass ; print("input:d_callback") - @m.input() - def d_errback(self): pass ; print("input:d_errback") - @m.input() - def d_cancel(self): pass ; print("input:d_cancel") - @m.input() - def onOpen(self): pass ; print("input:onOpen") - @m.input() - def onClose(self): pass ; print("input:onClose") - @m.input() - def expire(self): pass - @m.input() - def stop(self): pass - - # outputs - @m.output() - def ep_connect(self): - "ep.connect()" - self._c.ep_connect() - @m.output() - def reset_timer(self): - self._c.reset_timer() - @m.output() - def connection_established(self): - print("connection_established") - self._c.connection_established() - @m.output() - def M_lost(self): - self._c.M_lost() - @m.output() - def start_timer(self): - self._c.start_timer() - @m.output() - def cancel_timer(self): - self._c.cancel_timer() - @m.output() - def dropConnection(self): - self._c.dropConnection() - @m.output() - def notify_fail(self): - self._c.notify_fail() - @m.output() - def MC_stopped(self): - self._c.MC_stopped() - - initial.upon(start, enter=connecting, outputs=[ep_connect]) - connecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) - connecting.upon(d_errback, enter=failed, outputs=[notify_fail]) - connecting.upon(onClose, enter=failed, outputs=[notify_fail]) - connecting.upon(stop, enter=cancelling, outputs=[d_cancel]) - cancelling.upon(d_errback, enter=closed, outputs=[]) - - negotiating.upon(onOpen, enter=open, outputs=[connection_established]) - negotiating.upon(stop, enter=disconnecting, outputs=[dropConnection]) - negotiating.upon(onClose, enter=failed, outputs=[notify_fail]) - - open.upon(onClose, enter=waiting, outputs=[M_lost, start_timer]) - open.upon(stop, enter=disconnecting, outputs=[dropConnection, M_lost]) - reconnecting.upon(d_callback, enter=negotiating, outputs=[reset_timer]) - reconnecting.upon(d_errback, enter=waiting, outputs=[start_timer]) - reconnecting.upon(onClose, enter=waiting, outputs=[start_timer]) - reconnecting.upon(stop, enter=cancelling, outputs=[d_cancel]) - - waiting.upon(expire, enter=reconnecting, outputs=[ep_connect]) - waiting.upon(stop, enter=closed, outputs=[cancel_timer]) - disconnecting.upon(onClose, enter=closed, outputs=[MC_stopped]) - -# We have one WSRelayClient for each wsurl we know about, and it lasts -# as long as its parent Wormhole does. - -@attrs -class WSRelayClient(object): - _wormhole = attrib() - _mailbox = attrib() - _ws_url = attrib() - _reactor = attrib() - INITIAL_DELAY = 1.0 - - - def __init__(self): - self._m = _WSRelayClient_Machine(self) - self._f = f = WSFactory(self._ws_url) - f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - f.connection_machine = self # calls onOpen and onClose - p = urlparse(self._ws_url) - self._ep = self._make_endpoint(p.hostname, p.port or 80) - self._connector = None - self._done_d = defer.Deferred() - self._current_delay = self.INITIAL_DELAY - - def _make_endpoint(self, hostname, port): - return endpoints.HostnameEndpoint(self._reactor, hostname, port) - - # inputs from elsewhere - def d_callback(self, p): - self._p = p - self._m.d_callback() - def d_errback(self, f): - self._f = f - self._m.d_errback() - def protocol_onOpen(self, p): - self._m.onOpen() - def protocol_onClose(self, wasClean, code, reason): - self._m.onClose() - def C_stop(self): - self._m.stop() - def timer_expired(self): - self._m.expire() - - # outputs driven by the state machine - def ep_connect(self): - print("ep_connect()") - self._d = self._ep.connect(self._f) - self._d.addCallbacks(self.d_callback, self.d_errback) - def connection_established(self): - self._connection = WSConnection(ws, self._wormhole.appid, - self._wormhole.side, self) - self._mailbox.connected(ws) - self._wormhole.add_connection(self._connection) - self._ws_send_command("bind", appid=self._appid, side=self._side) - def M_lost(self): - self._wormhole.M_lost(self._connection) - self._connection = None - def start_timer(self): - print("start_timer") - self._t = self._reactor.callLater(3.0, self.expire) - def cancel_timer(self): - print("cancel_timer") - self._t.cancel() - self._t = None - def dropConnection(self): - print("dropConnection") - self._ws.dropConnection() - def notify_fail(self): - print("notify_fail", self._f.value if self._f else None) - self._done_d.errback(self._f) - def MC_stopped(self): - pass - - -def tryit(reactor): - cm = WSRelayClient(None, "ws://127.0.0.1:4000/v1", reactor) - print("_ConnectionMachine created") - print("start:", cm.start()) - print("waiting on _done_d to finish") - return cm._done_d - -# http://autobahn-python.readthedocs.io/en/latest/websocket/programming.html -# observed sequence of events: -# success: d_callback, onConnect(response), onOpen(), onMessage() -# negotifail (non-websocket): d_callback, onClose() -# noconnect: d_errback - -def tryws(reactor): - ws_url = "ws://127.0.0.1:40001/v1" - f = WSFactory(ws_url) - p = urlparse(ws_url) - ep = endpoints.HostnameEndpoint(reactor, p.hostname, p.port or 80) - d = ep.connect(f) - def _good(p): print("_good", p) - def _bad(f): print("_bad", f) - d.addCallbacks(_good, _bad) - return defer.Deferred() - -if __name__ == "__main__": - import sys - from twisted.python import log - log.startLogging(sys.stdout) - from twisted.internet.task import react - react(tryit) - -# ??? a new WSConnection is created each time the WSRelayClient gets through -# negotiation - -class NameplateListingMachine(object): - m = MethodicalMachine() - def __init__(self): - self._list_nameplate_waiters = [] - - # Ideally, each API request would spawn a new "list_nameplates" message - # to the server, so the response would be maximally fresh, but that would - # require correlating server request+response messages, and the protocol - # is intended to be less stateful than that. So we offer a weaker - # freshness property: if no server requests are in flight, then a new API - # request will provoke a new server request, and the result will be - # fresh. But if a server request is already in flight when a second API - # request arrives, both requests will be satisfied by the same response. - - @m.state(initial=True) - def idle(self): pass - @m.state() - def requesting(self): pass - - @m.input() - def list_nameplates(self): pass # returns Deferred - @m.input() - def response(self, message): pass - - @m.output() - def add_deferred(self): - d = defer.Deferred() - self._list_nameplate_waiters.append(d) - return d - @m.output() - def send_request(self): - self._connection.send_command("list") - @m.output() - def distribute_response(self, message): - nameplates = parse(message) - waiters = self._list_nameplate_waiters - self._list_nameplate_waiters = [] - for d in waiters: - d.callback(nameplates) - - idle.upon(list_nameplates, enter=requesting, - outputs=[add_deferred, send_request], - collector=lambda outs: outs[0]) - idle.upon(response, enter=idle, outputs=[]) - requesting.upon(list_nameplates, enter=requesting, - outputs=[add_deferred], - collector=lambda outs: outs[0]) - requesting.upon(response, enter=idle, outputs=[distribute_response]) - - # nlm._connection = c = Connection(ws) - # nlm.list_nameplates().addCallback(display_completions) - # c.register_dispatch("nameplates", nlm.response) - -class Wormhole: - m = MethodicalMachine() - - def __init__(self, ws_url, reactor): - self._relay_client = WSRelayClient(self, ws_url, reactor) - # This records all the messages we want the relay to have. Each time - # we establish a connection, we'll send them all (and the relay - # server will filter out duplicates). If we add any while a - # connection is established, we'll send the new ones. - self._outbound_messages = [] - - # these methods are called from outside - def start(self): - self._relay_client.start() - - # and these are the state-machine transition functions, which don't take - # args - @m.state() - def closed(initial=True): pass - @m.state() - def know_code_not_mailbox(): pass - @m.state() - def know_code_and_mailbox(): pass # no longer need nameplate - @m.state() - def waiting_first_msg(): pass # key is established, want any message - @m.state() - def processing_version(): pass - @m.state() - def processing_phase(): pass - @m.state() - def open(): pass # key is verified, can post app messages - @m.state(terminal=True) - def failed(): pass - - @m.input() - def deliver_message(self, message): pass - - def w_set_seed(self, code, mailbox): - """Call w_set_seed when we sprout a Wormhole Seed, which - contains both the code and the mailbox""" - self.w_set_code(code) - self.w_set_mailbox(mailbox) - - @m.input() - def w_set_code(self, code): - """Call w_set_code when you learn the code, probably because the user - typed it in.""" - @m.input() - def w_set_mailbox(self, mailbox): - """Call w_set_mailbox() when you learn the mailbox id, from the - response to claim_nameplate""" - pass - - - @m.input() - def rx_pake(self, pake): pass # reponse["message"][phase=pake] - - @m.input() - def rx_version(self, version): # response["message"][phase=version] - pass - @m.input() - def verify_good(self, verifier): pass - @m.input() - def verify_bad(self, f): pass - - @m.input() - def rx_phase(self, message): pass - @m.input() - def phase_good(self, message): pass - @m.input() - def phase_bad(self, f): pass - - @m.output() - def compute_and_post_pake(self, code): - self._code = code - self._pake = compute(code) - self._post(pake=self._pake) - self._ws_send_command("add", phase="pake", body=XXX(pake)) - @m.output() - def set_mailbox(self, mailbox): - self._mailbox = mailbox - @m.output() - def set_seed(self, code, mailbox): - self._code = code - self._mailbox = mailbox - - @m.output() - def process_version(self, version): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) - pass - - @m.output() - def notify_verified(self, verifier): - for d in self._verify_waiters: - d.callback(verifier) - @m.output() - def notify_failed(self, f): - for d in self._verify_waiters: - d.errback(f) - - @m.output() - def process_phase(self, message): # response["message"][phase=version] - their_verifier = com - if OK: - self.verify_good(verifier) - else: - self.verify_bad(f) - pass - - @m.output() - def post_inbound(self, message): - pass - - @m.output() - def deliver_message(self, message): - self._qc.deliver_message(message) - - @m.output() - def compute_key_and_post_version(self, pake): - self._key = x - self._verifier = x - plaintext = dict_to_bytes(self._my_versions) - phase = "version" - data_key = self._derive_phase_key(self._side, phase) - encrypted = self._encrypt_data(data_key, plaintext) - self._msg_send(phase, encrypted) - - closed.upon(w_set_code, enter=know_code_not_mailbox, - outputs=[compute_and_post_pake]) - know_code_not_mailbox.upon(w_set_mailbox, enter=know_code_and_mailbox, - outputs=[set_mailbox]) - know_code_and_mailbox.upon(rx_pake, enter=waiting_first_msg, - outputs=[compute_key_and_post_version]) - waiting_first_msg.upon(rx_version, enter=processing_version, - outputs=[process_version]) - processing_version.upon(verify_good, enter=open, outputs=[notify_verified]) - processing_version.upon(verify_bad, enter=failed, outputs=[notify_failed]) - open.upon(rx_phase, enter=processing_phase, outputs=[process_phase]) - processing_phase.upon(phase_good, enter=open, outputs=[post_inbound]) - processing_phase.upon(phase_bad, enter=failed, outputs=[notify_failed]) - -class QueueConnect: - m = MethodicalMachine() - def __init__(self): - self._outbound_messages = [] - self._connection = None - @m.state() - def disconnected(): pass - @m.state() - def connected(): pass - - @m.input() - def deliver_message(self, message): pass - @m.input() - def connect(self, connection): pass - @m.input() - def disconnect(self): pass - - @m.output() - def remember_connection(self, connection): - self._connection = connection - @m.output() - def forget_connection(self): - self._connection = None - @m.output() - def queue_message(self, message): - self._outbound_messages.append(message) - @m.output() - def send_message(self, message): - self._connection.send(message) - @m.output() - def send_queued_messages(self, connection): - for m in self._outbound_messages: - connection.send(m) - - disconnected.upon(deliver_message, enter=disconnected, outputs=[queue_message]) - disconnected.upon(connect, enter=connected, outputs=[remember_connection, - send_queued_messages]) - connected.upon(deliver_message, enter=connected, - outputs=[queue_message, send_message]) - connected.upon(disconnect, enter=disconnected, outputs=[forget_connection]) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index ec6b8e1..15023ba 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -1,4 +1,6 @@ from __future__ import print_function, absolute_import, unicode_literals +import sys +from .timing import DebugTiming from .journal import ImmediateJournal def wormhole(appid, relay_url, reactor, tor_manager=None, timing=None, @@ -33,7 +35,7 @@ class _Wormhole(_JournaledWormhole): def __init__(self, reactor): _JournaledWormhole.__init__(self, reactor, ImmediateJournal(), self) -def wormhole(reactor): +def wormhole2(reactor): w = _Wormhole(reactor) w.startService() return w From 88775d7f50b9fd824b94e2b81f8f5e7dee005e8b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 18:06:28 -0800 Subject: [PATCH 061/176] states.py: remove old file --- src/wormhole/states.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/wormhole/states.py diff --git a/src/wormhole/states.py b/src/wormhole/states.py deleted file mode 100644 index 8ad8089..0000000 --- a/src/wormhole/states.py +++ /dev/null @@ -1,11 +0,0 @@ - -from automat import MethodicalMachine - -class WormholeState(object): - _machine = MethodicalMachine() - - @_machine.state(initial=True) - def start(self): - pass - - From 7e7b43e910fa7943cee8541c9654df5b0cdca6a5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Feb 2017 18:21:47 -0800 Subject: [PATCH 062/176] start on top-level driver, wormhole.py --- src/wormhole/_boss.py | 45 +++++++++++++++------------ src/wormhole/wormhole.py | 66 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 21 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index bb15e82..15a86c5 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -11,21 +11,27 @@ from ._nameplate import NameplateListing from ._code import Code from .util import bytes_to_dict +@attrs @implementer(_interfaces.IBoss) class Boss: + _side = attrib(validator=instance_of(type(u""))) + _url = attrib(validator=instance_of(type(u""))) + _appid = attrib(validator=instance_of(type(u""))) + _reactor = attrib() + _journal = attrib(validator=provides(_interfaces.IJournal)) + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, side, reactor, timing): - self._reactor = reactor - - self._M = Mailbox(side) - self._S = Send(side, timing) - self._O = Order(side, timing) - self._K = Key(timing) - self._R = Receive(side, timing) - self._RC = RendezvousConnector(side, timing, reactor) + def __init__(self, wormhole): + self._W = wormhole + self._M = Mailbox(self._side) + self._S = Send(self._side, self._timing) + self._O = Order(self._side, self._timing) + self._K = Key(self._timing) + self._R = Receive(self._side, self._timing) + self._RC = RendezvousConnector(self._side, self._timing, self._reactor) self._NL = NameplateListing() - self._C = Code(timing) + self._C = Code(self._timing) self._M.wire(self, self._RC, self._O) self._S.wire(self._M) @@ -112,6 +118,7 @@ class Boss: nameplate = code.split("-")[0] self._M.set_nameplate(nameplate) self._K.got_code(code) + self._W.got_code(code) @m.output() def process_version(self, plaintext): self._their_versions = bytes_to_dict(plaintext) @@ -132,16 +139,16 @@ class Boss: self._M.close("happy") @m.output() - def A_received(self, phase, plaintext): - self._A.received(phase, plaintext) + def W_got_verifier(self, verifier): + self._W.got_verifier(verifier) @m.output() - def A_got_verifier(self, verifier): - self._A.got_verifier(verifier) + def W_received(self, phase, plaintext): + self._W.received(phase, plaintext) @m.output() - def A_closed(self): + def W_closed(self): result = "???" - self._A.closed(result) + self._W.closed(result) S0_empty.upon(send, enter=S0_empty, outputs=[S_send]) S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code]) @@ -149,8 +156,8 @@ class Boss: S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) - S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[A_got_verifier]) - S2_happy.upon(got_phase, enter=S2_happy, outputs=[A_received]) + S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) + 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(scared, enter=S3_closing, outputs=[close_scared]) S2_happy.upon(close, enter=S3_closing, outputs=[close_happy]) @@ -162,7 +169,7 @@ class Boss: S3_closing.upon(scared, enter=S3_closing, outputs=[]) S3_closing.upon(close, enter=S3_closing, outputs=[]) S3_closing.upon(send, enter=S3_closing, outputs=[]) - S3_closing.upon(closed, enter=S4_closed, outputs=[A_closed]) + S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed]) S4_closed.upon(got_phase, enter=S4_closed, outputs=[]) S4_closed.upon(got_version, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 15023ba..23a6c6a 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -2,10 +2,72 @@ from __future__ import print_function, absolute_import, unicode_literals import sys from .timing import DebugTiming from .journal import ImmediateJournal +from ._boss import Boss -def wormhole(appid, relay_url, reactor, tor_manager=None, timing=None, - stderr=sys.stderr): +class _Wormhole(object): + def __init__(self): + self._code = None + self._code_observers = [] + self._verifier = None + self._verifier_observers = [] + + def _set_boss(self, boss): + self._boss = boss + + # from above + + def when_code(self): + if self._code: + return defer.succeed(self._code) + d = defer.Deferred() + self._code_observers.append(d) + return d + + def when_verifier(self): + if self._verifier: + return defer.succeed(self._verifier) + d = defer.Deferred() + self._verifier_observers.append(d) + return d + + def send(self, phase, plaintext): + self._boss.send(phase, plaintext) + def close(self): + self._boss.close() + + # from below + def got_code(self, code): + self._code = code + for d in self._code_observers: + d.callback(code) + self._code_observers[:] = [] + def got_verifier(self, verifier): + self._verifier = verifier + for d in self._verifier_observers: + d.callback(verifier) + self._verifier_observers[:] = [] + + def received(self, phase, plaintext): + print(phase, plaintext) + + def closed(self, result): + print("closed", result) + +def wormhole(appid, relay_url, reactor, + tor_manager=None, timing=None, + journal=None, + stderr=sys.stderr, + ): timing = timing or DebugTiming() + code_length = 2 + side = bytes_to_hexstr(os.urandom(5)) + journal = journal or ImmediateJournal() + w = _Wormhole() + b = Boss(w, side, relay_url, appid, reactor, journal, timing) + w._set_boss(b) + # force allocate for now + b.start() + b.allocate(code_length) w = _Wormhole(appid, relay_url, reactor, tor_manager, timing, stderr) w._start() return w From 5d6989614b90eeb99ee26decb912ba816911ed07 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 23 Feb 2017 15:57:24 -0800 Subject: [PATCH 063/176] work on top-level stuff --- src/wormhole/_boss.py | 4 +- src/wormhole/wormhole.py | 114 +++++++++++++++++++++++---------------- 2 files changed, 72 insertions(+), 46 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 15a86c5..22f28eb 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -1,4 +1,6 @@ from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides, instance_of from automat import MethodicalMachine from . import _interfaces from ._mailbox import Mailbox @@ -13,7 +15,7 @@ from .util import bytes_to_dict @attrs @implementer(_interfaces.IBoss) -class Boss: +class Boss(object): _side = attrib(validator=instance_of(type(u""))) _url = attrib(validator=instance_of(type(u""))) _appid = attrib(validator=instance_of(type(u""))) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 23a6c6a..5351bb0 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -1,10 +1,54 @@ from __future__ import print_function, absolute_import, unicode_literals import sys +from attr import attrs, attrib from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss -class _Wormhole(object): +# We can provide different APIs to different apps: +# * Deferreds +# w.when_got_code().addCallback(print_code) +# w.send(data) +# w.receive().addCallback(got_data) +# w.close().addCallback(closed) + +# * delegate callbacks (better for journaled environments) +# w = wormhole(delegate=app) +# w.send(data) +# app.wormhole_got_code(code) +# app.wormhole_receive(data) +# w.close() +# app.wormhole_closed() +# +# * potential delegate options +# wormhole(delegate=app, delegate_prefix="wormhole_", +# delegate_args=(args, kwargs)) + +@attrs +class _DelegatedWormhole(object): + _delegate = attrib() + + def _set_boss(self, boss): + self._boss = boss + + # from above + def send(self, phase, plaintext): + self._boss.send(phase, plaintext) + def close(self): + self._boss.close() + + # from below + def got_code(self, code): + self._delegate.wormhole_got_code(code) + def got_verifier(self, verifier): + self._delegate.wormhole_got_verifier(verifier) + def received(self, phase, plaintext): + # TODO: deliver phases in order + self._delegate.wormhole_received(phase, plaintext) + def closed(self, result): + self._delegate.wormhole_closed(result) + +class _DeferredWormhole(object): def __init__(self): self._code = None self._code_observers = [] @@ -15,7 +59,6 @@ class _Wormhole(object): self._boss = boss # from above - def when_code(self): if self._code: return defer.succeed(self._code) @@ -53,58 +96,39 @@ class _Wormhole(object): def closed(self, result): print("closed", result) -def wormhole(appid, relay_url, reactor, - tor_manager=None, timing=None, - journal=None, - stderr=sys.stderr, - ): +def _wormhole(appid, relay_url, reactor, delegate=None, + tor_manager=None, timing=None, + journal=None, + stderr=sys.stderr, + ): timing = timing or DebugTiming() code_length = 2 side = bytes_to_hexstr(os.urandom(5)) journal = journal or ImmediateJournal() - w = _Wormhole() + if delegate: + w = _DelegatedWormhole(delegate) + else: + w = _DeferredWormhole() b = Boss(w, side, relay_url, appid, reactor, journal, timing) w._set_boss(b) # force allocate for now b.start() b.allocate(code_length) - w = _Wormhole(appid, relay_url, reactor, tor_manager, timing, stderr) - w._start() return w -#def wormhole_from_serialized(data, reactor, timing=None): -# timing = timing or DebugTiming() -# w = _Wormhole.from_serialized(data, reactor, timing) -# return w +def delegated_wormhole(appid, relay_url, reactor, delegate, + tor_manager=None, timing=None, + journal=None, + stderr=sys.stderr, + ): + assert delegate + return _wormhole(appid, relay_url, reactor, delegate, + tor_manager, timing, journal, stderr) - -# considerations for activity management: -# * websocket to server wants to be a t.a.i.ClientService -# * if Wormhole is a MultiService: -# * makes it easier to chain the ClientService to it -# * implies that nothing will happen before w.startService() -# * implies everything stops upon d=w.stopService() -# * if not: -# * - -class _JournaledWormhole(object): - def __init__(self, reactor, journal_manager, event_dispatcher, - event_dispatcher_args=()): - pass - -class _Wormhole(_JournaledWormhole): - # send events to self, deliver them via Deferreds - def __init__(self, reactor): - _JournaledWormhole.__init__(self, reactor, ImmediateJournal(), self) - -def wormhole2(reactor): - w = _Wormhole(reactor) - w.startService() - return w - -def journaled_from_data(state, reactor, journal, - event_handler, event_handler_args=()): - pass - -def journaled(reactor, journal, event_handler, event_handler_args=()): - pass +def deferred_wormhole(appid, relay_url, reactor, + tor_manager=None, timing=None, + journal=None, + stderr=sys.stderr, + ): + return _wormhole(appid, relay_url, reactor, delegate=None, + tor_manager, timing, journal, stderr) From c95b6d402cc8420c1c67bf2d771460eb5d984225 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 23 Feb 2017 17:11:54 -0800 Subject: [PATCH 064/176] Code: don't sent tx_allocate until we're connected So Code needs connected/lost from the RendezvousConnector --- docs/code.dot | 18 ++++++++--------- docs/machines.dot | 6 +++--- src/wormhole/_code.py | 40 ++++++++++++++++++++++++------------- src/wormhole/_rendezvous.py | 2 ++ 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/docs/code.dot b/docs/code.dot index 1a67209..b6fb71c 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -2,11 +2,8 @@ digraph { start [label="Wormhole Code\nMachine" style="dotted"] {rank=same; start S0} - {rank=same; P0_list_nameplates P0_allocate} - {rank=same; S1 S2} - {rank=same; S3 P1_generate} start -> S0 [style="invis"] - S0 [label="S0:\nunknown"] + S0 [label="S0:\nunknown\ndisconnected"] S0 -> P0_got_code [label="set"] P0_got_code [shape="box" label="B.got_code"] P0_got_code -> S4 @@ -35,11 +32,14 @@ digraph { S3 -> P0_got_code [label="" color="orange" fontcolor="orange"] - S0 -> P0_allocate [label="allocate"] - P0_allocate [shape="box" label="RC.tx_allocate"] - P0_allocate -> S1 - S1 [label="S1:\nallocating"] - S1 -> P1_generate [label="rx_allocated"] + S0 -> S1A [label="allocate"] + S1A [label="S1A:\nconnecting"] + S1A -> P1_allocate [label="connected"] + P1_allocate [shape="box" label="RC.tx_allocate"] + P1_allocate -> S1B + S1B [label="S1B:\nallocating"] + S1B -> P1_generate [label="rx_allocated"] + S1B -> S1A [label="lost"] P1_generate [shape="box" label="generate\nrandom code"] P1_generate -> P0_got_code diff --git a/docs/machines.dot b/docs/machines.dot index ee5a7a3..c6004ee 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -20,7 +20,7 @@ digraph { Wormhole -> Boss [style="dashed" label="allocate\ninput\nset_code\nsend\nclose\n(once)"] #Wormhole -> Boss [color="blue"] - Boss -> Wormhole [style="dashed" label="got_code\ngot_verifier\nreceived\nclosed\n(once)"] + Boss -> Wormhole [style="dashed" label="got_code\ngot_verifier\nreceived (seq)\nclosed\n(once)"] #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] @@ -61,11 +61,11 @@ digraph { ] #Boss -> Code [color="blue"] + Connection -> Code [style="dashed" + label="connected\nlost\nrx_allocated"] Code -> Connection [style="dashed" label="tx_allocate" ] - Connection -> Code [style="dashed" - label="rx_allocated"] Nameplates -> Code [style="dashed" label="got_nameplates" ] diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index efad69d..067ace2 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -25,15 +25,17 @@ class Code(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def wire(self, wormhole, rendezvous_connector, nameplate_lister): - self._W = _interfaces.IWormhole(wormhole) + def wire(self, boss, rendezvous_connector, nameplate_lister): + self._B = _interfaces.IBoss(boss) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._NL = _interfaces.INameplateLister(nameplate_lister) @m.state(initial=True) def S0_unknown(self): pass @m.state() - def S1_allocating(self): pass + def S1A_connecting(self): pass + @m.state() + def S1B_allocating(self): pass @m.state() def S2_typing_nameplate(self): pass @m.state() @@ -51,6 +53,10 @@ class Code(object): # from RendezvousConnector @m.input() + def connected(self): pass + @m.input() + def lost(self): pass + @m.input() def rx_allocated(self, nameplate): pass # from NameplateLister @@ -73,8 +79,10 @@ class Code(object): self._stdio = stdio self._NL.refresh_nameplates() @m.output() - def RC_tx_allocate(self, code_length): + def stash_code_length(self, code_length): self._code_length = code_length + @m.output() + def RC_tx_allocate(self): self._RC.tx_allocate() @m.output() def do_completion_nameplates(self): @@ -90,22 +98,26 @@ class Code(object): def do_completion_code(self): pass @m.output() - def generate_and_set(self, nameplate): + def generate_and_B_got_code(self, nameplate): self._code = make_code(nameplate, self._code_length) - self._W_got_code() + self._B_got_code() @m.output() - def W_got_code(self, code): + def B_got_code(self, code): self._code = code - self._W_got_code() + self._B_got_code() - def _W_got_code(self): - self._W.got_code(self._code) + def _B_got_code(self): + self._B.got_code(self._code) - S0_unknown.upon(allocate, enter=S1_allocating, outputs=[RC_tx_allocate]) - S1_allocating.upon(rx_allocated, enter=S4_known, outputs=[generate_and_set]) + S0_unknown.upon(set, enter=S4_known, outputs=[B_got_code]) - S0_unknown.upon(set, enter=S4_known, outputs=[W_got_code]) + S0_unknown.upon(allocate, enter=S1A_connecting, outputs=[stash_code_length]) + S1A_connecting.upon(connected, enter=S1B_allocating, + outputs=[RC_tx_allocate]) + S1B_allocating.upon(lost, enter=S1A_connecting, outputs=[]) + S1B_allocating.upon(rx_allocated, enter=S4_known, + outputs=[generate_and_B_got_code]) S0_unknown.upon(input, enter=S2_typing_nameplate, outputs=[start_input_and_NL_refresh_nameplates]) @@ -116,4 +128,4 @@ class Code(object): S2_typing_nameplate.upon(hyphen, enter=S3_typing_code, outputs=[lookup_wordlist]) S3_typing_code.upon(tab, enter=S3_typing_code, outputs=[do_completion_code]) - S3_typing_code.upon(RETURN, enter=S4_known, outputs=[W_got_code]) + S3_typing_code.upon(RETURN, enter=S4_known, outputs=[B_got_code]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 52a32fe..4a955b8 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -119,6 +119,7 @@ class RendezvousConnector(object): def ws_open(self, proto): self._ws = proto self._tx("bind", appid=self._appid, side=self._side) + self._C.connected() self._M.connected() self._NL.connected() @@ -136,6 +137,7 @@ class RendezvousConnector(object): def ws_close(self, wasClean, code, reason): self._ws = None + self._C.lost() self._M.lost() self._NL.lost() From ef1904bc5216e2d90f167250442df022adfd17d6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 23 Feb 2017 17:29:56 -0800 Subject: [PATCH 065/176] get null test working (open and immediate close) --- docs/boss.dot | 2 ++ docs/mailbox.dot | 9 --------- src/wormhole/_boss.py | 26 ++++++++++++++++++------ src/wormhole/_interfaces.py | 2 ++ src/wormhole/_mailbox.py | 14 +++---------- src/wormhole/_rendezvous.py | 2 +- src/wormhole/test/test_wormhole_new.py | 28 ++++++++++++++++++++++++++ src/wormhole/timing.py | 3 +++ src/wormhole/wormhole.py | 19 +++++++++++------ 9 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 src/wormhole/test/test_wormhole_new.py diff --git a/docs/boss.dot b/docs/boss.dot index 03062d3..8b34fc3 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -17,6 +17,8 @@ digraph { S0 [label="S0: empty"] S0 -> P0_build [label="set_code"] + S0 -> P_close_lonely [label="close"] + P0_build [shape="box" label="W.got_code\nM.set_nameplate\nK.got_code"] P0_build -> S1 S1 [label="S1: lonely" color="orange"] diff --git a/docs/mailbox.dot b/docs/mailbox.dot index 1353a14..881c617 100644 --- a/docs/mailbox.dot +++ b/docs/mailbox.dot @@ -1,17 +1,8 @@ digraph { /* new idea */ - {rank=same; title entry_whole_code entry_allocation entry_interactive} - entry_whole_code [label="whole\ncode"] - entry_whole_code -> S0A title [label="Message\nMachine" style="dotted"] - entry_allocation [label="allocation" color="orange"] - entry_allocation -> S0B [label="(already\nconnected)" - color="orange" fontcolor="orange"] - entry_interactive [label="interactive" color="orange"] - entry_interactive -> S0B [color="orange"] - {rank=same; S0A P0_connected S0B} S0A [label="S0A:\nknow nothing"] S0B [label="S0B:\nknow nothing\n(bound)" color="orange"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 22f28eb..6065bfc 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -16,6 +16,7 @@ from .util import bytes_to_dict @attrs @implementer(_interfaces.IBoss) class Boss(object): + _W = attrib() _side = attrib(validator=instance_of(type(u""))) _url = attrib(validator=instance_of(type(u""))) _appid = attrib(validator=instance_of(type(u""))) @@ -24,14 +25,15 @@ class Boss(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, wormhole): - self._W = wormhole + def __attrs_post_init__(self): self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) self._O = Order(self._side, self._timing) self._K = Key(self._timing) self._R = Receive(self._side, self._timing) - self._RC = RendezvousConnector(self._side, self._timing, self._reactor) + self._RC = RendezvousConnector(self._url, self._appid, self._side, + self._reactor, self._journal, + self._timing) self._NL = NameplateListing() self._C = Code(self._timing) @@ -44,6 +46,10 @@ class Boss(object): self._NL.wire(self._RC, self._C) self._C.wire(self, self._RC, self._NL) + self._next_tx_phase = 0 + self._next_rx_phase = 0 + self._rx_phases = {} # phase -> plaintext + # these methods are called from outside def start(self): self._RC.start() @@ -82,7 +88,7 @@ class Boss(object): self._C.set_code(code) @m.input() - def send(self, phase, plaintext): pass + def send(self, plaintext): pass @m.input() def close(self): pass @@ -127,7 +133,9 @@ class Boss(object): # ignored for now @m.output() - def S_send(self, phase, plaintext): + def S_send(self, plaintext): + phase = self._next_tx_phase + self._next_tx_phase += 1 self._S.send(phase, plaintext) @m.output() @@ -145,13 +153,19 @@ class Boss(object): self._W.got_verifier(verifier) @m.output() def W_received(self, phase, plaintext): - self._W.received(phase, plaintext) + # we call Wormhole.received() in strict phase order, with no gaps + self._rx_phases[phase] = plaintext + while self._next_rx_phase in self._rx_phases: + self._W.received(self._next_rx_phase, + self._rx_phases.pop(self._next_rx_phase)) + self._next_rx_phase += 1 @m.output() def W_closed(self): result = "???" self._W.closed(result) + S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely]) S0_empty.upon(send, enter=S0_empty, outputs=[S_send]) S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code]) S1_lonely.upon(happy, enter=S2_happy, outputs=[]) diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index b8abf49..ae8b459 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -1,5 +1,7 @@ from zope.interface import Interface +class IWormhole(Interface): + pass class IBoss(Interface): pass class IMailbox(Interface): diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 5469806..1aec665 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -22,15 +22,12 @@ class Mailbox(object): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._O = _interfaces.IOrder(ordering) - @m.state(initial=True) - def initial(self): pass - # all -A states: not connected # all -B states: yes connected # B states serialize as A, so they deserialize as unconnected # S0: know nothing - @m.state() + @m.state(initial=True) def S0A(self): pass @m.state() def S0B(self): pass @@ -87,11 +84,6 @@ class Mailbox(object): def Ss(self): pass - @m.input() - def start_unconnected(self): pass - @m.input() - def start_connected(self): pass - # from Boss @m.input() def set_nameplate(self, nameplate): pass @@ -211,12 +203,12 @@ class Mailbox(object): @m.output() def RC_stop(self): self._RC_stop() + def _RC_stop(self): + self._RC.stop() @m.output() def W_closed(self): self._B.closed() - initial.upon(start_unconnected, enter=S0A, outputs=[]) - initial.upon(start_connected, enter=S0B, outputs=[]) S0A.upon(connected, enter=S0B, outputs=[]) S0A.upon(set_nameplate, enter=S1A, outputs=[record_nameplate]) S0A.upon(add_message, enter=S0A, outputs=[queue]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 4a955b8..769cb25 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -61,7 +61,7 @@ class RendezvousConnector(object): _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) - def __init__(self): + def __attrs_post_init__(self): self._ws = None f = WSFactory(self, self._url) f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py new file mode 100644 index 0000000..821001b --- /dev/null +++ b/src/wormhole/test/test_wormhole_new.py @@ -0,0 +1,28 @@ +from __future__ import print_function, unicode_literals +from twisted.trial import unittest +from twisted.internet import reactor +from .common import ServerBase +from .. import wormhole + +APPID = "appid" + +class Delegate: + def __init__(self): + self.code = None + self.verifier = None + self.messages = [] + self.closed = None + def wormhole_got_code(self, code): + self.code = code + def wormhole_got_verifier(self, verifier): + self.verifier = verifier + def wormhole_receive(self, data): + self.messages.append(data) + def wormhole_closed(self, result): + self.closed = result + +class New(ServerBase, unittest.TestCase): + def test_basic(self): + dg = Delegate() + w = wormhole.delegated_wormhole(APPID, self.relayurl, reactor, dg) + w.close() diff --git a/src/wormhole/timing.py b/src/wormhole/timing.py index 0ecf1bc..8cb18e5 100644 --- a/src/wormhole/timing.py +++ b/src/wormhole/timing.py @@ -1,5 +1,7 @@ from __future__ import print_function, absolute_import, unicode_literals import json, time +from zope.interface import implementer +from ._interfaces import ITiming class Event: def __init__(self, name, when, **details): @@ -33,6 +35,7 @@ class Event: else: self.finish() +@implementer(ITiming) class DebugTiming: def __init__(self): self._events = [] diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 5351bb0..c37bad8 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -1,6 +1,10 @@ from __future__ import print_function, absolute_import, unicode_literals -import sys +import os, sys from attr import attrs, attrib +from zope.interface import implementer +from twisted.internet import defer +from ._interfaces import IWormhole +from .util import bytes_to_hexstr from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss @@ -16,6 +20,7 @@ from ._boss import Boss # w = wormhole(delegate=app) # w.send(data) # app.wormhole_got_code(code) +# app.wormhole_got_verifier(verifier) # app.wormhole_receive(data) # w.close() # app.wormhole_closed() @@ -25,6 +30,7 @@ from ._boss import Boss # delegate_args=(args, kwargs)) @attrs +@implementer(IWormhole) class _DelegatedWormhole(object): _delegate = attrib() @@ -32,8 +38,8 @@ class _DelegatedWormhole(object): self._boss = boss # from above - def send(self, phase, plaintext): - self._boss.send(phase, plaintext) + def send(self, plaintext): + self._boss.send(plaintext) def close(self): self._boss.close() @@ -48,6 +54,7 @@ class _DelegatedWormhole(object): def closed(self, result): self._delegate.wormhole_closed(result) +@implementer(IWormhole) class _DeferredWormhole(object): def __init__(self): self._code = None @@ -73,8 +80,8 @@ class _DeferredWormhole(object): self._verifier_observers.append(d) return d - def send(self, phase, plaintext): - self._boss.send(phase, plaintext) + def send(self, plaintext): + self._boss.send(plaintext) def close(self): self._boss.close() @@ -130,5 +137,5 @@ def deferred_wormhole(appid, relay_url, reactor, journal=None, stderr=sys.stderr, ): - return _wormhole(appid, relay_url, reactor, delegate=None, + return _wormhole(appid, relay_url, reactor, None, tor_manager, timing, journal, stderr) From 8a2810ba706719f78c21c00e3f30d0a39ee67b18 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 23 Feb 2017 18:11:07 -0800 Subject: [PATCH 066/176] test basic code allocation --- docs/boss.dot | 2 +- src/wormhole/_boss.py | 16 +++++++++++++++- src/wormhole/_code.py | 10 ++++++++++ src/wormhole/_key.py | 4 +++- src/wormhole/_mailbox.py | 19 +++++++++++++++++-- src/wormhole/_nameplate.py | 4 ++-- src/wormhole/_order.py | 3 ++- src/wormhole/_receive.py | 3 ++- src/wormhole/_rendezvous.py | 9 ++++++++- src/wormhole/_send.py | 10 +++++++--- src/wormhole/test/test_wormhole_new.py | 9 +++++++++ 11 files changed, 76 insertions(+), 13 deletions(-) diff --git a/docs/boss.dot b/docs/boss.dot index 8b34fc3..bff04d3 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -54,7 +54,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="send -> S.send\ngot_verifier -> A.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> A.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 6065bfc..7ba8978 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides, instance_of @@ -29,7 +30,7 @@ class Boss(object): self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) self._O = Order(self._side, self._timing) - self._K = Key(self._timing) + self._K = Key(self._appid, self._timing) self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, @@ -92,6 +93,10 @@ class Boss(object): @m.input() def close(self): pass + # from RendezvousConnector + @m.input() + def rx_welcome(self, welcome): pass + # from Code (provoked by input/allocate/set_code) @m.input() def got_code(self, code): pass @@ -121,6 +126,10 @@ class Boss(object): def closed(self): pass + @m.output() + def process_welcome(self, welcome): + pass # TODO: ignored for now + @m.output() def do_got_code(self, code): nameplate = code.split("-")[0] @@ -167,18 +176,22 @@ class Boss(object): S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely]) S0_empty.upon(send, enter=S0_empty, outputs=[S_send]) + S0_empty.upon(rx_welcome, enter=S0_empty, outputs=[process_welcome]) S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code]) + S1_lonely.upon(rx_welcome, enter=S1_lonely, outputs=[process_welcome]) S1_lonely.upon(happy, enter=S2_happy, outputs=[]) S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) + S2_happy.upon(rx_welcome, enter=S2_happy, outputs=[process_welcome]) 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(scared, enter=S3_closing, outputs=[close_scared]) S2_happy.upon(close, enter=S3_closing, outputs=[close_happy]) S2_happy.upon(send, enter=S2_happy, outputs=[S_send]) + S3_closing.upon(rx_welcome, 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(happy, enter=S3_closing, outputs=[]) @@ -187,6 +200,7 @@ class Boss(object): S3_closing.upon(send, enter=S3_closing, outputs=[]) S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed]) + S4_closed.upon(rx_welcome, 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(happy, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 067ace2..7f4a248 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals import os from zope.interface import implementer from attr import attrs, attrib @@ -127,5 +128,14 @@ class Code(object): outputs=[stash_nameplates]) S2_typing_nameplate.upon(hyphen, enter=S3_typing_code, outputs=[lookup_wordlist]) + # TODO: need a proper pair of connected/lost states around S2 + S2_typing_nameplate.upon(connected, enter=S2_typing_nameplate, outputs=[]) + S2_typing_nameplate.upon(lost, enter=S2_typing_nameplate, outputs=[]) + S3_typing_code.upon(tab, enter=S3_typing_code, outputs=[do_completion_code]) S3_typing_code.upon(RETURN, enter=S4_known, outputs=[B_got_code]) + S3_typing_code.upon(connected, enter=S3_typing_code, outputs=[]) + S3_typing_code.upon(lost, enter=S3_typing_code, outputs=[]) + + S4_known.upon(connected, enter=S4_known, outputs=[]) + S4_known.upon(lost, enter=S4_known, outputs=[]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index edbcda7..3472871 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -1,7 +1,8 @@ +from __future__ import print_function, absolute_import, unicode_literals from hashlib import sha256 from zope.interface import implementer from attr import attrs, attrib -from attr.validators import provides +from attr.validators import provides, instance_of from spake2 import SPAKE2_Symmetric from hkdf import Hkdf from nacl.secret import SecretBox @@ -53,6 +54,7 @@ def encrypt_data(key, plaintext): @attrs @implementer(_interfaces.IKey) class Key(object): + _appid = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 1aec665..5d9642f 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from attr import attrs, attrib from attr.validators import instance_of @@ -10,7 +11,7 @@ class Mailbox(object): _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() - def __init__(self): + def __attrs_post_init__(self): self._mood = None self._nameplate = None self._mailbox = None @@ -129,7 +130,7 @@ class Mailbox(object): @m.output() def record_nameplate_and_RC_tx_claim(self, nameplate): self._nameplate = nameplate - self._RX.tx_claim(self._nameplate) + self._RC.tx_claim(self._nameplate) @m.output() def RC_tx_claim(self): # when invoked via M.connected(), we must use the stored nameplate @@ -278,5 +279,19 @@ class Mailbox(object): ScB.upon(rx_closed, enter=SsB, outputs=[RC_stop]) ScA.upon(connected, enter=ScB, outputs=[RC_tx_close]) + SsB.upon(lost, enter=SsB, outputs=[]) SsB.upon(stopped, enter=Ss, outputs=[W_closed]) + SrcB.upon(rx_claimed, enter=SrcB, outputs=[]) + SrcB.upon(rx_message_theirs, enter=SrcB, outputs=[]) + SrcB.upon(rx_message_ours, enter=SrcB, outputs=[]) + SrB.upon(rx_claimed, enter=SrB, outputs=[]) + SrB.upon(rx_message_theirs, enter=SrB, outputs=[]) + SrB.upon(rx_message_ours, enter=SrB, outputs=[]) + ScB.upon(rx_claimed, enter=ScB, outputs=[]) + ScB.upon(rx_message_theirs, enter=ScB, outputs=[]) + ScB.upon(rx_message_ours, enter=ScB, outputs=[]) + SsB.upon(rx_claimed, enter=SsB, outputs=[]) + SsB.upon(rx_message_theirs, enter=SsB, outputs=[]) + SsB.upon(rx_message_ours, enter=SsB, outputs=[]) + diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 719f354..ee9631c 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces @@ -5,8 +6,7 @@ from . import _interfaces @implementer(_interfaces.INameplateLister) class NameplateListing(object): m = MethodicalMachine() - def __init__(self): - pass + def wire(self, rendezvous_connector, code): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._C = _interfaces.ICode(code) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index 73934e8..e16f2b6 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides, instance_of @@ -11,7 +12,7 @@ class Order(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self): + def __attrs_post_init__(self): self._key = None self._queue = [] def wire(self, key, receive): diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 8fec9a8..47a3c12 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides, instance_of @@ -12,7 +13,7 @@ class Receive(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self): + def __attrs_post_init__(self): self._key = None def wire(self, boss, key, send): diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 769cb25..c65dc68 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -1,3 +1,4 @@ +from __future__ import print_function, absolute_import, unicode_literals import os from six.moves.urllib_parse import urlparse from attr import attrs, attrib @@ -27,7 +28,12 @@ class WSClient(websocket.WebSocketClientProtocol): def onMessage(self, payload, isBinary): #print("onMessage") assert not isBinary - self._RC.ws_message(payload) + try: + self._RC.ws_message(payload) + except: + print("LOGGING") + log.err() + raise def onClose(self, wasClean, code, reason): #print("onClose") @@ -60,6 +66,7 @@ class RendezvousConnector(object): _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) + DEBUG = False def __attrs_post_init__(self): self._ws = None diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 3d6b1e1..355a267 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -1,14 +1,18 @@ +from __future__ import print_function, absolute_import, unicode_literals +from attr import attrs, attrib +from attr.validators import provides, instance_of from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces from ._key import derive_phase_key, encrypt_data +@attrs @implementer(_interfaces.ISend) class Send(object): + _side = attrib(validator=instance_of(type(u""))) + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - def __init__(self, side, timing): - self._side = side - self._timing = timing + def wire(self, mailbox): self._M = _interfaces.IMailbox(mailbox) diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 821001b..9710ae7 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -1,6 +1,7 @@ from __future__ import print_function, unicode_literals from twisted.trial import unittest from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks from .common import ServerBase from .. import wormhole @@ -26,3 +27,11 @@ class New(ServerBase, unittest.TestCase): dg = Delegate() w = wormhole.delegated_wormhole(APPID, self.relayurl, reactor, dg) w.close() + + @inlineCallbacks + def test_allocate(self): + w = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + code = yield w.when_code() + print("code:", code) + yield w.close() + test_allocate.timeout = 2 From b7df5e21eb0764cf1adfe277c2007a235bb8c1a5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 23 Feb 2017 18:23:55 -0800 Subject: [PATCH 067/176] more tests, still failing --- docs/code.dot | 6 ++-- docs/machines.dot | 4 +-- src/wormhole/_boss.py | 11 ++++---- src/wormhole/_code.py | 13 +++++---- src/wormhole/test/test_wormhole_new.py | 33 ++++++++++++++++++---- src/wormhole/wormhole.py | 38 +++++++++++++++++++++----- 6 files changed, 75 insertions(+), 30 deletions(-) diff --git a/docs/code.dot b/docs/code.dot index b6fb71c..2b76493 100644 --- a/docs/code.dot +++ b/docs/code.dot @@ -4,12 +4,12 @@ digraph { {rank=same; start S0} start -> S0 [style="invis"] S0 [label="S0:\nunknown\ndisconnected"] - S0 -> P0_got_code [label="set"] + S0 -> P0_got_code [label="set_code"] P0_got_code [shape="box" label="B.got_code"] P0_got_code -> S4 S4 [label="S4: known" color="green"] - S0 -> P0_list_nameplates [label="input"] + S0 -> P0_list_nameplates [label="input_code"] S2 [label="S2: typing\nnameplate"] S2 -> P2_completion [label=""] @@ -32,7 +32,7 @@ digraph { S3 -> P0_got_code [label="" color="orange" fontcolor="orange"] - S0 -> S1A [label="allocate"] + S0 -> S1A [label="allocate_code"] S1A [label="S1A:\nconnecting"] S1A -> P1_allocate [label="connected"] P1_allocate [shape="box" label="RC.tx_allocate"] diff --git a/docs/machines.dot b/docs/machines.dot index c6004ee..4c75515 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -18,7 +18,7 @@ digraph { Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] - Wormhole -> Boss [style="dashed" label="allocate\ninput\nset_code\nsend\nclose\n(once)"] + Wormhole -> Boss [style="dashed" label="allocate_code\ninput_code\nset_code\nsend\nclose\n(once)"] #Wormhole -> Boss [color="blue"] Boss -> Wormhole [style="dashed" label="got_code\ngot_verifier\nreceived (seq)\nclosed\n(once)"] @@ -74,7 +74,7 @@ digraph { label="refresh_nameplates" ] Boss -> Code [style="dashed" - label="allocate\ninput\nset_code"] + label="allocate_code\ninput_code\nset_code_code"] Code -> Boss [style="dashed" label="got_code"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 7ba8978..e6b9238 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -81,10 +81,10 @@ class Boss(object): # would require the Wormhole to be aware of Code (whereas right now # Wormhole only knows about this Boss instance, and everything else is # hidden away). - def input(self, stdio): - self._C.input(stdio) - def allocate(self, code_length): - self._C.allocate(code_length) + def input_code(self, stdio): + self._C.input_code(stdio) + def allocate_code(self, code_length): + self._C.allocate_code(code_length) def set_code(self, code): self._C.set_code(code) @@ -165,8 +165,7 @@ class Boss(object): # we call Wormhole.received() in strict phase order, with no gaps self._rx_phases[phase] = plaintext while self._next_rx_phase in self._rx_phases: - self._W.received(self._next_rx_phase, - self._rx_phases.pop(self._next_rx_phase)) + self._W.received(self._rx_phases.pop(self._next_rx_phase)) self._next_rx_phase += 1 @m.output() diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 7f4a248..544685b 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -46,11 +46,11 @@ class Code(object): # from App @m.input() - def allocate(self, code_length): pass + def allocate_code(self, code_length): pass @m.input() - def input(self, stdio): pass + def input_code(self, stdio): pass @m.input() - def set(self, code): pass + def set_code(self, code): pass # from RendezvousConnector @m.input() @@ -111,16 +111,17 @@ class Code(object): def _B_got_code(self): self._B.got_code(self._code) - S0_unknown.upon(set, enter=S4_known, outputs=[B_got_code]) + S0_unknown.upon(set_code, enter=S4_known, outputs=[B_got_code]) - S0_unknown.upon(allocate, enter=S1A_connecting, outputs=[stash_code_length]) + S0_unknown.upon(allocate_code, enter=S1A_connecting, + outputs=[stash_code_length]) S1A_connecting.upon(connected, enter=S1B_allocating, outputs=[RC_tx_allocate]) S1B_allocating.upon(lost, enter=S1A_connecting, outputs=[]) S1B_allocating.upon(rx_allocated, enter=S4_known, outputs=[generate_and_B_got_code]) - S0_unknown.upon(input, enter=S2_typing_nameplate, + S0_unknown.upon(input_code, enter=S2_typing_nameplate, outputs=[start_input_and_NL_refresh_nameplates]) S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate, outputs=[do_completion_nameplates]) diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 9710ae7..6e32a24 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -23,15 +23,36 @@ class Delegate: self.closed = result class New(ServerBase, unittest.TestCase): - def test_basic(self): + @inlineCallbacks + def test_allocate(self): + w = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w.allocate_code(2) + code = yield w.when_code() + print("code:", code) + yield w.close() + test_allocate.timeout = 2 + + def test_delegated(self): dg = Delegate() w = wormhole.delegated_wormhole(APPID, self.relayurl, reactor, dg) w.close() @inlineCallbacks - def test_allocate(self): - w = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) - code = yield w.when_code() + def test_basic(self): + w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w1.allocate_code(2) + code = yield w1.when_code() print("code:", code) - yield w.close() - test_allocate.timeout = 2 + w2 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w2.set_code(code) + code2 = yield w2.when_code() + self.assertEqual(code, code2) + + w1.send(b"data") + + data = yield w2.when_received() + self.assertEqual(data, b"data") + + yield w1.close() + yield w2.close() + test_basic.timeout = 2 diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index c37bad8..d965d72 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -38,6 +38,14 @@ class _DelegatedWormhole(object): self._boss = boss # from above + + def allocate_code(self, code_length=2): + self._boss.allocate_code(code_length) + def input_code(self, stdio): + self._boss.input_code(stdio) + def set_code(self, code): + self._boss.set_code(code) + def send(self, plaintext): self._boss.send(plaintext) def close(self): @@ -48,9 +56,8 @@ class _DelegatedWormhole(object): self._delegate.wormhole_got_code(code) def got_verifier(self, verifier): self._delegate.wormhole_got_verifier(verifier) - def received(self, phase, plaintext): - # TODO: deliver phases in order - self._delegate.wormhole_received(phase, plaintext) + def received(self, plaintext): + self._delegate.wormhole_received(plaintext) def closed(self, result): self._delegate.wormhole_closed(result) @@ -61,6 +68,8 @@ class _DeferredWormhole(object): self._code_observers = [] self._verifier = None self._verifier_observers = [] + self._received_data = [] + self._received_observers = [] def _set_boss(self, boss): self._boss = boss @@ -80,6 +89,20 @@ class _DeferredWormhole(object): self._verifier_observers.append(d) return d + def when_received(self): + if self._received_data: + return defer.succeed(self._received_data.pop(0)) + d = defer.Deferred() + self._received_observers.append(d) + return d + + def allocate_code(self, code_length=2): + self._boss.allocate_code(code_length) + def input_code(self, stdio): + self._boss.input_code(stdio) + def set_code(self, code): + self._boss.set_code(code) + def send(self, plaintext): self._boss.send(plaintext) def close(self): @@ -97,8 +120,11 @@ class _DeferredWormhole(object): d.callback(verifier) self._verifier_observers[:] = [] - def received(self, phase, plaintext): - print(phase, plaintext) + def received(self, plaintext): + if self._received_observers: + self._received_observers.pop(0).callback(plaintext) + return + self._received_data.append(plaintext) def closed(self, result): print("closed", result) @@ -109,7 +135,6 @@ def _wormhole(appid, relay_url, reactor, delegate=None, stderr=sys.stderr, ): timing = timing or DebugTiming() - code_length = 2 side = bytes_to_hexstr(os.urandom(5)) journal = journal or ImmediateJournal() if delegate: @@ -120,7 +145,6 @@ def _wormhole(appid, relay_url, reactor, delegate=None, w._set_boss(b) # force allocate for now b.start() - b.allocate(code_length) return w def delegated_wormhole(appid, relay_url, reactor, delegate, From 41b7bcfed598691bbdfd57775e120120e3b5c664 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 24 Feb 2017 18:30:00 -0800 Subject: [PATCH 068/176] working on fixes --- docs/boss.dot | 11 ++++++++--- docs/machines.dot | 2 +- src/wormhole/_boss.py | 38 ++++++++++++++++++++++++++++++++----- src/wormhole/_key.py | 16 +++++++++++----- src/wormhole/_mailbox.py | 14 ++++++++------ src/wormhole/_order.py | 1 + src/wormhole/_receive.py | 2 +- src/wormhole/_rendezvous.py | 28 ++++++++++++++++++++++----- src/wormhole/_send.py | 6 +++++- src/wormhole/wormhole.py | 21 ++++++++++++++++++-- 10 files changed, 110 insertions(+), 29 deletions(-) diff --git a/docs/boss.dot b/docs/boss.dot index bff04d3..053d06a 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -17,6 +17,9 @@ digraph { S0 [label="S0: empty"] S0 -> P0_build [label="set_code"] + S0 -> P_close_error [label="rx_error"] + P_close_error [shape="box" label="M.close(errory)"] + P_close_error -> S_closing S0 -> P_close_lonely [label="close"] P0_build [shape="box" label="W.got_code\nM.set_nameplate\nK.got_code"] @@ -25,6 +28,7 @@ digraph { S1 -> S2 [label="happy"] + S1 -> P_close_error [label="rx_error"] S1 -> P_close_scary [label="scared" color="red"] S1 -> P_close_lonely [label="close"] P_close_lonely [shape="box" label="M.close(lonely)"] @@ -39,22 +43,23 @@ digraph { P2_close -> S_closing S2 -> P2_got_message [label="got_message"] - P2_got_message [shape="box" label="A.received"] + P2_got_message [shape="box" label="W.received"] P2_got_message -> S2 + S2 -> P_close_error [label="rx_error"] S2 -> P_close_scary [label="scared" color="red"] S_closing [label="closing"] S_closing -> P_closed [label="closed"] S_closing -> S_closing [label="got_message\nhappy\nscared\nclose"] - P_closed [shape="box" label="A.closed(reason)"] + P_closed [shape="box" label="W.closed(reason)"] P_closed -> S_closed S_closed [label="closed"] {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> A.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/docs/machines.dot b/docs/machines.dot index 4c75515..68a6a5e 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -24,7 +24,7 @@ digraph { #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] - Connection -> Boss [style="dashed" label="rx_welcome"] + Connection -> Boss [style="dashed" label="rx_welcome\nrx_error"] Boss -> Send [style="dashed" label="send"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index e6b9238..1b5dd9a 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -1,7 +1,10 @@ from __future__ import print_function, absolute_import, unicode_literals +import re +import six from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides, instance_of +from twisted.python import log from automat import MethodicalMachine from . import _interfaces from ._mailbox import Mailbox @@ -12,8 +15,12 @@ from ._receive import Receive from ._rendezvous import RendezvousConnector from ._nameplate import NameplateListing from ._code import Code +from .errors import WrongPasswordError from .util import bytes_to_dict +class WormholeError(Exception): + pass + @attrs @implementer(_interfaces.IBoss) class Boss(object): @@ -30,7 +37,7 @@ class Boss(object): self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) self._O = Order(self._side, self._timing) - self._K = Key(self._appid, self._timing) + self._K = Key(self._appid, self._side, self._timing) self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, @@ -51,6 +58,8 @@ class Boss(object): self._next_rx_phase = 0 self._rx_phases = {} # phase -> plaintext + self._result = "empty" + # these methods are called from outside def start(self): self._RC.start() @@ -107,13 +116,20 @@ class Boss(object): def happy(self): pass @m.input() def scared(self): pass + @m.input() + def rx_error(self, err, orig): pass + def got_message(self, phase, plaintext): assert isinstance(phase, type("")), type(phase) assert isinstance(plaintext, type(b"")), type(plaintext) if phase == "version": self.got_version(plaintext) + elif re.search(r'^\d+$', phase): + self.got_phase(int(phase), plaintext) else: - self.got_phase(phase, plaintext) + # Ignore unrecognized phases, for forwards-compatibility. Use + # log.err so tests will catch surprises. + log.err("received unknown phase '%s'" % phase) @m.input() def got_version(self, plaintext): pass @m.input() @@ -143,18 +159,26 @@ class Boss(object): @m.output() def S_send(self, plaintext): + assert isinstance(plaintext, type(b"")), type(plaintext) phase = self._next_tx_phase self._next_tx_phase += 1 - self._S.send(phase, plaintext) + self._S.send("%d" % phase, plaintext) @m.output() + def close_error(self, err, orig): + self._result = WormholeError(err) + self._M.close("errory") + @m.output() def close_scared(self): + self._result = WrongPasswordError() self._M.close("scary") @m.output() def close_lonely(self): + self._result = WormholeError("lonely") self._M.close("lonely") @m.output() def close_happy(self): + self._result = "happy" self._M.close("happy") @m.output() @@ -162,6 +186,7 @@ class Boss(object): self._W.got_verifier(verifier) @m.output() def W_received(self, phase, plaintext): + assert isinstance(phase, six.integer_types), type(phase) # we call Wormhole.received() in strict phase order, with no gaps self._rx_phases[phase] = plaintext while self._next_rx_phase in self._rx_phases: @@ -170,27 +195,30 @@ class Boss(object): @m.output() def W_closed(self): - result = "???" - self._W.closed(result) + self._W.closed(self._result) S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely]) S0_empty.upon(send, enter=S0_empty, outputs=[S_send]) S0_empty.upon(rx_welcome, enter=S0_empty, outputs=[process_welcome]) S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code]) + S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error]) S1_lonely.upon(rx_welcome, enter=S1_lonely, outputs=[process_welcome]) S1_lonely.upon(happy, enter=S2_happy, outputs=[]) S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) + S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error]) S2_happy.upon(rx_welcome, enter=S2_happy, outputs=[process_welcome]) 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(scared, enter=S3_closing, outputs=[close_scared]) S2_happy.upon(close, enter=S3_closing, outputs=[close_happy]) S2_happy.upon(send, enter=S2_happy, outputs=[S_send]) + S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_error]) S3_closing.upon(rx_welcome, 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_version, enter=S3_closing, outputs=[]) S3_closing.upon(happy, enter=S3_closing, outputs=[]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 3472871..016bc86 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -1,5 +1,6 @@ from __future__ import print_function, absolute_import, unicode_literals from hashlib import sha256 +import six from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides, instance_of @@ -22,10 +23,10 @@ def HKDF(skm, outlen, salt=None, CTXinfo=b""): def derive_key(key, purpose, length=SecretBox.KEY_SIZE): if not isinstance(key, type(b"")): raise TypeError(type(key)) if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) - if not isinstance(length, int): raise TypeError(type(length)) + if not isinstance(length, six.integer_types): raise TypeError(type(length)) return HKDF(key, length, CTXinfo=purpose) -def derive_phase_key(side, phase): +def derive_phase_key(key, side, phase): assert isinstance(side, type("")), type(side) assert isinstance(phase, type("")), type(phase) side_bytes = side.encode("ascii") @@ -33,7 +34,7 @@ def derive_phase_key(side, phase): purpose = (b"wormhole:phase:" + sha256(side_bytes).digest() + sha256(phase_bytes).digest()) - return derive_key(purpose) + return derive_key(key, purpose) def decrypt_data(key, encrypted): assert isinstance(key, type(b"")), type(key) @@ -55,6 +56,7 @@ def encrypt_data(key, plaintext): @implementer(_interfaces.IKey) class Key(object): _appid = attrib(validator=instance_of(type(u""))) + _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @@ -106,9 +108,13 @@ class Key(object): assert isinstance(msg2, type(b"")) with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) - self._my_versions = {} - self._M.add_message("version", self._my_versions) self._B.got_verifier(derive_key(key, b"wormhole:verifier")) + phase = "version" + data_key = derive_phase_key(key, self._side, phase) + my_versions = {} # TODO: get from Wormhole? + plaintext = dict_to_bytes(my_versions) + encrypted = encrypt_data(data_key, plaintext) + self._M.add_message(phase, encrypted) self._R.got_key(key) S0_know_nothing.upon(got_code, enter=S1_know_code, outputs=[build_pake]) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 5d9642f..16dd882 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -121,7 +121,10 @@ class Mailbox(object): # from Send or Key @m.input() - def add_message(self, phase, body): pass + def add_message(self, phase, body): + assert isinstance(body, type(b"")), type(body) + #print("ADD_MESSAGE", phase, len(body)) + pass @m.output() @@ -142,7 +145,7 @@ class Mailbox(object): @m.output() def queue(self, phase, body): assert isinstance(phase, type("")), type(phase) - assert isinstance(body, type(b"")), type(body) + assert isinstance(body, type(b"")), (type(body), phase, body) self._pending_outbound[phase] = body @m.output() def store_mailbox_and_RC_tx_open_and_drain(self, mailbox): @@ -189,11 +192,11 @@ class Mailbox(object): self._accept(phase, body) def _accept(self, phase, body): if phase not in self._processed: - self._O.got_message(phase, body) self._processed.add(phase) + self._O.got_message(phase, body) @m.output() def dequeue(self, phase, body): - self._pending_outbound.pop(phase) + self._pending_outbound.pop(phase, None) @m.output() def record_mood(self, mood): self._mood = mood @@ -235,8 +238,7 @@ class Mailbox(object): S3B.upon(rx_claimed, enter=S3B, outputs=[]) S3B.upon(add_message, enter=S3B, outputs=[queue, RC_tx_add]) - S4A.upon(connected, enter=S4B, - outputs=[RC_tx_open, drain, RC_tx_release]) + S4A.upon(connected, enter=S4B, outputs=[RC_tx_open, drain, RC_tx_release]) S4A.upon(add_message, enter=S4A, outputs=[queue]) S4B.upon(lost, enter=S4A, outputs=[]) S4B.upon(add_message, enter=S4B, outputs=[queue, RC_tx_add]) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index e16f2b6..b21829d 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -25,6 +25,7 @@ class Order(object): def S1_yes_pake(self): pass def got_message(self, phase, body): + #print("ORDER[%s].got_message(%s)" % (self._side, phase)) assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) if phase == "pake": diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 47a3c12..ba2bbc3 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -35,7 +35,7 @@ class Receive(object): assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) assert self._key - data_key = derive_phase_key(self._side, phase) + data_key = derive_phase_key(self._key, self._side, phase) try: plaintext = decrypt_data(data_key, body) except CryptoError: diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index c65dc68..f6a34fa 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -26,12 +26,12 @@ class WSClient(websocket.WebSocketClientProtocol): self._RC.ws_open(self) def onMessage(self, payload, isBinary): - #print("onMessage") assert not isBinary try: self._RC.ws_message(payload) except: - print("LOGGING") + from twisted.python.failure import Failure + print("LOGGING", Failure()) log.err() raise @@ -57,6 +57,10 @@ class WSFactory(websocket.WebSocketClientFactory): #proto.wormhole_open = False return proto +def dmsg(side, text): + offset = int(side, 16) % 20 + print(" "*offset, text) + @attrs @implementer(_interfaces.IRendezvousConnector) class RendezvousConnector(object): @@ -66,7 +70,7 @@ class RendezvousConnector(object): _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) - DEBUG = False + DEBUG = True def __attrs_post_init__(self): self._ws = None @@ -124,6 +128,8 @@ class RendezvousConnector(object): # from our WSClient (the WebSocket protocol) def ws_open(self, proto): + if self.DEBUG: + dmsg(self._side, "R.connected") self._ws = proto self._tx("bind", appid=self._appid, side=self._side) self._C.connected() @@ -132,7 +138,11 @@ class RendezvousConnector(object): def ws_message(self, payload): msg = bytes_to_dict(payload) - if self.DEBUG and msg["type"]!="ack": print("DIS", msg["type"], msg) + if self.DEBUG and msg["type"]!="ack": + dmsg(self._side, "R.rx(%s %s%s)" % + (msg["type"], msg.get("phase",""), + "[mine]" if msg.get("side","") == self._side else "", + )) self._timing.add("ws_receive", _side=self._side, message=msg) mtype = msg["type"] meth = getattr(self, "_response_handle_"+mtype, None) @@ -143,6 +153,8 @@ class RendezvousConnector(object): return meth(msg) def ws_close(self, wasClean, code, reason): + if self.DEBUG: + dmsg(self._side, "R.lost") self._ws = None self._C.lost() self._M.lost() @@ -158,9 +170,10 @@ class RendezvousConnector(object): # their receives, and vice versa. They are also correlated with the # ACKs we get back from the server (which we otherwise ignore). There # are so few messages, 16 bits is enough to be mostly-unique. - if self.DEBUG: print("SEND", mtype) kwargs["id"] = bytes_to_hexstr(os.urandom(2)) kwargs["type"] = mtype + if self.DEBUG: + dmsg(self._side, "R.tx(%s %s)" % (mtype.upper(), kwargs.get("phase", ""))) payload = dict_to_bytes(kwargs) self._timing.add("ws_send", _side=self._side, **kwargs) self._ws.sendMessage(payload, False) @@ -184,6 +197,11 @@ class RendezvousConnector(object): def _response_handle_ack(self, msg): pass + def _response_handle_error(self, msg): + err = msg["error"] + orig = msg["orig"] + self._B.rx_error(err, orig) + def _response_handle_welcome(self, msg): self._B.rx_welcome(msg["welcome"]) diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 355a267..fd3e590 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -13,6 +13,9 @@ class Send(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + def __attrs_post_init__(self): + self._queue = [] + def wire(self, mailbox): self._M = _interfaces.IMailbox(mailbox) @@ -49,7 +52,8 @@ class Send(object): self._encrypt_and_send(phase, plaintext) def _encrypt_and_send(self, phase, plaintext): - data_key = derive_phase_key(self._side, phase) + assert self._key + data_key = derive_phase_key(self._key, self._side, phase) encrypted = encrypt_data(data_key, plaintext) self._M.add_message(phase, encrypted) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index d965d72..1977d16 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -7,7 +7,7 @@ from ._interfaces import IWormhole from .util import bytes_to_hexstr from .timing import DebugTiming from .journal import ImmediateJournal -from ._boss import Boss +from ._boss import Boss, WormholeError # We can provide different APIs to different apps: # * Deferreds @@ -61,6 +61,9 @@ class _DelegatedWormhole(object): def closed(self, result): self._delegate.wormhole_closed(result) +class WormholeClosed(Exception): + pass + @implementer(IWormhole) class _DeferredWormhole(object): def __init__(self): @@ -70,6 +73,7 @@ class _DeferredWormhole(object): self._verifier_observers = [] self._received_data = [] self._received_observers = [] + self._closed_observers = [] def _set_boss(self, boss): self._boss = boss @@ -107,6 +111,9 @@ class _DeferredWormhole(object): self._boss.send(plaintext) def close(self): self._boss.close() + d = defer.Deferred() + self._closed_observers.append(d) + return d # from below def got_code(self, code): @@ -127,7 +134,17 @@ class _DeferredWormhole(object): self._received_data.append(plaintext) def closed(self, result): - print("closed", result) + print("closed", result, type(result)) + if isinstance(result, WormholeError): + e = result + else: + e = WormholeClosed(result) + for d in self._verifier_observers: + d.errback(e) + for d in self._received_observers: + d.errback(e) + for d in self._closed_observers: + d.callback(result) def _wormhole(appid, relay_url, reactor, delegate=None, tor_manager=None, timing=None, From 97d1ff859b42836eeb5629018a4be11e3755edc2 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 25 Feb 2017 13:12:56 -0800 Subject: [PATCH 069/176] logic bug: M.S4B.close() must not re-send RELEASE --- docs/mailbox_close.dot | 4 +++- src/wormhole/_mailbox.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/mailbox_close.dot b/docs/mailbox_close.dot index 6765d84..b4daeba 100644 --- a/docs/mailbox_close.dot +++ b/docs/mailbox_close.dot @@ -27,6 +27,8 @@ digraph { SrcA -> Prc [label="connected"] Prc [shape="box" label="C.tx_release\nC.tx_close" color="orange"] Prc -> SrcB [color="orange"] + Prc2 [shape="box" label="C.tx_close" color="orange"] + Prc2 -> SrcB [color="orange"] SrcB [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] SrcB -> SrcA [label="lost"] SrcB -> ScB [label="rx_released" color="orange" fontcolor="orange"] @@ -65,7 +67,7 @@ digraph { S4A [label="S4A" style="dashed"] S4B [label="S4B" color="orange" style="dashed"] S4A -> SrcA [style="dashed"] - S4B -> Prc [color="orange" style="dashed"] + S4B -> Prc2 [color="orange" style="dashed"] S5A [label="S5A" style="dashed"] S5B [label="S5B" color="green" style="dashed"] diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 16dd882..628d40d 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -263,8 +263,7 @@ class Mailbox(object): S3B.upon(close, enter=SrcB, outputs=[record_mood_and_RC_tx_release_and_RC_tx_close]) S4A.upon(close, enter=SrcA, outputs=[record_mood]) - S4B.upon(close, enter=SrcB, - outputs=[record_mood_and_RC_tx_release_and_RC_tx_close]) + S4B.upon(close, enter=SrcB, outputs=[record_mood_and_RC_tx_close]) S5A.upon(close, enter=ScA, outputs=[record_mood]) S5B.upon(close, enter=ScB, outputs=[record_mood_and_RC_tx_close]) From c8be98880152c083af9daf0d0ab1ac7981430c29 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 25 Feb 2017 13:13:30 -0800 Subject: [PATCH 070/176] add some state-machine tracing needs warner/automat/36-tracing branch --- src/wormhole/_mailbox.py | 2 ++ src/wormhole/test/test_wormhole_new.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 628d40d..f3c9cda 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -10,6 +10,8 @@ from . import _interfaces class Mailbox(object): _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() + @m.setTrace() + def setTrace(): pass def __attrs_post_init__(self): self._mood = None diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 6e32a24..c917109 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -40,6 +40,9 @@ class New(ServerBase, unittest.TestCase): @inlineCallbacks def test_basic(self): w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + def trace(old_state, input, new_state): + print("W1._M[%s].%s -> [%s]" % (old_state, input, new_state)) + w1._boss._M.setTrace(trace) w1.allocate_code(2) code = yield w1.when_code() print("code:", code) From 02bea0036650c56993be358ecc587dd5e70f85f3 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 26 Feb 2017 02:33:14 -0800 Subject: [PATCH 071/176] dot: split Mailbox and Nameplate into separate machines add Terminator for shutdown --- docs/machines.dot | 62 +++-- docs/mailbox.dot | 225 ++++++------------ docs/mailbox_close.dot | 77 ------ docs/nameplate.dot | 101 ++++++++ docs/{nameplates.dot => nameplate_lister.dot} | 0 docs/terminator.dot | 50 ++++ 6 files changed, 260 insertions(+), 255 deletions(-) delete mode 100644 docs/mailbox_close.dot create mode 100644 docs/nameplate.dot rename docs/{nameplates.dot => nameplate_lister.dot} (100%) create mode 100644 docs/terminator.dot diff --git a/docs/machines.dot b/docs/machines.dot index 68a6a5e..c3713b6 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -2,6 +2,7 @@ digraph { Wormhole [shape="oval" color="blue" fontcolor="blue"] Boss [shape="box" label="Boss\n(manager)" color="blue" fontcolor="blue"] + Nameplate [shape="box" color="blue" fontcolor="blue"] Mailbox [shape="box" color="blue" fontcolor="blue"] Connection [label="Rendezvous\nConnector" shape="oval" color="blue" fontcolor="blue"] @@ -11,9 +12,9 @@ digraph { Send [shape="box" label="Send" color="blue" fontcolor="blue"] Receive [shape="box" label="Receive" color="blue" fontcolor="blue"] Code [shape="box" label="Code" color="blue" fontcolor="blue"] - Nameplates [shape="box" label="Nameplate\nLister" - color="blue" fontcolor="blue" - ] + NameplateLister [shape="box" label="Nameplate\nLister" + color="blue" fontcolor="blue"] + Terminator [shape="box" color="blue" fontcolor="blue"] Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] @@ -28,37 +29,41 @@ digraph { Boss -> Send [style="dashed" label="send"] - Boss -> Mailbox [style="dashed" - label="set_nameplate\nclose\n(once)" - ] + Boss -> Nameplate [style="dashed" label="set_nameplate"] #Boss -> Mailbox [color="blue"] - Mailbox -> Boss [style="dashed" label="closed\n(once)"] Mailbox -> Order [style="dashed" label="got_message (once)"] Boss -> Key [style="dashed" label="got_code"] Key -> Boss [style="dashed" label="got_verifier\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] #Boss -> Key [color="blue"] - Key -> Mailbox [style="dashed" label="add_message (pake)\nadd_message (version)"] + Key -> Mailbox [style="dashed" + label="add_message (pake)\nadd_message (version)"] Receive -> Send [style="dashed" label="got_verified_key"] Send -> Mailbox [style="dashed" label="add_message (phase)"] Key -> Receive [style="dashed" label="got_key"] Receive -> Boss [style="dashed" - label="happy\nscared\ngot_message"] + label="happy\nscared\ngot_message"] + Nameplate -> Connection [style="dashed" + label="tx_claim\ntx_release"] + Connection -> Nameplate [style="dashed" + label="connected\nlost\nrx_claimed\nrx_released"] + Mailbox -> Nameplate [style="dashed" label="release"] + Nameplate -> Mailbox [style="dashed" label="got_mailbox"] Mailbox -> Connection [style="dashed" - label="tx_claim\ntx_open\ntx_add\ntx_release\ntx_close\nstop" + label="tx_open\ntx_add\ntx_close" ] Connection -> Mailbox [style="dashed" - label="connected\nlost\nrx_claimed\nrx_message\nrx_released\nrx_closed\nstopped"] + label="connected\nlost\nrx_message\nrx_closed\nstopped"] - Connection -> Nameplates [style="dashed" - label="connected\nlost\nrx_nameplates" - ] - Nameplates -> Connection [style="dashed" - label="tx_list" - ] + Connection -> NameplateLister [style="dashed" + label="connected\nlost\nrx_nameplates" + ] + NameplateLister -> Connection [style="dashed" + label="tx_list" + ] #Boss -> Code [color="blue"] Connection -> Code [style="dashed" @@ -66,17 +71,24 @@ digraph { Code -> Connection [style="dashed" label="tx_allocate" ] - Nameplates -> Code [style="dashed" - label="got_nameplates" - ] - #Code -> Nameplates [color="blue"] - Code -> Nameplates [style="dashed" - label="refresh_nameplates" - ] + NameplateLister -> Code [style="dashed" + label="got_nameplates" + ] + #Code -> NameplateLister [color="blue"] + Code -> NameplateLister [style="dashed" + label="refresh_nameplates" + ] Boss -> Code [style="dashed" label="allocate_code\ninput_code\nset_code_code"] Code -> Boss [style="dashed" label="got_code"] - + Nameplate -> Terminator [style="dashed" label="nameplate_done"] + Mailbox -> Terminator [style="dashed" label="mailbox_done"] + Terminator -> Nameplate [style="dashed" label="close"] + Terminator -> Mailbox [style="dashed" label="close"] + Terminator -> Connection [style="dashed" label="stop"] + Connection -> Terminator [style="dashed" label="stopped"] + Terminator -> Boss [style="dashed" label="closed\n(once)"] + Boss -> Terminator [style="dashed" label="close"] } diff --git a/docs/mailbox.dot b/docs/mailbox.dot index 881c617..712fcd7 100644 --- a/docs/mailbox.dot +++ b/docs/mailbox.dot @@ -3,174 +3,93 @@ digraph { title [label="Message\nMachine" style="dotted"] - {rank=same; S0A P0_connected S0B} - S0A [label="S0A:\nknow nothing"] - S0B [label="S0B:\nknow nothing\n(bound)" color="orange"] - S0A -> P0_connected [label="connected"] - P0_connected [label="(nothing)" shape="box" style="dashed"] - P0_connected -> S0B + {rank=same; S0A S0B} + S0A [label="S0A:\nunknown"] + S0A -> S0B [label="connected"] + S0B [label="S0B:\nunknown\n(bound)" color="orange"] + S0B -> S0A [label="lost"] - S0A -> S1A [label="set_nameplate"] - S0B -> P2_connected [label="set_nameplate" color="orange" fontcolor="orange"] - P0A_queue [shape="box" label="queue" style="dotted"] S0A -> P0A_queue [label="add_message" style="dotted"] + P0A_queue [shape="box" label="queue" style="dotted"] P0A_queue -> S0A [style="dotted"] + S0B -> P0B_queue [label="add_message" style="dotted"] + P0B_queue [shape="box" label="queue" style="dotted"] + P0B_queue -> S0B [style="dotted"] - {rank=same; S1A P1A_queue} - S1A [label="S1A:\nnot claimed"] - S1A -> P2_connected [label="connected"] - S1A -> P1A_queue [label="add_message" style="dotted"] - P1A_queue [shape="box" label="queue" style="dotted"] - P1A_queue -> S1A [style="dotted"] + subgraph {rank=same; S1A P_open} + S0A -> S1A [label="got_mailbox"] + S1A [label="S1A:\nknown"] + S1A -> P_open [label="connected"] + S1A -> S2A [style="invis"] + P_open -> P2_connected [style="invis"] - {rank=same; S2A P2_connected S2B} - S2A [label="S2A:\nmaybe claimed"] + S0A -> S2A [style="invis"] + S0B -> P_open [label="got_mailbox" color="orange" fontcolor="orange"] + P_open [shape="box" + label="store mailbox\nRC.tx_open\nRC.tx_add(queued)" color="orange"] + P_open -> S2B [color="orange"] + + subgraph {rank=same; S2A S2B P2_connected} + S2A [label="S2A:\nknown\nmaybe opened"] + S2B [label="S2B:\nopened\n(bound)" color="green"] S2A -> P2_connected [label="connected"] - P2_connected [shape="box" label="RC.tx_claim" color="orange"] - P2_connected -> S2B [color="orange"] - S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] - #S2B -> SrB [label="close()" style="dashed"] - #SrB [label="SrB" style="dashed"] - #S2A -> SrA [label="close()" style="dashed"] - #SrA [label="SrA" style="dashed"] + S2B -> S2A [label="lost"] - #S2B -> S2A [label="lost"] # causes bad layout - S2B -> foo [label="lost"] - foo [label="" style="dashed"] - foo -> S2A + P2_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)"] + P2_connected -> S2B - S2A -> P2C_queue [label="add_message" style="dotted"] - P2C_queue [shape="box" label="queue" style="dotted"] - P2C_queue -> S2A [style="dotted"] - S2B -> P2B_queue [label="add_message" style="dotted"] - P2B_queue [shape="box" label="queue" style="dotted"] - P2B_queue -> S2B [style="dotted"] + S2A -> P2_queue [label="add_message" style="dotted"] + P2_queue [shape="box" label="queue" style="dotted"] + P2_queue -> S2A [style="dotted"] - S1A -> S3A [label="(none)" style="invis"] - S2B -> P_open [label="rx_claimed" color="orange" fontcolor="orange"] - P_open [shape="box" label="store mailbox\nRC.tx_open\nRC.tx_add(queued)" color="orange"] - P_open -> S3B [color="orange"] + S2B -> P2_send [label="add_message"] + P2_send [shape="box" label="queue\nRC.tx_add(msg)"] + P2_send -> S2B - subgraph {rank=same; S3A S3B P3_connected} - S3A [label="S3A:\nclaimed\nopened >=once"] - S3B [label="S3B:\nclaimed\nmaybe open now\n(bound)" color="orange"] + {rank=same; P2_send P2_close P2_process_theirs} + P2_process_theirs -> P2_close [style="invis"] + S2B -> P2_process_ours [label="rx_message\n(ours)"] + P2_process_ours [shape="box" label="dequeue"] + P2_process_ours -> S2B + S2B -> P2_process_theirs [label="rx_message\n(theirs)" + color="orange" fontcolor="orange"] + P2_process_theirs [shape="box" color="orange" + label="N.release\nO.got_message if new\nrecord" + ] + P2_process_theirs -> S2B [color="orange"] + + S2B -> P2_close [label="close" color="red"] + P2_close [shape="box" label="RC.tx_close" color="red"] + P2_close -> S3B [color="red"] + + subgraph {rank=same; S3A P3_connected S3B} + S3A [label="S3A:\nclosing"] S3A -> P3_connected [label="connected"] + P3_connected [shape="box" label="RC.tx_close"] + P3_connected -> S3B + #S3A -> S3A [label="add_message"] # implicit + S3B [label="S3B:\nclosing\n(bound)" color="red"] + S3B -> S3B [label="add_message\nrx_message\nclose"] S3B -> S3A [label="lost"] - P3_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)"] - P3_connected -> S3B + subgraph {rank=same; P3A_done P3B_done} + P3A_done [shape="box" label="T.mailbox_done" color="red"] + P3A_done -> S4A + S3B -> P3B_done [label="rx_closed" color="red"] + P3B_done [shape="box" label="T.mailbox_done" color="red"] + P3B_done -> S4B - S3A -> P3_queue [label="add_message" style="dotted"] - P3_queue [shape="box" label="queue" style="dotted"] - P3_queue -> S3A [style="dotted"] - - S3B -> S3B [label="rx_claimed"] - - S3B -> P3_send [label="add_message"] - P3_send [shape="box" label="queue\nRC.tx_add(msg)"] - P3_send -> S3B - - S3A -> S4A [label="(none)" style="invis"] - S3B -> P3_process_ours [label="rx_message\n(ours)"] - P3_process_ours [shape="box" label="dequeue"] - P3_process_ours -> S3B - S3B -> P3_process_theirs [label="rx_message\n(theirs)" - color="orange" fontcolor="orange"] - P3_process_theirs [shape="box" color="orange" - label="RC.tx_release\nO.got_message if new\nrecord" - ] - /* pay attention to the race here: this process_message() will - deliver msg_pake to the WormholeMachine, which will compute_key() and - send(version), and we're in between S1A (where send gets - queued) and S3A (where send gets sent and queued), and we're no - longer passing through the P3_connected phase (which drains the queue). - So there's a real possibility of the outbound msg_version getting - dropped on the floor, or put in a queue but never delivered. */ - P3_process_theirs -> S4B [color="orange"] - - subgraph {rank=same; S4A P4_connected S4B} - S4A [label="S4A:\nmaybe released\nopened >=once\n"] - - S4B [label="S4B:\nmaybe released\nmaybe open now\n(bound)" color="orange"] - S4A -> P4_connected [label="connected"] - P4_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)\nRC.tx_release"] - S4B -> P4_send [label="add_message"] - P4_send [shape="box" label="queue\nRC.tx_add(msg)"] - P4_send -> S4B - S4A -> P4_queue [label="add_message" style="dotted"] - P4_queue [shape="box" label="queue" style="dotted"] - P4_queue -> S4A [style="dotted"] - - P4_connected -> S4B + subgraph {rank=same; S4A S4B} + S4A [label="S4A:\nclosed"] + S4B [label="S4B:\nclosed"] + S4A -> S4B [label="connected"] S4B -> S4A [label="lost"] - S4B -> P4_process_ours [label="rx_message\n(ours)"] - P4_process_ours [shape="box" label="dequeue"] - P4_process_ours -> S4B - S4B -> P4_process_theirs [label="rx_message\n(theirs)"] - P4_process_theirs [shape="box" label="O.got_message if new\nrecord"] - P4_process_theirs -> S4B - - S4A -> S5A [label="(none)" style="invis"] - S4B -> S5B [label="rx released" color="orange" fontcolor="orange"] - - P4_queue -> S5A [style="invis"] - subgraph {S5A P5_connected S5B} - {rank=same; S5A P5_connected S5B} - - S5A [label="S5A:\nreleased\nopened >=once"] - S5A -> P5_connected [label="connected"] - P5_connected [shape="box" label="RC.tx_open\nRC.tx_add(queued)"] - - S5B -> P5_send [label="add_message" color="green" fontcolor="green"] - P5_send [shape="box" label="queue\nRC.tx_add(msg)" color="green"] - P5_send -> S5B [color="green"] - S5A -> P5_queue [label="add_message" style="dotted"] - P5_queue [shape="box" label="queue" style="dotted"] - P5_queue -> S5A [style="dotted"] - - P5_connected -> S5B - S5B [label="S5B:\nreleased\nmaybe open now\n(bound)" color="green"] - S5B -> S5A [label="lost"] - - S5B -> P5_process_ours [label="rx_message\n(ours)"] - P5_process_ours [shape="box" label="dequeue"] - P5_process_ours -> S5B - S5B -> P5_process_theirs [label="rx_message\n(theirs)"] - P5_process_theirs [shape="box" label="O.got_message if new\nrecord"] - P5_process_theirs -> S5B - - foo5 [label="" style="invis"] - S5A -> foo5 [style="invis"] - foo5 -> P5_close [style="invis"] - S5B -> P5_close [label="close" style="dashed" color="orange" fontcolor="orange"] - P5_close [shape="box" label="tx_close" style="dashed" color="orange"] + S4B -> S4B [label="add_message\nrx_message\nclose"] + S0A -> P3A_done [label="close" color="red"] + S0B -> P3B_done [label="close" color="red"] + S1A -> P3A_done [label="close" color="red"] + S2A -> S3A [label="close" color="red"] + } - -/* - -Can this be split into one machine for the Nameplate, and a second for the -Mailbox? - -Nameplate: - -* 0: know nothing (connected, not connected) -* 1: know nameplate, never claimed, need to claim -* 2: maybe claimed, need to claim -* 3: definitely claimed, need to claim -* 4: definitely claimed, need to release -* 5: maybe released -* 6: definitely released - -Mailbox: -* 0: unknown -* 1: know mailbox, need open, not open -* 2: know mailbox, need open, maybe open -* 3: definitely open, need open -* 4: need closed, maybe open -* 5: need closed, maybe closed ? -* 6: definitely closed - - -*/ \ No newline at end of file diff --git a/docs/mailbox_close.dot b/docs/mailbox_close.dot deleted file mode 100644 index b4daeba..0000000 --- a/docs/mailbox_close.dot +++ /dev/null @@ -1,77 +0,0 @@ -digraph { - /* M_close pathways */ - title [label="Mailbox\nClose\nMachine" style="dotted"] - title -> S2B [style="invis"] - - /* All dashed states are from the main Mailbox Machine diagram, and - all dashed lines indicate M_close() pathways in from those states. - Within this graph, all M_close() events leave the state unchanged. */ - - SrA [label="SrA:\nwaiting for:\nrelease"] - SrA -> Pr [label="connected"] - Pr [shape="box" label="C.tx_release" color="orange"] - Pr -> SrB [color="orange"] - SrB [label="SrB:\nwaiting for:\nrelease" color="orange"] - SrB -> SrA [label="lost"] - SrB -> P_stop [label="rx_released" color="orange" fontcolor="orange"] - - ScA [label="ScA:\nwaiting for:\nclosed"] - ScA -> Pc [label="connected"] - Pc [shape="box" label="C.tx_close" color="orange"] - Pc -> ScB [color="orange"] - ScB [label="ScB:\nwaiting for:\nclosed" color="orange"] - ScB -> ScA [label="lost"] - ScB -> P_stop [label="rx_closed" color="orange" fontcolor="orange"] - - SrcA [label="SrcA:\nwaiting for:\nrelease\nclose"] - SrcA -> Prc [label="connected"] - Prc [shape="box" label="C.tx_release\nC.tx_close" color="orange"] - Prc -> SrcB [color="orange"] - Prc2 [shape="box" label="C.tx_close" color="orange"] - Prc2 -> SrcB [color="orange"] - SrcB [label="SrcB:\nwaiting for:\nrelease\nclosed" color="orange"] - SrcB -> SrcA [label="lost"] - SrcB -> ScB [label="rx_released" color="orange" fontcolor="orange"] - SrcB -> SrB [label="rx_closed" color="orange" fontcolor="orange"] - - - P_stop [shape="box" label="C.stop"] - P_stop -> SsB - - SsB [label="SsB: closed\nstopping"] - SsB -> Pss [label="stopped"] - Pss [shape="box" label="B.closed"] - Pss -> Ss - - Ss [label="Ss: closed" color="green"] - - S0A [label="S0A" style="dashed"] - S0A -> P_stop [style="dashed"] - S0B [label="S0B" style="dashed" color="orange"] - S0B -> P_stop [style="dashed" color="orange"] - - {rank=same; S2A S2B S3A S3B S4A S4B S5A S5B} - S1A [label="S1A" style="dashed"] - S1A -> P_stop [style="dashed"] - - S2A [label="S2A" style="dashed"] - S2A -> SrA [label="stop" style="dashed"] - S2B [label="S2B" color="orange" style="dashed"] - S2B -> Pr [color="orange" style="dashed"] - - S3A [label="S3A" style="dashed"] - S3B [label="S3B" color="orange" style="dashed"] - S3A -> SrcA [style="dashed"] - S3B -> Prc [color="orange" style="dashed"] - - S4A [label="S4A" style="dashed"] - S4B [label="S4B" color="orange" style="dashed"] - S4A -> SrcA [style="dashed"] - S4B -> Prc2 [color="orange" style="dashed"] - - S5A [label="S5A" style="dashed"] - S5B [label="S5B" color="green" style="dashed"] - S5A -> ScA [style="dashed"] - S5B -> Pc [style="dashed" color="green"] - -} diff --git a/docs/nameplate.dot b/docs/nameplate.dot new file mode 100644 index 0000000..4e6e682 --- /dev/null +++ b/docs/nameplate.dot @@ -0,0 +1,101 @@ +digraph { + /* new idea */ + + title [label="Nameplate\nMachine" style="dotted"] + title -> S0A [style="invis"] + + {rank=same; S0A S0B} + S0A [label="S0A:\nknow nothing"] + S0B [label="S0B:\nknow nothing\n(bound)" color="orange"] + S0A -> S0B [label="connected"] + S0B -> S0A [label="lost"] + + S0A -> S1A [label="set_nameplate"] + S0B -> P2_connected [label="set_nameplate" color="orange" fontcolor="orange"] + + S1A [label="S1A:\nnever claimed"] + S1A -> P2_connected [label="connected"] + + S1A -> S2A [style="invis"] + S1B [style="invis"] + S0B -> S1B [style="invis"] + S1B -> S2B [style="invis"] + {rank=same; S1A S1B} + S1A -> S1B [style="invis"] + + {rank=same; S2A P2_connected S2B} + S2A [label="S2A:\nmaybe claimed"] + S2A -> P2_connected [label="connected"] + P2_connected [shape="box" + label="RC.tx_claim" color="orange"] + P2_connected -> S2B [color="orange"] + S2B [label="S2B:\nmaybe claimed\n(bound)" color="orange"] + + #S2B -> S2A [label="lost"] # causes bad layout + S2B -> foo2 [label="lost"] + foo2 [label="" style="dashed"] + foo2 -> S2A + + S2A -> S3A [label="(none)" style="invis"] + S2B -> P_open [label="rx_claimed" color="orange" fontcolor="orange"] + P_open [shape="box" label="M.got_mailbox" color="orange"] + P_open -> S3B [color="orange"] + + subgraph {rank=same; S3A S3B} + S3A [label="S3A:\nclaimed"] + S3B [label="S3B:\nclaimed\n(bound)" color="orange"] + S3A -> S3B [label="connected"] + S3B -> foo3 [label="lost"] + foo3 [label="" style="dashed"] + foo3 -> S3A + + #S3B -> S3B [label="rx_claimed"] # shouldn't happen + + S3B -> P3_release [label="release" color="orange" fontcolor="orange"] + P3_release [shape="box" color="orange" label="RC.tx_release"] + P3_release -> S4B [color="orange"] + + subgraph {rank=same; S4A P4_connected S4B} + S4A [label="S4A:\nmaybe released\n"] + + S4B [label="S4B:\nmaybe released\n(bound)" color="orange"] + S4A -> P4_connected [label="connected"] + P4_connected [shape="box" label="RC.tx_release"] + S4B -> S4B [label="release"] + + P4_connected -> S4B + S4B -> foo4 [label="lost"] + foo4 [label="" style="dashed"] + foo4 -> S4A + + S4A -> S5B [style="invis"] + P4_connected -> S5B [style="invis"] + + subgraph {rank=same; P5A_done P5B_done} + S4B -> P5B_done [label="rx released" color="orange" fontcolor="orange"] + P5B_done [shape="box" label="T.nameplate_done" color="orange"] + P5B_done -> S5B [color="orange"] + + subgraph {rank=same; S5A S5B} + S5A [label="S5A:\nreleased"] + S5A -> S5B [label="connected"] + S5B -> S5A [label="lost"] + S5B [label="S5B:\nreleased" color="green"] + + S5B -> S5B [label="release\nclose"] + + P5A_done [shape="box" label="T.nameplate_done"] + P5A_done -> S5A + + S0A -> P5A_done [label="close" color="red"] + S1A -> P5A_done [label="close" color="red"] + S2A -> S4A [label="close" color="red"] + S3A -> S4A [label="close" color="red"] + S4A -> S4A [label="close" color="red"] + S0B -> P5B_done [label="close" color="red"] + S2B -> P3_release [label="close" color="red"] + S3B -> P3_release [label="close" color="red"] + S4B -> S4B [label="close" color="red"] + + +} diff --git a/docs/nameplates.dot b/docs/nameplate_lister.dot similarity index 100% rename from docs/nameplates.dot rename to docs/nameplate_lister.dot diff --git a/docs/terminator.dot b/docs/terminator.dot new file mode 100644 index 0000000..2fe01b9 --- /dev/null +++ b/docs/terminator.dot @@ -0,0 +1,50 @@ +digraph { + /* M_close pathways */ + title [label="Terminator\nMachine" style="dotted"] + + initial [style="invis"] + initial -> Snm [style="dashed"] + + Snm [label="Snm:\nnameplate active\nmailbox active" color="orange"] + Sn [label="Sn:\nnameplate active\nmailbox done"] + Sm [label="Sm:\nnameplate done\nmailbox active" color="green"] + S0 [label="S0:\nnameplate done\nmailbox done"] + + Snm -> Sn [label="mailbox_done"] + Snm -> Sm [label="nameplate_done" color="orange"] + Sn -> S0 [label="nameplate_done"] + Sm -> S0 [label="mailbox_done"] + + Snm -> Snm_closing [label="close"] + Sn -> Sn_closing [label="close"] + Sm -> Sm_closing [label="close" color="red"] + S0 -> P_stop [label="close"] + + Snm_closing [label="Snm_closing:\nnameplate active\nmailbox active" + style="dashed"] + Sn_closing [label="Sn_closing:\nnameplate active\nmailbox done" + style="dashed"] + Sm_closing [label="Sm_closing:\nnameplate done\nmailbox active" + style="dashed" color="red"] + + Snm_closing -> Sn_closing [label="mailbox_done"] + Snm_closing -> Sm_closing [label="nameplate_done"] + Sn_closing -> P_stop [label="nameplate_done"] + Sm_closing -> P_stop [label="mailbox_done" color="red"] + + {rank=same; S_stopping Pss S_stopped} + P_stop [shape="box" label="C.stop" color="red"] + P_stop -> S_stopping [color="red"] + + S_stopping [label="S_stopping" color="red"] + S_stopping -> Pss [label="stopped"] + Pss [shape="box" label="B.closed"] + Pss -> S_stopped + + S_stopped [label="S_stopped"] + + other [shape="box" style="dashed" + label="close -> N.close, M.close"] + + +} From 26adaabe1829c2ffc8d8dda65145cf4bc8783cf1 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 26 Feb 2017 03:57:58 -0800 Subject: [PATCH 072/176] implement new split nameplate/mailbox/terminator fails even worse than before, of course --- docs/boss.dot | 10 +- docs/terminator.dot | 42 +++--- src/wormhole/_boss.py | 22 +-- src/wormhole/_interfaces.py | 4 + src/wormhole/_mailbox.py | 214 ++++++++---------------------- src/wormhole/_nameplate.py | 161 ++++++++++++++++------ src/wormhole/_nameplate_lister.py | 65 +++++++++ src/wormhole/_rendezvous.py | 8 +- src/wormhole/_terminator.py | 104 +++++++++++++++ 9 files changed, 394 insertions(+), 236 deletions(-) create mode 100644 src/wormhole/_nameplate_lister.py create mode 100644 src/wormhole/_terminator.py diff --git a/docs/boss.dot b/docs/boss.dot index 053d06a..7f8d255 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -18,11 +18,11 @@ digraph { S0 -> P0_build [label="set_code"] S0 -> P_close_error [label="rx_error"] - P_close_error [shape="box" label="M.close(errory)"] + P_close_error [shape="box" label="T.close(errory)"] P_close_error -> S_closing S0 -> P_close_lonely [label="close"] - P0_build [shape="box" label="W.got_code\nM.set_nameplate\nK.got_code"] + P0_build [shape="box" label="W.got_code\nN.set_nameplate\nK.got_code"] P0_build -> S1 S1 [label="S1: lonely" color="orange"] @@ -31,15 +31,15 @@ digraph { S1 -> P_close_error [label="rx_error"] S1 -> P_close_scary [label="scared" color="red"] S1 -> P_close_lonely [label="close"] - P_close_lonely [shape="box" label="M.close(lonely)"] + P_close_lonely [shape="box" label="T.close(lonely)"] P_close_lonely -> S_closing - P_close_scary [shape="box" label="M.close(scary)" color="red"] + P_close_scary [shape="box" label="T.close(scary)" color="red"] P_close_scary -> S_closing [color="red"] S2 [label="S2: happy" color="green"] S2 -> P2_close [label="close"] - P2_close [shape="box" label="M.close(happy)"] + P2_close [shape="box" label="T.close(happy)"] P2_close -> S_closing S2 -> P2_got_message [label="got_message"] diff --git a/docs/terminator.dot b/docs/terminator.dot index 2fe01b9..749eb3c 100644 --- a/docs/terminator.dot +++ b/docs/terminator.dot @@ -3,37 +3,37 @@ digraph { title [label="Terminator\nMachine" style="dotted"] initial [style="invis"] - initial -> Snm [style="dashed"] + initial -> Snmo [style="dashed"] - Snm [label="Snm:\nnameplate active\nmailbox active" color="orange"] - Sn [label="Sn:\nnameplate active\nmailbox done"] - Sm [label="Sm:\nnameplate done\nmailbox active" color="green"] - S0 [label="S0:\nnameplate done\nmailbox done"] + Snmo [label="Snmo:\nnameplate active\nmailbox active\nopen" color="orange"] + Sno [label="Sno:\nnameplate active\nmailbox done\nopen"] + Smo [label="Smo:\nnameplate done\nmailbox active\nopen" color="green"] + S0o [label="S0o:\nnameplate done\nmailbox done\nopen"] - Snm -> Sn [label="mailbox_done"] - Snm -> Sm [label="nameplate_done" color="orange"] - Sn -> S0 [label="nameplate_done"] - Sm -> S0 [label="mailbox_done"] + Snmo -> Sno [label="mailbox_done"] + Snmo -> Smo [label="nameplate_done" color="orange"] + Sno -> S0o [label="nameplate_done"] + Smo -> S0o [label="mailbox_done"] - Snm -> Snm_closing [label="close"] - Sn -> Sn_closing [label="close"] - Sm -> Sm_closing [label="close" color="red"] - S0 -> P_stop [label="close"] + Snmo -> Snm [label="close"] + Sno -> Sn [label="close"] + Smo -> Sm [label="close" color="red"] + S0o -> P_stop [label="close"] - Snm_closing [label="Snm_closing:\nnameplate active\nmailbox active" + Snm [label="Snm:\nnameplate active\nmailbox active\nclosing" style="dashed"] - Sn_closing [label="Sn_closing:\nnameplate active\nmailbox done" + Sn [label="Sn:\nnameplate active\nmailbox done\nclosing" style="dashed"] - Sm_closing [label="Sm_closing:\nnameplate done\nmailbox active" + Sm [label="Sm:\nnameplate done\nmailbox active\nclosing" style="dashed" color="red"] - Snm_closing -> Sn_closing [label="mailbox_done"] - Snm_closing -> Sm_closing [label="nameplate_done"] - Sn_closing -> P_stop [label="nameplate_done"] - Sm_closing -> P_stop [label="mailbox_done" color="red"] + Snm -> Sn [label="mailbox_done"] + Snm -> Sm [label="nameplate_done"] + Sn -> P_stop [label="nameplate_done"] + Sm -> P_stop [label="mailbox_done" color="red"] {rank=same; S_stopping Pss S_stopped} - P_stop [shape="box" label="C.stop" color="red"] + P_stop [shape="box" label="RC.stop" color="red"] P_stop -> S_stopping [color="red"] S_stopping [label="S_stopping" color="red"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 1b5dd9a..dbe59fa 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -7,14 +7,16 @@ from attr.validators import provides, instance_of from twisted.python import log from automat import MethodicalMachine from . import _interfaces +from ._nameplate import Nameplate from ._mailbox import Mailbox from ._send import Send from ._order import Order from ._key import Key from ._receive import Receive from ._rendezvous import RendezvousConnector -from ._nameplate import NameplateListing +from ._nameplate_lister import NameplateListing from ._code import Code +from ._terminator import Terminator from .errors import WrongPasswordError from .util import bytes_to_dict @@ -34,6 +36,7 @@ class Boss(object): m = MethodicalMachine() def __attrs_post_init__(self): + self._N = Nameplate() self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) self._O = Order(self._side, self._timing) @@ -44,8 +47,10 @@ class Boss(object): self._timing) self._NL = NameplateListing() self._C = Code(self._timing) + self._T = Terminator() - self._M.wire(self, self._RC, self._O) + self._N.wire(self._M, self._RC, self._T) + self._M.wire(self._N, self._RC, self._O, self._T) self._S.wire(self._M) self._O.wire(self._K, self._R) self._K.wire(self, self._M, self._R) @@ -53,6 +58,7 @@ class Boss(object): self._RC.wire(self, self._M, self._C, self._NL) self._NL.wire(self._RC, self._C) self._C.wire(self, self._RC, self._NL) + self._T.wire(self, self._RC, self._N, self._M) self._next_tx_phase = 0 self._next_rx_phase = 0 @@ -137,7 +143,7 @@ class Boss(object): @m.input() def got_verifier(self, verifier): pass - # Mailbox sends closed + # Terminator sends closed @m.input() def closed(self): pass @@ -149,7 +155,7 @@ class Boss(object): @m.output() def do_got_code(self, code): nameplate = code.split("-")[0] - self._M.set_nameplate(nameplate) + self._N.set_nameplate(nameplate) self._K.got_code(code) self._W.got_code(code) @m.output() @@ -167,19 +173,19 @@ class Boss(object): @m.output() def close_error(self, err, orig): self._result = WormholeError(err) - self._M.close("errory") + self._T.close("errory") @m.output() def close_scared(self): self._result = WrongPasswordError() - self._M.close("scary") + self._T.close("scary") @m.output() def close_lonely(self): self._result = WormholeError("lonely") - self._M.close("lonely") + self._T.close("lonely") @m.output() def close_happy(self): self._result = "happy" - self._M.close("happy") + self._T.close("happy") @m.output() def W_got_verifier(self, verifier): diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index ae8b459..0fbeaa4 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -4,6 +4,8 @@ class IWormhole(Interface): pass class IBoss(Interface): pass +class INameplate(Interface): + pass class IMailbox(Interface): pass class ISend(Interface): @@ -20,6 +22,8 @@ class INameplateLister(Interface): pass class ICode(Interface): pass +class ITerminator(Interface): + pass class ITiming(Interface): pass diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index f3c9cda..b6a6f5d 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -14,16 +14,15 @@ class Mailbox(object): def setTrace(): pass def __attrs_post_init__(self): - self._mood = None - self._nameplate = None self._mailbox = None self._pending_outbound = {} self._processed = set() - def wire(self, boss, rendezvous_connector, ordering): - self._B = _interfaces.IBoss(boss) + def wire(self, nameplate, rendezvous_connector, ordering, terminator): + self._N = _interfaces.INameplate(nameplate) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._O = _interfaces.IOrder(ordering) + self._T = _interfaces.ITerminator(terminator) # all -A states: not connected # all -B states: yes connected @@ -35,73 +34,49 @@ class Mailbox(object): @m.state() def S0B(self): pass - # S1: nameplate known, not claimed + # S1: mailbox known, not opened @m.state() def S1A(self): pass - # S2: nameplate known, maybe claimed + # S2: mailbox known, opened + # We've definitely tried to open the mailbox at least once, but it must + # be re-opened with each connection, because open() is also subscribe() @m.state() def S2A(self): pass @m.state() def S2B(self): pass - # S3: nameplate claimed, mailbox known, maybe open + # S3: closing @m.state() def S3A(self): pass @m.state() def S3B(self): pass - # S4: mailbox maybe open, nameplate maybe released - # We've definitely opened the mailbox at least once, but it must be - # re-opened with each connection, because open() is also subscribe() - @m.state() - def S4A(self): pass - @m.state() - def S4B(self): pass - - # S5: mailbox maybe open, nameplate released - @m.state() - def S5A(self): pass - @m.state() - def S5B(self): pass - - # Src: waiting for release+close - @m.state() - def SrcA(self): pass - @m.state() - def SrcB(self): pass - # Sr: closed (or never opened), waiting for release - @m.state() - def SrA(self): pass - @m.state() - def SrB(self): pass - # Sc: released (or never claimed), waiting for close - @m.state() - def ScA(self): pass - @m.state() - def ScB(self): pass - # Ss: closed and released, waiting for stop - @m.state() - def SsB(self): pass + # S4: closed. We no longer care whether we're connected or not + #@m.state() + #def S4A(self): pass + #@m.state() + #def S4B(self): pass @m.state(terminal=True) - def Ss(self): pass + def S4(self): pass + S4A = S4 + S4B = S4 - # from Boss - @m.input() - def set_nameplate(self, nameplate): pass + # from Terminator @m.input() def close(self, mood): pass + # from Nameplate + @m.input() + def got_mailbox(self, mailbox): pass + # from RendezvousConnector @m.input() def connected(self): pass @m.input() def lost(self): pass - @m.input() - def rx_claimed(self, mailbox): pass - def rx_message(self, side, phase, body): assert isinstance(side, type("")), type(side) assert isinstance(phase, type("")), type(phase) @@ -115,11 +90,7 @@ class Mailbox(object): @m.input() def rx_message_theirs(self, phase, body): pass @m.input() - def rx_released(self): pass - @m.input() def rx_closed(self): pass - @m.input() - def stopped(self): pass # from Send or Key @m.input() @@ -130,16 +101,8 @@ class Mailbox(object): @m.output() - def record_nameplate(self, nameplate): - self._nameplate = nameplate - @m.output() - def record_nameplate_and_RC_tx_claim(self, nameplate): - self._nameplate = nameplate - self._RC.tx_claim(self._nameplate) - @m.output() - def RC_tx_claim(self): - # when invoked via M.connected(), we must use the stored nameplate - self._RC.tx_claim(self._nameplate) + def record_mailbox(self, mailbox): + self._mailbox = mailbox @m.output() def RC_tx_open(self): assert self._mailbox @@ -150,7 +113,7 @@ class Mailbox(object): assert isinstance(body, type(b"")), (type(body), phase, body) self._pending_outbound[phase] = body @m.output() - def store_mailbox_and_RC_tx_open_and_drain(self, mailbox): + def record_mailbox_and_RC_tx_open_and_drain(self, mailbox): self._mailbox = mailbox self._RC.tx_open(mailbox) self._drain() @@ -166,29 +129,15 @@ class Mailbox(object): assert isinstance(body, type(b"")), type(body) self._RC.tx_add(phase, body) @m.output() - def RC_tx_release(self): - self._RC.tx_release() - @m.output() - def RC_tx_release_and_accept(self, phase, body): - self._RC.tx_release() + def N_release_and_accept(self, phase, body): + self._N.release() self._accept(phase, body) @m.output() - def record_mood_and_RC_tx_release(self, mood): - self._mood = mood - self._RC.tx_release() - @m.output() - def record_mood_and_RC_tx_release_and_RC_tx_close(self, mood): - self._mood = mood - self._RC.tx_release() - self._RC.tx_close(self._mood) - @m.output() def RC_tx_close(self): assert self._mood - self._RC.tx_close(self._mood) - @m.output() - def record_mood_and_RC_tx_close(self, mood): - self._mood = mood - self._RC.tx_close(self._mood) + self._RC_tx_close() + def _RC_tx_close(self): + self._RC.tx_close(self._mailbox, self._mood) @m.output() def accept(self, phase, body): self._accept(phase, body) @@ -203,98 +152,49 @@ class Mailbox(object): def record_mood(self, mood): self._mood = mood @m.output() - def record_mood_and_RC_stop(self, mood): + def record_mood_and_RC_tx_close(self, mood): self._mood = mood - self._RC_stop() + self._RC_rx_close() @m.output() - def RC_stop(self): - self._RC_stop() - def _RC_stop(self): - self._RC.stop() + def ignore_mood_and_T_mailbox_done(self, mood): + self._T.mailbox_done() @m.output() - def W_closed(self): - self._B.closed() + def T_mailbox_done(self): + self._T.mailbox_done() S0A.upon(connected, enter=S0B, outputs=[]) - S0A.upon(set_nameplate, enter=S1A, outputs=[record_nameplate]) + S0A.upon(got_mailbox, enter=S1A, outputs=[record_mailbox]) S0A.upon(add_message, enter=S0A, outputs=[queue]) + S0A.upon(close, enter=S4A, outputs=[ignore_mood_and_T_mailbox_done]) S0B.upon(lost, enter=S0A, outputs=[]) - S0B.upon(set_nameplate, enter=S2B, outputs=[record_nameplate_and_RC_tx_claim]) S0B.upon(add_message, enter=S0B, outputs=[queue]) + S0B.upon(close, enter=S4B, outputs=[ignore_mood_and_T_mailbox_done]) + S0B.upon(got_mailbox, enter=S2B, + outputs=[record_mailbox_and_RC_tx_open_and_drain]) - S1A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) + S1A.upon(connected, enter=S2B, outputs=[RC_tx_open, drain]) S1A.upon(add_message, enter=S1A, outputs=[queue]) + S1A.upon(close, enter=S4A, outputs=[ignore_mood_and_T_mailbox_done]) - S2A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) + S2A.upon(connected, enter=S2B, outputs=[RC_tx_open, drain]) S2A.upon(add_message, enter=S2A, outputs=[queue]) + S2A.upon(close, enter=S3A, outputs=[record_mood]) S2B.upon(lost, enter=S2A, outputs=[]) - S2B.upon(add_message, enter=S2B, outputs=[queue]) - S2B.upon(rx_claimed, enter=S3B, - outputs=[store_mailbox_and_RC_tx_open_and_drain]) + S2B.upon(add_message, enter=S2B, outputs=[queue, RC_tx_add]) + S2B.upon(rx_message_theirs, enter=S2B, outputs=[N_release_and_accept]) + S2B.upon(rx_message_ours, enter=S2B, outputs=[dequeue]) + S2B.upon(close, enter=S3B, outputs=[record_mood_and_RC_tx_close]) - S3A.upon(connected, enter=S3B, outputs=[RC_tx_open, drain]) - S3A.upon(add_message, enter=S3A, outputs=[queue]) + S3A.upon(connected, enter=S3B, outputs=[RC_tx_close]) S3B.upon(lost, enter=S3A, outputs=[]) - S3B.upon(rx_message_theirs, enter=S4B, outputs=[RC_tx_release_and_accept]) - S3B.upon(rx_message_ours, enter=S3B, outputs=[dequeue]) - S3B.upon(rx_claimed, enter=S3B, outputs=[]) - S3B.upon(add_message, enter=S3B, outputs=[queue, RC_tx_add]) + S3B.upon(rx_closed, enter=S4B, outputs=[T_mailbox_done]) + S3B.upon(add_message, enter=S3B, outputs=[]) + S3B.upon(rx_message_theirs, enter=S3B, outputs=[]) + S3B.upon(rx_message_ours, enter=S3B, outputs=[]) - S4A.upon(connected, enter=S4B, outputs=[RC_tx_open, drain, RC_tx_release]) - S4A.upon(add_message, enter=S4A, outputs=[queue]) + S4A.upon(connected, enter=S4B, outputs=[]) S4B.upon(lost, enter=S4A, outputs=[]) - S4B.upon(add_message, enter=S4B, outputs=[queue, RC_tx_add]) - S4B.upon(rx_message_theirs, enter=S4B, outputs=[accept]) - S4B.upon(rx_message_ours, enter=S4B, outputs=[dequeue]) - S4B.upon(rx_released, enter=S5B, outputs=[]) - - S5A.upon(connected, enter=S5B, outputs=[RC_tx_open, drain]) - S5A.upon(add_message, enter=S5A, outputs=[queue]) - S5B.upon(lost, enter=S5A, outputs=[]) - S5B.upon(add_message, enter=S5B, outputs=[queue, RC_tx_add]) - S5B.upon(rx_message_theirs, enter=S5B, outputs=[accept]) - S5B.upon(rx_message_ours, enter=S5B, outputs=[dequeue]) - - if True: - S0A.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop]) - S0B.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop]) - S1A.upon(close, enter=SsB, outputs=[record_mood_and_RC_stop]) - S2A.upon(close, enter=SrA, outputs=[record_mood]) - S2B.upon(close, enter=SrB, outputs=[record_mood_and_RC_tx_release]) - S3A.upon(close, enter=SrcA, outputs=[record_mood]) - S3B.upon(close, enter=SrcB, - outputs=[record_mood_and_RC_tx_release_and_RC_tx_close]) - S4A.upon(close, enter=SrcA, outputs=[record_mood]) - S4B.upon(close, enter=SrcB, outputs=[record_mood_and_RC_tx_close]) - S5A.upon(close, enter=ScA, outputs=[record_mood]) - S5B.upon(close, enter=ScB, outputs=[record_mood_and_RC_tx_close]) - - SrcA.upon(connected, enter=SrcB, outputs=[RC_tx_release, RC_tx_close]) - SrcB.upon(lost, enter=SrcA, outputs=[]) - SrcB.upon(rx_closed, enter=SrB, outputs=[]) - SrcB.upon(rx_released, enter=ScB, outputs=[]) - - SrB.upon(lost, enter=SrA, outputs=[]) - SrA.upon(connected, enter=SrB, outputs=[RC_tx_release]) - SrB.upon(rx_released, enter=SsB, outputs=[RC_stop]) - - ScB.upon(lost, enter=ScA, outputs=[]) - ScB.upon(rx_closed, enter=SsB, outputs=[RC_stop]) - ScA.upon(connected, enter=ScB, outputs=[RC_tx_close]) - - SsB.upon(lost, enter=SsB, outputs=[]) - SsB.upon(stopped, enter=Ss, outputs=[W_closed]) - - SrcB.upon(rx_claimed, enter=SrcB, outputs=[]) - SrcB.upon(rx_message_theirs, enter=SrcB, outputs=[]) - SrcB.upon(rx_message_ours, enter=SrcB, outputs=[]) - SrB.upon(rx_claimed, enter=SrB, outputs=[]) - SrB.upon(rx_message_theirs, enter=SrB, outputs=[]) - SrB.upon(rx_message_ours, enter=SrB, outputs=[]) - ScB.upon(rx_claimed, enter=ScB, outputs=[]) - ScB.upon(rx_message_theirs, enter=ScB, outputs=[]) - ScB.upon(rx_message_ours, enter=ScB, outputs=[]) - SsB.upon(rx_claimed, enter=SsB, outputs=[]) - SsB.upon(rx_message_theirs, enter=SsB, outputs=[]) - SsB.upon(rx_message_ours, enter=SsB, outputs=[]) + S4.upon(add_message, enter=S4, outputs=[]) + S4.upon(rx_message_theirs, enter=S4, outputs=[]) + S4.upon(rx_message_ours, enter=S4, outputs=[]) diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index ee9631c..ddc6ac8 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -3,63 +3,142 @@ from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces -@implementer(_interfaces.INameplateLister) -class NameplateListing(object): +@implementer(_interfaces.INameplate) +class Nameplate(object): m = MethodicalMachine() + @m.setTrace() + def setTrace(): pass - def wire(self, rendezvous_connector, code): + def __init__(self): + self._nameplate = None + + def wire(self, mailbox, rendezvous_connector, terminator): + self._M = _interfaces.IMailbox(mailbox) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) - self._C = _interfaces.ICode(code) + self._T = _interfaces.ITerminator(terminator) - # Ideally, each API request would spawn a new "list_nameplates" message - # to the server, so the response would be maximally fresh, but that would - # require correlating server request+response messages, and the protocol - # is intended to be less stateful than that. So we offer a weaker - # freshness property: if no server requests are in flight, then a new API - # request will provoke a new server request, and the result will be - # fresh. But if a server request is already in flight when a second API - # request arrives, both requests will be satisfied by the same response. + # all -A states: not connected + # all -B states: yes connected + # B states serialize as A, so they deserialize as unconnected + # S0: know nothing @m.state(initial=True) - def S0A_idle_disconnected(self): pass + def S0A(self): pass @m.state() - def S1A_wanting_disconnected(self): pass - @m.state() - def S0B_idle_connected(self): pass - @m.state() - def S1B_wanting_connected(self): pass + def S0B(self): pass + # S1: nameplate known, never claimed + @m.state() + def S1A(self): pass + + # S2: nameplate known, maybe claimed + @m.state() + def S2A(self): pass + @m.state() + def S2B(self): pass + + # S3: nameplate claimed + @m.state() + def S3A(self): pass + @m.state() + def S3B(self): pass + + # S4: maybe released + @m.state() + def S4A(self): pass + @m.state() + def S4B(self): pass + + # S5: released + # we no longer care whether we're connected or not + #@m.state() + #def S5A(self): pass + #@m.state() + #def S5B(self): pass + @m.state() + def S5(self): pass + S5A = S5 + S5B = S5 + + # from Boss + @m.input() + def set_nameplate(self, nameplate): pass + + # from Mailbox + @m.input() + def release(self): pass + + # from Terminator + @m.input() + def close(self): pass + + # from RendezvousConnector @m.input() def connected(self): pass @m.input() def lost(self): pass + @m.input() - def refresh_nameplates(self): pass + def rx_claimed(self, mailbox): pass @m.input() - def rx_nameplates(self, message): pass + def rx_released(self): pass + @m.output() - def RC_tx_list(self): - self._RC.tx_list() + def record_nameplate(self, nameplate): + self._nameplate = nameplate @m.output() - def C_got_nameplates(self, message): - self._C.got_nameplates(message["nameplates"]) + def record_nameplate_and_RC_tx_claim(self, nameplate): + self._nameplate = nameplate + self._RC.tx_claim(self._nameplate) + @m.output() + def RC_tx_claim(self): + # when invoked via M.connected(), we must use the stored nameplate + self._RC.tx_claim(self._nameplate) + @m.output() + def M_got_mailbox(self, mailbox): + self._M.got_mailbox(mailbox) + @m.output() + def RC_tx_release(self): + assert self._nameplate + self._RC.tx_release(self._nameplate) + @m.output() + def T_nameplate_done(self): + self._T.nameplate_done() - S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) - S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) + S0A.upon(set_nameplate, enter=S1A, outputs=[record_nameplate]) + S0A.upon(connected, enter=S0B, outputs=[]) + S0A.upon(close, enter=S5A, outputs=[T_nameplate_done]) + S0B.upon(set_nameplate, enter=S2B, + outputs=[record_nameplate_and_RC_tx_claim]) + S0B.upon(lost, enter=S0A, outputs=[]) + S0B.upon(close, enter=S5A, outputs=[T_nameplate_done]) - S0A_idle_disconnected.upon(refresh_nameplates, - enter=S1A_wanting_disconnected, outputs=[]) - S1A_wanting_disconnected.upon(refresh_nameplates, - enter=S1A_wanting_disconnected, outputs=[]) - S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, - outputs=[RC_tx_list]) - S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, - outputs=[RC_tx_list]) - S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, - outputs=[C_got_nameplates]) - S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) - S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, - outputs=[RC_tx_list]) - S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, - outputs=[C_got_nameplates]) + S1A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) + S1A.upon(close, enter=S5A, outputs=[T_nameplate_done]) + + S2A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) + S2A.upon(close, enter=S4A, outputs=[]) + S2B.upon(lost, enter=S2A, outputs=[]) + S2B.upon(rx_claimed, enter=S3B, outputs=[M_got_mailbox]) + S2B.upon(close, enter=S4B, outputs=[RC_tx_release]) + + S3A.upon(connected, enter=S3B, outputs=[]) + S3A.upon(close, enter=S4A, outputs=[]) + S3B.upon(lost, enter=S3A, outputs=[]) + #S3B.upon(rx_claimed, enter=S3B, outputs=[]) # shouldn't happen + S3B.upon(release, enter=S4B, outputs=[RC_tx_release]) + S3B.upon(close, enter=S4B, outputs=[RC_tx_release]) + + S4A.upon(connected, enter=S4B, outputs=[RC_tx_release]) + S4A.upon(close, enter=S4A, outputs=[]) + S4B.upon(lost, enter=S4A, outputs=[]) + S4B.upon(rx_released, enter=S5B, outputs=[T_nameplate_done]) + S4B.upon(release, enter=S4B, outputs=[]) # mailbox is lazy + # Mailbox doesn't remember how many times it's sent a release, and will + # re-send a new one for each peer message it receives. Ignoring it here + # is easier than adding a new pair of states to Mailbox. + S4B.upon(close, enter=S4B, outputs=[]) + + S5A.upon(connected, enter=S5B, outputs=[]) + S5B.upon(lost, enter=S5A, outputs=[]) diff --git a/src/wormhole/_nameplate_lister.py b/src/wormhole/_nameplate_lister.py new file mode 100644 index 0000000..ee9631c --- /dev/null +++ b/src/wormhole/_nameplate_lister.py @@ -0,0 +1,65 @@ +from __future__ import print_function, absolute_import, unicode_literals +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces + +@implementer(_interfaces.INameplateLister) +class NameplateListing(object): + m = MethodicalMachine() + + def wire(self, rendezvous_connector, code): + self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) + self._C = _interfaces.ICode(code) + + # Ideally, each API request would spawn a new "list_nameplates" message + # to the server, so the response would be maximally fresh, but that would + # require correlating server request+response messages, and the protocol + # is intended to be less stateful than that. So we offer a weaker + # freshness property: if no server requests are in flight, then a new API + # request will provoke a new server request, and the result will be + # fresh. But if a server request is already in flight when a second API + # request arrives, both requests will be satisfied by the same response. + + @m.state(initial=True) + def S0A_idle_disconnected(self): pass + @m.state() + def S1A_wanting_disconnected(self): pass + @m.state() + def S0B_idle_connected(self): pass + @m.state() + def S1B_wanting_connected(self): pass + + @m.input() + def connected(self): pass + @m.input() + def lost(self): pass + @m.input() + def refresh_nameplates(self): pass + @m.input() + def rx_nameplates(self, message): pass + + @m.output() + def RC_tx_list(self): + self._RC.tx_list() + @m.output() + def C_got_nameplates(self, message): + self._C.got_nameplates(message["nameplates"]) + + S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) + S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) + + S0A_idle_disconnected.upon(refresh_nameplates, + enter=S1A_wanting_disconnected, outputs=[]) + S1A_wanting_disconnected.upon(refresh_nameplates, + enter=S1A_wanting_disconnected, outputs=[]) + S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, + outputs=[RC_tx_list]) + S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + outputs=[RC_tx_list]) + S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, + outputs=[C_got_nameplates]) + S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) + S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + outputs=[RC_tx_list]) + S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, + outputs=[C_got_nameplates]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index f6a34fa..0e7ab4d 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -106,11 +106,11 @@ class RendezvousConnector(object): assert isinstance(body, type(b"")), type(body) self._tx("add", phase=phase, body=bytes_to_hexstr(body)) - def tx_release(self): - self._tx("release") + def tx_release(self, nameplate): + self._tx("release", nameplate=nameplate) - def tx_close(self, mood): - self._tx("close", mood=mood) + def tx_close(self, mailbox, mood): + self._tx("close", mailbox=mailbox, mood=mood) def stop(self): d = defer.maybeDeferred(self._connector.stopService) diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py new file mode 100644 index 0000000..45496f4 --- /dev/null +++ b/src/wormhole/_terminator.py @@ -0,0 +1,104 @@ +from __future__ import print_function, absolute_import, unicode_literals +from zope.interface import implementer +from automat import MethodicalMachine +from . import _interfaces + +@implementer(_interfaces.ITerminator) +class Terminator(object): + m = MethodicalMachine() + @m.setTrace() + def setTrace(): pass + + def __attrs_post_init__(self): + self._mood = None + + def wire(self, boss, rendezvous_connector, nameplate, mailbox): + self._B = _interfaces.IBoss(boss) + self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) + self._N = _interfaces.INameplate(nameplate) + self._M = _interfaces.IMailbox(mailbox) + + # 4*2-1 main states: + # (nm, m, n, 0): nameplate and/or mailbox is active + # (o, ""): open (not-yet-closing), or trying to close + # S0 is special: we don't hang out in it + + # We start in Snmo (non-closing). When both nameplate and mailboxes are + # done, and we're closing, then we stop the RendezvousConnector + + @m.state(initial=True) + def Snmo(self): pass + @m.state() + def Smo(self): pass + @m.state() + def Sno(self): pass + @m.state() + def S0o(self): pass + + @m.state() + def Snm(self): pass + @m.state() + def Sm(self): pass + @m.state() + def Sn(self): pass + #@m.state() + #def S0(self): pass # unused + + @m.state() + def S_stopping(self): pass + @m.state() + def S_stopped(self, terminal=True): pass + + # from Boss + @m.input() + def close(self, mood): pass + + # from Nameplate + @m.input() + def nameplate_done(self): pass + + # from Mailbox + @m.input() + def mailbox_done(self): pass + + # from RendezvousConnector + @m.input() + def stopped(self): pass + + + @m.output() + def close_nameplate(self, mood): + self._N.close() # ignores mood + @m.output() + def close_mailbox(self, mood): + self._M.close(mood) + + @m.output() + def ignore_mood_and_RC_stop(self, mood): + self._RC.stop() + @m.output() + def RC_stop(self): + self._RC.stop() + @m.output() + def B_closed(self): + self._B.closed() + + Snmo.upon(mailbox_done, enter=Sno, outputs=[]) + Snmo.upon(close, enter=Snm, outputs=[close_nameplate, close_mailbox]) + Snmo.upon(nameplate_done, enter=Smo, outputs=[]) + + Sno.upon(close, enter=Sn, outputs=[close_nameplate, close_mailbox]) + Sno.upon(nameplate_done, enter=S0o, outputs=[]) + + Smo.upon(close, enter=Sm, outputs=[close_nameplate, close_mailbox]) + Smo.upon(mailbox_done, enter=S0o, outputs=[]) + + Snm.upon(mailbox_done, enter=Sn, outputs=[]) + Snm.upon(nameplate_done, enter=Sm, outputs=[]) + + Sn.upon(nameplate_done, enter=S_stopping, outputs=[RC_stop]) + S0o.upon(close, enter=S_stopping, + outputs=[close_nameplate, close_mailbox, ignore_mood_and_RC_stop]) + Sm.upon(mailbox_done, enter=S_stopping, outputs=[RC_stop]) + + S_stopping.upon(stopped, enter=S_stopped, outputs=[B_closed]) From 1beae97ec46de4747aedc60d342b68318e05b979 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 26 Feb 2017 04:03:54 -0800 Subject: [PATCH 073/176] fix things back to the previous point of not working --- src/wormhole/_boss.py | 2 +- src/wormhole/_mailbox.py | 2 +- src/wormhole/_nameplate.py | 2 ++ src/wormhole/_rendezvous.py | 12 ++++++++---- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index dbe59fa..2b5d0a0 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -55,7 +55,7 @@ class Boss(object): self._O.wire(self._K, self._R) self._K.wire(self, self._M, self._R) self._R.wire(self, self._K, self._S) - self._RC.wire(self, self._M, self._C, self._NL) + self._RC.wire(self, self._N, self._M, self._C, self._NL, self._T) self._NL.wire(self._RC, self._C) self._C.wire(self, self._RC, self._NL) self._T.wire(self, self._RC, self._N, self._M) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index b6a6f5d..89eccba 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -154,7 +154,7 @@ class Mailbox(object): @m.output() def record_mood_and_RC_tx_close(self, mood): self._mood = mood - self._RC_rx_close() + self._RC_tx_close() @m.output() def ignore_mood_and_T_mailbox_done(self, mood): self._T.mailbox_done() diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index ddc6ac8..0b9ef8c 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -142,3 +142,5 @@ class Nameplate(object): S5A.upon(connected, enter=S5B, outputs=[]) S5B.upon(lost, enter=S5A, outputs=[]) + S5.upon(release, enter=S5, outputs=[]) # mailbox is lazy + S5.upon(close, enter=S5, outputs=[]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 0e7ab4d..44d7b75 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -84,11 +84,13 @@ class RendezvousConnector(object): # TODO: Tor goes here return endpoints.HostnameEndpoint(self._reactor, hostname, port) - def wire(self, boss, mailbox, code, nameplate_lister): + def wire(self, boss, nameplate, mailbox, code, nameplate_lister, terminator): self._B = _interfaces.IBoss(boss) + self._N = _interfaces.INameplate(nameplate) self._M = _interfaces.IMailbox(mailbox) self._C = _interfaces.ICode(code) self._NL = _interfaces.INameplateLister(nameplate_lister) + self._T = _interfaces.ITerminator(terminator) # from Boss def start(self): @@ -133,6 +135,7 @@ class RendezvousConnector(object): self._ws = proto self._tx("bind", appid=self._appid, side=self._side) self._C.connected() + self._N.connected() self._M.connected() self._NL.connected() @@ -157,12 +160,13 @@ class RendezvousConnector(object): dmsg(self._side, "R.lost") self._ws = None self._C.lost() + self._N.lost() self._M.lost() self._NL.lost() # internal def _stopped(self, res): - self._M.stopped() + self._T.stopped() def _tx(self, mtype, **kwargs): assert self._ws @@ -208,7 +212,7 @@ class RendezvousConnector(object): def _response_handle_claimed(self, msg): mailbox = msg["mailbox"] assert isinstance(mailbox, type("")), type(mailbox) - self._M.rx_claimed(mailbox) + self._N.rx_claimed(mailbox) def _response_handle_message(self, msg): side = msg["side"] @@ -218,7 +222,7 @@ class RendezvousConnector(object): self._M.rx_message(side, phase, body) def _response_handle_released(self, msg): - self._M.rx_released() + self._N.rx_released() def _response_handle_closed(self, msg): self._M.rx_closed() From b0c9c9bb4cd60a518aef345b5bcdc6376cfcb187 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 26 Feb 2017 04:13:57 -0800 Subject: [PATCH 074/176] fix basic test --- src/wormhole/_mailbox.py | 16 ++++++++-------- src/wormhole/_order.py | 32 +++++++++++++++++--------------- src/wormhole/_receive.py | 5 +++-- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 89eccba..9dc8493 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -84,11 +84,11 @@ class Mailbox(object): if side == self._side: self.rx_message_ours(phase, body) else: - self.rx_message_theirs(phase, body) + self.rx_message_theirs(side, phase, body) @m.input() def rx_message_ours(self, phase, body): pass @m.input() - def rx_message_theirs(self, phase, body): pass + def rx_message_theirs(self, side, phase, body): pass @m.input() def rx_closed(self): pass @@ -129,9 +129,9 @@ class Mailbox(object): assert isinstance(body, type(b"")), type(body) self._RC.tx_add(phase, body) @m.output() - def N_release_and_accept(self, phase, body): + def N_release_and_accept(self, side, phase, body): self._N.release() - self._accept(phase, body) + self._accept(side, phase, body) @m.output() def RC_tx_close(self): assert self._mood @@ -139,12 +139,12 @@ class Mailbox(object): def _RC_tx_close(self): self._RC.tx_close(self._mailbox, self._mood) @m.output() - def accept(self, phase, body): - self._accept(phase, body) - def _accept(self, phase, body): + def accept(self, side, phase, body): + self._accept(side, phase, body) + def _accept(self, side, phase, body): if phase not in self._processed: self._processed.add(phase) - self._O.got_message(phase, body) + self._O.got_message(side, phase, body) @m.output() def dequeue(self, phase, body): self._pending_outbound.pop(phase, None) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index b21829d..987649f 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -24,41 +24,43 @@ class Order(object): @m.state(terminal=True) def S1_yes_pake(self): pass - def got_message(self, phase, body): + def got_message(self, side, phase, body): #print("ORDER[%s].got_message(%s)" % (self._side, phase)) + assert isinstance(side, type("")), type(phase) assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) if phase == "pake": - self.got_pake(phase, body) + self.got_pake(side, phase, body) else: - self.got_non_pake(phase, body) + self.got_non_pake(side, phase, body) @m.input() - def got_pake(self, phase, body): pass + def got_pake(self, side, phase, body): pass @m.input() - def got_non_pake(self, phase, body): pass + def got_non_pake(self, side, phase, body): pass @m.output() - def queue(self, phase, body): + def queue(self, side, phase, body): + assert isinstance(side, type("")), type(phase) assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) - self._queue.append((phase, body)) + self._queue.append((side, phase, body)) @m.output() - def notify_key(self, phase, body): + def notify_key(self, side, phase, body): self._K.got_pake(body) @m.output() - def drain(self, phase, body): + def drain(self, side, phase, body): del phase del body - for (phase, body) in self._queue: - self._deliver(phase, body) + for (side, phase, body) in self._queue: + self._deliver(side, phase, body) self._queue[:] = [] @m.output() - def deliver(self, phase, body): - self._deliver(phase, body) + def deliver(self, side, phase, body): + self._deliver(side, phase, body) - def _deliver(self, phase, body): - self._R.got_message(phase, body) + def _deliver(self, side, phase, body): + self._R.got_message(side, phase, body) S0_no_pake.upon(got_non_pake, enter=S0_no_pake, outputs=[queue]) S0_no_pake.upon(got_pake, enter=S1_yes_pake, outputs=[notify_key, drain]) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index ba2bbc3..5405531 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -31,11 +31,12 @@ class Receive(object): def S3_scared(self): pass # from Ordering - def got_message(self, phase, body): + def got_message(self, side, phase, body): + assert isinstance(side, type("")), type(phase) assert isinstance(phase, type("")), type(phase) assert isinstance(body, type(b"")), type(body) assert self._key - data_key = derive_phase_key(self._key, self._side, phase) + data_key = derive_phase_key(self._key, side, phase) try: plaintext = decrypt_data(data_key, body) except CryptoError: From 4793208d4e9741aa135a5e939348f371c336dc9c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 1 Mar 2017 19:55:13 -0800 Subject: [PATCH 075/176] rewrite debug tracing, add to all machines --- src/wormhole/_boss.py | 13 ++++++++++ src/wormhole/_code.py | 2 ++ src/wormhole/_key.py | 2 ++ src/wormhole/_mailbox.py | 2 +- src/wormhole/_nameplate.py | 2 +- src/wormhole/_nameplate_lister.py | 2 ++ src/wormhole/_order.py | 2 ++ src/wormhole/_receive.py | 2 ++ src/wormhole/_rendezvous.py | 33 ++++++++++++++++---------- src/wormhole/_send.py | 2 ++ src/wormhole/_terminator.py | 2 +- src/wormhole/test/test_wormhole_new.py | 5 ++-- src/wormhole/wormhole.py | 12 ++++++++++ 13 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 2b5d0a0..9dd1611 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -34,6 +34,8 @@ class Boss(object): _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def __attrs_post_init__(self): self._N = Nameplate() @@ -70,6 +72,17 @@ class Boss(object): def start(self): self._RC.start() + def _set_trace(self, client_name, which, logger): + names = {"B": self, "N": self._N, "M": self._M, "S": self._S, + "O": self._O, "K": self._K, "R": self._R, + "RC": self._RC, "NL": self._NL, "C": self._C, + "T": self._T} + for machine in which.split(): + def tracer(old_state, input, new_state, machine=machine): + print("%s.%s[%s].%s -> [%s]" % (client_name, machine, + old_state, input, new_state)) + names[machine].set_trace(tracer) + # and these are the state-machine transition functions, which don't take # args @m.state(initial=True) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 544685b..3dab38c 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -25,6 +25,8 @@ def make_code(nameplate, code_length): class Code(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def wire(self, boss, rendezvous_connector, nameplate_lister): self._B = _interfaces.IBoss(boss) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 016bc86..49fa9e2 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -59,6 +59,8 @@ class Key(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def wire(self, boss, mailbox, receive): self._B = _interfaces.IBoss(boss) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 9dc8493..db7de74 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -11,7 +11,7 @@ class Mailbox(object): _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() @m.setTrace() - def setTrace(): pass + def set_trace(): pass def __attrs_post_init__(self): self._mailbox = None diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 0b9ef8c..8e25801 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -7,7 +7,7 @@ from . import _interfaces class Nameplate(object): m = MethodicalMachine() @m.setTrace() - def setTrace(): pass + def set_trace(): pass def __init__(self): self._nameplate = None diff --git a/src/wormhole/_nameplate_lister.py b/src/wormhole/_nameplate_lister.py index ee9631c..27d064c 100644 --- a/src/wormhole/_nameplate_lister.py +++ b/src/wormhole/_nameplate_lister.py @@ -6,6 +6,8 @@ from . import _interfaces @implementer(_interfaces.INameplateLister) class NameplateListing(object): m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def wire(self, rendezvous_connector, code): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index 987649f..c4df241 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -11,6 +11,8 @@ class Order(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def __attrs_post_init__(self): self._key = None diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 5405531..8445756 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -12,6 +12,8 @@ class Receive(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def __attrs_post_init__(self): self._key = None diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 44d7b75..e9a183c 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -70,9 +70,9 @@ class RendezvousConnector(object): _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) - DEBUG = True def __attrs_post_init__(self): + self._trace = None self._ws = None f = WSFactory(self, self._url) f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) @@ -80,6 +80,12 @@ class RendezvousConnector(object): ep = self._make_endpoint(p.hostname, p.port or 80) self._connector = internet.ClientService(ep, f) + def set_trace(self, f): + self._trace = f + def _debug(self, what): + if self._trace: + self._trace(old_state="", input=what, new_state="") + def _make_endpoint(self, hostname, port): # TODO: Tor goes here return endpoints.HostnameEndpoint(self._reactor, hostname, port) @@ -130,8 +136,7 @@ class RendezvousConnector(object): # from our WSClient (the WebSocket protocol) def ws_open(self, proto): - if self.DEBUG: - dmsg(self._side, "R.connected") + self._debug("R.connected") self._ws = proto self._tx("bind", appid=self._appid, side=self._side) self._C.connected() @@ -141,11 +146,17 @@ class RendezvousConnector(object): def ws_message(self, payload): msg = bytes_to_dict(payload) - if self.DEBUG and msg["type"]!="ack": - dmsg(self._side, "R.rx(%s %s%s)" % - (msg["type"], msg.get("phase",""), - "[mine]" if msg.get("side","") == self._side else "", - )) + #if self.DEBUG and msg["type"]!="ack": + # dmsg(self._side, "R.rx(%s %s%s)" % + # (msg["type"], msg.get("phase",""), + # "[mine]" if msg.get("side","") == self._side else "", + # )) + if msg["type"] != "ack": + self._debug("R.rx(%s %s%s)" % + (msg["type"], msg.get("phase",""), + "[mine]" if msg.get("side","") == self._side else "", + )) + self._timing.add("ws_receive", _side=self._side, message=msg) mtype = msg["type"] meth = getattr(self, "_response_handle_"+mtype, None) @@ -156,8 +167,7 @@ class RendezvousConnector(object): return meth(msg) def ws_close(self, wasClean, code, reason): - if self.DEBUG: - dmsg(self._side, "R.lost") + self._debug("R.lost") self._ws = None self._C.lost() self._N.lost() @@ -176,8 +186,7 @@ class RendezvousConnector(object): # are so few messages, 16 bits is enough to be mostly-unique. kwargs["id"] = bytes_to_hexstr(os.urandom(2)) kwargs["type"] = mtype - if self.DEBUG: - dmsg(self._side, "R.tx(%s %s)" % (mtype.upper(), kwargs.get("phase", ""))) + self._debug("R.tx(%s %s)" % (mtype.upper(), kwargs.get("phase", ""))) payload = dict_to_bytes(kwargs) self._timing.add("ws_send", _side=self._side, **kwargs) self._ws.sendMessage(payload, False) diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index fd3e590..76617a3 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -12,6 +12,8 @@ class Send(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass def __attrs_post_init__(self): self._queue = [] diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py index 45496f4..92c037a 100644 --- a/src/wormhole/_terminator.py +++ b/src/wormhole/_terminator.py @@ -7,7 +7,7 @@ from . import _interfaces class Terminator(object): m = MethodicalMachine() @m.setTrace() - def setTrace(): pass + def set_trace(): pass def __attrs_post_init__(self): self._mood = None diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index c917109..4333d61 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -26,6 +26,7 @@ class New(ServerBase, unittest.TestCase): @inlineCallbacks def test_allocate(self): w = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w.debug_set_trace("W1") w.allocate_code(2) code = yield w.when_code() print("code:", code) @@ -40,9 +41,7 @@ class New(ServerBase, unittest.TestCase): @inlineCallbacks def test_basic(self): w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) - def trace(old_state, input, new_state): - print("W1._M[%s].%s -> [%s]" % (old_state, input, new_state)) - w1._boss._M.setTrace(trace) + w1.debug_set_trace("W1") w1.allocate_code(2) code = yield w1.when_code() print("code:", code) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 1977d16..36456a3 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -29,6 +29,10 @@ from ._boss import Boss, WormholeError # wormhole(delegate=app, delegate_prefix="wormhole_", # delegate_args=(args, kwargs)) +def _log(client_name, machine_name, old_state, input, new_state): + print("%s.%s[%s].%s -> [%s]" % (client_name, machine_name, + old_state, input, new_state)) + @attrs @implementer(IWormhole) class _DelegatedWormhole(object): @@ -51,6 +55,10 @@ class _DelegatedWormhole(object): def close(self): self._boss.close() + def debug_set_trace(self, client_name, which="B N M S O K R RC NL C T", + logger=_log): + self._boss.set_trace(client_name, which, logger) + # from below def got_code(self, code): self._delegate.wormhole_got_code(code) @@ -115,6 +123,10 @@ class _DeferredWormhole(object): self._closed_observers.append(d) return d + def debug_set_trace(self, client_name, which="B N M S O K R RC NL C T", + logger=_log): + self._boss._set_trace(client_name, which, logger) + # from below def got_code(self, code): self._code = code From 422205490301154f14a2913edd5aa9d5c2846a9f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 1 Mar 2017 19:55:23 -0800 Subject: [PATCH 076/176] nameplate: tolerate rx_claimed during shutdown --- src/wormhole/_nameplate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 8e25801..a33cdde 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -133,6 +133,7 @@ class Nameplate(object): S4A.upon(connected, enter=S4B, outputs=[RC_tx_release]) S4A.upon(close, enter=S4A, outputs=[]) S4B.upon(lost, enter=S4A, outputs=[]) + S4B.upon(rx_claimed, enter=S4B, outputs=[]) S4B.upon(rx_released, enter=S5B, outputs=[T_nameplate_done]) S4B.upon(release, enter=S4B, outputs=[]) # mailbox is lazy # Mailbox doesn't remember how many times it's sent a release, and will From fb92922918733d0d5ae6291cf8f5dc58cfbd69ba Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 1 Mar 2017 19:55:38 -0800 Subject: [PATCH 077/176] terminator: renaming TODO note --- src/wormhole/_terminator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py index 92c037a..d764a05 100644 --- a/src/wormhole/_terminator.py +++ b/src/wormhole/_terminator.py @@ -23,6 +23,9 @@ class Terminator(object): # (o, ""): open (not-yet-closing), or trying to close # S0 is special: we don't hang out in it + # TODO: rename o to 0, "" to 1. "S1" is special/terminal + # so S0nm/S0n/S0m/S0, S1nm/S1n/S1m/(S1) + # We start in Snmo (non-closing). When both nameplate and mailboxes are # done, and we're closing, then we stop the RendezvousConnector From 610db612ba390b929cb005a01a9848a880849deb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 2 Mar 2017 23:55:59 -0800 Subject: [PATCH 078/176] improve error handling errors raised while processing a received message will cause the Wormhole to close-with-error, and any pending Deferreds will be errbacked --- docs/boss.dot | 6 +++- docs/machines.dot | 2 +- src/wormhole/_boss.py | 35 +++++++++++++------ src/wormhole/_rendezvous.py | 10 +++++- src/wormhole/errors.py | 40 +++++++++------------ src/wormhole/test/test_wormhole_new.py | 48 ++++++++++++++++++++------ src/wormhole/wormhole.py | 23 +++++++----- 7 files changed, 108 insertions(+), 56 deletions(-) diff --git a/docs/boss.dot b/docs/boss.dot index 7f8d255..6bf3942 100644 --- a/docs/boss.dot +++ b/docs/boss.dot @@ -50,13 +50,17 @@ digraph { S2 -> P_close_scary [label="scared" color="red"] S_closing [label="closing"] - S_closing -> P_closed [label="closed"] + S_closing -> P_closed [label="closed\nerror"] S_closing -> S_closing [label="got_message\nhappy\nscared\nclose"] P_closed [shape="box" label="W.closed(reason)"] P_closed -> S_closed S_closed [label="closed"] + S0 -> P_closed [label="error"] + S1 -> P_closed [label="error"] + S2 -> P_closed [label="error"] + {rank=same; Other S_closed} Other [shape="box" style="dashed" label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" diff --git a/docs/machines.dot b/docs/machines.dot index c3713b6..14955fd 100644 --- a/docs/machines.dot +++ b/docs/machines.dot @@ -25,7 +25,7 @@ digraph { #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] - Connection -> Boss [style="dashed" label="rx_welcome\nrx_error"] + Connection -> Boss [style="dashed" label="rx_welcome\nrx_error\nerror"] Boss -> Send [style="dashed" label="send"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 9dd1611..3d5794d 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -17,12 +17,9 @@ from ._rendezvous import RendezvousConnector from ._nameplate_lister import NameplateListing from ._code import Code from ._terminator import Terminator -from .errors import WrongPasswordError +from .errors import ServerError, LonelyError, WrongPasswordError from .util import bytes_to_dict -class WormholeError(Exception): - pass - @attrs @implementer(_interfaces.IBoss) class Boss(object): @@ -121,9 +118,15 @@ class Boss(object): @m.input() def close(self): pass - # from RendezvousConnector + # from RendezvousConnector. rx_error an error message from the server + # (probably because of something we did, or due to CrowdedError). error + # is when an exception happened while it tried to deliver something else @m.input() def rx_welcome(self, welcome): pass + @m.input() + def rx_error(self, errmsg, orig): pass + @m.input() + def error(self, err): pass # from Code (provoked by input/allocate/set_code) @m.input() @@ -135,8 +138,6 @@ class Boss(object): def happy(self): pass @m.input() def scared(self): pass - @m.input() - def rx_error(self, err, orig): pass def got_message(self, phase, plaintext): assert isinstance(phase, type("")), type(phase) @@ -184,8 +185,8 @@ class Boss(object): self._S.send("%d" % phase, plaintext) @m.output() - def close_error(self, err, orig): - self._result = WormholeError(err) + def close_error(self, errmsg, orig): + self._result = ServerError(errmsg) self._T.close("errory") @m.output() def close_scared(self): @@ -193,7 +194,7 @@ class Boss(object): self._T.close("scary") @m.output() def close_lonely(self): - self._result = WormholeError("lonely") + self._result = LonelyError() self._T.close("lonely") @m.output() def close_happy(self): @@ -212,8 +213,14 @@ class Boss(object): self._W.received(self._rx_phases.pop(self._next_rx_phase)) self._next_rx_phase += 1 + @m.output() + def W_close_with_error(self, err): + self._result = err # exception + self._W.closed(self._result) + @m.output() def W_closed(self): + # result is either "happy" or a WormholeError of some sort self._W.closed(self._result) S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely]) @@ -221,6 +228,8 @@ class Boss(object): S0_empty.upon(rx_welcome, enter=S0_empty, outputs=[process_welcome]) 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(error, enter=S4_closed, outputs=[W_close_with_error]) + S1_lonely.upon(rx_welcome, enter=S1_lonely, outputs=[process_welcome]) S1_lonely.upon(happy, enter=S2_happy, outputs=[]) S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) @@ -228,6 +237,8 @@ class Boss(object): S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_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(got_phase, enter=S2_happy, outputs=[W_received]) S2_happy.upon(got_version, enter=S2_happy, outputs=[process_version]) @@ -235,6 +246,7 @@ class Boss(object): S2_happy.upon(close, enter=S3_closing, outputs=[close_happy]) S2_happy.upon(send, enter=S2_happy, outputs=[S_send]) S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_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_error, enter=S3_closing, outputs=[]) @@ -245,6 +257,7 @@ class Boss(object): S3_closing.upon(close, enter=S3_closing, outputs=[]) S3_closing.upon(send, enter=S3_closing, outputs=[]) S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed]) + S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error]) S4_closed.upon(rx_welcome, enter=S4_closed, outputs=[]) S4_closed.upon(got_phase, enter=S4_closed, outputs=[]) @@ -253,4 +266,4 @@ class Boss(object): S4_closed.upon(scared, enter=S4_closed, outputs=[]) S4_closed.upon(close, enter=S4_closed, outputs=[]) S4_closed.upon(send, enter=S4_closed, outputs=[]) - + S4_closed.upon(error, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index e9a183c..63cea71 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -164,7 +164,11 @@ class RendezvousConnector(object): # make tests fail, but real application will ignore it log.err(ValueError("Unknown inbound message type %r" % (msg,))) return - return meth(msg) + try: + return meth(msg) + except Exception as e: + self._B.error(e) + raise def ws_close(self, wasClean, code, reason): self._debug("R.lost") @@ -211,6 +215,10 @@ class RendezvousConnector(object): pass def _response_handle_error(self, msg): + # the server sent us a type=error. Most cases are due to our mistakes + # (malformed protocol messages, sending things in the wrong order), + # but it can also result from CrowdedError (more than two clients + # using the same channel). err = msg["error"] orig = msg["orig"] self._B.rx_error(err, orig) diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index 7eff520..d13aa87 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -1,33 +1,25 @@ from __future__ import unicode_literals -import functools -class ServerError(Exception): - def __init__(self, message, relay): - self.message = message - self.relay = relay - def __str__(self): - return self.message +class WormholeError(Exception): + """Parent class for all wormhole-related errors""" -def handle_server_error(func): - @functools.wraps(func) - def _wrap(*args, **kwargs): - try: - return func(*args, **kwargs) - except ServerError as e: - print("Server error (from %s):\n%s" % (e.relay, e.message)) - return 1 - return _wrap +class ServerError(WormholeError): + """The relay server complained about something we did.""" -class Timeout(Exception): +class Timeout(WormholeError): pass -class WelcomeError(Exception): +class WelcomeError(WormholeError): """ The relay server told us to signal an error, probably because our version is too old to possibly work. The server said:""" pass -class WrongPasswordError(Exception): +class LonelyError(WormholeError): + """wormhole.close() was called before the peer connection could be + established""" + +class WrongPasswordError(WormholeError): """ Key confirmation failed. Either you or your correspondent typed the code wrong, or a would-be man-in-the-middle attacker guessed incorrectly. You @@ -37,24 +29,24 @@ class WrongPasswordError(Exception): # or the data blob was corrupted, and that's why decrypt failed pass -class KeyFormatError(Exception): +class KeyFormatError(WormholeError): """ The key you entered contains spaces. Magic-wormhole expects keys to be separated by dashes. Please reenter the key you were given separating the words with dashes. """ -class ReflectionAttack(Exception): +class ReflectionAttack(WormholeError): """An attacker (or bug) reflected our outgoing message back to us.""" -class InternalError(Exception): +class InternalError(WormholeError): """The programmer did something wrong.""" class WormholeClosedError(InternalError): """API calls may not be made after close() is called.""" -class TransferError(Exception): +class TransferError(WormholeError): """Something bad happened and the transfer failed.""" -class NoTorError(Exception): +class NoTorError(WormholeError): """--tor was requested, but 'txtorcon' is not installed.""" diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 4333d61..8eadb20 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -1,9 +1,10 @@ from __future__ import print_function, unicode_literals +import re from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks from .common import ServerBase -from .. import wormhole +from .. import wormhole, errors APPID = "appid" @@ -23,15 +24,19 @@ class Delegate: self.closed = result class New(ServerBase, unittest.TestCase): + timeout = 2 + @inlineCallbacks def test_allocate(self): w = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) - w.debug_set_trace("W1") + #w.debug_set_trace("W1") w.allocate_code(2) code = yield w.when_code() - print("code:", code) - yield w.close() - test_allocate.timeout = 2 + self.assertEqual(type(code), type("")) + mo = re.search(r"^\d+-\w+-\w+$", code) + self.assert_(mo, code) + # w.close() fails because we closed before connecting + self.assertFailure(w.close(), errors.LonelyError) def test_delegated(self): dg = Delegate() @@ -41,10 +46,11 @@ class New(ServerBase, unittest.TestCase): @inlineCallbacks def test_basic(self): w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) - w1.debug_set_trace("W1") + #w1.debug_set_trace("W1") w1.allocate_code(2) code = yield w1.when_code() - print("code:", code) + mo = re.search(r"^\d+-\w+-\w+$", code) + self.assert_(mo, code) w2 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) w2.set_code(code) code2 = yield w2.when_code() @@ -55,6 +61,28 @@ class New(ServerBase, unittest.TestCase): data = yield w2.when_received() self.assertEqual(data, b"data") - yield w1.close() - yield w2.close() - test_basic.timeout = 2 + w2.send(b"data2") + data2 = yield w1.when_received() + self.assertEqual(data2, b"data2") + + c1 = yield w1.close() + self.assertEqual(c1, "happy") + c2 = yield w2.close() + self.assertEqual(c2, "happy") + + @inlineCallbacks + def test_wrong_password(self): + w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + #w1.debug_set_trace("W1") + w1.allocate_code(2) + code = yield w1.when_code() + w2 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w2.set_code(code+", NOT") + code2 = yield w2.when_code() + self.assertNotEqual(code, code2) + + w1.send(b"data") + + self.assertFailure(w2.when_received(), errors.WrongPasswordError) + self.assertFailure(w1.close(), errors.WrongPasswordError) + self.assertFailure(w2.close(), errors.WrongPasswordError) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 36456a3..527bcde 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -2,12 +2,13 @@ from __future__ import print_function, absolute_import, unicode_literals import os, sys from attr import attrs, attrib from zope.interface import implementer +from twisted.python import failure from twisted.internet import defer from ._interfaces import IWormhole from .util import bytes_to_hexstr from .timing import DebugTiming from .journal import ImmediateJournal -from ._boss import Boss, WormholeError +from ._boss import Boss # We can provide different APIs to different apps: # * Deferreds @@ -118,6 +119,9 @@ class _DeferredWormhole(object): def send(self, plaintext): self._boss.send(plaintext) def close(self): + # fails with WormholeError unless we established a connection + # (state=="happy"). Fails with WrongPasswordError (a subclass of + # WormholeError) if state=="scary". self._boss.close() d = defer.Deferred() self._closed_observers.append(d) @@ -146,17 +150,20 @@ class _DeferredWormhole(object): self._received_data.append(plaintext) def closed(self, result): - print("closed", result, type(result)) - if isinstance(result, WormholeError): - e = result + #print("closed", result, type(result)) + if isinstance(result, Exception): + observer_result = close_result = failure.Failure(result) else: - e = WormholeClosed(result) + # pending w.verify() or w.read() get an error + observer_result = WormholeClosed(result) + # but w.close() only gets error if we're unhappy + close_result = result for d in self._verifier_observers: - d.errback(e) + d.errback(observer_result) for d in self._received_observers: - d.errback(e) + d.errback(observer_result) for d in self._closed_observers: - d.callback(result) + d.callback(close_result) def _wormhole(appid, relay_url, reactor, delegate=None, tor_manager=None, timing=None, From db7b24086faeb066e7dfcc4831f03beac3316f96 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 2 Mar 2017 23:59:24 -0800 Subject: [PATCH 079/176] set no-cover on all state-definition lines, and set_trace --- src/wormhole/_boss.py | 12 ++++++------ src/wormhole/_code.py | 14 +++++++------- src/wormhole/_key.py | 10 +++++----- src/wormhole/_mailbox.py | 18 +++++++++--------- src/wormhole/_nameplate.py | 22 +++++++++++----------- src/wormhole/_nameplate_lister.py | 10 +++++----- src/wormhole/_order.py | 6 +++--- src/wormhole/_receive.py | 10 +++++----- src/wormhole/_send.py | 6 +++--- src/wormhole/_terminator.py | 20 ++++++++++---------- 10 files changed, 64 insertions(+), 64 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 3d5794d..3e1a843 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -32,7 +32,7 @@ class Boss(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): self._N = Nameplate() @@ -83,15 +83,15 @@ class Boss(object): # and these are the state-machine transition functions, which don't take # args @m.state(initial=True) - def S0_empty(self): pass + def S0_empty(self): pass # pragma: no cover @m.state() - def S1_lonely(self): pass + def S1_lonely(self): pass # pragma: no cover @m.state() - def S2_happy(self): pass + def S2_happy(self): pass # pragma: no cover @m.state() - def S3_closing(self): pass + def S3_closing(self): pass # pragma: no cover @m.state(terminal=True) - def S4_closed(self): pass + def S4_closed(self): pass # pragma: no cover # from the Wormhole diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 3dab38c..b483f07 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -26,7 +26,7 @@ class Code(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def wire(self, boss, rendezvous_connector, nameplate_lister): self._B = _interfaces.IBoss(boss) @@ -34,17 +34,17 @@ class Code(object): self._NL = _interfaces.INameplateLister(nameplate_lister) @m.state(initial=True) - def S0_unknown(self): pass + def S0_unknown(self): pass # pragma: no cover @m.state() - def S1A_connecting(self): pass + def S1A_connecting(self): pass # pragma: no cover @m.state() - def S1B_allocating(self): pass + def S1B_allocating(self): pass # pragma: no cover @m.state() - def S2_typing_nameplate(self): pass + def S2_typing_nameplate(self): pass # pragma: no cover @m.state() - def S3_typing_code(self): pass + def S3_typing_code(self): pass # pragma: no cover @m.state() - def S4_known(self): pass + def S4_known(self): pass # pragma: no cover # from App @m.input() diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 49fa9e2..e5f54fd 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -60,7 +60,7 @@ class Key(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def wire(self, boss, mailbox, receive): self._B = _interfaces.IBoss(boss) @@ -68,13 +68,13 @@ class Key(object): self._R = _interfaces.IReceive(receive) @m.state(initial=True) - def S0_know_nothing(self): pass + def S0_know_nothing(self): pass # pragma: no cover @m.state() - def S1_know_code(self): pass + def S1_know_code(self): pass # pragma: no cover @m.state() - def S2_know_key(self): pass + def S2_know_key(self): pass # pragma: no cover @m.state(terminal=True) - def S3_scared(self): pass + def S3_scared(self): pass # pragma: no cover # from Boss @m.input() diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index db7de74..958d6f8 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -11,7 +11,7 @@ class Mailbox(object): _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): self._mailbox = None @@ -30,27 +30,27 @@ class Mailbox(object): # S0: know nothing @m.state(initial=True) - def S0A(self): pass + def S0A(self): pass # pragma: no cover @m.state() - def S0B(self): pass + def S0B(self): pass # pragma: no cover # S1: mailbox known, not opened @m.state() - def S1A(self): pass + def S1A(self): pass # pragma: no cover # S2: mailbox known, opened # We've definitely tried to open the mailbox at least once, but it must # be re-opened with each connection, because open() is also subscribe() @m.state() - def S2A(self): pass + def S2A(self): pass # pragma: no cover @m.state() - def S2B(self): pass + def S2B(self): pass # pragma: no cover # S3: closing @m.state() - def S3A(self): pass + def S3A(self): pass # pragma: no cover @m.state() - def S3B(self): pass + def S3B(self): pass # pragma: no cover # S4: closed. We no longer care whether we're connected or not #@m.state() @@ -58,7 +58,7 @@ class Mailbox(object): #@m.state() #def S4B(self): pass @m.state(terminal=True) - def S4(self): pass + def S4(self): pass # pragma: no cover S4A = S4 S4B = S4 diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index a33cdde..777098d 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -7,7 +7,7 @@ from . import _interfaces class Nameplate(object): m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __init__(self): self._nameplate = None @@ -23,31 +23,31 @@ class Nameplate(object): # S0: know nothing @m.state(initial=True) - def S0A(self): pass + def S0A(self): pass # pragma: no cover @m.state() - def S0B(self): pass + def S0B(self): pass # pragma: no cover # S1: nameplate known, never claimed @m.state() - def S1A(self): pass + def S1A(self): pass # pragma: no cover # S2: nameplate known, maybe claimed @m.state() - def S2A(self): pass + def S2A(self): pass # pragma: no cover @m.state() - def S2B(self): pass + def S2B(self): pass # pragma: no cover # S3: nameplate claimed @m.state() - def S3A(self): pass + def S3A(self): pass # pragma: no cover @m.state() - def S3B(self): pass + def S3B(self): pass # pragma: no cover # S4: maybe released @m.state() - def S4A(self): pass + def S4A(self): pass # pragma: no cover @m.state() - def S4B(self): pass + def S4B(self): pass # pragma: no cover # S5: released # we no longer care whether we're connected or not @@ -56,7 +56,7 @@ class Nameplate(object): #@m.state() #def S5B(self): pass @m.state() - def S5(self): pass + def S5(self): pass # pragma: no cover S5A = S5 S5B = S5 diff --git a/src/wormhole/_nameplate_lister.py b/src/wormhole/_nameplate_lister.py index 27d064c..e40e406 100644 --- a/src/wormhole/_nameplate_lister.py +++ b/src/wormhole/_nameplate_lister.py @@ -7,7 +7,7 @@ from . import _interfaces class NameplateListing(object): m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def wire(self, rendezvous_connector, code): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) @@ -23,13 +23,13 @@ class NameplateListing(object): # request arrives, both requests will be satisfied by the same response. @m.state(initial=True) - def S0A_idle_disconnected(self): pass + def S0A_idle_disconnected(self): pass # pragma: no cover @m.state() - def S1A_wanting_disconnected(self): pass + def S1A_wanting_disconnected(self): pass # pragma: no cover @m.state() - def S0B_idle_connected(self): pass + def S0B_idle_connected(self): pass # pragma: no cover @m.state() - def S1B_wanting_connected(self): pass + def S1B_wanting_connected(self): pass # pragma: no cover @m.input() def connected(self): pass diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index c4df241..81cb088 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -12,7 +12,7 @@ class Order(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): self._key = None @@ -22,9 +22,9 @@ class Order(object): self._R = _interfaces.IReceive(receive) @m.state(initial=True) - def S0_no_pake(self): pass + def S0_no_pake(self): pass # pragma: no cover @m.state(terminal=True) - def S1_yes_pake(self): pass + def S1_yes_pake(self): pass # pragma: no cover def got_message(self, side, phase, body): #print("ORDER[%s].got_message(%s)" % (self._side, phase)) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 8445756..83dc92a 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -13,7 +13,7 @@ class Receive(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): self._key = None @@ -24,13 +24,13 @@ class Receive(object): self._S = _interfaces.ISend(send) @m.state(initial=True) - def S0_unknown_key(self): pass + def S0_unknown_key(self): pass # pragma: no cover @m.state() - def S1_unverified_key(self): pass + def S1_unverified_key(self): pass # pragma: no cover @m.state() - def S2_verified_key(self): pass + def S2_verified_key(self): pass # pragma: no cover @m.state(terminal=True) - def S3_scared(self): pass + def S3_scared(self): pass # pragma: no cover # from Ordering def got_message(self, side, phase, body): diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 76617a3..ccd4699 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -13,7 +13,7 @@ class Send(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): self._queue = [] @@ -22,9 +22,9 @@ class Send(object): self._M = _interfaces.IMailbox(mailbox) @m.state(initial=True) - def S0_no_key(self): pass + def S0_no_key(self): pass # pragma: no cover @m.state(terminal=True) - def S1_verified_key(self): pass + def S1_verified_key(self): pass # pragma: no cover # from Receive @m.input() diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py index d764a05..f7da3cc 100644 --- a/src/wormhole/_terminator.py +++ b/src/wormhole/_terminator.py @@ -7,7 +7,7 @@ from . import _interfaces class Terminator(object): m = MethodicalMachine() @m.setTrace() - def set_trace(): pass + def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): self._mood = None @@ -30,27 +30,27 @@ class Terminator(object): # done, and we're closing, then we stop the RendezvousConnector @m.state(initial=True) - def Snmo(self): pass + def Snmo(self): pass # pragma: no cover @m.state() - def Smo(self): pass + def Smo(self): pass # pragma: no cover @m.state() - def Sno(self): pass + def Sno(self): pass # pragma: no cover @m.state() - def S0o(self): pass + def S0o(self): pass # pragma: no cover @m.state() - def Snm(self): pass + def Snm(self): pass # pragma: no cover @m.state() - def Sm(self): pass + def Sm(self): pass # pragma: no cover @m.state() - def Sn(self): pass + def Sn(self): pass # pragma: no cover #@m.state() #def S0(self): pass # unused @m.state() - def S_stopping(self): pass + def S_stopping(self): pass # pragma: no cover @m.state() - def S_stopped(self, terminal=True): pass + def S_stopped(self, terminal=True): pass # pragma: no cover # from Boss @m.input() From 2dcfb07ba1fc7794dd3bd4ad343238676f85433a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 2 Mar 2017 23:59:45 -0800 Subject: [PATCH 080/176] Receive does not need access to Key --- src/wormhole/_boss.py | 2 +- src/wormhole/_receive.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 3e1a843..709fd62 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -53,7 +53,7 @@ class Boss(object): self._S.wire(self._M) self._O.wire(self._K, self._R) self._K.wire(self, self._M, self._R) - self._R.wire(self, self._K, self._S) + self._R.wire(self, self._S) self._RC.wire(self, self._N, self._M, self._C, self._NL, self._T) self._NL.wire(self._RC, self._C) self._C.wire(self, self._RC, self._NL) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 83dc92a..389c0f4 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -18,9 +18,8 @@ class Receive(object): def __attrs_post_init__(self): self._key = None - def wire(self, boss, key, send): + def wire(self, boss, send): self._B = _interfaces.IBoss(boss) - self._K = _interfaces.IKey(key) self._S = _interfaces.ISend(send) @m.state(initial=True) From 88cb42f95bedd0ccf9b21e63f5fb33112f4fc809 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 2 Mar 2017 23:59:53 -0800 Subject: [PATCH 081/176] test_machines: exercise state machines better --- src/wormhole/test/test_machines.py | 189 +++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/wormhole/test/test_machines.py diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py new file mode 100644 index 0000000..661d71b --- /dev/null +++ b/src/wormhole/test/test_machines.py @@ -0,0 +1,189 @@ +from __future__ import print_function, unicode_literals +import json +from zope.interface import directlyProvides +from twisted.trial import unittest +from .. import timing, _order, _receive, _key +from .._interfaces import IKey, IReceive, IBoss, ISend, IMailbox +from .._key import derive_key, derive_phase_key, encrypt_data +from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes +from spake2 import SPAKE2_Symmetric + +class Dummy: + def __init__(self, name, events, iface, *meths): + self.name = name + self.events = events + directlyProvides(self, iface) + for meth in meths: + self.mock(meth) + def mock(self, meth): + def log(*args): + self.events.append(("%s.%s" % (self.name, meth),) + args) + setattr(self, meth, log) + +class Order(unittest.TestCase): + def build(self): + events = [] + o = _order.Order(u"side", timing.DebugTiming()) + k = Dummy("k", events, IKey, "got_pake") + r = Dummy("r", events, IReceive, "got_message") + o.wire(k, r) + return o, k, r, events + + def test_in_order(self): + o, k, r, events = self.build() + o.got_message(u"side", u"pake", b"body") + self.assertEqual(events, [("k.got_pake", b"body")]) # right away + o.got_message(u"side", u"version", b"body") + o.got_message(u"side", u"1", b"body") + self.assertEqual(events, + [("k.got_pake", b"body"), + ("r.got_message", u"side", u"version", b"body"), + ("r.got_message", u"side", u"1", b"body"), + ]) + + def test_out_of_order(self): + o, k, r, events = self.build() + o.got_message(u"side", u"version", b"body") + self.assertEqual(events, []) # nothing yet + o.got_message(u"side", u"1", b"body") + self.assertEqual(events, []) # nothing yet + o.got_message(u"side", u"pake", b"body") + # got_pake is delivered first + self.assertEqual(events, + [("k.got_pake", b"body"), + ("r.got_message", u"side", u"version", b"body"), + ("r.got_message", u"side", u"1", b"body"), + ]) + +class Receive(unittest.TestCase): + def build(self): + events = [] + r = _receive.Receive(u"side", timing.DebugTiming()) + b = Dummy("b", events, IBoss, "happy", "scared", "got_message") + s = Dummy("s", events, ISend, "got_verified_key") + r.wire(b, s) + return r, b, s, events + + def test_good(self): + r, b, s, events = self.build() + key = b"key" + r.got_key(key) + self.assertEqual(events, []) + phase1_key = derive_phase_key(key, u"side", u"phase1") + data1 = b"data1" + good_body = encrypt_data(phase1_key, data1) + r.got_message(u"side", u"phase1", good_body) + self.assertEqual(events, [("s.got_verified_key", key), + ("b.happy",), + ("b.got_message", u"phase1", data1), + ]) + + phase2_key = derive_phase_key(key, u"side", u"phase2") + data2 = b"data2" + good_body = encrypt_data(phase2_key, data2) + r.got_message(u"side", u"phase2", good_body) + self.assertEqual(events, [("s.got_verified_key", key), + ("b.happy",), + ("b.got_message", u"phase1", data1), + ("b.got_message", u"phase2", data2), + ]) + + def test_early_bad(self): + r, b, s, events = self.build() + key = b"key" + r.got_key(key) + self.assertEqual(events, []) + phase1_key = derive_phase_key(key, u"side", u"bad") + data1 = b"data1" + bad_body = encrypt_data(phase1_key, data1) + r.got_message(u"side", u"phase1", bad_body) + self.assertEqual(events, [("b.scared",), + ]) + + phase2_key = derive_phase_key(key, u"side", u"phase2") + data2 = b"data2" + good_body = encrypt_data(phase2_key, data2) + r.got_message(u"side", u"phase2", good_body) + self.assertEqual(events, [("b.scared",), + ]) + + def test_late_bad(self): + r, b, s, events = self.build() + key = b"key" + r.got_key(key) + self.assertEqual(events, []) + phase1_key = derive_phase_key(key, u"side", u"phase1") + data1 = b"data1" + good_body = encrypt_data(phase1_key, data1) + r.got_message(u"side", u"phase1", good_body) + self.assertEqual(events, [("s.got_verified_key", key), + ("b.happy",), + ("b.got_message", u"phase1", data1), + ]) + + phase2_key = derive_phase_key(key, u"side", u"bad") + data2 = b"data2" + bad_body = encrypt_data(phase2_key, data2) + r.got_message(u"side", u"phase2", bad_body) + self.assertEqual(events, [("s.got_verified_key", key), + ("b.happy",), + ("b.got_message", u"phase1", data1), + ("b.scared",), + ]) + r.got_message(u"side", u"phase1", good_body) + r.got_message(u"side", u"phase2", bad_body) + self.assertEqual(events, [("s.got_verified_key", key), + ("b.happy",), + ("b.got_message", u"phase1", data1), + ("b.scared",), + ]) + +class Key(unittest.TestCase): + def test_derive_errors(self): + self.assertRaises(TypeError, derive_key, 123, b"purpose") + self.assertRaises(TypeError, derive_key, b"key", 123) + self.assertRaises(TypeError, derive_key, b"key", b"purpose", "not len") + + def build(self): + events = [] + k = _key.Key(u"appid", u"side", timing.DebugTiming()) + b = Dummy("b", events, IBoss, "scared", "got_verifier") + m = Dummy("m", events, IMailbox, "add_message") + r = Dummy("r", events, IReceive, "got_key") + k.wire(b, m, r) + return k, b, m, r, events + + def test_good(self): + k, b, m, r, events = self.build() + code = u"1-foo" + k.got_code(code) + self.assertEqual(len(events), 1) + self.assertEqual(events[0][:2], ("m.add_message", "pake")) + msg1_json = events[0][2] + events[:] = [] + msg1 = json.loads(msg1_json) + msg1_bytes = hexstr_to_bytes(msg1["pake_v1"]) + sp = SPAKE2_Symmetric(to_bytes(code), idSymmetric=to_bytes(u"appid")) + msg2_bytes = sp.start() + key2 = sp.finish(msg1_bytes) + msg2 = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg2_bytes)}) + k.got_pake(msg2) + self.assertEqual(len(events), 3) + self.assertEqual(events[0][0], "b.got_verifier") + self.assertEqual(events[1][:2], ("m.add_message", "version")) + self.assertEqual(events[2], ("r.got_key", key2)) + + + def test_bad(self): + k, b, m, r, events = self.build() + code = u"1-foo" + k.got_code(code) + self.assertEqual(len(events), 1) + self.assertEqual(events[0][:2], ("m.add_message", "pake")) + pake_1_json = events[0][2] + pake_1 = json.loads(pake_1_json) + self.assertEqual(pake_1.keys(), ["pake_v1"]) # value is PAKE stuff + events[:] = [] + bad_pake_d = {"not_pake_v1": "stuff"} + k.got_pake(dict_to_bytes(bad_pake_d)) + self.assertEqual(events, [("b.scared",)]) From fcdcf30ba8a64a16a43b43e63a29a7c4edec4392 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 3 Mar 2017 05:32:35 -0800 Subject: [PATCH 082/176] docs: move state machine diagrams into separate directory --- .gitignore | 4 +- docs/_events.dot | 98 ------------------- docs/_states-code.dot | 18 ---- docs/state-machines/Makefile | 9 ++ docs/{ => state-machines}/_connection.dot | 0 docs/{ => state-machines}/boss.dot | 0 docs/{ => state-machines}/code.dot | 0 docs/{ => state-machines}/key.dot | 0 docs/{ => state-machines}/machines.dot | 0 docs/{ => state-machines}/mailbox.dot | 0 docs/{ => state-machines}/nameplate.dot | 0 .../{ => state-machines}/nameplate_lister.dot | 0 docs/{ => state-machines}/order.dot | 0 docs/{ => state-machines}/receive.dot | 0 docs/{ => state-machines}/send.dot | 0 docs/{ => state-machines}/terminator.dot | 0 16 files changed, 10 insertions(+), 119 deletions(-) delete mode 100644 docs/_events.dot delete mode 100644 docs/_states-code.dot create mode 100644 docs/state-machines/Makefile rename docs/{ => state-machines}/_connection.dot (100%) rename docs/{ => state-machines}/boss.dot (100%) rename docs/{ => state-machines}/code.dot (100%) rename docs/{ => state-machines}/key.dot (100%) rename docs/{ => state-machines}/machines.dot (100%) rename docs/{ => state-machines}/mailbox.dot (100%) rename docs/{ => state-machines}/nameplate.dot (100%) rename docs/{ => state-machines}/nameplate_lister.dot (100%) rename docs/{ => state-machines}/order.dot (100%) rename docs/{ => state-machines}/receive.dot (100%) rename docs/{ => state-machines}/send.dot (100%) rename docs/{ => state-machines}/terminator.dot (100%) diff --git a/.gitignore b/.gitignore index 046e229..d1e78f0 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,5 @@ target/ /twistd.pid /relay.sqlite /misc/node_modules/ -/docs/events.png -/docs/states-code.png -/docs/*.png /.automat_visualize/ +/docs/state-machines/*.png diff --git a/docs/_events.dot b/docs/_events.dot deleted file mode 100644 index ca1710d..0000000 --- a/docs/_events.dot +++ /dev/null @@ -1,98 +0,0 @@ -digraph { - /*rankdir=LR*/ - api_get_code [label="get_code" shape="hexagon" color="red"] - api_input_code [label="input_code" shape="hexagon" color="red"] - api_set_code [label="set_code" shape="hexagon" color="red"] - verify [label="verify" shape="hexagon" color="red"] - send [label="API\nsend" shape="hexagon" color="red"] - get [label="API\nget" shape="hexagon" color="red"] - close [label="API\nclose" shape="hexagon" color="red"] - - event_connected [label="connected" shape="box"] - event_learned_code [label="learned\ncode" shape="box"] - event_learned_nameplate [label="learned\nnameplate" shape="box"] - event_received_mailbox [label="received\nmailbox" shape="box"] - event_opened_mailbox [label="opened\nmailbox" shape="box"] - event_built_msg1 [label="built\nmsg1" shape="box"] - event_mailbox_used [label="mailbox\nused" shape="box"] - event_learned_PAKE [label="learned\nmsg2" shape="box"] - event_established_key [label="established\nkey" shape="box"] - event_computed_verifier [label="computed\nverifier" shape="box"] - event_received_confirm [label="received\nconfirm" shape="box"] - event_received_message [label="received\nmessage" shape="box"] - event_received_released [label="ack\nreleased" shape="box"] - event_received_closed [label="ack\nclosed" shape="box"] - - event_connected -> api_get_code - event_connected -> api_input_code - api_get_code -> event_learned_code - api_input_code -> event_learned_code - api_set_code -> event_learned_code - - - maybe_build_msg1 [label="build\nmsg1"] - maybe_claim_nameplate [label="claim\nnameplate"] - maybe_send_pake [label="send\npake"] - maybe_send_phase_messages [label="send\nphase\nmessages"] - - event_connected -> maybe_claim_nameplate - event_connected -> maybe_send_pake - - event_built_msg1 -> maybe_send_pake - - event_learned_code -> maybe_build_msg1 - event_learned_code -> event_learned_nameplate - - maybe_build_msg1 -> event_built_msg1 - event_learned_nameplate -> maybe_claim_nameplate - maybe_claim_nameplate -> event_received_mailbox [style="dashed"] - - event_received_mailbox -> event_opened_mailbox - maybe_claim_nameplate -> event_learned_PAKE [style="dashed"] - maybe_claim_nameplate -> event_received_confirm [style="dashed"] - - event_opened_mailbox -> event_learned_PAKE [style="dashed"] - event_learned_PAKE -> event_mailbox_used [style="dashed"] - event_learned_PAKE -> event_received_confirm [style="dashed"] - event_received_confirm -> event_received_message [style="dashed"] - - send -> maybe_send_phase_messages - release_nameplate [label="release\nnameplate"] - event_mailbox_used -> release_nameplate - event_opened_mailbox -> maybe_send_pake - event_opened_mailbox -> maybe_send_phase_messages - - event_learned_PAKE -> event_established_key - event_established_key -> event_computed_verifier - event_established_key -> check_confirmation - event_established_key -> maybe_send_phase_messages - - check_confirmation [label="check\nconfirmation"] - event_received_confirm -> check_confirmation - - notify_verifier [label="notify\nverifier"] - check_confirmation -> notify_verifier - verify -> notify_verifier - event_computed_verifier -> notify_verifier - - check_confirmation -> error - event_received_message -> error - event_received_message -> get - event_established_key -> get - - close -> close_mailbox - close -> release_nameplate - error [label="signal\nerror"] - error -> close_mailbox - error -> release_nameplate - - release_nameplate -> event_received_released [style="dashed"] - close_mailbox [label="close\nmailbox"] - close_mailbox -> event_received_closed [style="dashed"] - - maybe_close_websocket [label="close\nwebsocket"] - event_received_released -> maybe_close_websocket - event_received_closed -> maybe_close_websocket - maybe_close_websocket -> event_websocket_closed [style="dashed"] - event_websocket_closed [label="websocket\nclosed"] -} diff --git a/docs/_states-code.dot b/docs/_states-code.dot deleted file mode 100644 index c32bca2..0000000 --- a/docs/_states-code.dot +++ /dev/null @@ -1,18 +0,0 @@ -/* this state machine is just about the code */ - -digraph { - need_code [label="need\ncode"] - asking_for_code [label="asking\nuser\nfor\ncode"] - creating_code [label="allocating\nnameplate"] - creating_code2 [label="generating\nsecret"] - know_code - - need_code -> know_code [label="set_code()"] - - need_code -> asking_for_code [label="input_code()"] - asking_for_code -> know_code [label="user typed code"] - - need_code -> creating_code [label="get_code()"] - creating_code -> creating_code2 [label="rx allocation"] - creating_code2 -> know_code [label="generated secret"] -} diff --git a/docs/state-machines/Makefile b/docs/state-machines/Makefile new file mode 100644 index 0000000..d9c6ab1 --- /dev/null +++ b/docs/state-machines/Makefile @@ -0,0 +1,9 @@ + +default: images + +images: boss.png code.png key.png machines.png mailbox.png nameplate.png nameplate_lister.png order.png receive.png send.png terminator.png + +.PHONY: default images + +%.png: %.dot + dot -T png $< >$@ diff --git a/docs/_connection.dot b/docs/state-machines/_connection.dot similarity index 100% rename from docs/_connection.dot rename to docs/state-machines/_connection.dot diff --git a/docs/boss.dot b/docs/state-machines/boss.dot similarity index 100% rename from docs/boss.dot rename to docs/state-machines/boss.dot diff --git a/docs/code.dot b/docs/state-machines/code.dot similarity index 100% rename from docs/code.dot rename to docs/state-machines/code.dot diff --git a/docs/key.dot b/docs/state-machines/key.dot similarity index 100% rename from docs/key.dot rename to docs/state-machines/key.dot diff --git a/docs/machines.dot b/docs/state-machines/machines.dot similarity index 100% rename from docs/machines.dot rename to docs/state-machines/machines.dot diff --git a/docs/mailbox.dot b/docs/state-machines/mailbox.dot similarity index 100% rename from docs/mailbox.dot rename to docs/state-machines/mailbox.dot diff --git a/docs/nameplate.dot b/docs/state-machines/nameplate.dot similarity index 100% rename from docs/nameplate.dot rename to docs/state-machines/nameplate.dot diff --git a/docs/nameplate_lister.dot b/docs/state-machines/nameplate_lister.dot similarity index 100% rename from docs/nameplate_lister.dot rename to docs/state-machines/nameplate_lister.dot diff --git a/docs/order.dot b/docs/state-machines/order.dot similarity index 100% rename from docs/order.dot rename to docs/state-machines/order.dot diff --git a/docs/receive.dot b/docs/state-machines/receive.dot similarity index 100% rename from docs/receive.dot rename to docs/state-machines/receive.dot diff --git a/docs/send.dot b/docs/state-machines/send.dot similarity index 100% rename from docs/send.dot rename to docs/state-machines/send.dot diff --git a/docs/terminator.dot b/docs/state-machines/terminator.dot similarity index 100% rename from docs/terminator.dot rename to docs/state-machines/terminator.dot From 9a2d992815fb104bf65e653ed7f8cac551408ebf Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 3 Mar 2017 05:37:41 -0800 Subject: [PATCH 083/176] reminder how ConnectionService should fail if first attempt fails --- docs/state-machines/_connection.dot | 15 +++++++++++++++ src/wormhole/_rendezvous.py | 1 + 2 files changed, 16 insertions(+) diff --git a/docs/state-machines/_connection.dot b/docs/state-machines/_connection.dot index 5fd6c52..3101f18 100644 --- a/docs/state-machines/_connection.dot +++ b/docs/state-machines/_connection.dot @@ -1,4 +1,19 @@ digraph { + /* note: this is nominally what we want from the machine that + establishes the WebSocket connection (and re-establishes it when it + is lost). We aren't using this yet; for now we're relying upon + twisted.application.internet.ClientService, which does reconnection + and random exponential backoff. + + The one thing it doesn't do is fail entirely when the first + connection attempt fails, which I think would be good for usability. + If the first attempt fails, it's probably because you don't have a + network connection, or the hostname is wrong, or the service has + been retired entirely. And retrying silently forever is not being + honest with the user. + + So I'm keeping this diagram around, as a reminder of how we'd like + to modify ClientService. */ /* ConnectionMachine */ diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 63cea71..0806495 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -78,6 +78,7 @@ class RendezvousConnector(object): f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) p = urlparse(self._url) ep = self._make_endpoint(p.hostname, p.port or 80) + # TODO: change/wrap ClientService to fail if the first attempt fails self._connector = internet.ClientService(ep, f) def set_trace(self, f): From e22657cf4b9face36dcb7b13e1e01aef7ef106d1 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 3 Mar 2017 05:58:10 -0800 Subject: [PATCH 084/176] remove _c2, no longer need it --- _c2.py | 131 --------------------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 _c2.py diff --git a/_c2.py b/_c2.py deleted file mode 100644 index d4c6708..0000000 --- a/_c2.py +++ /dev/null @@ -1,131 +0,0 @@ -from six.moves.urllib_parse import urlparse -from twisted.internet import defer, reactor -from ._machine import Machine - -class ConnectionMachine: - def __init__(self, ws_url): - self._ws_url = ws_url - #self._f = f = WSFactory(self._ws_url) - #f.setProtocolOptions(autoPingInterval=60, autoPingTimeout=600) - #f.connection_machine = self # calls onOpen and onClose - p = urlparse(self._ws_url) - self._ep = self._make_endpoint(p.hostname, p.port or 80) - self._connector = None - self._done_d = defer.Deferred() - - def _make_endpoint(self, hostname, port): - return None - - # "@action" marks a method as doing something, then moving to a state or - # another action. "=State()" marks a state, where we want for an event. - # "=Event()" marks an event, which causes us to move out of a state, - # through zero or more actions, and eventually landing in some other - # state. - - m = Machine() - starting = m.State("starting", initial=True, color="orange") - connecting = m.State("connecting", color="orange") - negotiating = m.State("negotiating", color="orange") - open = m.State("open", color="green") - waiting = m.State("waiting", color="blue") - reconnecting = m.State("reconnecting", color="blue") - disconnecting = m.State("disconnecting", color="orange") - cancelling = m.State("cancelling") - stopped = m.State("stopped", color="orange") - - CM_start = m.Event("CM_start") - d_callback = m.Event("d_callback") - d_errback = m.Event("d_errback") - onOpen = m.Event("onOpen") - onClose = m.Event("onClose") - stop = m.Event("stop") - expire = m.Event("expire") - - @m.action(color="orange") - def connect1(self): - d = self._ep.connect() - d.addCallbacks(self.c1_d_callback, self.c1_d_errback) - @m.action(color="red") - def notify_fail(self, ARGS): - self._done_d.errback("ERR") - @m.action(color="orange") - def opened(self): - self._p.send("bind") - self._M.connected() - @m.action() - def dropConnectionWhileNegotiating(self): - self._p.dropConnection() - @m.action(color="orange") - def dropOpenConnection(self): - self._p.dropOpenConnection() - self._M.lost() - @m.action(color="blue") - def lostConnection(self): - self._M.lost() - @m.action(color="blue") - def start_timer(self): - self._timer = reactor.callLater(self._timeout, self.expire) - @m.action(color="blue") - def reconnect(self): - d = self._ep.connect() - d.addCallbacks(self.c1_d_callback, self.c1_d_errback) - @m.action(color="blue") - def reset_timer(self): - self._timeout = self.INITIAL_TIMEOUT - @m.action() - def cancel_timer(self): - self._timer.cancel() - @m.action() - def d_cancel(self): - self._d.cancel() - @m.action(color="orange") - def MC_stopped(self): - self.MC.stopped() - - def c1_d_callback(self, p): - self.d_callback() - def c1_d_errback(self, f): - self.d_errback() - def p_onClose(self, why): - self.onClose() - def p_onOpen(self): - self.onOpen() - - starting.upon(CM_start, goto=connect1, color="orange") - connecting.upon(d_callback, goto=negotiating, color="orange") - connecting.upon(d_errback, goto=notify_fail, color="red") - connecting.upon(onClose, goto=notify_fail, color="red") - connecting.upon(stop, goto=d_cancel) - negotiating.upon(onOpen, goto=opened, color="orange") - negotiating.upon(onClose, goto=notify_fail, color="red") - negotiating.upon(stop, goto=dropConnectionWhileNegotiating) - open.upon(onClose, goto=lostConnection, color="blue") - open.upon(stop, goto=dropOpenConnection, color="orange") - waiting.upon(expire, goto=reconnect, color="blue") - waiting.upon(stop, goto=cancel_timer) - reconnecting.upon(d_callback, goto=reset_timer, color="blue") - reconnecting.upon(d_errback, goto=start_timer) - reconnecting.upon(stop, goto=d_cancel) - disconnecting.upon(onClose, goto=MC_stopped, color="orange") - cancelling.upon(d_errback, goto=MC_stopped) - - connect1.goto(connecting, color="orange") - notify_fail.goto(MC_stopped, color="red") - opened.goto(open, color="orange") - dropConnectionWhileNegotiating.goto(disconnecting) - dropOpenConnection.goto(disconnecting, color="orange") - lostConnection.goto(start_timer, color="blue") - start_timer.goto(waiting, color="blue") - reconnect.goto(reconnecting, color="blue") - reset_timer.goto(negotiating, color="blue") - cancel_timer.goto(MC_stopped) - d_cancel.goto(cancelling) - MC_stopped.goto(stopped, color="orange") - - -CM = ConnectionMachine("ws://host") -#CM.CM_start() - -if __name__ == "__main__": - import sys - CM.m._dump_dot(sys.stdout) From b7b8df17be8d0abde77edd431e582a35dfacc53f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 3 Mar 2017 06:22:40 -0800 Subject: [PATCH 085/176] rename NameplateLister to Lister (unique prefix L) --- docs/state-machines/Makefile | 2 +- docs/state-machines/code.dot | 2 +- .../{nameplate_lister.dot => lister.dot} | 4 +-- docs/state-machines/machines.dot | 30 +++++++++---------- src/wormhole/_boss.py | 12 ++++---- src/wormhole/_code.py | 16 +++++----- src/wormhole/_interfaces.py | 2 +- .../{_nameplate_lister.py => _lister.py} | 4 +-- src/wormhole/_rendezvous.py | 12 ++++---- src/wormhole/wormhole.py | 2 +- 10 files changed, 43 insertions(+), 43 deletions(-) rename docs/state-machines/{nameplate_lister.dot => lister.dot} (89%) rename src/wormhole/{_nameplate_lister.py => _lister.py} (97%) diff --git a/docs/state-machines/Makefile b/docs/state-machines/Makefile index d9c6ab1..b64cf90 100644 --- a/docs/state-machines/Makefile +++ b/docs/state-machines/Makefile @@ -1,7 +1,7 @@ default: images -images: boss.png code.png key.png machines.png mailbox.png nameplate.png nameplate_lister.png order.png receive.png send.png terminator.png +images: boss.png code.png key.png machines.png mailbox.png nameplate.png lister.png order.png receive.png send.png terminator.png .PHONY: default images diff --git a/docs/state-machines/code.dot b/docs/state-machines/code.dot index 2b76493..b56fa1d 100644 --- a/docs/state-machines/code.dot +++ b/docs/state-machines/code.dot @@ -15,7 +15,7 @@ digraph { S2 -> P2_completion [label=""] P2_completion [shape="box" label="do completion"] P2_completion -> P0_list_nameplates - P0_list_nameplates [shape="box" label="NL.refresh_nameplates"] + P0_list_nameplates [shape="box" label="L.refresh_nameplates"] P0_list_nameplates -> S2 S2 -> P2_got_nameplates [label="got_nameplates"] diff --git a/docs/state-machines/nameplate_lister.dot b/docs/state-machines/lister.dot similarity index 89% rename from docs/state-machines/nameplate_lister.dot rename to docs/state-machines/lister.dot index 77b7e1e..c7300e7 100644 --- a/docs/state-machines/nameplate_lister.dot +++ b/docs/state-machines/lister.dot @@ -1,6 +1,6 @@ digraph { {rank=same; title S0A S0B} - title [label="Nameplate\nLister" style="dotted"] + title [label="(Nameplate)\nLister" style="dotted"] S0A [label="S0A:\nnot wanting\nunconnected"] S0B [label="S0B:\nnot wanting\nconnected" color="orange"] @@ -38,5 +38,5 @@ digraph { {rank=same; foo foo2 legend} legend [shape="box" style="dotted" - label="refresh: NL.refresh_nameplates()\nrx: NL.rx_nameplates()"] + label="refresh: L.refresh_nameplates()\nrx: L.rx_nameplates()"] } diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index 14955fd..e3d48d8 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -12,8 +12,8 @@ digraph { Send [shape="box" label="Send" color="blue" fontcolor="blue"] Receive [shape="box" label="Receive" color="blue" fontcolor="blue"] Code [shape="box" label="Code" color="blue" fontcolor="blue"] - NameplateLister [shape="box" label="Nameplate\nLister" - color="blue" fontcolor="blue"] + Lister [shape="box" label="(Nameplate)\nLister" + color="blue" fontcolor="blue"] Terminator [shape="box" color="blue" fontcolor="blue"] Connection -> websocket [color="blue"] @@ -58,12 +58,12 @@ digraph { Connection -> Mailbox [style="dashed" label="connected\nlost\nrx_message\nrx_closed\nstopped"] - Connection -> NameplateLister [style="dashed" - label="connected\nlost\nrx_nameplates" - ] - NameplateLister -> Connection [style="dashed" - label="tx_list" - ] + Connection -> Lister [style="dashed" + label="connected\nlost\nrx_nameplates" + ] + Lister -> Connection [style="dashed" + label="tx_list" + ] #Boss -> Code [color="blue"] Connection -> Code [style="dashed" @@ -71,13 +71,13 @@ digraph { Code -> Connection [style="dashed" label="tx_allocate" ] - NameplateLister -> Code [style="dashed" - label="got_nameplates" - ] - #Code -> NameplateLister [color="blue"] - Code -> NameplateLister [style="dashed" - label="refresh_nameplates" - ] + Lister -> Code [style="dashed" + label="got_nameplates" + ] + #Code -> Lister [color="blue"] + Code -> Lister [style="dashed" + label="refresh_nameplates" + ] Boss -> Code [style="dashed" label="allocate_code\ninput_code\nset_code_code"] Code -> Boss [style="dashed" diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 709fd62..f89b964 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -14,7 +14,7 @@ from ._order import Order from ._key import Key from ._receive import Receive from ._rendezvous import RendezvousConnector -from ._nameplate_lister import NameplateListing +from ._lister import Lister from ._code import Code from ._terminator import Terminator from .errors import ServerError, LonelyError, WrongPasswordError @@ -44,7 +44,7 @@ class Boss(object): self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, self._timing) - self._NL = NameplateListing() + self._L = Lister() self._C = Code(self._timing) self._T = Terminator() @@ -54,9 +54,9 @@ class Boss(object): self._O.wire(self._K, self._R) self._K.wire(self, self._M, self._R) self._R.wire(self, self._S) - self._RC.wire(self, self._N, self._M, self._C, self._NL, self._T) - self._NL.wire(self._RC, self._C) - self._C.wire(self, self._RC, self._NL) + self._RC.wire(self, self._N, self._M, self._C, self._L, self._T) + self._L.wire(self._RC, self._C) + self._C.wire(self, self._RC, self._L) self._T.wire(self, self._RC, self._N, self._M) self._next_tx_phase = 0 @@ -72,7 +72,7 @@ class Boss(object): def _set_trace(self, client_name, which, logger): names = {"B": self, "N": self._N, "M": self._M, "S": self._S, "O": self._O, "K": self._K, "R": self._R, - "RC": self._RC, "NL": self._NL, "C": self._C, + "RC": self._RC, "L": self._L, "C": self._C, "T": self._T} for machine in which.split(): def tracer(old_state, input, new_state, machine=machine): diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index b483f07..eeba08f 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -28,10 +28,10 @@ class Code(object): @m.setTrace() def set_trace(): pass # pragma: no cover - def wire(self, boss, rendezvous_connector, nameplate_lister): + def wire(self, boss, rendezvous_connector, lister): self._B = _interfaces.IBoss(boss) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) - self._NL = _interfaces.INameplateLister(nameplate_lister) + self._L = _interfaces.ILister(lister) @m.state(initial=True) def S0_unknown(self): pass # pragma: no cover @@ -62,7 +62,7 @@ class Code(object): @m.input() def rx_allocated(self, nameplate): pass - # from NameplateLister + # from Lister @m.input() def got_nameplates(self, nameplates): pass @@ -75,12 +75,12 @@ class Code(object): def RETURN(self, code): pass @m.output() - def NL_refresh_nameplates(self): - self._NL.refresh_nameplates() + def L_refresh_nameplates(self): + self._L.refresh_nameplates() @m.output() - def start_input_and_NL_refresh_nameplates(self, stdio): + def start_input_and_L_refresh_nameplates(self, stdio): self._stdio = stdio - self._NL.refresh_nameplates() + self._L.refresh_nameplates() @m.output() def stash_code_length(self, code_length): self._code_length = code_length @@ -124,7 +124,7 @@ class Code(object): outputs=[generate_and_B_got_code]) S0_unknown.upon(input_code, enter=S2_typing_nameplate, - outputs=[start_input_and_NL_refresh_nameplates]) + outputs=[start_input_and_L_refresh_nameplates]) S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate, outputs=[do_completion_nameplates]) S2_typing_nameplate.upon(got_nameplates, enter=S2_typing_nameplate, diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index 0fbeaa4..e9df8c5 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -18,7 +18,7 @@ class IReceive(Interface): pass class IRendezvousConnector(Interface): pass -class INameplateLister(Interface): +class ILister(Interface): pass class ICode(Interface): pass diff --git a/src/wormhole/_nameplate_lister.py b/src/wormhole/_lister.py similarity index 97% rename from src/wormhole/_nameplate_lister.py rename to src/wormhole/_lister.py index e40e406..c43a927 100644 --- a/src/wormhole/_nameplate_lister.py +++ b/src/wormhole/_lister.py @@ -3,8 +3,8 @@ from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces -@implementer(_interfaces.INameplateLister) -class NameplateListing(object): +@implementer(_interfaces.ILister) +class Lister(object): m = MethodicalMachine() @m.setTrace() def set_trace(): pass # pragma: no cover diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 0806495..1074931 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -91,12 +91,12 @@ class RendezvousConnector(object): # TODO: Tor goes here return endpoints.HostnameEndpoint(self._reactor, hostname, port) - def wire(self, boss, nameplate, mailbox, code, nameplate_lister, terminator): + def wire(self, boss, nameplate, mailbox, code, lister, terminator): self._B = _interfaces.IBoss(boss) self._N = _interfaces.INameplate(nameplate) self._M = _interfaces.IMailbox(mailbox) self._C = _interfaces.ICode(code) - self._NL = _interfaces.INameplateLister(nameplate_lister) + self._L = _interfaces.ILister(lister) self._T = _interfaces.ITerminator(terminator) # from Boss @@ -127,7 +127,7 @@ class RendezvousConnector(object): d.addBoth(self._stopped) - # from NameplateLister + # from Lister def tx_list(self): self._tx("list") @@ -143,7 +143,7 @@ class RendezvousConnector(object): self._C.connected() self._N.connected() self._M.connected() - self._NL.connected() + self._L.connected() def ws_message(self, payload): msg = bytes_to_dict(payload) @@ -177,7 +177,7 @@ class RendezvousConnector(object): self._C.lost() self._N.lost() self._M.lost() - self._NL.lost() + self._L.lost() # internal def _stopped(self, res): @@ -210,7 +210,7 @@ class RendezvousConnector(object): nameplate_id = n["id"] assert isinstance(nameplate_id, type("")), type(nameplate_id) nids.append(nameplate_id) - self._NL.rx_nameplates(nids) + self._L.rx_nameplates(nids) def _response_handle_ack(self, msg): pass diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 527bcde..42cab86 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -127,7 +127,7 @@ class _DeferredWormhole(object): self._closed_observers.append(d) return d - def debug_set_trace(self, client_name, which="B N M S O K R RC NL C T", + def debug_set_trace(self, client_name, which="B N M S O K R RC L C T", logger=_log): self._boss._set_trace(client_name, which, logger) From c499fce9f55bf9e707034314776bf55f43f100d5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Fri, 3 Mar 2017 23:19:48 +0100 Subject: [PATCH 086/176] change API (wormhole.create), start on serialization --- src/wormhole/_boss.py | 3 ++ src/wormhole/cli/cmd_receive.py | 15 +++++----- src/wormhole/cli/cmd_send.py | 24 ++++++++------- src/wormhole/wormhole.py | 53 ++++++++++++++++++++------------- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index f89b964..9aa73fd 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -80,6 +80,9 @@ class Boss(object): old_state, input, new_state)) names[machine].set_trace(tracer) + def serialize(self): + raise NotImplemented + # and these are the state-machine transition functions, which don't take # args @m.state(initial=True) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index c262b9a..0c2c438 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -5,7 +5,7 @@ from humanize import naturalsize from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue from twisted.python import log -from ..wormhole import wormhole +from .. import wormhole from ..transit import TransitReceiver from ..errors import TransferError, WormholeClosedError, NoTorError from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, @@ -61,8 +61,10 @@ class TwistedReceiver: # with the user handing off the wormhole code yield self._tor_manager.start() - w = wormhole(self.args.appid or APPID, self.args.relay_url, - self._reactor, self._tor_manager, timing=self.args.timing) + w = wormhole.create(self.args.appid or APPID, self.args.relay_url, + self._reactor, + tor_manager=self._tor_manager, + timing=self.args.timing) # I wanted to do this instead: # # try: @@ -80,13 +82,12 @@ class TwistedReceiver: @inlineCallbacks def _go(self, w): yield self._handle_code(w) - yield w.establish_key() def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=self.args.stderr) notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) try: - verifier = yield w.verify() + verifier = yield w.when_verifier() finally: if not notify.called: notify.cancel() @@ -127,7 +128,7 @@ class TwistedReceiver: @inlineCallbacks def _get_data(self, w): # this may raise WrongPasswordError - them_bytes = yield w.get() + them_bytes = yield w.when_received() them_d = bytes_to_dict(them_bytes) if "error" in them_d: raise TransferError(them_d["error"]) @@ -142,7 +143,7 @@ class TwistedReceiver: if code: w.set_code(code) else: - yield w.input_code("Enter receive wormhole code: ", + yield w.input_code("Enter receive wormhole code: ", # TODO self.args.code_length) def _show_verifier(self, verifier): diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index 12dd3bb..30436b8 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -7,7 +7,7 @@ from twisted.protocols import basic from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue from ..errors import TransferError, WormholeClosedError, NoTorError -from ..wormhole import wormhole +from .. import wormhole from ..transit import TransitSender from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr @@ -52,9 +52,10 @@ class Sender: # with the user handing off the wormhole code yield self._tor_manager.start() - w = wormhole(self._args.appid or APPID, self._args.relay_url, - self._reactor, self._tor_manager, - timing=self._timing) + w = wormhole.create(self._args.appid or APPID, self._args.relay_url, + self._reactor, + tor_manager=self._tor_manager, + timing=self._timing) d = self._go(w) d.addBoth(w.close) # must wait for ack from close() yield d @@ -83,25 +84,25 @@ class Sender: if args.code: w.set_code(args.code) - code = args.code else: - code = yield w.get_code(args.code_length) + w.allocate_code(args.code_length) + code = yield w.when_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) - yield w.establish_key() def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=args.stderr) notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) - # TODO: don't stall on w.verify() unless they want it + # TODO: maybe don't stall on verifier unless they want it try: - verifier_bytes = yield w.verify() # this may raise WrongPasswordError + # this may raise WrongPasswordError + verifier_bytes = yield w.when_verifier() finally: if not notify.called: notify.cancel() @@ -146,12 +147,13 @@ class Sender: while True: try: - them_d_bytes = yield w.get() + them_d_bytes = yield w.when_received() except WormholeClosedError: if done: returnValue(None) raise TransferError("unexpected close") - # TODO: get() fired, so now it's safe to use w.derive_key() + # TODO: when_received() 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 diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 42cab86..15449c6 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -51,6 +51,12 @@ class _DelegatedWormhole(object): def set_code(self, code): self._boss.set_code(code) + def serialize(self): + s = {"serialized_wormhole_version": 1, + "boss": self._boss.serialize(), + } + return s + def send(self, plaintext): self._boss.send(plaintext) def close(self): @@ -116,6 +122,7 @@ class _DeferredWormhole(object): def set_code(self, code): self._boss.set_code(code) + # no .serialize in Deferred-mode def send(self, plaintext): self._boss.send(plaintext) def close(self): @@ -165,11 +172,8 @@ class _DeferredWormhole(object): for d in self._closed_observers: d.callback(close_result) -def _wormhole(appid, relay_url, reactor, delegate=None, - tor_manager=None, timing=None, - journal=None, - stderr=sys.stderr, - ): +def create(appid, relay_url, reactor, delegate=None, journal=None, + tor_manager=None, timing=None, stderr=sys.stderr): timing = timing or DebugTiming() side = bytes_to_hexstr(os.urandom(5)) journal = journal or ImmediateJournal() @@ -179,23 +183,30 @@ def _wormhole(appid, relay_url, reactor, delegate=None, w = _DeferredWormhole() b = Boss(w, side, relay_url, appid, reactor, journal, timing) w._set_boss(b) - # force allocate for now b.start() return w -def delegated_wormhole(appid, relay_url, reactor, delegate, - tor_manager=None, timing=None, - journal=None, - stderr=sys.stderr, - ): - assert delegate - return _wormhole(appid, relay_url, reactor, delegate, - tor_manager, timing, journal, stderr) +def from_serialized(serialized, reactor, delegate, + journal=None, tor_manager=None, + timing=None, stderr=sys.stderr): + assert serialized["serialized_wormhole_version"] == 1 + timing = timing or DebugTiming() + w = _DelegatedWormhole(delegate) + # now unpack state machines, including the SPAKE2 in Key + b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing) + w._set_boss(b) + b.start() # ?? + raise NotImplemented + # should the new Wormhole call got_code? only if it wasn't called before. + +# after creating the wormhole object, app must call exactly one of: +# set_code(code), generate_code(), helper=type_code(), and then (if they need +# to know the code) wait for delegate.got_code() or d=w.when_code() + +# the helper for type_code() can be asked for completions: +# d=helper.get_completions(text_so_far), which will fire with a list of +# strings that could usefully be appended to text_so_far. + +# wormhole.type_code_readline(w) is a wrapper that knows how to use +# w.type_code() to drive rlcompleter -def deferred_wormhole(appid, relay_url, reactor, - tor_manager=None, timing=None, - journal=None, - stderr=sys.stderr, - ): - return _wormhole(appid, relay_url, reactor, None, - tor_manager, timing, journal, stderr) From 0474c39bab1971a4939c65e74290882af68dd08c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 11:34:45 +0100 Subject: [PATCH 087/176] tests: match API change --- src/wormhole/test/test_wormhole_new.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 8eadb20..9a01702 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -28,7 +28,7 @@ class New(ServerBase, unittest.TestCase): @inlineCallbacks def test_allocate(self): - w = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w = wormhole.create(APPID, self.relayurl, reactor) #w.debug_set_trace("W1") w.allocate_code(2) code = yield w.when_code() @@ -40,18 +40,19 @@ class New(ServerBase, unittest.TestCase): def test_delegated(self): dg = Delegate() - w = wormhole.delegated_wormhole(APPID, self.relayurl, reactor, dg) + w = wormhole.create(APPID, self.relayurl, reactor, delegate=dg) w.close() @inlineCallbacks def test_basic(self): - w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) #w1.debug_set_trace("W1") w1.allocate_code(2) code = yield w1.when_code() mo = re.search(r"^\d+-\w+-\w+$", code) self.assert_(mo, code) - w2 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + #w2.debug_set_trace(" W2") w2.set_code(code) code2 = yield w2.when_code() self.assertEqual(code, code2) @@ -72,11 +73,11 @@ class New(ServerBase, unittest.TestCase): @inlineCallbacks def test_wrong_password(self): - w1 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) #w1.debug_set_trace("W1") w1.allocate_code(2) code = yield w1.when_code() - w2 = wormhole.deferred_wormhole(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) w2.set_code(code+", NOT") code2 = yield w2.when_code() self.assertNotEqual(code, code2) From 60a61c995b8ed72f43f0bf229ecf31f9907490a5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 10:55:42 +0100 Subject: [PATCH 088/176] implement w.derive_key() --- docs/state-machines/boss.dot | 2 +- docs/state-machines/key.dot | 2 +- docs/state-machines/machines.dot | 4 ++-- src/wormhole/_boss.py | 8 ++++++- src/wormhole/_key.py | 1 + src/wormhole/errors.py | 3 +++ src/wormhole/test/test_machines.py | 11 +++++----- src/wormhole/wormhole.py | 35 ++++++++++++++++++++++++++++++ 8 files changed, 56 insertions(+), 10 deletions(-) diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index 6bf3942..2fe7bef 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -63,7 +63,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="rx_welcome -> process\nsend -> S.send\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + label="rx_welcome -> process\nsend -> S.send\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/docs/state-machines/key.dot b/docs/state-machines/key.dot index 5e4e23c..b6d49f8 100644 --- a/docs/state-machines/key.dot +++ b/docs/state-machines/key.dot @@ -35,7 +35,7 @@ digraph { S1 -> P1_compute [label="got_pake\npake good"] #S1 -> P_mood_lonely [label="close"] - P1_compute [label="compute_key\nM.add_message(version)\nW.got_verifier\nR.got_key" shape="box"] + P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nB.got_verifier\nR.got_key" shape="box"] P1_compute -> S2 S2 [label="S2: know_key" color="green"] diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index e3d48d8..a4cd0da 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -21,7 +21,7 @@ digraph { Wormhole -> Boss [style="dashed" label="allocate_code\ninput_code\nset_code\nsend\nclose\n(once)"] #Wormhole -> Boss [color="blue"] - Boss -> Wormhole [style="dashed" label="got_code\ngot_verifier\nreceived (seq)\nclosed\n(once)"] + Boss -> Wormhole [style="dashed" label="got_code\ngot_key\ngot_verifier\nreceived (seq)\nclosed\n(once)"] #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] @@ -33,7 +33,7 @@ digraph { #Boss -> Mailbox [color="blue"] Mailbox -> Order [style="dashed" label="got_message (once)"] Boss -> Key [style="dashed" label="got_code"] - Key -> Boss [style="dashed" label="got_verifier\nscared"] + Key -> Boss [style="dashed" label="got_key\ngot_verifier\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] #Boss -> Key [color="blue"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 9aa73fd..2ff775c 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -135,7 +135,7 @@ class Boss(object): @m.input() def got_code(self, code): pass - # Key sends (got_verifier, scared) + # Key sends (got_key, got_verifier, scared) # Receive sends (got_message, happy, scared) @m.input() def happy(self): pass @@ -158,6 +158,8 @@ class Boss(object): @m.input() def got_phase(self, phase, plaintext): pass @m.input() + def got_key(self, key): pass + @m.input() def got_verifier(self, verifier): pass # Terminator sends closed @@ -205,6 +207,9 @@ class Boss(object): self._T.close("happy") @m.output() + def W_got_key(self, key): + self._W.got_key(key) + @m.output() def W_got_verifier(self, verifier): self._W.got_verifier(verifier) @m.output() @@ -238,6 +243,7 @@ class Boss(object): S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared]) S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) + S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key]) S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error]) S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index e5f54fd..6370df2 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -110,6 +110,7 @@ class Key(object): assert isinstance(msg2, type(b"")) with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) + self._B.got_key(key) self._B.got_verifier(derive_key(key, b"wormhole:verifier")) phase = "version" data_key = derive_phase_key(key, self._side, phase) diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index d13aa87..865b5e2 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -50,3 +50,6 @@ class TransferError(WormholeError): class NoTorError(WormholeError): """--tor was requested, but 'txtorcon' is not installed.""" + +class NoKeyError(WormholeError): + """w.derive_key() was called before got_verifier() fired""" diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 661d71b..a5c3730 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -147,7 +147,7 @@ class Key(unittest.TestCase): def build(self): events = [] k = _key.Key(u"appid", u"side", timing.DebugTiming()) - b = Dummy("b", events, IBoss, "scared", "got_verifier") + b = Dummy("b", events, IBoss, "scared", "got_key", "got_verifier") m = Dummy("m", events, IMailbox, "add_message") r = Dummy("r", events, IReceive, "got_key") k.wire(b, m, r) @@ -168,10 +168,11 @@ class Key(unittest.TestCase): key2 = sp.finish(msg1_bytes) msg2 = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg2_bytes)}) k.got_pake(msg2) - self.assertEqual(len(events), 3) - self.assertEqual(events[0][0], "b.got_verifier") - self.assertEqual(events[1][:2], ("m.add_message", "version")) - self.assertEqual(events[2], ("r.got_key", key2)) + self.assertEqual(len(events), 4, events) + self.assertEqual(events[0], ("b.got_key", key2)) + self.assertEqual(events[1][0], "b.got_verifier") + self.assertEqual(events[2][:2], ("m.add_message", "version")) + self.assertEqual(events[3], ("r.got_key", key2)) def test_bad(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 15449c6..aed3931 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -9,6 +9,9 @@ from .util import bytes_to_hexstr from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss +from ._key import derive_key +from .errors import NoKeyError +from .util import to_bytes # We can provide different APIs to different apps: # * Deferreds @@ -39,6 +42,9 @@ def _log(client_name, machine_name, old_state, input, new_state): class _DelegatedWormhole(object): _delegate = attrib() + def __attrs_post_init__(self): + self._key = None + def _set_boss(self, boss): self._boss = boss @@ -59,6 +65,18 @@ class _DelegatedWormhole(object): def send(self, plaintext): self._boss.send(plaintext) + + def derive_key(self, purpose, length): + """Derive a new key from the established wormhole channel for some + other purpose. This is a deterministic randomized function of the + session key and the 'purpose' string (unicode/py3-string). This + cannot be called until when_verifier() has fired, nor after close() + was called. + """ + if not isinstance(purpose, type("")): raise TypeError(type(purpose)) + if not self._key: raise NoKeyError() + return derive_key(self._key, to_bytes(purpose), length) + def close(self): self._boss.close() @@ -69,6 +87,8 @@ class _DelegatedWormhole(object): # from below def got_code(self, code): self._delegate.wormhole_got_code(code) + def got_key(self, key): + self._key = key # for derive_key() def got_verifier(self, verifier): self._delegate.wormhole_got_verifier(verifier) def received(self, plaintext): @@ -84,6 +104,7 @@ class _DeferredWormhole(object): def __init__(self): self._code = None self._code_observers = [] + self._key = None self._verifier = None self._verifier_observers = [] self._received_data = [] @@ -125,6 +146,18 @@ class _DeferredWormhole(object): # no .serialize in Deferred-mode def send(self, plaintext): self._boss.send(plaintext) + + def derive_key(self, purpose, length): + """Derive a new key from the established wormhole channel for some + other purpose. This is a deterministic randomized function of the + session key and the 'purpose' string (unicode/py3-string). This + cannot be called until when_verifier() has fired, nor after close() + was called. + """ + if not isinstance(purpose, type("")): raise TypeError(type(purpose)) + if not self._key: raise NoKeyError() + return derive_key(self._key, to_bytes(purpose), length) + def close(self): # fails with WormholeError unless we established a connection # (state=="happy"). Fails with WrongPasswordError (a subclass of @@ -144,6 +177,8 @@ class _DeferredWormhole(object): for d in self._code_observers: d.callback(code) self._code_observers[:] = [] + def got_key(self, key): + self._key = key # for derive_key() def got_verifier(self, verifier): self._verifier = verifier for d in self._verifier_observers: From e9f31071270ac8f876df7dd516d4fb7d48ac04a1 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 11:36:19 +0100 Subject: [PATCH 089/176] deliver app-versions up to Wormhole --- docs/state-machines/boss.dot | 14 +++++++++----- docs/state-machines/machines.dot | 2 +- src/wormhole/_boss.py | 6 +++++- src/wormhole/test/test_wormhole_new.py | 10 ++++++++++ src/wormhole/wormhole.py | 23 +++++++++++++++++++++-- 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index 2fe7bef..d8e3380 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -42,16 +42,20 @@ digraph { P2_close [shape="box" label="T.close(happy)"] P2_close -> S_closing - S2 -> P2_got_message [label="got_message"] - P2_got_message [shape="box" label="W.received"] - P2_got_message -> S2 + S2 -> P2_got_phase [label="got_phase"] + P2_got_phase [shape="box" label="W.received"] + P2_got_phase -> S2 + + S2 -> P2_got_version [label="got_version"] + P2_got_version [shape="box" label="W.got_version"] + P2_got_version -> S2 S2 -> P_close_error [label="rx_error"] S2 -> P_close_scary [label="scared" color="red"] S_closing [label="closing"] S_closing -> P_closed [label="closed\nerror"] - S_closing -> S_closing [label="got_message\nhappy\nscared\nclose"] + S_closing -> S_closing [label="got_version\ngot_phase\nhappy\nscared\nclose"] P_closed [shape="box" label="W.closed(reason)"] P_closed -> S_closed @@ -63,7 +67,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - label="rx_welcome -> process\nsend -> S.send\ngot_key -> W.got_key\ngot_verifier -> W.got_verifier\nallocate -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + 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 -> C.allocate\ninput -> C.input\nset_code -> C.set_code" ] diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index a4cd0da..c1a6b6c 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -21,7 +21,7 @@ digraph { Wormhole -> Boss [style="dashed" label="allocate_code\ninput_code\nset_code\nsend\nclose\n(once)"] #Wormhole -> Boss [color="blue"] - Boss -> Wormhole [style="dashed" label="got_code\ngot_key\ngot_verifier\nreceived (seq)\nclosed\n(once)"] + Boss -> Wormhole [style="dashed" label="got_code\ngot_key\ngot_verifier\ngot_version\nreceived (seq)\nclosed\n(once)"] #Boss -> Connection [color="blue"] Boss -> Connection [style="dashed" label="start"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 2ff775c..97fbc62 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -179,8 +179,12 @@ class Boss(object): self._W.got_code(code) @m.output() def process_version(self, plaintext): + # most of this is wormhole-to-wormhole, ignored for now + # in the future, this is how Dilation is signalled self._their_versions = bytes_to_dict(plaintext) - # ignored for now + # but this part is app-to-app + app_versions = self._their_versions.get("app_versions", {}) + self._W.got_version(app_versions) @m.output() def S_send(self, plaintext): diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 9a01702..2a271d0 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -57,6 +57,16 @@ class New(ServerBase, unittest.TestCase): code2 = yield w2.when_code() self.assertEqual(code, code2) + verifier1 = yield w1.when_verifier() + verifier2 = yield w2.when_verifier() + self.assertEqual(verifier1, verifier2) + + version1 = yield w1.when_version() + version2 = yield w2.when_version() + # TODO: add the ability to set app-versions + self.assertEqual(version1, {}) + self.assertEqual(version2, {}) + w1.send(b"data") data = yield w2.when_received() diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index aed3931..9885272 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -25,6 +25,7 @@ from .util import to_bytes # w.send(data) # app.wormhole_got_code(code) # app.wormhole_got_verifier(verifier) +# app.wormhole_got_version(version) # app.wormhole_receive(data) # w.close() # app.wormhole_closed() @@ -91,6 +92,8 @@ class _DelegatedWormhole(object): self._key = key # for derive_key() def got_verifier(self, verifier): self._delegate.wormhole_got_verifier(verifier) + def got_version(self, version): + self._delegate.wormhole_got_version(version) def received(self, plaintext): self._delegate.wormhole_received(plaintext) def closed(self, result): @@ -107,6 +110,8 @@ class _DeferredWormhole(object): self._key = None self._verifier = None self._verifier_observers = [] + self._version = None + self._version_observers = [] self._received_data = [] self._received_observers = [] self._closed_observers = [] @@ -129,6 +134,13 @@ class _DeferredWormhole(object): self._verifier_observers.append(d) return d + def when_version(self): + if self._version is not None: + return defer.succeed(self._version) + d = defer.Deferred() + self._version_observers.append(d) + return d + def when_received(self): if self._received_data: return defer.succeed(self._received_data.pop(0)) @@ -138,7 +150,7 @@ class _DeferredWormhole(object): def allocate_code(self, code_length=2): self._boss.allocate_code(code_length) - def input_code(self, stdio): + def input_code(self, stdio): # TODO self._boss.input_code(stdio) def set_code(self, code): self._boss.set_code(code) @@ -184,6 +196,11 @@ class _DeferredWormhole(object): for d in self._verifier_observers: d.callback(verifier) self._verifier_observers[:] = [] + def got_version(self, version): + self._version = version + for d in self._version_observers: + d.callback(version) + self._version_observers[:] = [] def received(self, plaintext): if self._received_observers: @@ -196,12 +213,14 @@ class _DeferredWormhole(object): if isinstance(result, Exception): observer_result = close_result = failure.Failure(result) else: - # pending w.verify() or w.read() get an error + # pending w.verify()/w.version()/w.read() get an error observer_result = WormholeClosed(result) # but w.close() only gets error if we're unhappy close_result = result for d in self._verifier_observers: d.errback(observer_result) + for d in self._version_observers: + d.errback(observer_result) for d in self._received_observers: d.errback(observer_result) for d in self._closed_observers: From 8ee342ad826530e726bca2a3ee77360a09df4e2f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 11:44:40 +0100 Subject: [PATCH 090/176] make cmd_send/cmd_receive basically work again --- src/wormhole/cli/cmd_receive.py | 11 +++++++++-- src/wormhole/cli/cmd_send.py | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index 0c2c438..c15b458 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -76,18 +76,23 @@ class TwistedReceiver: # as coming from the "yield self._go" line, which wasn't very useful # for tracking it down. d = self._go(w) - d.addBoth(w.close) + @inlineCallbacks + def _close(res): + yield w.close() + returnValue(res) + d.addBoth(_close) yield d @inlineCallbacks def _go(self, w): yield self._handle_code(w) + verifier = yield w.when_verifier() def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=self.args.stderr) notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) try: - verifier = yield w.when_verifier() + yield w.when_version() finally: if not notify.called: notify.cancel() @@ -143,8 +148,10 @@ class TwistedReceiver: if code: w.set_code(code) else: + raise NotImplemented yield w.input_code("Enter receive wormhole code: ", # TODO self.args.code_length) + yield w.when_code() def _show_verifier(self, verifier): verifier_hex = bytes_to_hexstr(verifier) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index 30436b8..337f314 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -57,7 +57,11 @@ class Sender: tor_manager=self._tor_manager, timing=self._timing) d = self._go(w) - d.addBoth(w.close) # must wait for ack from close() + @inlineCallbacks + def _close(res): + yield w.close() # must wait for ack from close() + returnValue(res) + d.addBoth(_close) yield d def _send_data(self, data, w): @@ -94,15 +98,20 @@ class Sender: args.stderr.flush() print(u"", file=args.stderr) + verifier_bytes = yield w.when_verifier() + # we've seen PAKE, but not yet VERSION, so we don't know if they got + # the right password or not + def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=args.stderr) notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) - # TODO: maybe don't stall on verifier unless they want it + # TODO: maybe don't stall for VERSION, if they don't want + # verification, to save a roundtrip? try: + yield w.when_version() # this may raise WrongPasswordError - verifier_bytes = yield w.when_verifier() finally: if not notify.called: notify.cancel() From 6ada8252b76bbb7dd79bf31ff66fc68e249344a9 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 12:40:19 +0100 Subject: [PATCH 091/176] Code: handle being connected before being told what to do --- docs/state-machines/code.dot | 19 +++++++--- src/wormhole/_code.py | 26 ++++++++++--- src/wormhole/test/test_machines.py | 61 ++++++++++++++++++++++++++++-- 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/docs/state-machines/code.dot b/docs/state-machines/code.dot index b56fa1d..0cde977 100644 --- a/docs/state-machines/code.dot +++ b/docs/state-machines/code.dot @@ -1,15 +1,20 @@ digraph { start [label="Wormhole Code\nMachine" style="dotted"] - {rank=same; start S0} - start -> S0 [style="invis"] - S0 [label="S0:\nunknown\ndisconnected"] - S0 -> P0_got_code [label="set_code"] + {rank=same; start S0A S0B} + start -> S0A [style="invis"] + S0A [label="S0A:\nunknown\ndisconnected"] + S0A -> S0B [label="connected"] + S0B -> S0A [label="lost"] + S0B [label="S0B:\nunknown\nconnected"] + S0A -> P0_got_code [label="set_code"] + S0B -> P0_got_code [label="set_code"] P0_got_code [shape="box" label="B.got_code"] P0_got_code -> S4 S4 [label="S4: known" color="green"] - S0 -> P0_list_nameplates [label="input_code"] + S0A -> P0_list_nameplates [label="input_code"] + S0B -> P0_list_nameplates [label="input_code"] S2 [label="S2: typing\nnameplate"] S2 -> P2_completion [label=""] @@ -32,7 +37,7 @@ digraph { S3 -> P0_got_code [label="" color="orange" fontcolor="orange"] - S0 -> S1A [label="allocate_code"] + S0A -> S1A [label="allocate_code"] S1A [label="S1A:\nconnecting"] S1A -> P1_allocate [label="connected"] P1_allocate [shape="box" label="RC.tx_allocate"] @@ -42,5 +47,7 @@ digraph { S1B -> S1A [label="lost"] P1_generate [shape="box" label="generate\nrandom code"] P1_generate -> P0_got_code + + S0B -> P1_allocate [label="allocate_code"] } diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index eeba08f..307dc5e 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -34,7 +34,9 @@ class Code(object): self._L = _interfaces.ILister(lister) @m.state(initial=True) - def S0_unknown(self): pass # pragma: no cover + def S0A_unknown(self): pass # pragma: no cover + @m.state() + def S0B_unknown_connected(self): pass # pragma: no cover @m.state() def S1A_connecting(self): pass # pragma: no cover @m.state() @@ -82,6 +84,10 @@ class Code(object): self._stdio = stdio self._L.refresh_nameplates() @m.output() + def stash_code_length_and_RC_tx_allocate(self, code_length): + self._code_length = code_length + self._RC.tx_allocate() + @m.output() def stash_code_length(self, code_length): self._code_length = code_length @m.output() @@ -113,18 +119,26 @@ class Code(object): def _B_got_code(self): self._B.got_code(self._code) - S0_unknown.upon(set_code, enter=S4_known, outputs=[B_got_code]) + S0A_unknown.upon(connected, enter=S0B_unknown_connected, outputs=[]) + S0B_unknown_connected.upon(lost, enter=S0A_unknown, outputs=[]) - S0_unknown.upon(allocate_code, enter=S1A_connecting, - outputs=[stash_code_length]) + S0A_unknown.upon(set_code, enter=S4_known, outputs=[B_got_code]) + S0B_unknown_connected.upon(set_code, enter=S4_known, outputs=[B_got_code]) + + S0A_unknown.upon(allocate_code, enter=S1A_connecting, + outputs=[stash_code_length]) + S0B_unknown_connected.upon(allocate_code, enter=S1B_allocating, + outputs=[stash_code_length_and_RC_tx_allocate]) S1A_connecting.upon(connected, enter=S1B_allocating, outputs=[RC_tx_allocate]) S1B_allocating.upon(lost, enter=S1A_connecting, outputs=[]) S1B_allocating.upon(rx_allocated, enter=S4_known, outputs=[generate_and_B_got_code]) - S0_unknown.upon(input_code, enter=S2_typing_nameplate, - outputs=[start_input_and_L_refresh_nameplates]) + S0A_unknown.upon(input_code, enter=S2_typing_nameplate, + outputs=[start_input_and_L_refresh_nameplates]) + S0B_unknown_connected.upon(input_code, enter=S2_typing_nameplate, + outputs=[start_input_and_L_refresh_nameplates]) S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate, outputs=[do_completion_nameplates]) S2_typing_nameplate.upon(got_nameplates, enter=S2_typing_nameplate, diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index a5c3730..4830901 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -2,8 +2,9 @@ from __future__ import print_function, unicode_literals import json from zope.interface import directlyProvides from twisted.trial import unittest -from .. import timing, _order, _receive, _key -from .._interfaces import IKey, IReceive, IBoss, ISend, IMailbox +from .. import timing, _order, _receive, _key, _code +from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, + IRendezvousConnector, ILister) from .._key import derive_key, derive_phase_key, encrypt_data from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes from spake2 import SPAKE2_Symmetric @@ -174,7 +175,6 @@ class Key(unittest.TestCase): self.assertEqual(events[2][:2], ("m.add_message", "version")) self.assertEqual(events[3], ("r.got_key", key2)) - def test_bad(self): k, b, m, r, events = self.build() code = u"1-foo" @@ -188,3 +188,58 @@ class Key(unittest.TestCase): bad_pake_d = {"not_pake_v1": "stuff"} k.got_pake(dict_to_bytes(bad_pake_d)) self.assertEqual(events, [("b.scared",)]) + +class Code(unittest.TestCase): + def build(self): + events = [] + c = _code.Code(timing.DebugTiming()) + b = Dummy("b", events, IBoss, "got_code") + rc = Dummy("rc", events, IRendezvousConnector, "tx_allocate") + l = Dummy("l", events, ILister, "refresh_nameplates") + c.wire(b, rc, l) + return c, b, rc, l, events + + def test_set_disconnected(self): + c, b, rc, l, events = self.build() + c.set_code(u"code") + self.assertEqual(events, [("b.got_code", u"code")]) + + def test_set_connected(self): + c, b, rc, l, events = self.build() + c.connected() + c.set_code(u"code") + self.assertEqual(events, [("b.got_code", u"code")]) + + def test_allocate_disconnected(self): + c, b, rc, l, events = self.build() + c.allocate_code(2) + self.assertEqual(events, []) + c.connected() + self.assertEqual(events, [("rc.tx_allocate",)]) + events[:] = [] + c.lost() + self.assertEqual(events, []) + c.connected() + self.assertEqual(events, [("rc.tx_allocate",)]) + events[:] = [] + c.rx_allocated("4") + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "b.got_code") + code = events[0][1] + self.assert_(code.startswith("4-"), code) + + def test_allocate_connected(self): + c, b, rc, l, events = self.build() + c.connected() + c.allocate_code(2) + self.assertEqual(events, [("rc.tx_allocate",)]) + events[:] = [] + c.rx_allocated("4") + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "b.got_code") + code = events[0][1] + self.assert_(code.startswith("4-"), code) + + # TODO: input_code + + From dfe9fd2395738181f5e9246f68c44d704c759ca6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 12:40:52 +0100 Subject: [PATCH 092/176] RC: internal errors during ws_open should halt boss --- src/wormhole/_rendezvous.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 1074931..1789333 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -139,11 +139,16 @@ class RendezvousConnector(object): def ws_open(self, proto): self._debug("R.connected") self._ws = proto - self._tx("bind", appid=self._appid, side=self._side) - self._C.connected() - self._N.connected() - self._M.connected() - self._L.connected() + try: + self._tx("bind", appid=self._appid, side=self._side) + self._C.connected() + self._N.connected() + self._M.connected() + self._L.connected() + except Exception as e: + self._B.error(e) + raise + self._debug("R.connected finished notifications") def ws_message(self, payload): msg = bytes_to_dict(payload) From 4c6cb1dddc3736f368b2764cff52486ad4107c21 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 12:41:54 +0100 Subject: [PATCH 093/176] xfer_util: update to new API --- src/wormhole/xfer_util.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/wormhole/xfer_util.py b/src/wormhole/xfer_util.py index a3c3dd0..7dcfba9 100644 --- a/src/wormhole/xfer_util.py +++ b/src/wormhole/xfer_util.py @@ -1,7 +1,7 @@ import json from twisted.internet.defer import inlineCallbacks, returnValue -from .wormhole import wormhole +from . import wormhole from .tor_manager import TorManager from .errors import NoTorError @@ -38,16 +38,17 @@ def receive(reactor, appid, relay_url, code, raise NoTorError() yield tm.start() - wh = wormhole(appid, relay_url, reactor, tor_manager=tm) + wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) if code is None: - code = yield wh.get_code() + wh.allocate_code() + code = yield wh.when_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.get() + data = yield wh.when_received() data = json.loads(data.decode("utf-8")) offer = data.get('offer', None) if not offer: @@ -100,9 +101,10 @@ def send(reactor, appid, relay_url, data, code, if not tm.tor_available(): raise NoTorError() yield tm.start() - wh = wormhole(appid, relay_url, reactor, tor_manager=tm) + wh = wormhole.create(appid, relay_url, reactor, tor_manager=tm) if code is None: - code = yield wh.get_code() + wh.allocate_code() + code = yield wh.when_code() else: wh.set_code(code) if on_code: @@ -115,7 +117,7 @@ def send(reactor, appid, relay_url, data, code, } }).encode("utf-8") ) - data = yield wh.get() + data = yield wh.when_received() data = json.loads(data.decode("utf-8")) answer = data.get('answer', None) yield wh.close() From 105d9cc59fe688f2770810fa06fb4558955f8c60 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 12:43:42 +0100 Subject: [PATCH 094/176] work on WelcomeHandler, still incomplete --- src/wormhole/_boss.py | 3 ++- src/wormhole/wormhole.py | 51 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 97fbc62..4cb1e36 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -27,6 +27,7 @@ class Boss(object): _side = attrib(validator=instance_of(type(u""))) _url = attrib(validator=instance_of(type(u""))) _appid = attrib(validator=instance_of(type(u""))) + _welcome_handler = attrib() # TODO: validator: callable _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) _timing = attrib(validator=provides(_interfaces.ITiming)) @@ -169,7 +170,7 @@ class Boss(object): @m.output() def process_welcome(self, welcome): - pass # TODO: ignored for now + self._welcome_handler(welcome) @m.output() def do_got_code(self, code): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 9885272..7dec829 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -10,14 +10,14 @@ from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss from ._key import derive_key -from .errors import NoKeyError +from .errors import WelcomeError, NoKeyError from .util import to_bytes # We can provide different APIs to different apps: # * Deferreds -# w.when_got_code().addCallback(print_code) +# w.when_code().addCallback(print_code) # w.send(data) -# w.receive().addCallback(got_data) +# w.when_received().addCallback(got_data) # w.close().addCallback(closed) # * delegate callbacks (better for journaled environments) @@ -38,6 +38,36 @@ def _log(client_name, machine_name, old_state, input, new_state): print("%s.%s[%s].%s -> [%s]" % (client_name, machine_name, old_state, input, new_state)) +class _WelcomeHandler: + def __init__(self, url, current_version, signal_error): + self._ws_url = url + self._version_warning_displayed = False + self._current_version = current_version + self._signal_error = signal_error + + 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._ws_url, motd_formatted), file=sys.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 @implementer(IWormhole) class _DelegatedWormhole(object): @@ -88,6 +118,8 @@ class _DelegatedWormhole(object): # from below def got_code(self, code): self._delegate.wormhole_got_code(code) + def got_welcome(self, welcome): + pass # TODO def got_key(self, key): self._key = key # for derive_key() def got_verifier(self, verifier): @@ -189,6 +221,8 @@ class _DeferredWormhole(object): for d in self._code_observers: d.callback(code) self._code_observers[:] = [] + def got_welcome(self, welcome): + pass # TODO def got_key(self, key): self._key = key # for derive_key() def got_verifier(self, verifier): @@ -226,16 +260,23 @@ class _DeferredWormhole(object): for d in self._closed_observers: d.callback(close_result) + def create(appid, relay_url, reactor, delegate=None, journal=None, - tor_manager=None, timing=None, stderr=sys.stderr): + tor_manager=None, timing=None, welcome_handler=None, + stderr=sys.stderr): timing = timing or DebugTiming() side = bytes_to_hexstr(os.urandom(5)) journal = journal or ImmediateJournal() + if not welcome_handler: + from . import __version__ + signal_error = NotImplemented # TODO + wh = _WelcomeHandler(relay_url, __version__, signal_error) + welcome_handler = wh.handle_welcome if delegate: w = _DelegatedWormhole(delegate) else: w = _DeferredWormhole() - b = Boss(w, side, relay_url, appid, reactor, journal, timing) + b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal, timing) w._set_boss(b) b.start() return w From 4234e791612d6b229fc45ab20974157a17aa507f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 12:44:07 +0100 Subject: [PATCH 095/176] test_wormhole: fix message-doubling test --- src/wormhole/test/test_wormhole.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index e7b4f5c..9f65bd6 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -6,7 +6,7 @@ from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks from .common import ServerBase -from .. import wormhole +from .. import wormhole, _order from ..errors import (WrongPasswordError, WelcomeError, InternalError, KeyFormatError) from spake2 import SPAKE2_Symmetric @@ -924,9 +924,9 @@ class Wormholes(ServerBase, unittest.TestCase): # SPAKE2.finish() to be called a second time, which throws an error # (which, being somewhat unexpected, caused a hang rather than a # clear exception). - with mock.patch("wormhole.wormhole._Wormhole", MessageDoublingReceiver): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) + with mock.patch("wormhole.wormhole._order", MessageDoubler): + w1 = wormhole.create(APPID, self.relayurl, reactor) + 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") @@ -937,16 +937,16 @@ class Wormholes(ServerBase, unittest.TestCase): yield w1.close() yield w2.close() -class MessageDoublingReceiver(wormhole._Wormhole): +class MessageDoubler(_order.Order): # we could double messages on the sending side, but a future server will # strip those duplicates, so to really exercise the receiver, we must # double them on the inbound side instead #def _msg_send(self, phase, body): # wormhole._Wormhole._msg_send(self, phase, body) # self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) - def _event_received_peer_message(self, side, phase, body): - wormhole._Wormhole._event_received_peer_message(self, side, phase, body) - wormhole._Wormhole._event_received_peer_message(self, side, phase, body) + def got_message(self, side, phase, body): + _order.Order.got_message(self, side, phase, body) + _order.Order.got_message(self, side, phase, body) class Errors(ServerBase, unittest.TestCase): @inlineCallbacks From 9ca657a7c6be133952d8a76d491913480e70d1e4 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 13:07:31 +0100 Subject: [PATCH 096/176] reenable TorManager --- src/wormhole/_boss.py | 3 ++- src/wormhole/_interfaces.py | 2 ++ src/wormhole/_rendezvous.py | 4 +++- src/wormhole/tor_manager.py | 4 ++++ src/wormhole/wormhole.py | 3 ++- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 4cb1e36..aefd2d0 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -30,6 +30,7 @@ class Boss(object): _welcome_handler = attrib() # TODO: validator: callable _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) + _tor_manager = attrib() # TODO: ITorManager or None _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() @@ -44,7 +45,7 @@ class Boss(object): self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, - self._timing) + self._tor_manager, self._timing) self._L = Lister() self._C = Code(self._timing) self._T = Terminator() diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index e9df8c5..565924c 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -27,6 +27,8 @@ class ITerminator(Interface): class ITiming(Interface): pass +class ITorManager(Interface): + pass class IJournal(Interface): # TODO: this needs to be public pass diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 1789333..7566a66 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -69,6 +69,7 @@ class RendezvousConnector(object): _side = attrib(validator=instance_of(type(u""))) _reactor = attrib() _journal = attrib(validator=provides(_interfaces.IJournal)) + _tor_manager = attrib() # TODO: ITorManager or None _timing = attrib(validator=provides(_interfaces.ITiming)) def __attrs_post_init__(self): @@ -88,7 +89,8 @@ class RendezvousConnector(object): self._trace(old_state="", input=what, new_state="") def _make_endpoint(self, hostname, port): - # TODO: Tor goes here + if self._tor_manager: + return self._tor_manager.get_endpoint_for(hostname, port) return endpoints.HostnameEndpoint(self._reactor, hostname, port) def wire(self, boss, nameplate, mailbox, code, lister, terminator): diff --git a/src/wormhole/tor_manager.py b/src/wormhole/tor_manager.py index 85834bc..b74b207 100644 --- a/src/wormhole/tor_manager.py +++ b/src/wormhole/tor_manager.py @@ -1,6 +1,7 @@ from __future__ import print_function, unicode_literals import sys, re import six +from zope.interface import implementer from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.error import ConnectError from twisted.internet.endpoints import clientFromString @@ -14,9 +15,12 @@ except ImportError: TorClientEndpoint = None DEFAULT_VALUE = "DEFAULT_VALUE" import ipaddress +from . import _interfaces from .timing import DebugTiming from .transit import allocate_tcp_port + +@implementer(_interfaces.ITorManager) class TorManager: def __init__(self, reactor, launch_tor=False, tor_control_port=None, timing=None, stderr=sys.stderr): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 7dec829..887cf4e 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -276,7 +276,8 @@ def create(appid, relay_url, reactor, delegate=None, journal=None, w = _DelegatedWormhole(delegate) else: w = _DeferredWormhole() - b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal, timing) + b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal, + tor_manager, timing) w._set_boss(b) b.start() return w From 2422ee0b88c9d6b8068dc41c46d9b25c638babdb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 13:07:53 +0100 Subject: [PATCH 097/176] disable NotWelcome test until signal_error is done --- src/wormhole/test/test_scripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 49562e0..4cbe720 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -719,7 +719,7 @@ class NotWelcome(ServerBase, unittest.TestCase): receive_d = cmd_receive.receive(self.cfg) f = yield self.assertFailure(receive_d, WelcomeError) self.assertEqual(str(f), "please upgrade XYZ") - +NotWelcome.skip = "not yet" class Cleanup(ServerBase, unittest.TestCase): From ddb83e9d59496c01c0a035817df1328a84043281 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 4 Mar 2017 23:05:52 +0100 Subject: [PATCH 098/176] wormhole: handle w.close() after error-induced closure --- src/wormhole/wormhole.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 887cf4e..d316daf 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -146,6 +146,7 @@ class _DeferredWormhole(object): self._version_observers = [] self._received_data = [] self._received_observers = [] + self._closed_result = None self._closed_observers = [] def _set_boss(self, boss): @@ -206,7 +207,9 @@ class _DeferredWormhole(object): # fails with WormholeError unless we established a connection # (state=="happy"). Fails with WrongPasswordError (a subclass of # WormholeError) if state=="scary". - self._boss.close() + if self._closed_result: + return defer.succeed(self._closed_result) # maybe Failure + self._boss.close() # only need to close if it wasn't already d = defer.Deferred() self._closed_observers.append(d) return d @@ -245,12 +248,12 @@ class _DeferredWormhole(object): def closed(self, result): #print("closed", result, type(result)) if isinstance(result, Exception): - observer_result = close_result = failure.Failure(result) + observer_result = self._closed_result = failure.Failure(result) else: # pending w.verify()/w.version()/w.read() get an error observer_result = WormholeClosed(result) # but w.close() only gets error if we're unhappy - close_result = result + self._closed_result = result for d in self._verifier_observers: d.errback(observer_result) for d in self._version_observers: @@ -258,7 +261,7 @@ class _DeferredWormhole(object): for d in self._received_observers: d.errback(observer_result) for d in self._closed_observers: - d.callback(close_result) + d.callback(self._closed_result) def create(appid, relay_url, reactor, delegate=None, journal=None, From 9314c6918fd421f7b32bedddd986273c0bb761b5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 5 Mar 2017 23:09:58 +0100 Subject: [PATCH 099/176] start documenting the protocols --- docs/introduction.md | 56 ++++++++++ docs/server-protocol.md | 235 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 docs/introduction.md create mode 100644 docs/server-protocol.md diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..7e2c255 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,56 @@ +# Magic-Wormhole + +The magic-wormhole (Python) distribution provides several things: an +executable tool ("bin/wormhole"), an importable library (`import wormhole`), +the URL of a publically-available Rendezvous Server, and the definition of a +protocol used by all three. + +The executable tool provides basic sending and receiving of files, +directories, and short text strings. These all use `wormhole send` and +`wormhole receive` (which can be abbreviated as `wormhole tx` and `wormhole +rx`). It also has a mode to facilitate the transfer of SSH keys. This tool, +while useful on its own, is just one possible use of the protocol. + +The `wormhole` library provides an API to establish a bidirectional ordered +encrypted record pipe to another instance (where each record is an +arbitrary-sized bytestring). This does not provide file-transfer directly: +the "bin/wormhole" tool speaks a simple protocol through this record pipe to +negotiate and perform the file transfer. + +`wormhole/cli/public_relay.py` contains the URLs of a Rendezvous Server and a +Transit Relay which I provide to support the file-transfer tools, which other +developers should feel free to use for their applications as well. I cannot +make any guarantees about performance or uptime for these servers: if you +want to use Magic Wormhole in a production environment, please consider +running a server on your own infrastructure (just run `wormhole-server start` +and modify the URLs in your application to point at it). + +## The Magic-Wormhole Protocol + +There are several layers to the protocol. + +At the bottom level, each client opens a WebSocket to the Rendezvous Server, +sending JSON-based commands to the server, and receiving similarly-encoded +messages. Some of these commands are addressed to the server itself, while +others are instructions to queue a message to other clients, or are +indications of messages coming from other clients. All these messages are +described in "server-protocol.md". + +These inter-client messages are used to convey the PAKE protocol exchange, +then a "VERSION" message (which doubles to verify the session key), then some +number of encrypted application-level data messages. "client-protocol.md" +describes these wormhole-to-wormhole messages. + +Each wormhole-using application is then free to interpret the data messages +as it pleases. The file-transfer app sends an "offer" from the `wormhole +send` side, to which the `wormhole receive` side sends a response, after +which the Transit connection is negotiated (if necessary), and finally the +data is sent through the Transit connection. "file-transfer-protocol.md" +describes this application's use of the client messages. + +## The `wormhole` API + +Application use the `wormhole` library to establish wormhole connections and +exchange data through them. Please see `api.md` for a complete description of +this interface. + diff --git a/docs/server-protocol.md b/docs/server-protocol.md new file mode 100644 index 0000000..de52cd5 --- /dev/null +++ b/docs/server-protocol.md @@ -0,0 +1,235 @@ +# Rendezvous Server Protocol + +## Concepts + +The Rendezvous Server provides queued delivery of binary messages from one +client to a second, and vice versa. Each message contains a "phase" (a +string) and a body (bytestring). These messages are queued in a "Mailbox" +until the other side connects and retrieves them, but are delivered +immediately if both sides are connected to the server at the same time. + +Mailboxes are identified by a large random string. "Nameplates", in contrast, +have short numeric identities: in a wormhole code like "4-purple-sausages", +the "4" is the nameplate. + +Each client has a randomly-generated "side", a short hex string, used to +differentiate between echoes of a client's own message, and real messages +from the other client. + +## Application IDs + +The server isolates each application from the others. Each client provides an +"App Id" when it first connects (via the "BIND" message), and all subsequent +commands are scoped to this application. This means that nameplates +(described below) and mailboxes can be re-used between different apps. The +AppID is a unicode string. Both sides of the wormhole must use the same +AppID, of course, or they'll never see each other. The server keeps track of +which applications are in use for maintenance purposes. + +Each application should use a unique AppID. Developers are encouraged to use +"DNSNAME/APPNAME" to obtain a unique one: e.g. the `bin/wormhole` +file-transfer tool uses `lothar.com/wormhole/text-or-file-xfer`. + +## WebSocket Transport + +At the lowest level, each client establishes (and maintains) a WebSocket +connection to the Rendezvous Server. If the connection is lost (which could +happen because the server was rebooted for maintenance, or because the +client's network connection migrated from one network to another, or because +the resident network gremlins decided to mess with you today), clients should +reconnect after waiting a random (and exponentially-growing) delay. The +Python implementation waits about 1 second after the first connection loss, +growing by 50% each time, capped at 1 minute. + +Each message to the server is a dictionary, with at least a `type` key, and +other keys that depend upon the particular message type. Messages from server +to client follow the same format. + +`misc/dump-timing.py` is a debug tool which renders timing data gathered from +the server and both clients, to identify protocol slowdowns and guide +optimization efforts. To support this, the client/server messages include +additional keys. Client->Server messages include a random `id` key, which is +copied into the `ack` that is immediately sent back to the client for all +commands (and is ignored except for the timing tool). Some client->server +messages (`list`, `allocate`, `claim`, `release`, `close`, `ping`) provoke a +direct response by the server: for these, `id` is copied into the response. +This helps the tool correlate the command and response. All server->client +messages have a `server_tx` timestamp (seconds since epoch, as a float), +which records when the message left the server. Direct responses include a +`server_rx` timestamp, to record when the client's command was received. The +tool combines these with local timestamps (recorded by the client and not +shared with the server) to build a full picture of network delays and +round-trip times. + +All messages are serialized as JSON, encoded to UTF-8, and the resulting +bytes sent as a single "binary-mode" WebSocket payload. + +Servers can signal `error` for any message type it does not recognize. +Clients and Servers must ignore unrecognized keys in otherwise-recognized +messages. + +## Connection-Specific (Client-to-Server) Messages + +The first thing each client sends to the server, immediately after the +WebSocket connection is established, is a `bind` message. This specifies the +AppID and side (in keys `appid` and `side`, respectively) that all subsequent +messages will be scoped to. While technically each message could be +independent, I thought it would be less confusing to use exactly one +WebSocket per logical wormhole connection. + +The first thing the server sends to each client is the `welcome` message. +This is intended to deliver important status information to the client that +might influence its operation. The Python client currently reacts to the +following keys (and ignores all others): + +* `current_cli_version`: prompts the user to upgrade if the server's + advertised version is greater than the client's version (as derived from + the git tag) +* `motd`: prints this message, if present; intended to inform users about + performance problems, scheduled downtime, or to beg for donations to keep + the server running +* `error`: causes the client to print the message and then terminate. If a + future version of the protocol requires a rate-limiting CAPTCHA ticket or + other authorization record, the server can send `error` (explaining the + requirement) if it does not see this ticket arrive before the `bind`. + +A `ping` will provoke a `pong`: these are only used by unit tests for +synchronization purposes (to detect when a batch of messages have been fully +processed by the server). NAT-binding refresh messages are handled by the +WebSocket layer (by asking Autobahn to send a keepalive messages every 60 +seconds), and do not use `ping`. + +If any client->server command is invalid (e.g. it lacks a necessary key, or +was sent in the wrong order), an `error` response will be sent, This response +will include the error string in the `error` key, and a full copy of the +original message dictionary in `orig`. + +## Nameplates + +Wormhole codes look like `4-purple-sausages`, consisting of a number followed +by some random words. This number is called a "Nameplate". + +On the Rendezvous Server, the Nameplate contains a pointer to a Mailbox. +Clients can "claim" a nameplate, and then later "release" it. Each claim is +for a specific side (so one client claiming the same nameplate multiple times +only counts as one claim). Nameplates are deleted once the last client has +released it, or after some period of inactivity. + +Clients can either make up nameplates themselves, or (more commonly) ask the +server to allocate one for them. Allocating a nameplate automatically claims +it (to avoid a race condition), but for simplicity, clients send a claim for +all nameplates, even ones which they've allocated themselves. + +Nameplates (on the server) must live until the second client has learned +about the associated mailbox, after which point they can be reused by other +clients. So if two clients connect quickly, but then maintain a long-lived +wormhole connection, the do not need to consume the limited spare of short +nameplates for that whole time. + +The `allocate` command allocates a nameplate (the server returns one that is +as short as possible), and the `allocated` response provides the answer. +Clients can also send a `list` command to get back a `nameplates` response +with all allocated nameplates for the bound AppID: this helps the code-input +tab-completion feature know which prefixes to offer. The `nameplates` +response returns a list of dictionaries, one per claimed nameplate, with at +least an `id` key in each one (with the nameplate string). Future versions +may record additional attributes in the nameplate records. + +## Mailboxes + +The server provides a single "Mailbox" to each pair of connecting Wormhole +clients. This holds an unordered set of messages, delivered immediately to +connected clients, and queued for delivery to clients which connect later. +Messages from both clients are merged together: clients use the included +`side` identifier to distinguish echoes of their own messages from those +coming from the other client. + +Each mailbox is "opened" by some number of clients at a time, until all +clients have closed it. Mailboxes are kept alive by either an open client, or +a Nameplate which points to the mailbox (so when a Nameplate is deleted from +inactivity, the corresponding Mailbox will be too). + +The `open` command both marks the mailbox as being opened by the bound side, +and also adds the WebSocket as subscribed to that mailbox, so new messages +are delivered immediately to the connected client. There is no explicit ack +to the `open` command, but since all clients add a message to the mailbox as +soon as they connect, there will always be a `message` reponse shortly after +the `open` goes through. The `close` command provokes a `closed` response. + +The `close` command accepts an optional "mood" string: this allows clients to +tell the server (in general terms) about their experiences with the wormhole +interaction. The server records the mood in its "usage" record, so the server +operator can get a sense of how many connections are succeeding and failing. +The moods currently recognized by the Rendezvous Server are: + +* happy (default): the PAKE key-establishment worked, and the client saw a + valid encrypted message from its peer +* lonely: the client gave up without hearing anything from its peer +* scary: the client saw an invalid encrypted message from its peer, + indicating that either the wormhole code was typed in wrong, or an attacker + tried (and failed) to guess the code +* errory: the client encountered some other error: protocol problem or + internal error + +The server will also record "pruney" if it deleted the mailbox due to +inactivity, or "crowded" if more than two sides tried to access the mailbox. + +When clients use the `add` command to add a client-to-client message, they +will put the body (a bytestring) into the command as a hex-encoded string in +the `body` key. They will also put the message's "phase", as a string, into +the `phase` key. See client-protocol.md for details about how different +phases are used. + +When a client sends `open`, it will get back a `message` response for every +message in the mailbox. It will also get a real-time `message` for every +`add` performed by clients later. These `message` responses include "side" +and "phase" from the sending client, and "body" (as a hex string, encoding +the binary message body). The decoded "body" will either by a random-looking +cryptographic value (for the PAKE message), or a random-looking encrypted +blob (for the VERSION message, as well as all application-provided payloads). +The `message` response will also include `id`, copied from the `id` of the +`add` message (and used only by the timing-diagram tool). + +The Rendezvous Server does not de-duplicate messages, nor does it retain +ordering: clients must do both if they need to. + +## All Message Types + +This lists all message types, along with the type-specific keys for each (if +any), and which ones provoke direct responses: + +* S->C welcome {welcome:} +* (C->S) bind {appid:, side:} +* (C->S) list {} -> nameplates +* S->C nameplates {nameplates: [{id: str},..]} +* (C->S) allocate {} -> allocated +* S->C allocated {nameplate:} +* (C->S) claim {nameplate:} -> claimed +* S->C claimed {mailbox:} +* (C->S) release {nameplate:?} -> released +* S->C released +* (C->S) open {mailbox:} +* (C->S) add {phase: str, body: hex} -> message (to all connected clients) +* S->C message {side:, phase:, body:, id:} +* (C->S) close {mailbox:?, mood:?} -> closed +* S->C closed +* S->C ack +* (C->S) ping {ping: int} -> ping +* S->C pong {pong: int} +* S->C error {error: str, orig:} + +# Persistence + +The server stores all messages in a database, so it should not lose any +information when it is restarted. The server will not send a direct +response until any side-effects (such as the message being added to the +mailbox) being safely committed to the database. + +The client library knows how to resume the protocol after a reconnection +event, assuming the client process itself continues to run. + +Clients which terminate entirely between messages (e.g. a secure chat +application, which requires multiple wormhole messages to exchange +address-book entries, and which must function even if the two apps are never +both running at the same time) can use "Journal Mode" to ensure forward +progress is made: see "api.md" (Journal Mode) for details. From 187e14862d9a8c93876c7bed40c8a0491e0d582c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 5 Mar 2017 23:21:08 +0100 Subject: [PATCH 100/176] document client-to-client protocol --- docs/client-protocol.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/client-protocol.md diff --git a/docs/client-protocol.md b/docs/client-protocol.md new file mode 100644 index 0000000..4a2f559 --- /dev/null +++ b/docs/client-protocol.md @@ -0,0 +1,33 @@ +# Client-to-Client Protocol + +Wormhole clients do not talk directly to each other (at least at first): they +only connect directly to the Rendezvous Server. They ask this server to +convey messages to the other client (via the `add` command and the `message` +response). This document explains the format of these client-to-client +messages. + +Each such message contains a "phase" string, and a hex-encoded binary "body". + +Any phase which is purely numeric (`^\d+$`) is reserved for application data, +and will be delivered in numeric order. All other phases are reserved for the +Wormhole client itself. Clients will ignore any phase they do not recognize. + +Immediately upon opening the mailbox, clients send the `pake` phase, which +contains the binary SPAKE2 message (the one computed as `X+M*pw` or +`Y+N*pw`). + +Upon receiving their peer's `pake` phase, clients compute and remember the +shared key. Then they send the encrypted `version` phase, whose plaintext +payload is a UTF-8-encoded JSON-encoded dictionary of metadata. This allows +the two Wormhole instances to signal their ability to do other things (like +"dilate" the wormhole). The version data will also include an `app_versions` +key which contains a dictionary of metadata provided by the application, +allowing apps to perform similar negotiation. + +Both `version` and all numeric (app-specific) phases are encrypted. The +message body will be the hex-encoded output of a NACL SecretBox, keyed by a +phase+side -specific key (computed with HKDF-SHA256, using the shared PAKE +key as the secret input, and `wormhole:phase:%s%s % (SHA256(side), +SHA256(phase))` as the CTXinfo), with a random nonce. + + From dd6e139c19f68ffc716c37a39f278ba29512d989 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 6 Mar 2017 00:42:58 +0100 Subject: [PATCH 101/176] document file-transfer protocol and Transit --- docs/file-transfer-protocol.md | 191 +++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/file-transfer-protocol.md diff --git a/docs/file-transfer-protocol.md b/docs/file-transfer-protocol.md new file mode 100644 index 0000000..ed26b73 --- /dev/null +++ b/docs/file-transfer-protocol.md @@ -0,0 +1,191 @@ +# File-Transfer Protocol + +The `bin/wormhole` tool uses a Wormhole to establish a connection, then +speaks a file-transfer -specific protocol over that Wormhole to decide how to +transfer the data. This application-layer protocol is described here. + +All application-level messages are dictionaries, which are JSON-encoded and +and UTF-8 encoded before being handed to `wormhole.send` (which then encrypts +them before sending through the rendezvous server to the peer). + +## Sender + +`wormhole send` has two main modes: file/directory (which requires a +non-wormhole Transit connection), or text (which does not). + +If the sender is doing files or directories, its first message contains just +a `transit` key, whose value is a dictionary with `abilities-v1` and +`hints-v1` keys. These are given to the Transit object, described below. + +Then (for both files/directories and text) it sends a message with an `offer` +key. The offer contains a single key, exactly one of (`message`, `file`, or +`directory`). For `message`, the value is the message being sent. For `file` +and `directory`, it contains a dictionary with additional information: + +* `message`: the text message, for text-mode +* `file`: for file-mode, a dict with `filename` and `filesize` +* `directory`: for directory-mode, a dict with: + * `mode`: the compression mode, currently always `zipfile/deflated` + * `dirname` + * `zipsize`: integer, size of the transmitted data in bytes + * `numbytes`: integer, estimated total size of the uncompressed directory + * `numfiles`: integer, number of files+directories being sent + +The sender runs a loop where it waits for similar dictionary-shaped messages +from the recipient, and processes them. It reacts to the following keys: + +* `error`: use the value to throw a TransferError and terminates +* `transit`: use the value to build the Transit instance +* `answer`: + * if `message_ack: ok` is in the value (we're in text-mode), then exit with success + * if `file_ack: ok` in the value (and we're in file/directory mode), then + wait for Transit to connect, then send the file through Transit, then wait + for an ack (via Transit), then exit + +The sender can handle all of these keys in the same message, or spaced out +over multiple ones. It will ignore any keys it doesn't recognize, and will +completely ignore messages that don't contain any recognized key. The only +constraint is that the message containing `message_ack` or `file_ack` is the +last one: it will stop looking for wormhole messages at that point. + +## Recipient + +`wormhole receive` is used for both file/directory-mode and text-mode: it +learns which is being used from the `offer` message. + +The recipient enters a loop where it processes the following keys from each +received message: + +* `error`: if present in any message, the recipient raises TransferError +(with the value) and exits immediately (before processing any other keys) +* `transit`: the value is used to build the Transit instance +* `offer`: parse the offer: + * `message`: accept the message and terminate + * `file`: connect a Transit instance, wait for it to deliver the indicated + number of bytes, then write them to the target filename + * `directory`: as with `file`, but unzip the bytes into the target directory + +## Transit + +The Wormhole API does not currently provide for large-volume data transfer +(this feature will be added to a future version, under the name "Dilated +Wormhole"). For now, bulk data is sent through a "Transit" object, which does +not use the Rendezvous Server. Instead, it tries to establish a direct TCP +connection from sender to recipient (or vice versa). If that fails, both +sides connect to a "Transit Relay", a very simple Server that just glues two +TCP sockets together when asked. + +The Transit object is created with a key (the same key on each side), and all +data sent through it will be encrypted with a derivation of that key. The +transit key is also used to derive handshake messages which are used to make +sure we're talking to the right peer, and to help the Transit Relay match up +the two client connections. Unlike Wormhole objects (which are symmetric), +Transit objects come in pairs: one side is the Sender, and the other is the +Receiver. + +Like Wormhole, Transit provides an encrypted record pipe. If you call +`.send()` with 40 bytes, the other end will see a `.gotData()` with exactly +40 bytes: no splitting, merging, dropping, or re-ordering. The Transit object +also functions as a twisted Producer/Consumer, so it can be connected +directly to file-readers and writers, and does flow-control properly. + +Most of the complexity of the Transit object has to do with negotiating and +scheduling likely targets for the TCP connection. + +Each Transit object has a set of "abilities". These are outbound connection +mechanisms that the client is capable of using. The basic CLI tool (running +on a normal computer) has two abilities: `direct-tcp-v1` and `relay-v1`. + +* `direct-tcp-v1` indicates that it can make outbound TCP connections to a + requested host and port number. "v1" means that the first thing sent over + these connections is a specific derived handshake message, e.g. `transit + sender HEXHEX ready\n\n`. +* `relay-v1` indicates it can connect to the Transit Relay and speak the + matching protocol (in which the first message is `please relay HEXHEX for + side HEX\n`, and the relay might eventually say `ok\n`). + +Future implementations may have additional abilities, such as connecting +directly to Tor onion services, I2P services, WebSockets, WebRTC, or other +connection technologies. Implementations on some platforms (such as web +browsers) may lack `direct-tcp-v1` or `relay-v1`. + +While it isn't strictly necessary for both sides to emit what they're capable +of using, it does help performance: a Tor Onion-service -capable receiver +shouldn't spend the time and energy to set up an onion service if the sender +can't use it. + +After learning the abilities of its peer, the Transit object can create a +list of "hints", which are endpoints that the peer should try to connect to. +Each hint will fall under one of the abilities that the peer indicated it +could use. Hints have types like `direct-tcp-v1`, `tor-tcp-v1`, and +`relay-v1`. Hints are encoded into dictionaries (with a mandatory `type` key, +and other keys as necessary): + +* `direct-tcp-v1` {hostname:, port:, priority:?} +* `tor-tcp-v1` {hostname:, port:, priority:?} +* `relay-v1` {hints: [{hostname:, port:, priority:?}, ..]} + +For example, if our peer can use `direct-tcp-v1`, then our Transit object +will deduce our local IP addresses (unless forbidden, i.e. we're using Tor), +listen on a TCP port, then send a list of `direct-tcp-v1` hints pointing at +all of them. If our peer can use `relay-v1`, then we'll connect to our relay +server and give the peer a hint to the same. + +`tor-tcp-v1` hints indicate an Onion service, which cannot be reached without +Tor. `direct-tcp-v1` hints can be reached with direct TCP connections (unless +forbidden) or by proxying through Tor. Onion services take about 30 seconds +to spin up, but bypass NAT, allowing two clients behind NAT boxes to connect +without a transit relay (really, the entire Tor network is acting as a +relay). + +The file-transfer application uses `transit` messages to convey these +abilities and hints from one Transit object to the other. After updating the +Transit objects, it then asks the Transit object to connect, whereupon +Transit will try to connect to all the hints that it can, and will use the +first one that succeeds. + +The file-transfer application, when actually sending file/directory data, +will close the Wormhole as soon as it has enough information to begin opening +the Transit connection. The final ack of the received data is sent through +the Transit object, as a UTF-8-encoded JSON-encoded dictionary with `ack: ok` +and `sha256: HEXHEX` containing the hash of the received data. + + +## Future Extensions + +Transit will be extended to provide other connection techniques: + +* WebSocket: usable by web browsers, not too hard to use by normal computers, + requires direct (or relayed) TCP connection +* WebRTC: usable by web browsers, hard-but-technically-possible to use by + normal computers, provides NAT hole-punching for "free" +* (web browsers cannot make direct TCP connections, so interop between + browsers and CLI clients will either require adding WebSocket to CLI, or a + relay that is capable of speaking/bridging both) +* I2P: like Tor, but not capable of proxying to normal TCP hints. +* ICE-mediated STUN/STUNT: NAT hole-punching, assisted somewhat by a server + that can tell you your external IP address and port. Maybe implemented as a + uTP stream (which is UDP based, and thus easier to get through NAT). + +The file-transfer protocol will be extended too: + +* "command mode": establish the connection, *then* figure out what we want to + use it for, allowing multiple files to be exchanged, in either direction. + This is to support a GUI that lets you open the wormhole, then drop files + into it on either end. +* some Transit messages being sent early, so ports and Onion services can be + spun up earier, to reduce overall waiting time +* transit messages being sent in multiple phases: maybe the transit + connection can progress while waiting for the user to confirm the transfer + +The hope is that by sending everything in dictionaries and multiple messages, +there will be enough wiggle room to make these extensions in a +backwards-compatible way. For example, to add "command mode" while allowing +the fancy new (as yet unwritten) GUI client to interoperate with +old-fashioned one-file-only CLI clients, we need the GUI tool to send an "I'm +capable of command mode" in the VERSION message, and look for it in the +received VERSION. If it isn't present, it will either expect to see an offer +(if the other side is sending), or nothing (if it is waiting to receive), and +can explain the situation to the user accordingly. It might show a locked set +of bars over the wormhole graphic to mean "cannot send", or a "waiting to +send them a file" overlay for send-only. From 51a73d6962e87a631bb13f9d257f55dd746dcd4c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 6 Mar 2017 00:55:07 +0100 Subject: [PATCH 102/176] client-protocol docs: improve --- docs/client-protocol.md | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/client-protocol.md b/docs/client-protocol.md index 4a2f559..037d69b 100644 --- a/docs/client-protocol.md +++ b/docs/client-protocol.md @@ -17,12 +17,40 @@ contains the binary SPAKE2 message (the one computed as `X+M*pw` or `Y+N*pw`). Upon receiving their peer's `pake` phase, clients compute and remember the -shared key. Then they send the encrypted `version` phase, whose plaintext -payload is a UTF-8-encoded JSON-encoded dictionary of metadata. This allows -the two Wormhole instances to signal their ability to do other things (like -"dilate" the wormhole). The version data will also include an `app_versions` -key which contains a dictionary of metadata provided by the application, -allowing apps to perform similar negotiation. +shared key. They derive the "verifier" (a hash of the shared key) and deliver +it to the application by calling `got_verifier`: applications can display +this to users who want additional assurance (by manually comparing the values +from both sides: they ought to be identical). At this point clients also send +the encrypted `version` phase, whose plaintext payload is a UTF-8-encoded +JSON-encoded dictionary of metadata. This allows the two Wormhole instances +to signal their ability to do other things (like "dilate" the wormhole). The +version data will also include an `app_versions` key which contains a +dictionary of metadata provided by the application, allowing apps to perform +similar negotiation. + +At this stage, the client knows the supposed shared key, but has not yet seen +evidence that the peer knows it too. When the first peer message arrives +(i.e. the first message with a `.side` that does not equal our own), it will +be decrypted: if this decryption succeeds, then we're confident that +*somebody* used the same wormhole code as us. This event pushes the client +mood from "lonely" to "happy". + +This might be triggered by the peer's `version` message, but if we had to +re-establish the Rendezvous Server connection, we might get peer messages out +of order and see some application-level message first. + +When a `version` message is successfully decrypted, the application is +signaled with `got_version`. When any application message is successfully +decrypted, `received` is signaled. Application messages are delivered +strictly in-order: if we see phases 3 then 2 then 1, all three will be +delivered in sequence after phase 1 is received. + +If any message cannot be successfully decrypted, the mood is set to "scary", +and the wormhole is closed. All pending Deferreds will be errbacked with some +kind of WormholeError, the nameplate/mailbox will be released, and the +WebSocket connection will be dropped. If the application calls `close()`, the +resulting Deferred will not fire until deallocation has finished and the +WebSocket is closed, and then it will fire with an errback. Both `version` and all numeric (app-specific) phases are encrypted. The message body will be the hex-encoded output of a NACL SecretBox, keyed by a From b4fdcfe53b7bf22caacf1be951fe0f3fdae2f887 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 6 Mar 2017 19:49:11 +0100 Subject: [PATCH 103/176] update api.md --- docs/api.md | 432 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 298 insertions(+), 134 deletions(-) diff --git a/docs/api.md b/docs/api.md index 0555c48..2c60a91 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,14 +1,14 @@ # Magic-Wormhole -This library provides a primitive function to securely transfer small amounts +This library provides a mechanism to securely transfer small amounts of data between two computers. Both machines must be connected to the internet, but they do not need to have public IP addresses or know how to contact each other ahead of time. -Security and connectivity is provided by means of an "invitation code": a -short string that is transcribed from one machine to the other by the users -at the keyboard. This works in conjunction with a baked-in "rendezvous -server" that relays information from one machine to the other. +Security and connectivity is provided by means of an "wormhole code": a short +string that is transcribed from one machine to the other by the users at the +keyboard. This works in conjunction with a baked-in "rendezvous server" that +relays information from one machine to the other. The "Wormhole" object provides a secure record pipe between any two programs that use the same wormhole code (and are configured with the same application @@ -17,141 +17,64 @@ but the encrypted data for all messages must pass through (and be temporarily stored on) the rendezvous server, which is a shared resource. For this reason, larger data (including bulk file transfers) should use the Transit class instead. The Wormhole object has a method to create a Transit object -for this purpose. +for this purpose. In the future, Transit will be deprecated, and this +functionality will be incorporated directly as a "dilated wormhole". + +A quick example: + +```python +import wormhole +from twisted.internet.defer import inlineCallbacks + +@inlineCallbacks +def go(): + w = wormhole.create(appid, relay_url, reactor) + w.generate_code() + code = yield w.when_code() + print "code:", code + w.send(b"outbound data") + inbound = yield w.when_received() + yield w.close() +``` ## Modes -This library will eventually offer multiple modes. For now, only "transcribe -mode" is available. +The API comes in two flavors: Delegated and Deferred. Controlling the +Wormhole and sending data is identical in both, but they differ in how +inbound data and events are delivered to the application. -Transcribe mode has two variants. In the "machine-generated" variant, the -"initiator" machine creates the invitation code, displays it to the first -user, they convey it (somehow) to the second user, who transcribes it into -the second ("receiver") machine. In the "human-generated" variant, the two -humans come up with the code (possibly without computers), then later -transcribe it into both machines. +In Delegated mode, the Wormhole is given a "delegate" object, on which +certain methods will be called when information is available (e.g. when the +code is established, or when data messages are received). In Deferred mode, +the Wormhole object has methods which return Deferreds that will fire at +these same times. -When the initiator machine generates the invitation code, the initiator -contacts the rendezvous server and allocates a "channel ID", which is a small -integer. The initiator then displays the invitation code, which is the -channel-ID plus a few secret words. The user copies the code to the second -machine. The receiver machine connects to the rendezvous server, and uses the -invitation code to contact the initiator. They agree upon an encryption key, -and exchange a small encrypted+authenticated data message. - -When the humans create an invitation code out-of-band, they are responsible -for choosing an unused channel-ID (simply picking a random 3-or-more digit -number is probably enough), and some random words. The invitation code uses -the same format in either variant: channel-ID, a hyphen, and an arbitrary -string. - -The two machines participating in the wormhole setup are not distinguished: -it doesn't matter which one goes first, and both use the same Wormhole class. -In the first variant, one side calls `get_code()` while the other calls -`set_code()`. In the second variant, both sides call `set_code()`. (Note that -this is not true for the "Transit" protocol used for bulk data-transfer: the -Transit class currently distinguishes "Sender" from "Receiver", so the -programs on each side must have some way to decide ahead of time which is -which). - -Each side can then do an arbitrary number of `send()` and `get()` calls. -`send()` writes a message into the channel. `get()` waits for a new message -to be available, then returns it. 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). - -Note: the application developer must be careful to avoid deadlocks (if both -sides want to `get()`, somebody has to `send()` first). - -When both sides are done, they must call `close()`, to flush all pending -`send()` calls, deallocate the channel, and close the websocket connection. - -## Twisted - -The Twisted-friendly flow looks like this (note that passing `reactor` is how -you get a non-blocking Wormhole): +Delegated mode: ```python -from twisted.internet import reactor -from wormhole.public_relay import RENDEZVOUS_RELAY -from wormhole import wormhole -w1 = wormhole(u"appid", RENDEZVOUS_RELAY, reactor) -d = w1.get_code() -def _got_code(code): - print "Invitation Code:", code - return w1.send(b"outbound data") -d.addCallback(_got_code) -d.addCallback(lambda _: w1.get()) -def _got(inbound_message): - print "Inbound message:", inbound_message -d.addCallback(_got) -d.addCallback(w1.close) -d.addBoth(lambda _: reactor.stop()) -reactor.run() +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)) + +w = wormhole.create(appid, relay_url, reactor, delegate=MyDelegate()) +w.generate_code() ``` -On the other side, you call `set_code()` instead of waiting for `get_code()`: +Deferred mode: ```python -w2 = wormhole(u"appid", RENDEZVOUS_RELAY, reactor) -w2.set_code(code) -d = w2.send(my_message) -... +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 ``` -Note that the Twisted-form `close()` accepts (and returns) an optional -argument, so you can use `d.addCallback(w.close)` instead of -`d.addCallback(lambda _: w.close())`. - -## 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. - -To retrieve the verifier, you call `d=w.verify()` before any calls to -`send()/get()`. The Deferred will not fire until internal key-confirmation -has taken place (meaning the two sides have exchanged their initial PAKE -messages, and the wormhole codes matched), so `verify()` is also a good way -to detect typos or mistakes entering the code. The Deferred will errback with -wormhole.WrongPasswordError if the codes did not match, or it will callback -with the verifier bytes if they did match. - -Once retrieved, you can turn this into hex or Base64 to print it, or render -it as ASCII-art, etc. Once the users are convinced that `verify()` from both -sides are the same, call `send()/get()` to continue the protocol. If you call -`send()/get()` before `verify()`, it will perform the complete protocol -without pausing. - -## Generating the Invitation Code - -In most situations, the "sending" or "initiating" side will call `get_code()` -to generate the invitation code. This returns a string in the form -`NNN-code-words`. The numeric "NNN" prefix is the "channel id", and is a -short integer allocated by talking to the rendezvous server. The rest is a -randomly-generated selection from the PGP wordlist, providing a default of 16 -bits of entropy. The initiating program should display this code to the user, -who should transcribe it to the receiving user, who gives it to the Receiver -object by calling `set_code()`. The receiving program can also use -`input_code()` to use a readline-based input function: this offers tab -completion of allocated channel-ids and known codewords. - -Alternatively, the human users can agree upon an invitation code themselves, -and provide it to both programs later (both sides call `set_code()`). They -should choose a channel-id that is unlikely to already be in use (3 or more -digits are recommended), append a hyphen, and then include randomly-selected -words or characters. Dice, coin flips, shuffled cards, or repeated sampling -of a high-resolution stopwatch are all useful techniques. - -Note that the code is a human-readable string (the python "unicode" type in -python2, "str" in python3). - ## Application Identifier Applications using this library must provide an "application identifier", a @@ -167,18 +90,259 @@ ten Wormholes are active for a given app-id, the connection-id will only need to contain a single digit, even if some other app-id is currently using thousands of concurrent sessions. -## Rendezvous Relays +## Rendezvous Servers -The library depends upon a "rendezvous relay", which is a server (with a +The library depends upon a "rendezvous server", which is a service (on a public IP address) that delivers small encrypted messages from one client to the other. This must be the same for both clients, and is generally baked-in to the application source code or default config. -This library includes the URL of a public relay run by the author. -Application developers can use this one, or they can run their own (see the -`wormhole-server` command and the `src/wormhole/server/` directory) and -configure their clients to use it instead. This URL is passed as a unicode -string. +This library includes the URL of a public rendezvous server run by the +author. Application developers can use this one, or they can run their own +(see the `wormhole-server` command and the `src/wormhole/server/` directory) +and configure their clients to use it instead. This URL is passed as a +unicode string. Note that because the server actually speaks WebSockets, the +URL starts with `ws:` instead of `http:`. + +## Wormhole Parameters + +All wormholes must be created with at least three parameters: + +* `appid`: a (unicode) string +* `relay_url`: a (unicode) string +* `reactor`: the Twisted reactor object + +In addition to these three, the `wormhole.create()` function takes several +optional arguments: + +* `delegate`: provide a Delegate object to enable "delegated mode", or pass + None (the default) to get "deferred mode" +* `journal`: provide a Journal object to enable journaled mode. See + journal.md for details. Note that journals only work with delegated mode, + not with deferred mode. +* `tor_manager`: to enable Tor support, create a `wormhole.TorManager` + instance and pass it here. This will hide the client's IP address by + proxying all connections (rendezvous and transit) through Tor. It also + enables connecting to Onion-service transit hints, and (in the future) will + enable the creation of Onion-services for transit purposes. +* `timing`: this accepts a DebugTiming instance, mostly for internal + diagnostic purposes, to record the transmit/receive timestamps for all + messages. The `wormhole --dump-timing=` feature uses this to build a + JSON-format data bundle, and the `misc/dump-timing.py` tool can build a + scrollable timing diagram from these bundles. +* `welcome_handler`: this is a function that will be called when the + Rendezvous Server's "welcome" message is received. It is used to display + important server messages in an application-specific way. +* `app_versions`: this can accept a dictionary (JSON-encodable) of data that + will be made available to the peer via the `got_version` event. This data + is delivered before any data messages, and can be used to indicate peer + capabilities. + +## Code Management + +Each wormhole connection is defined by a shared secret "wormhole code". These +codes can be generated offline (by picking a unique number and some secret +words), but are more commonly generated by whoever creates the first +wormhole. In the "bin/wormhole" file-transfer tool, the default behavior is +for the sender to create the code, and for the receiver to type it in. + +The code is a (unicode) string in the form `NNN-code-words`. The numeric +"NNN" prefix is the "channel id" or "nameplate", and is a short integer +allocated by talking to the rendezvous server. The rest is a +randomly-generated selection from the PGP wordlist, providing a default of 16 +bits of entropy. The initiating program should display this code to the user, +who should transcribe it to the receiving user, who gives it to their local +Wormhole object by calling `set_code()`. The receiving program can also use +`type_code()` to use a readline-based input function: this offers tab +completion of allocated channel-ids and known codewords. + +The Wormhole object has three APIs for generating or accepting a code: + +* `w.generate_code(length=2)`: this contacts the Rendezvous Server, allocates + a short numeric nameplate, chooses a configurable number of random words, + then assembles them into the code +* `w.set_code(code)`: this accepts the code as an argument +* `helper = w.type_code()`: this facilitates interactive entry of the code, + with tab-completion. The helper object has methods to return a list of + viable completions for whatever portion of the code has been entered so + 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 +`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 +other way to learn what code was created, but it may be useful in other modes +for consistency. + +The code-entry Helper object has the following API: + +* `d = h.get_nameplates()`: returns a Deferred that fires with a list of + (string) nameplates. These form the first portion of the wormhole code + (e.g. "4" in "4-purple-sausages"). The list is requested from the server + when `w.type_code()` is first called, and if the response arrives before + `h.get_nameplates()` is called, it will be used without delay. All + subsequent calls to `h.get_nameplates()` will provoke a fresh request to + the server, so hitting Tab too early won't condemn the client to using a + stale list. +* `h.set_nameplate(nameplate)`: commit to using a specific nameplate. Once + this is called, `h.get_nameplates()` will raise an immediate exception +* `completions = h.get_completions_for(prefix)`: given a prefix like "su", + this returns (synchronously) a list of strings which are appropriate to + append to the prefix (e.g. `["pportive", "rrender", "spicious"]`, for + expansion into "supportive", "surrender", and "suspicious". The prefix + should not include the nameplate, but *should* include whatever words and + hyphens have been typed so far (the default wordlist uses alternate lists, + where even numbered words have three syllables, and odd numbered words have + two, so the completions depend upon how many words are present, not just + the partial last word). E.g. `get_completions_for("pr")` will return + `["ocessor", "ovincial", "oximate"]`, while + `get_completions_for("opulent-pr")` will return `["eclude", "efer", + "eshrunk", "inter", "owler"]`. +* `h.set_words(suffix)`: this accepts a string (e.g. "purple-sausages"), and + commits to the code. `h.set_nameplate()` must be called before this, and no + other methods may be called afterwards. Calling this causes the + `w.when_code()` Deferred or corresponding delegate callback to fire, and + triggers the wormhole connection process. + +The `rlcompleter` wrapper is a function that knows how to use the code-entry +helper to do tab completion of wormhole codes: + +```python +from wormhole import create, rlcompleter_helper +w = create(appid, relay_url, reactor) +rlcompleter_helper("Wormhole code:", w.type_code()) +d = w.when_code() +``` + +This helper runs python's `rawinput()` function inside a thread, since +`rawinput()` normally blocks. + +The two machines participating in the wormhole setup are not distinguished: +it doesn't matter which one goes first, and both use the same Wormhole +constructor function. However if `w.generate_code()` is used, only one side +should use it. + +## Offline Codes + +In most situations, the "sending" or "initiating" side will call +`w.generate_code()` and display the resulting code. The sending human reads +it and speaks, types, performs charades, or otherwise transmits the code to +the receiving human. The receiving human then types it into the receiving +computer, where it either calls `w.set_code()` (if the code is passed in via +argv) or `w.type_code()` (for interactive entry). + +Usually one machine generates the code, and a pair of humans transcribes it +to the second machine (so `w.generate_code()` on one side, and `w.set_code()` +or `w.type_code()` on the other). But it is also possible for the humans to +generate the code offline, perhaps at a face-to-face meeting, and then take +the code back to their computers. In this case, `w.set_code()` will be used +on both sides. It is unlikely that the humans will restrict themselves to a +pre-established wordlist when manually generating codes, so the completion +feature of `w.type_code()` is not helpful. + +When the humans create an invitation code out-of-band, they are responsible +for choosing an unused channel-ID (simply picking a random 3-or-more digit +number is probably enough), and some random words. Dice, coin flips, shuffled +cards, or repeated sampling of a high-resolution stopwatch are all useful +techniques. The invitation code uses the same format either way: channel-ID, +a hyphen, and an arbitrary string. There is no need to encode the sampled +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. + +Once retrieved, you can turn this into hex or Base64 to print it, or render +it as ASCII-art, etc. Once the users are convinced that `verify()` from both +sides are the same, call `send()` to continue the protocol. If you call +`send()` before `verify()`, it will perform the complete protocol without +pausing. + +## Events + +As the wormhole connection is established, several events may be dispatched +to the application. In Delegated mode, these are dispatched by calling +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_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.type_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. +* got_verifier (`yield w.when_verifier()` / `dg.wormhole_got_verifier(verf)`: + fired when the key-exchange process has completed, and this side has + learned the shared key. 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 has not yet seen evidence that the + peer has used the correct wormhole code. +* got_version (`yield w.when_version()` / `dg.wormhole_got_version(version)`: + fired when the VERSION message arrives from the peer. This serves two + purposes. The first is that it provide confirmation that the peer (or a + man-in-the-middle) has used the correct wormhole code. The second is + delivery of the "app_versions" data (passed into `wormhole.create`). +* received (`yield w.when_received()` / `dg.wormhole_received(data)`: fired + each time a data message arrives from the peer, with the bytestring that + the peer passed into `w.send(data)`. +* 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 + connection has been closed. This also fires if an internal error occurs + (specifically WrongPasswordError, which indicates that an invalid encrypted + message was received), which also shuts everything down. The `result` value + is an exception (or Failure) object if the wormhole closed badly, or a + string like "happy" if it had no problems before shutdown. + +## 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. + +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 +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). + +For bulk data transfer, see "transit.md", or the "Dilation" section below. + +## Closing + +When the application is done with the wormhole, it should call `w.close()`, +and wait for a `closed` event. This ensures that all server-side resources +are released (allowing the nameplate to be re-used by some other client), and +all network sockets are shut down. + +In Deferred mode, this just means waiting for the Deferred returned by +`w.close()` to fire. In Delegated mode, this means calling `w.close()` (which +doesn't return anything) and waiting for the delegate's `wormhole_closed()` +method to be called. + ## Bytes, Strings, Unicode, and Python 3 From 20ec911b6c46bb6d8da6e1836a0cce584ffad8e7 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 7 Mar 2017 08:45:56 +0100 Subject: [PATCH 104/176] add API list, and speculative sections on serialization and dilation --- docs/api.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/docs/api.md b/docs/api.md index 2c60a91..922ce23 100644 --- a/docs/api.md +++ b/docs/api.md @@ -343,6 +343,32 @@ In Deferred mode, this just means waiting for the Deferred returned by doesn't return anything) and waiting for the delegate's `wormhole_closed()` method to be called. +## Serialization + +(this section is speculative: this code has not yet been written) + +Wormhole objects can be serialized. This can be useful for apps which save +their own state before shutdown, and restore it when they next start up +again. + + +The `w.serialize()` method returns a dictionary which can be JSON encoded +into a unicode string (most applications will probably want to UTF-8 -encode +this into a bytestring before saving on disk somewhere). + +To restore a Wormhole, call `wormhole.from_serialized(data, reactor, +delegate)`. This will return a wormhole in roughly the same state as was +serialized (of course all the network connections will be disconnected). + +Serialization only works for delegated-mode wormholes (since Deferreds point +at functions, which cannot be serialized easily). It also only works for +"non-dilated" wormholes (see below). + +To ensure correct behavior, serialization should probably only be done in +"journaled mode". See journal.md for details. + +If you use serialization, be careful to never use the same partial wormhole +object twice. ## Bytes, Strings, Unicode, and Python 3 @@ -362,3 +388,70 @@ in python3): * transit connection hints (e.g. "host:port") * application identifier * derived-key "purpose" string: `w.derive_key(PURPOSE, LENGTH)` + +## Full API list + +action | Deferred-Mode | Delegated-Mode +-------------------------- | --------------------- | ---------------------------- +w.generate_code(length=2) | | +w.set_code(code) | | +h=w.type_code() | | + | d=w.when_code() | dg.wormhole_got_code(code) + | d=w.when_verifier() | dg.wormhole_got_verifier(verf) + | d=w.when_version() | dg.wormhole_got_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() | + + +## Dilation + +(this section is speculative: this code has not yet been written) + +In the longer term, the Wormhole object will incorporate the "Transit" +functionality (see transit.md) directly, removing the need to instantiate a +second object. A Wormhole can be "dilated" into a form that is suitable for +bulk data transfer. + +All wormholes start out "undilated". In this state, all messages are queued +on the Rendezvous Server for the lifetime of the wormhole, and server-imposed +number/size/rate limits apply. Calling `w.dilate()` initiates the dilation +process, and success is signalled via either `d=w.when_dilated()` firing, or +`dg.wormhole_dilated()` being called. Once dilated, the Wormhole can be used +as an IConsumer/IProducer, and messages will be sent on a direct connection +(if possible) or through the transit relay (if not). + +What's good about a non-dilated wormhole?: + +* setup is faster: no delay while it tries to make a direct connection +* survives temporary network outages, since messages are queued +* works with "journaled mode", allowing progress to be made even when both + sides are never online at the same time, by serializing the wormhole + +What's good about dilated wormholes?: + +* they support bulk data transfer +* you get flow control (backpressure), and provide IProducer/IConsumer +* throughput is faster: no store-and-forward step + +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. + +Dilated wormholes can provide multiple "channels": these are multiplexed +through the single (encrypted) TCP connection. Each channel is a separate +stream (offering IProducer/IConsumer) + +To create a channel, call `c = w.create_channel()` on a dilated wormhole. The +"channel ID" can be obtained with `c.get_id()`. This ID will be a short +(unicode) string, which can be sent to the other side via a normal +`w.send()`, or any other means. On the other side, use `c = +w.open_channel(channel_id)` to get a matching channel object. + +Then use `c.send(data)` and `d=c.when_received()` to exchange data, or wire +them up with `c.registerProducer()`. Note that channels do not close until +the wormhole connection is closed, so they do not have separate `close()` +methods or events. Therefore if you plan to send files through them, you'll +need to inform the recipient ahead of time about how many bytes to expect. From 9571fcd388f1963ec71b827f2fda336595e0adce Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 7 Mar 2017 09:40:39 +0100 Subject: [PATCH 105/176] docs: write up "journaled mode" --- docs/journal.md | 148 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/journal.md diff --git a/docs/journal.md b/docs/journal.md new file mode 100644 index 0000000..072c01f --- /dev/null +++ b/docs/journal.md @@ -0,0 +1,148 @@ +# Journaled Mode + +(note: this section is speculative, the code has not yet been written) + +Magic-Wormhole supports applications which are written in a "journaled" or +"checkpointed" style. These apps store their entire state in a well-defined +checkpoint (perhaps in a database), and react to inbound events or messages +by carefully moving from one state to another, then releasing any outbound +messages. As a result, they can be terminated safely at any moment, without +warning, and ensure that the externally-visible behavior is deterministic and +independent of this stop/restart timing. + +This is the style encouraged by the E event loop, the +original [Waterken Server](http://waterken.sourceforge.net/), and the more +modern [Ken Platform](http://web.eecs.umich.edu/~tpkelly/Ken/), all +influencial in the object-capability security community. + +## Requirements + +Applications written in this style must follow some strict rules: + +* all state goes into the checkpoint +* the only way to affect the state is by processing an input message +* event processing is deterministic (any non-determinism must be implemented + as a message, e.g. from a clock service or a random-number generator) +* apps must never forget a message for which they've accepted reponsibility + +The main processing function takes the previous state checkpoint and a single +input message, and produces a new state checkpoint and a set of output +messages. For performance, the state might be kept in memory between events, +but the behavior should be indistinguishable from that of a server which +terminates completely between events. + +In general, applications must tolerate duplicate inbound messages, and should +re-send outbound messages until the recipient acknowledges them. Any outbound +responses to an inbound message must be queued until the checkpoint is +recorded. If outbound messages were delivered before the checkpointing, then +a crash just after delivery would roll the process back to a state where it +forgot about the inbound event, causing observably inconsistent behavior that +depends upon whether the outbound message successfully escaped the dying +process or not. + +As a result, journaled-style applications use a very specific process when +interacting with the outside world. Their event-processing function looks +like: + +* receive inbound event +* (load state) +* create queue for any outbound messages +* process message (changing state and queuing outbound messages) +* serialize state, record in checkpoint +* deliver any queued outbound messages + +In addition, the protocols used to exchange messages should include message +IDs and acks. Part of the state vector will include a set of unacknowledged +outbound messages. When a connection is established, all outbound messages +should be re-sent, and messages are removed from the pending set when an +inbound ack is received. The state must include a set of inbound message ids +which have been processed already. All inbound messages receive an ack, but +only new ones are processed. Connection establishment/loss is not strictly +included in the journaled-app model (in Waterken/Ken, message delivery is +provided by the platform, and apps do not know about connections), but +general: + +* "I want to have a connection" is stored in the state vector +* "I am connected" is not +* when a connection is established, code can run to deliver pending messages, + and this does not qualify as an inbound event +* inbound events can only happen when at least one connection is established +* immediately after restarting from a checkpoint, no connections are + established, but the app might initiate outbound connections, or prepare to + accept inbound ones + +## Wormhole Support + +To support this mode, the Wormhole constructor accepts a `journal=` argument. +If provided, it must be an object that implements the `wormhole.IJournal` +interface, which consists of two methods: + +* `j.queue_outbound(fn, *args, **kwargs)`: used to delay delivery of outbound + messages until the checkpoint has been recorded +* `j.process()`: a context manager which should be entered before processing + inbound messages + +`wormhole.Journal` is an implementation of this interface, which is +constructed with a (synchronous) `save_checkpoint` function. Applications can +use it, or bring their own. + +The Wormhole object, when configured with a journal, will wrap all inbound +WebSocket message processing with the `j.process()` context manager, and will +deliver all outbound messages through `j.queue_outbound`. Applications using +such a Wormhole must also use the same journal for their own (non-wormhole) +events. It is important to coordinate multiple sources of events: e.g. a UI +event may cause the application to call `w.send(data)`, and the outbound +wormhole message should be checkpointed along with the app's state changes +caused by the UI event. Using a shared journal for both wormhole- and +non-wormhole- events provides this coordination. + +The `save_checkpoint` function should serialize application state along with +any Wormholes that are active. Wormhole state can be obtained by calling +`w.serialize()`, which will return a dictionary (that can be +JSON-serialized). At application startup (or checkpoint resumption), +Wormholes can be regenerated with `wormhole.from_serialized()`. Note that +only "delegated-mode" wormholes can be serialized: Deferreds are not amenable +to usage beyond a single process lifetime. + +For a functioning example of a journaled-mode application, see +misc/demo-journal.py. The following snippet may help illustrate the concepts: + +```python +class App: + @classmethod + def new(klass): + self = klass() + self.state = {} + self.j = wormhole.Journal(self.save_checkpoint) + self.w = wormhole.create(.., delegate=self, journal=self.j) + + @classmethod + def from_serialized(klass): + self = klass() + self.j = wormhole.Journal(self.save_checkpoint) + with open("state.json", "r") as f: + data = json.load(f) + self.state = data["state"] + self.w = wormhole.from_serialized(data["wormhole"], reactor, + delegate=self, journal=self.j) + + def inbound_event(self, event): + # non-wormhole events must be performed in the journal context + with self.j.process(): + parse_event(event) + change_state() + self.j.queue_outbound(self.send, outbound_message) + + def wormhole_received(self, data): + # wormhole events are already performed in the journal context + change_state() + self.j.queue_outbound(self.send, stuff) + + def send(self, outbound_message): + actually_send_message(outbound_message) + + def save_checkpoint(self): + app_state = {"state": self.state, "wormhole": self.w.serialize()} + with open("state.json", "w") as f: + json.dump(app_state, f) +``` From aebee618167bbd5ce926fa6997197063bc6e22c7 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 7 Mar 2017 12:09:06 +0100 Subject: [PATCH 106/176] fix close behavior: Deferreds should errback once closed --- src/wormhole/errors.py | 6 ++++++ src/wormhole/wormhole.py | 32 ++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index 865b5e2..b6ba419 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -53,3 +53,9 @@ class NoTorError(WormholeError): class NoKeyError(WormholeError): """w.derive_key() was called before got_verifier() fired""" + +class WormholeClosed(Exception): + """Deferred-returning API calls errback with WormholeClosed if the + wormhole was already closed, or if it closes before a real result can be + obtained.""" + diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index d316daf..e73eb1b 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -10,7 +10,7 @@ from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss from ._key import derive_key -from .errors import WelcomeError, NoKeyError +from .errors import WelcomeError, NoKeyError, WormholeClosed from .util import to_bytes # We can provide different APIs to different apps: @@ -131,9 +131,6 @@ class _DelegatedWormhole(object): def closed(self, result): self._delegate.wormhole_closed(result) -class WormholeClosed(Exception): - pass - @implementer(IWormhole) class _DeferredWormhole(object): def __init__(self): @@ -146,6 +143,7 @@ class _DeferredWormhole(object): self._version_observers = [] self._received_data = [] self._received_observers = [] + self._observer_result = None self._closed_result = None self._closed_observers = [] @@ -154,20 +152,28 @@ class _DeferredWormhole(object): # from above def when_code(self): - if self._code: + # TODO: consider throwing error unless one of allocate/set/input_code + # was called first + if self._observer_result is not None: + return defer.fail(self._observer_result) + if self._code is not None: return defer.succeed(self._code) d = defer.Deferred() self._code_observers.append(d) return d def when_verifier(self): - if self._verifier: + if self._observer_result is not None: + return defer.fail(self._observer_result) + if self._verifier is not None: return defer.succeed(self._verifier) d = defer.Deferred() self._verifier_observers.append(d) return d def when_version(self): + if self._observer_result is not None: + return defer.fail(self._observer_result) if self._version is not None: return defer.succeed(self._version) d = defer.Deferred() @@ -175,6 +181,8 @@ class _DeferredWormhole(object): return d def when_received(self): + if self._observer_result is not None: + return defer.fail(self._observer_result) if self._received_data: return defer.succeed(self._received_data.pop(0)) d = defer.Deferred() @@ -209,9 +217,9 @@ class _DeferredWormhole(object): # WormholeError) if state=="scary". if self._closed_result: return defer.succeed(self._closed_result) # maybe Failure - self._boss.close() # only need to close if it wasn't already d = defer.Deferred() self._closed_observers.append(d) + self._boss.close() # only need to close if it wasn't already return d def debug_set_trace(self, client_name, which="B N M S O K R RC L C T", @@ -248,18 +256,18 @@ class _DeferredWormhole(object): def closed(self, result): #print("closed", result, type(result)) if isinstance(result, Exception): - observer_result = self._closed_result = failure.Failure(result) + self._observer_result = self._closed_result = failure.Failure(result) else: # pending w.verify()/w.version()/w.read() get an error - observer_result = WormholeClosed(result) + self._observer_result = WormholeClosed(result) # but w.close() only gets error if we're unhappy self._closed_result = result for d in self._verifier_observers: - d.errback(observer_result) + d.errback(self._observer_result) for d in self._version_observers: - d.errback(observer_result) + d.errback(self._observer_result) for d in self._received_observers: - d.errback(observer_result) + d.errback(self._observer_result) for d in self._closed_observers: d.callback(self._closed_result) From e518f2b7995a0828fc1bb2b79accce2626976821 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 7 Mar 2017 12:09:25 +0100 Subject: [PATCH 107/176] throw KeyFormatError when given a code with spaces --- src/wormhole/_boss.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index aefd2d0..fb0474c 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -17,7 +17,7 @@ from ._rendezvous import RendezvousConnector from ._lister import Lister from ._code import Code from ._terminator import Terminator -from .errors import ServerError, LonelyError, WrongPasswordError +from .errors import ServerError, LonelyError, WrongPasswordError, KeyFormatError from .util import bytes_to_dict @attrs @@ -116,6 +116,8 @@ class Boss(object): def allocate_code(self, code_length): self._C.allocate_code(code_length) def set_code(self, code): + if ' ' in code: + raise KeyFormatError("code (%s) contains spaces." % code) self._C.set_code(code) @m.input() From 5f9894ca63713de2d7ac7f36ccb17c0220a4c77c Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 7 Mar 2017 12:34:36 +0100 Subject: [PATCH 108/176] API updates, make most tests pass, disable others * finally wire up "application versions" * remove when_verifier (which used to fire after key establishment, but before the VERSION message was received or verified) * fire when_verified and when_version at the same time (after VERSION is verified), but with different args --- docs/api.md | 53 +++---- src/wormhole/_boss.py | 5 +- src/wormhole/_key.py | 4 +- src/wormhole/test/test_wormhole.py | 191 ++++++++++++++----------- src/wormhole/test/test_wormhole_new.py | 6 +- src/wormhole/wormhole.py | 34 +++-- 6 files changed, 164 insertions(+), 129 deletions(-) diff --git a/docs/api.md b/docs/api.md index 922ce23..f4c4eeb 100644 --- a/docs/api.md +++ b/docs/api.md @@ -274,24 +274,25 @@ 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_got_code(code)`): fired when the +* 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.type_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. -* got_verifier (`yield w.when_verifier()` / `dg.wormhole_got_verifier(verf)`: - fired when the key-exchange process has completed, and this side has - learned the shared key. 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 has not yet seen evidence that the - peer has used the correct wormhole code. -* got_version (`yield w.when_version()` / `dg.wormhole_got_version(version)`: - fired when the VERSION message arrives from the peer. This serves two - purposes. The first is that it provide confirmation that the peer (or a - man-in-the-middle) has used the correct wormhole code. The second is - delivery of the "app_versions" data (passed into `wormhole.create`). +* 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 + (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 + 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(version)`: + fired when the VERSION message arrives from the peer. This fires at the + same time as `verified`, but delivers the "app_versions" data (passed into + `wormhole.create`) instead of the verifier string. * received (`yield w.when_received()` / `dg.wormhole_received(data)`: fired each time a data message arrives from the peer, with the bytestring that the peer passed into `w.send(data)`. @@ -391,19 +392,19 @@ in python3): ## Full API list -action | Deferred-Mode | Delegated-Mode --------------------------- | --------------------- | ---------------------------- -w.generate_code(length=2) | | -w.set_code(code) | | -h=w.type_code() | | - | d=w.when_code() | dg.wormhole_got_code(code) - | d=w.when_verifier() | dg.wormhole_got_verifier(verf) - | d=w.when_version() | dg.wormhole_got_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 +-------------------------- | -------------------- | -------------- +w.generate_code(length=2) | | +w.set_code(code) | | +h=w.type_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() | ## Dilation diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index fb0474c..530d7ae 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -27,6 +27,7 @@ class Boss(object): _side = attrib(validator=instance_of(type(u""))) _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)) @@ -41,7 +42,7 @@ class Boss(object): self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) self._O = Order(self._side, self._timing) - self._K = Key(self._appid, self._side, self._timing) + self._K = Key(self._appid, self._versions, self._side, self._timing) self._R = Receive(self._side, self._timing) self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, @@ -188,7 +189,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): diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 6370df2..2436de4 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -56,6 +56,7 @@ def encrypt_data(key, plaintext): @implementer(_interfaces.IKey) class Key(object): _appid = attrib(validator=instance_of(type(u""))) + _versions = attrib(validator=instance_of(dict)) _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @@ -114,8 +115,7 @@ class Key(object): self._B.got_verifier(derive_key(key, b"wormhole:verifier")) phase = "version" data_key = derive_phase_key(key, self._side, phase) - my_versions = {} # TODO: get from Wormhole? - plaintext = dict_to_bytes(my_versions) + plaintext = dict_to_bytes(self._versions) encrypted = encrypt_data(data_key, plaintext) self._M.add_message(phase, encrypted) self._R.got_key(key) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 9f65bd6..b632f84 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -6,9 +6,9 @@ from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks from .common import ServerBase -from .. import wormhole, _order +from .. import wormhole, _rendezvous from ..errors import (WrongPasswordError, WelcomeError, InternalError, - KeyFormatError) + KeyFormatError, WormholeClosed, LonelyError) from spake2 import SPAKE2_Symmetric from ..timing import DebugTiming from ..util import (bytes_to_dict, dict_to_bytes, @@ -116,7 +116,7 @@ class InputCode(unittest.TestCase): res = self.successResultOf(d) self.assertEqual(res, ["123"]) self.assertEqual(stderr.getvalue(), "") - +InputCode.skip = "not yet" class GetCode(unittest.TestCase): def test_get(self): @@ -134,6 +134,7 @@ class GetCode(unittest.TestCase): pieces = code.split("-") self.assertEqual(len(pieces), 3) # nameplate plus two words self.assert_(re.search(r'^\d+-\w+-\w+$', code), code) +GetCode.skip = "not yet" class Basic(unittest.TestCase): def tearDown(self): @@ -714,7 +715,7 @@ class Basic(unittest.TestCase): w.derive_key, "foo", SecretBox.KEY_SIZE) self.failureResultOf(w.get(), WrongPasswordError) self.failureResultOf(w.verify(), WrongPasswordError) - +Basic.skip = "being replaced by test_wormhole_new" # event orderings to exercise: # @@ -735,14 +736,15 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_basic(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data1") w2.send(b"data2") - dataX = yield w1.get() - dataY = yield w2.get() + dataX = yield w1.when_received() + dataY = yield w2.when_received() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") yield w1.close() @@ -753,14 +755,15 @@ class Wormholes(ServerBase, unittest.TestCase): # the two sides use random nonces for their messages, so it's ok for # both to try and send the same body: they'll result in distinct # encrypted messages - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data") w2.send(b"data") - dataX = yield w1.get() - dataY = yield w2.get() + dataX = yield w1.when_received() + dataY = yield w2.when_received() self.assertEqual(dataX, b"data") self.assertEqual(dataY, b"data") yield w1.close() @@ -768,14 +771,15 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_interleaved(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data1") - dataY = yield w2.get() + dataY = yield w2.when_received() self.assertEqual(dataY, b"data1") - d = w1.get() + d = w1.when_received() w2.send(b"data2") dataX = yield d self.assertEqual(dataX, b"data2") @@ -784,22 +788,23 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_unidirectional(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) w1.send(b"data1") - dataY = yield w2.get() + dataY = yield w2.when_received() self.assertEqual(dataY, b"data1") yield w1.close() yield w2.close() @inlineCallbacks def test_early(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) w1.send(b"data1") - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - d = w2.get() + w2 = wormhole.create(APPID, self.relayurl, reactor) + d = w2.when_received() w1.set_code("123-abc-def") w2.set_code("123-abc-def") dataY = yield d @@ -809,12 +814,12 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_fixed_code(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) + 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.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") @@ -824,28 +829,52 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_multiple_messages(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) + w1 = wormhole.create(APPID, self.relayurl, reactor) + 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.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") - dl = yield self.doBoth(w1.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data4") self.assertEqual(dataY, b"data3") yield w1.close() yield w2.close() + + @inlineCallbacks + def test_closed(self): + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.set_code("123-foo") + w2.set_code("123-foo") + + # let it connect and become HAPPY + yield w1.when_version() + yield w2.when_version() + + yield w1.close() + yield w2.close() + + # once closed, all Deferred-yielding API calls get an error + e = yield self.assertFailure(w1.when_code(), WormholeClosed) + self.assertEqual(e.args[0], "happy") + yield self.assertFailure(w1.when_verified(), WormholeClosed) + yield self.assertFailure(w1.when_version(), WormholeClosed) + yield self.assertFailure(w1.when_received(), WormholeClosed) + + @inlineCallbacks def test_wrong_password(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code+"not") # That's enough to allow both sides to discover the mismatch, but # only after the confirmation message gets through. API calls that @@ -855,44 +884,41 @@ class Wormholes(ServerBase, unittest.TestCase): w2.send(b"should still work") # API calls that wait (i.e. get) will errback - yield self.assertFailure(w2.get(), WrongPasswordError) - yield self.assertFailure(w1.get(), WrongPasswordError) + yield self.assertFailure(w2.when_received(), WrongPasswordError) + yield self.assertFailure(w1.when_received(), WrongPasswordError) + + yield self.assertFailure(w1.when_verified(), WrongPasswordError) + yield self.assertFailure(w1.when_version(), WrongPasswordError) + + yield self.assertFailure(w1.close(), WrongPasswordError) + yield self.assertFailure(w2.close(), WrongPasswordError) - yield w1.close() - yield w2.close() - self.flushLoggedErrors(WrongPasswordError) @inlineCallbacks def test_wrong_password_with_spaces(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() - code_no_dashes = code.replace('-', ' ') - + w = wormhole.create(APPID, self.relayurl, reactor) + badcode = "4 oops spaces" with self.assertRaises(KeyFormatError) as ex: - w2.set_code(code_no_dashes) - - expected_msg = "code (%s) contains spaces." % (code_no_dashes,) + w.set_code(badcode) + expected_msg = "code (%s) contains spaces." % (badcode,) self.assertEqual(expected_msg, str(ex.exception)) - - yield w1.close() - yield w2.close() - self.flushLoggedErrors(KeyFormatError) + yield self.assertFailure(w.close(), LonelyError) @inlineCallbacks def test_verifier(self): - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) - v1 = yield w1.verify() - v2 = yield w2.verify() + v1 = yield w1.when_verified() + v2 = yield w2.when_verified() self.failUnlessEqual(type(v1), type(b"")) self.failUnlessEqual(v1, v2) w1.send(b"data1") w2.send(b"data2") - dataX = yield w1.get() - dataY = yield w2.get() + dataX = yield w1.when_received() + dataY = yield w2.when_received() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") yield w1.close() @@ -901,16 +927,17 @@ class Wormholes(ServerBase, unittest.TestCase): @inlineCallbacks def test_versions(self): # there's no API for this yet, but make sure the internals work - w1 = wormhole.wormhole(APPID, self.relayurl, reactor) - w1._my_versions = {"w1": 123} - w2 = wormhole.wormhole(APPID, self.relayurl, reactor) - w2._my_versions = {"w2": 456} - code = yield w1.get_code() + w1 = wormhole.create(APPID, self.relayurl, reactor, + versions={"w1": 123}) + w2 = wormhole.create(APPID, self.relayurl, reactor, + versions={"w2": 456}) + w1.allocate_code() + code = yield w1.when_code() w2.set_code(code) - yield w1.verify() - self.assertEqual(w1._their_versions, {"w2": 456}) - yield w2.verify() - self.assertEqual(w2._their_versions, {"w1": 123}) + w1_versions = yield w2.when_version() + self.assertEqual(w1_versions, {"w1": 123}) + w2_versions = yield w1.when_version() + self.assertEqual(w2_versions, {"w2": 456}) yield w1.close() yield w2.close() @@ -923,50 +950,52 @@ class Wormholes(ServerBase, unittest.TestCase): # incoming PAKE message was received, which would cause # SPAKE2.finish() to be called a second time, which throws an error # (which, being somewhat unexpected, caused a hang rather than a - # clear exception). - with mock.patch("wormhole.wormhole._order", MessageDoubler): + # clear exception). The Mailbox object is responsible for + # deduplication, so we must patch the RendezvousConnector to simulate + # duplicated messages. + with mock.patch("wormhole._boss.RendezvousConnector", MessageDoubler): w1 = wormhole.create(APPID, self.relayurl, reactor) 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.get(), w2.get()) + dl = yield self.doBoth(w1.when_received(), w2.when_received()) (dataX, dataY) = dl self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") yield w1.close() yield w2.close() -class MessageDoubler(_order.Order): +class MessageDoubler(_rendezvous.RendezvousConnector): # we could double messages on the sending side, but a future server will # strip those duplicates, so to really exercise the receiver, we must # double them on the inbound side instead #def _msg_send(self, phase, body): # wormhole._Wormhole._msg_send(self, phase, body) # self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) - def got_message(self, side, phase, body): - _order.Order.got_message(self, side, phase, body) - _order.Order.got_message(self, side, phase, body) + def _response_handle_message(self, msg): + _rendezvous.RendezvousConnector._response_handle_message(self, msg) + _rendezvous.RendezvousConnector._response_handle_message(self, msg) class Errors(ServerBase, unittest.TestCase): @inlineCallbacks def test_codes_1(self): - w = wormhole.wormhole(APPID, self.relayurl, reactor) + w = wormhole.create(APPID, self.relayurl, reactor) # definitely too early self.assertRaises(InternalError, w.derive_key, "purpose", 12) w.set_code("123-purple-elephant") # code can only be set once self.assertRaises(InternalError, w.set_code, "123-nope") - yield self.assertFailure(w.get_code(), InternalError) + yield self.assertFailure(w.when_code(), InternalError) yield self.assertFailure(w.input_code(), InternalError) yield w.close() @inlineCallbacks def test_codes_2(self): - w = wormhole.wormhole(APPID, self.relayurl, reactor) - yield w.get_code() + w = wormhole.create(APPID, self.relayurl, reactor) + yield w.when_code() self.assertRaises(InternalError, w.set_code, "123-nope") - yield self.assertFailure(w.get_code(), InternalError) + yield self.assertFailure(w.when_code(), InternalError) yield self.assertFailure(w.input_code(), InternalError) yield w.close() diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 2a271d0..7455acb 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -57,8 +57,8 @@ class New(ServerBase, unittest.TestCase): code2 = yield w2.when_code() self.assertEqual(code, code2) - verifier1 = yield w1.when_verifier() - verifier2 = yield w2.when_verifier() + verifier1 = yield w1.when_verified() + verifier2 = yield w2.when_verified() self.assertEqual(verifier1, verifier2) version1 = yield w1.when_version() @@ -88,7 +88,7 @@ class New(ServerBase, unittest.TestCase): w1.allocate_code(2) code = yield w1.when_code() w2 = wormhole.create(APPID, self.relayurl, reactor) - w2.set_code(code+", NOT") + w2.set_code(code+"NOT") code2 = yield w2.when_code() self.assertNotEqual(code, code2) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index e73eb1b..291a466 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -25,7 +25,7 @@ from .util import to_bytes # w.send(data) # app.wormhole_got_code(code) # app.wormhole_got_verifier(verifier) -# app.wormhole_got_version(version) +# app.wormhole_got_version(versions) # app.wormhole_receive(data) # w.close() # app.wormhole_closed() @@ -117,15 +117,15 @@ class _DelegatedWormhole(object): # from below def got_code(self, code): - self._delegate.wormhole_got_code(code) + self._delegate.wormhole_code(code) def got_welcome(self, welcome): pass # TODO def got_key(self, key): self._key = key # for derive_key() def got_verifier(self, verifier): - self._delegate.wormhole_got_verifier(verifier) - def got_version(self, version): - self._delegate.wormhole_got_version(version) + self._delegate.wormhole_verified(verifier) + def got_versions(self, versions): + self._delegate.wormhole_version(versions) def received(self, plaintext): self._delegate.wormhole_received(plaintext) def closed(self, result): @@ -139,7 +139,7 @@ class _DeferredWormhole(object): self._key = None self._verifier = None self._verifier_observers = [] - self._version = None + self._versions = None self._version_observers = [] self._received_data = [] self._received_observers = [] @@ -162,7 +162,7 @@ class _DeferredWormhole(object): self._code_observers.append(d) return d - def when_verifier(self): + def when_verified(self): if self._observer_result is not None: return defer.fail(self._observer_result) if self._verifier is not None: @@ -174,8 +174,8 @@ class _DeferredWormhole(object): def when_version(self): if self._observer_result is not None: return defer.fail(self._observer_result) - if self._version is not None: - return defer.succeed(self._version) + if self._versions is not None: + return defer.succeed(self._versions) d = defer.Deferred() self._version_observers.append(d) return d @@ -241,10 +241,10 @@ class _DeferredWormhole(object): for d in self._verifier_observers: d.callback(verifier) self._verifier_observers[:] = [] - def got_version(self, version): - self._version = version + def got_versions(self, versions): + self._versions = versions for d in self._version_observers: - d.callback(version) + d.callback(versions) self._version_observers[:] = [] def received(self, plaintext): @@ -272,8 +272,9 @@ class _DeferredWormhole(object): d.callback(self._closed_result) -def create(appid, relay_url, reactor, delegate=None, journal=None, - tor_manager=None, timing=None, welcome_handler=None, +def create(appid, relay_url, reactor, versions={}, + delegate=None, journal=None, tor_manager=None, + timing=None, welcome_handler=None, stderr=sys.stderr): timing = timing or DebugTiming() side = bytes_to_hexstr(os.urandom(5)) @@ -287,7 +288,10 @@ def create(appid, relay_url, reactor, delegate=None, journal=None, w = _DelegatedWormhole(delegate) else: w = _DeferredWormhole() - b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal, + 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) w._set_boss(b) b.start() From 2054e4c76bbb5cdf19c17d9f498026feb0d9b84e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 8 Mar 2017 08:44:44 +0100 Subject: [PATCH 109/176] test app versions --- src/wormhole/test/test_machines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 4830901..b412344 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -147,7 +147,7 @@ class Key(unittest.TestCase): def build(self): events = [] - k = _key.Key(u"appid", u"side", timing.DebugTiming()) + k = _key.Key(u"appid", {}, u"side", timing.DebugTiming()) b = Dummy("b", events, IBoss, "scared", "got_key", "got_verifier") m = Dummy("m", events, IMailbox, "add_message") r = Dummy("r", events, IReceive, "got_key") From 7c18fb81dd62a250af2755f781707c9a3811b962 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 8 Mar 2017 08:45:11 +0100 Subject: [PATCH 110/176] only allow code to be set once --- src/wormhole/_boss.py | 13 +++++++++++- src/wormhole/errors.py | 3 +++ src/wormhole/test/test_wormhole.py | 32 ++++++++++++++++-------------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 530d7ae..a8cf5e4 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -17,7 +17,8 @@ from ._rendezvous import RendezvousConnector from ._lister import Lister from ._code import Code from ._terminator import Terminator -from .errors import ServerError, LonelyError, WrongPasswordError, KeyFormatError +from .errors import (ServerError, LonelyError, WrongPasswordError, + KeyFormatError, OnlyOneCodeError) from .util import bytes_to_dict @attrs @@ -62,6 +63,7 @@ class Boss(object): self._C.wire(self, self._RC, self._L) self._T.wire(self, self._RC, self._N, self._M) + self._did_start_code = False self._next_tx_phase = 0 self._next_rx_phase = 0 self._rx_phases = {} # phase -> plaintext @@ -113,12 +115,21 @@ class Boss(object): # Wormhole only knows about this Boss instance, and everything else is # hidden away). def input_code(self, stdio): + if self._did_start_code: + raise OnlyOneCodeError() + self._did_start_code = True self._C.input_code(stdio) def allocate_code(self, code_length): + if self._did_start_code: + raise OnlyOneCodeError() + self._did_start_code = True self._C.allocate_code(code_length) def set_code(self, code): if ' ' in code: raise KeyFormatError("code (%s) contains spaces." % code) + if self._did_start_code: + raise OnlyOneCodeError() + self._did_start_code = True self._C.set_code(code) @m.input() diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index b6ba419..81fd989 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -54,6 +54,9 @@ class NoTorError(WormholeError): class NoKeyError(WormholeError): """w.derive_key() was called before got_verifier() fired""" +class OnlyOneCodeError(WormholeError): + """Only one w.generate_code/w.set_code/w.type_code may be called""" + class WormholeClosed(Exception): """Deferred-returning API calls errback with WormholeClosed if the wormhole was already closed, or if it closes before a real result can be diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index b632f84..3033903 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -8,7 +8,8 @@ from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks from .common import ServerBase from .. import wormhole, _rendezvous from ..errors import (WrongPasswordError, WelcomeError, InternalError, - KeyFormatError, WormholeClosed, LonelyError) + KeyFormatError, WormholeClosed, LonelyError, + NoKeyError, OnlyOneCodeError) from spake2 import SPAKE2_Symmetric from ..timing import DebugTiming from ..util import (bytes_to_dict, dict_to_bytes, @@ -979,23 +980,24 @@ class MessageDoubler(_rendezvous.RendezvousConnector): class Errors(ServerBase, unittest.TestCase): @inlineCallbacks - def test_codes_1(self): + def test_derive_key_early(self): w = wormhole.create(APPID, self.relayurl, reactor) # definitely too early - self.assertRaises(InternalError, w.derive_key, "purpose", 12) - - w.set_code("123-purple-elephant") - # code can only be set once - self.assertRaises(InternalError, w.set_code, "123-nope") - yield self.assertFailure(w.when_code(), InternalError) - yield self.assertFailure(w.input_code(), InternalError) - yield w.close() + self.assertRaises(NoKeyError, w.derive_key, "purpose", 12) + yield self.assertFailure(w.close(), LonelyError) @inlineCallbacks - def test_codes_2(self): + def test_multiple_set_code(self): w = wormhole.create(APPID, self.relayurl, reactor) + w.set_code("123-purple-elephant") + # code can only be set once + self.assertRaises(OnlyOneCodeError, w.set_code, "123-nope") + yield self.assertFailure(w.close(), LonelyError) + + @inlineCallbacks + def test_allocate_and_set_code(self): + w = wormhole.create(APPID, self.relayurl, reactor) + w.allocate_code() yield w.when_code() - self.assertRaises(InternalError, w.set_code, "123-nope") - yield self.assertFailure(w.when_code(), InternalError) - yield self.assertFailure(w.input_code(), InternalError) - yield w.close() + self.assertRaises(OnlyOneCodeError, w.set_code, "123-nope") + yield self.assertFailure(w.close(), LonelyError) From 921228a702af4c8f084b731f5e24b842e4220022 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 8 Mar 2017 08:45:33 +0100 Subject: [PATCH 111/176] log errors better --- src/wormhole/_rendezvous.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 7566a66..ebe53f0 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -175,6 +175,7 @@ class RendezvousConnector(object): try: return meth(msg) except Exception as e: + log.err(e) self._B.error(e) raise From 29f467e9d844eaf8b64f3fc8f17154a0eaff9baf Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 8 Mar 2017 08:46:15 +0100 Subject: [PATCH 112/176] CLI: don't hide errors, fuss with verifier API --- src/wormhole/cli/cmd_receive.py | 24 +++++++++++++++--- src/wormhole/cli/cmd_send.py | 43 +++++++++++++++++++++------------ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index c15b458..c6ec093 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -76,17 +76,33 @@ class TwistedReceiver: # as coming from the "yield self._go" line, which wasn't very useful # for tracking it down. d = self._go(w) + + # if we succeed, we should close and return the w.close results + # (which might be an error) @inlineCallbacks - def _close(res): - yield w.close() + def _good(res): + yield w.close() # wait for ack returnValue(res) - d.addBoth(_close) + + # if we raise an error, we should close and then return the original + # error (the close might give us an error, but it isn't as important + # as the original one) + @inlineCallbacks + def _bad(f): + log.err(f) + try: + yield w.close() # might be an error too + except: + pass + returnValue(f) + + d.addCallbacks(_good, _bad) yield d @inlineCallbacks def _go(self, w): yield self._handle_code(w) - verifier = yield w.when_verifier() + verifier = yield w.when_verified() def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=self.args.stderr) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index 337f314..99052a9 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -57,11 +57,27 @@ class Sender: tor_manager=self._tor_manager, timing=self._timing) d = self._go(w) + + # if we succeed, we should close and return the w.close results + # (which might be an error) @inlineCallbacks - def _close(res): - yield w.close() # must wait for ack from close() + def _good(res): + yield w.close() # wait for ack returnValue(res) - d.addBoth(_close) + + # if we raise an error, we should close and then return the original + # error (the close might give us an error, but it isn't as important + # as the original one) + @inlineCallbacks + def _bad(f): + log.err(f) + try: + yield w.close() # might be an error too + except: + pass + returnValue(f) + + d.addCallbacks(_good, _bad) yield d def _send_data(self, data, w): @@ -98,23 +114,18 @@ class Sender: args.stderr.flush() print(u"", file=args.stderr) - verifier_bytes = yield w.when_verifier() - # we've seen PAKE, but not yet VERSION, so we don't know if they got - # the right password or not - def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=args.stderr) - notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) + #notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) - # TODO: maybe don't stall for VERSION, if they don't want - # verification, to save a roundtrip? - try: - yield w.when_version() - # this may raise WrongPasswordError - finally: - if not notify.called: - notify.cancel() + # TODO: don't stall on w.verify() unless they want it + #try: + # verifier_bytes = yield w.when_verified() # might WrongPasswordError + #finally: + # if not notify.called: + # notify.cancel() + yield w.when_verified() if args.verify: verifier = bytes_to_hexstr(verifier_bytes) From 276fdd367375323454e7ab12ff3923c0396d47a8 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 9 Mar 2017 11:01:11 +0100 Subject: [PATCH 113/176] fix tests that exercise failure --- src/wormhole/test/test_scripts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 4cbe720..db52693 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -682,6 +682,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): # check server stats self._rendezvous.get_stats() + self.flushLoggedErrors(TransferError) def test_fail_file_noclobber(self): return self._do_test_fail("file", "noclobber") From 4bd9d3579c8054d709199baadb2d74832fe9c333 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 9 Mar 2017 11:05:36 +0100 Subject: [PATCH 114/176] go back to "input_code" instead of "type_code" --- docs/api.md | 18 +++++++++--------- src/wormhole/errors.py | 2 +- src/wormhole/wormhole.py | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api.md b/docs/api.md index f4c4eeb..5438b71 100644 --- a/docs/api.md +++ b/docs/api.md @@ -153,7 +153,7 @@ randomly-generated selection from the PGP wordlist, providing a default of 16 bits of entropy. The initiating program should display this code to the user, who should transcribe it to the receiving user, who gives it to their local Wormhole object by calling `set_code()`. The receiving program can also use -`type_code()` to use a readline-based input function: this offers tab +`input_code()` to use a readline-based input function: this offers tab completion of allocated channel-ids and known codewords. The Wormhole object has three APIs for generating or accepting a code: @@ -162,7 +162,7 @@ The Wormhole object has three APIs for generating or accepting a code: a short numeric nameplate, chooses a configurable number of random words, then assembles them into the code * `w.set_code(code)`: this accepts the code as an argument -* `helper = w.type_code()`: this facilitates interactive entry of the code, +* `helper = w.input_code()`: this facilitates interactive entry of the code, with tab-completion. The helper object has methods to return a list of viable completions for whatever portion of the code has been entered so far. A convenience wrapper is provided to attach this to the `rlcompleter` @@ -179,7 +179,7 @@ The code-entry Helper object has the following API: * `d = h.get_nameplates()`: returns a Deferred that fires with a list of (string) nameplates. These form the first portion of the wormhole code (e.g. "4" in "4-purple-sausages"). The list is requested from the server - when `w.type_code()` is first called, and if the response arrives before + when `w.input_code()` is first called, and if the response arrives before `h.get_nameplates()` is called, it will be used without delay. All subsequent calls to `h.get_nameplates()` will provoke a fresh request to the server, so hitting Tab too early won't condemn the client to using a @@ -210,7 +210,7 @@ helper to do tab completion of wormhole codes: ```python from wormhole import create, rlcompleter_helper w = create(appid, relay_url, reactor) -rlcompleter_helper("Wormhole code:", w.type_code()) +rlcompleter_helper("Wormhole code:", w.input_code()) d = w.when_code() ``` @@ -229,16 +229,16 @@ In most situations, the "sending" or "initiating" side will call it and speaks, types, performs charades, or otherwise transmits the code to the receiving human. The receiving human then types it into the receiving computer, where it either calls `w.set_code()` (if the code is passed in via -argv) or `w.type_code()` (for interactive entry). +argv) or `w.input_code()` (for interactive entry). Usually one machine generates the code, and a pair of humans transcribes it to the second machine (so `w.generate_code()` on one side, and `w.set_code()` -or `w.type_code()` on the other). But it is also possible for the humans to +or `w.input_code()` on the other). But it is also possible for the humans to generate the code offline, perhaps at a face-to-face meeting, and then take the code back to their computers. In this case, `w.set_code()` will be used on both sides. It is unlikely that the humans will restrict themselves to a pre-established wordlist when manually generating codes, so the completion -feature of `w.type_code()` is not helpful. +feature of `w.input_code()` is not helpful. When the humans create an invitation code out-of-band, they are responsible for choosing an unused channel-ID (simply picking a random 3-or-more digit @@ -276,7 +276,7 @@ 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.type_code()` + 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. @@ -396,7 +396,7 @@ action | Deferred-Mode | Delegated-Mode -------------------------- | -------------------- | -------------- w.generate_code(length=2) | | w.set_code(code) | | -h=w.type_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) diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index 81fd989..d8f8fee 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -55,7 +55,7 @@ class NoKeyError(WormholeError): """w.derive_key() was called before got_verifier() fired""" class OnlyOneCodeError(WormholeError): - """Only one w.generate_code/w.set_code/w.type_code may be called""" + """Only one w.generate_code/w.set_code/w.input_code may be called""" class WormholeClosed(Exception): """Deferred-returning API calls errback with WormholeClosed if the diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 291a466..f730195 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -311,13 +311,13 @@ def from_serialized(serialized, reactor, delegate, # should the new Wormhole call got_code? only if it wasn't called before. # after creating the wormhole object, app must call exactly one of: -# set_code(code), generate_code(), helper=type_code(), and then (if they need +# set_code(code), generate_code(), helper=input_code(), and then (if they need # to know the code) wait for delegate.got_code() or d=w.when_code() -# the helper for type_code() can be asked for completions: +# the helper for input_code() can be asked for completions: # d=helper.get_completions(text_so_far), which will fire with a list of # strings that could usefully be appended to text_so_far. -# wormhole.type_code_readline(w) is a wrapper that knows how to use -# w.type_code() to drive rlcompleter +# wormhole.input_code_readline(w) is a wrapper that knows how to use +# w.input_code() to drive rlcompleter From 299f89c01f5d0281dc1ae43ff3d6c4fbf1ae654e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 11 Mar 2017 10:03:05 +0100 Subject: [PATCH 115/176] new idea on code-input helper API --- docs/api.md | 56 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/api.md b/docs/api.md index 5438b71..b8c6a15 100644 --- a/docs/api.md +++ b/docs/api.md @@ -176,33 +176,47 @@ for consistency. The code-entry Helper object has the following API: -* `d = h.get_nameplates()`: returns a Deferred that fires with a list of - (string) nameplates. These form the first portion of the wormhole code - (e.g. "4" in "4-purple-sausages"). The list is requested from the server - when `w.input_code()` is first called, and if the response arrives before - `h.get_nameplates()` is called, it will be used without delay. All - subsequent calls to `h.get_nameplates()` will provoke a fresh request to - the server, so hitting Tab too early won't condemn the client to using a - stale list. -* `h.set_nameplate(nameplate)`: commit to using a specific nameplate. Once - this is called, `h.get_nameplates()` will raise an immediate exception -* `completions = h.get_completions_for(prefix)`: given a prefix like "su", - this returns (synchronously) a list of strings which are appropriate to +* `update_nameplates()`: requests an updated list of nameplates from the + Rendezvous Server. These form the first portion of the wormhole code (e.g. + "4" in "4-purple-sausages"). Note that they are unicode strings (so "4", + not 4). The Helper will get the response in the background, and calls to + `complete_nameplate()` after the response will use the new list. +* `completions = h.complete_nameplate(prefix)`: returns (synchronously) a + list of suffixes for the given nameplate prefix. For example, if the server + reports nameplates 1, 12, 13, 24, and 170 are in use, + `complete_nameplate("1")` will return `["", "2", "3", "70"]`. Raises + `AlreadyClaimedNameplateError` if called after `h.claim_nameplate`. +* `d = h.claim_nameplate(nameplate)`: accepts a string with the chosen + nameplate. May only be called once, after which `OnlyOneNameplateError` is + raised. Returns a Deferred that fires (with None) when the nameplate's + wordlist is known (which happens after the nameplate is claimed, requiring + a roundtrip to the server). +* `completions = h.complete_words(prefix)`: return (synchronously) a list of + suffixes for the given words prefix. The possible completions depend upon + the wordlist in use for the previously-claimed nameplate, so calling this + before `claim_nameplate` will raise `MustClaimNameplateFirstError`. Given a + prefix like "su", this returns a list of strings which are appropriate to append to the prefix (e.g. `["pportive", "rrender", "spicious"]`, for expansion into "supportive", "surrender", and "suspicious". The prefix should not include the nameplate, but *should* include whatever words and hyphens have been typed so far (the default wordlist uses alternate lists, where even numbered words have three syllables, and odd numbered words have two, so the completions depend upon how many words are present, not just - the partial last word). E.g. `get_completions_for("pr")` will return - `["ocessor", "ovincial", "oximate"]`, while - `get_completions_for("opulent-pr")` will return `["eclude", "efer", - "eshrunk", "inter", "owler"]`. -* `h.set_words(suffix)`: this accepts a string (e.g. "purple-sausages"), and - commits to the code. `h.set_nameplate()` must be called before this, and no - other methods may be called afterwards. Calling this causes the - `w.when_code()` Deferred or corresponding delegate callback to fire, and - triggers the wormhole connection process. + the partial last word). E.g. `complete_words("pr")` will return + `["ocessor", "ovincial", "oximate"]`, while `complete_words("opulent-pr")` + will return `["eclude", "efer", "eshrunk", "inter", "owler"]`. + If the wordlist is not yet known (i.e. the Deferred from `claim_nameplate` + has not yet fired), this returns an empty list. It will also return an + empty list if the prefix is complete (the last word matches something in + the completion list, and there are no longer extension words), although the + code may not yet be complete if there are additional words. The completions + will never include a hyphen: the UI frontend must supply these if desired. +* `h.submit_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 + wormhole connection process. This accepts a string like "purple-sausages", + without the nameplate. It must be called after `h.claim_nameplate()` or + `MustClaimNameplateFirstError` will be raised. The `rlcompleter` wrapper is a function that knows how to use the code-entry helper to do tab completion of wormhole codes: From e2c0f082160169bace0a8222bd7c23969da6e35d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 11 Mar 2017 10:03:24 +0100 Subject: [PATCH 116/176] minor renames for code-input helper stuff --- src/wormhole/_boss.py | 4 ++-- src/wormhole/_code.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index a8cf5e4..f6c89f7 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -114,11 +114,11 @@ class Boss(object): # would require the Wormhole to be aware of Code (whereas right now # Wormhole only knows about this Boss instance, and everything else is # hidden away). - def input_code(self, stdio): + def input_code(self, helper): if self._did_start_code: raise OnlyOneCodeError() self._did_start_code = True - self._C.input_code(stdio) + self._C.input_code(helper) def allocate_code(self, code_length): if self._did_start_code: raise OnlyOneCodeError() diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 307dc5e..ac4d30c 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -52,7 +52,7 @@ class Code(object): @m.input() def allocate_code(self, code_length): pass @m.input() - def input_code(self, stdio): pass + def input_code(self, input_helper): pass @m.input() def set_code(self, code): pass @@ -80,8 +80,8 @@ class Code(object): def L_refresh_nameplates(self): self._L.refresh_nameplates() @m.output() - def start_input_and_L_refresh_nameplates(self, stdio): - self._stdio = stdio + def start_input_and_L_refresh_nameplates(self, input_helper): + self._input_helper = input_helper self._L.refresh_nameplates() @m.output() def stash_code_length_and_RC_tx_allocate(self, code_length): From 0ddc93110baf6b92084645db40b8d1f08fcec247 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 11 Mar 2017 10:16:21 +0100 Subject: [PATCH 117/176] work on new Code state machine design --- docs/state-machines/code.dot | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/state-machines/code.dot b/docs/state-machines/code.dot index 0cde977..53d3cca 100644 --- a/docs/state-machines/code.dot +++ b/docs/state-machines/code.dot @@ -10,32 +10,27 @@ digraph { S0A -> P0_got_code [label="set_code"] S0B -> P0_got_code [label="set_code"] P0_got_code [shape="box" label="B.got_code"] - P0_got_code -> S4 - S4 [label="S4: known" color="green"] + P0_got_code -> S5 + S5 [label="S5: known" color="green"] S0A -> P0_list_nameplates [label="input_code"] S0B -> P0_list_nameplates [label="input_code"] S2 [label="S2: typing\nnameplate"] - S2 -> P2_completion [label=""] - P2_completion [shape="box" label="do completion"] - P2_completion -> P0_list_nameplates + S2 -> P0_list_nameplates [label="update_nameplates"] P0_list_nameplates [shape="box" label="L.refresh_nameplates"] P0_list_nameplates -> S2 - S2 -> P2_got_nameplates [label="got_nameplates"] - P2_got_nameplates [shape="box" label="stash nameplates\nfor completion"] - P2_got_nameplates -> S2 - S2 -> P2_finish [label="" color="orange" fontcolor="orange"] - P2_finish [shape="box" label="lookup wordlist\nfor completion"] - P2_finish -> S3 - S3 [label="S3: typing\ncode"] - S3 -> P3_completion [label=""] - P3_completion [shape="box" label="do completion"] - P3_completion -> S3 - - S3 -> P0_got_code [label="" - color="orange" fontcolor="orange"] + S2 -> P2_claim [label="claim_nameplate" color="orange" fontcolor="orange"] + P2_claim [shape="box" label="N.set_nameplate"] + P2_claim -> S3 + S3 [label="S3: typing\ncode\n(no wordlist)"] + S3 -> P3_stash_wordlist [label="got_nameplates" color="orange"] + P3_stash_wordlist [shape="box" label="stash\nwordlist" color="orange"] + P3_stash_wordlist -> S4 [color="orange"] + S3 -> P0_got_code [label="submit_words"] + S4 [label="S4: typing\ncode\n(yes wordlist)" color="orange"] + S4 -> P0_got_code [label="submit_words" color="orange" fontcolor="orange"] S0A -> S1A [label="allocate_code"] S1A [label="S1A:\nconnecting"] From 0be5aba77d69d20043d7878fc9a06501b878ff4d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 11 Mar 2017 23:18:58 +0100 Subject: [PATCH 118/176] begin worm on new Code machine --- src/wormhole/_code.py | 76 ++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index ac4d30c..b97f536 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -44,9 +44,11 @@ class Code(object): @m.state() def S2_typing_nameplate(self): pass # pragma: no cover @m.state() - def S3_typing_code(self): pass # pragma: no cover + def S3_typing_code_no_wordlist(self): pass # pragma: no cover @m.state() - def S4_known(self): pass # pragma: no cover + def S4_typing_code_wordlist(self): pass # pragma: no cover + @m.state() + def S5_known(self): pass # pragma: no cover # from App @m.input() @@ -68,13 +70,17 @@ class Code(object): @m.input() def got_nameplates(self, nameplates): pass - # from stdin/readline/??? + # from Nameplate @m.input() - def tab(self): pass + def got_wordlist(self, wordlist): pass + + # from CodeInputHelper @m.input() - def hyphen(self): pass + def update_nameplates(self): pass @m.input() - def RETURN(self, code): pass + def claim_nameplate(self, nameplate): pass + @m.input() + def submit_words(self, words): pass @m.output() def L_refresh_nameplates(self): @@ -94,7 +100,8 @@ class Code(object): def RC_tx_allocate(self): self._RC.tx_allocate() @m.output() - def do_completion_nameplates(self): + def stash_wordlist(self, wordlist): + # TODO pass @m.output() def stash_nameplates(self, nameplates): @@ -107,10 +114,23 @@ class Code(object): def do_completion_code(self): pass @m.output() + def record_nameplate(self, nameplate): + self._nameplate = nameplate + @m.output() + def N_set_nameplate(self, nameplate): + self._N.set_nameplate(nameplate) + + @m.output() def generate_and_B_got_code(self, nameplate): self._code = make_code(nameplate, self._code_length) self._B_got_code() + @m.output() + def submit_words_and_B_got_code(self, words): + assert self._nameplate + self._code = self._nameplate + "-" + words + self._B_got_code() + @m.output() def B_got_code(self, code): self._code = code @@ -122,8 +142,8 @@ class Code(object): S0A_unknown.upon(connected, enter=S0B_unknown_connected, outputs=[]) S0B_unknown_connected.upon(lost, enter=S0A_unknown, outputs=[]) - S0A_unknown.upon(set_code, enter=S4_known, outputs=[B_got_code]) - S0B_unknown_connected.upon(set_code, enter=S4_known, outputs=[B_got_code]) + S0A_unknown.upon(set_code, enter=S5_known, outputs=[B_got_code]) + S0B_unknown_connected.upon(set_code, enter=S5_known, outputs=[B_got_code]) S0A_unknown.upon(allocate_code, enter=S1A_connecting, outputs=[stash_code_length]) @@ -132,27 +152,39 @@ class Code(object): S1A_connecting.upon(connected, enter=S1B_allocating, outputs=[RC_tx_allocate]) S1B_allocating.upon(lost, enter=S1A_connecting, outputs=[]) - S1B_allocating.upon(rx_allocated, enter=S4_known, + S1B_allocating.upon(rx_allocated, enter=S5_known, outputs=[generate_and_B_got_code]) S0A_unknown.upon(input_code, enter=S2_typing_nameplate, outputs=[start_input_and_L_refresh_nameplates]) S0B_unknown_connected.upon(input_code, enter=S2_typing_nameplate, outputs=[start_input_and_L_refresh_nameplates]) - S2_typing_nameplate.upon(tab, enter=S2_typing_nameplate, - outputs=[do_completion_nameplates]) - S2_typing_nameplate.upon(got_nameplates, enter=S2_typing_nameplate, + S2_typing_nameplate.upon(update_nameplates, enter=S2_typing_nameplate, + outputs=[L_refresh_nameplates]) + S2_typing_nameplate.upon(got_nameplates, + enter=S2_typing_nameplate, outputs=[stash_nameplates]) - S2_typing_nameplate.upon(hyphen, enter=S3_typing_code, - outputs=[lookup_wordlist]) - # TODO: need a proper pair of connected/lost states around S2 + S2_typing_nameplate.upon(claim_nameplate, enter=S3_typing_code_no_wordlist, + outputs=[record_nameplate, N_set_nameplate]) S2_typing_nameplate.upon(connected, enter=S2_typing_nameplate, outputs=[]) S2_typing_nameplate.upon(lost, enter=S2_typing_nameplate, outputs=[]) - S3_typing_code.upon(tab, enter=S3_typing_code, outputs=[do_completion_code]) - S3_typing_code.upon(RETURN, enter=S4_known, outputs=[B_got_code]) - S3_typing_code.upon(connected, enter=S3_typing_code, outputs=[]) - S3_typing_code.upon(lost, enter=S3_typing_code, outputs=[]) + S3_typing_code_no_wordlist.upon(got_wordlist, + enter=S4_typing_code_wordlist, + outputs=[stash_wordlist]) + S3_typing_code_no_wordlist.upon(submit_words, enter=S5_known, + outputs=[submit_words_and_B_got_code]) + S3_typing_code_no_wordlist.upon(connected, enter=S3_typing_code_no_wordlist, + outputs=[]) + S3_typing_code_no_wordlist.upon(lost, enter=S3_typing_code_no_wordlist, + outputs=[]) - S4_known.upon(connected, enter=S4_known, outputs=[]) - S4_known.upon(lost, enter=S4_known, outputs=[]) + S4_typing_code_wordlist.upon(submit_words, enter=S5_known, + outputs=[submit_words_and_B_got_code]) + S4_typing_code_wordlist.upon(connected, enter=S4_typing_code_wordlist, + outputs=[]) + S4_typing_code_wordlist.upon(lost, enter=S4_typing_code_wordlist, + outputs=[]) + + S5_known.upon(connected, enter=S5_known, outputs=[]) + S5_known.upon(lost, enter=S5_known, outputs=[]) From ae8652daf6b5092ddb6d300af213be8f417f6a7f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 12 Mar 2017 13:04:15 +0100 Subject: [PATCH 119/176] code: internal name changes --- src/wormhole/_code.py | 58 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index b97f536..563eca4 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -42,11 +42,11 @@ class Code(object): @m.state() def S1B_allocating(self): pass # pragma: no cover @m.state() - def S2_typing_nameplate(self): pass # pragma: no cover + def S2_input_nameplate(self): pass # pragma: no cover @m.state() - def S3_typing_code_no_wordlist(self): pass # pragma: no cover + def S3_input_code_no_wordlist(self): pass # pragma: no cover @m.state() - def S4_typing_code_wordlist(self): pass # pragma: no cover + def S4_input_code_wordlist(self): pass # pragma: no cover @m.state() def S5_known(self): pass # pragma: no cover @@ -155,36 +155,36 @@ class Code(object): S1B_allocating.upon(rx_allocated, enter=S5_known, outputs=[generate_and_B_got_code]) - S0A_unknown.upon(input_code, enter=S2_typing_nameplate, + S0A_unknown.upon(input_code, enter=S2_input_nameplate, outputs=[start_input_and_L_refresh_nameplates]) - S0B_unknown_connected.upon(input_code, enter=S2_typing_nameplate, + S0B_unknown_connected.upon(input_code, enter=S2_input_nameplate, outputs=[start_input_and_L_refresh_nameplates]) - S2_typing_nameplate.upon(update_nameplates, enter=S2_typing_nameplate, - outputs=[L_refresh_nameplates]) - S2_typing_nameplate.upon(got_nameplates, - enter=S2_typing_nameplate, - outputs=[stash_nameplates]) - S2_typing_nameplate.upon(claim_nameplate, enter=S3_typing_code_no_wordlist, - outputs=[record_nameplate, N_set_nameplate]) - S2_typing_nameplate.upon(connected, enter=S2_typing_nameplate, outputs=[]) - S2_typing_nameplate.upon(lost, enter=S2_typing_nameplate, outputs=[]) + S2_input_nameplate.upon(update_nameplates, enter=S2_input_nameplate, + outputs=[L_refresh_nameplates]) + S2_input_nameplate.upon(got_nameplates, + enter=S2_input_nameplate, + outputs=[stash_nameplates]) + S2_input_nameplate.upon(claim_nameplate, enter=S3_input_code_no_wordlist, + outputs=[record_nameplate, N_set_nameplate]) + S2_input_nameplate.upon(connected, enter=S2_input_nameplate, outputs=[]) + S2_input_nameplate.upon(lost, enter=S2_input_nameplate, outputs=[]) - S3_typing_code_no_wordlist.upon(got_wordlist, - enter=S4_typing_code_wordlist, - outputs=[stash_wordlist]) - S3_typing_code_no_wordlist.upon(submit_words, enter=S5_known, - outputs=[submit_words_and_B_got_code]) - S3_typing_code_no_wordlist.upon(connected, enter=S3_typing_code_no_wordlist, - outputs=[]) - S3_typing_code_no_wordlist.upon(lost, enter=S3_typing_code_no_wordlist, - outputs=[]) + S3_input_code_no_wordlist.upon(got_wordlist, + enter=S4_input_code_wordlist, + outputs=[stash_wordlist]) + S3_input_code_no_wordlist.upon(submit_words, enter=S5_known, + outputs=[submit_words_and_B_got_code]) + S3_input_code_no_wordlist.upon(connected, enter=S3_input_code_no_wordlist, + outputs=[]) + S3_input_code_no_wordlist.upon(lost, enter=S3_input_code_no_wordlist, + outputs=[]) - S4_typing_code_wordlist.upon(submit_words, enter=S5_known, - outputs=[submit_words_and_B_got_code]) - S4_typing_code_wordlist.upon(connected, enter=S4_typing_code_wordlist, - outputs=[]) - S4_typing_code_wordlist.upon(lost, enter=S4_typing_code_wordlist, - outputs=[]) + S4_input_code_wordlist.upon(submit_words, enter=S5_known, + outputs=[submit_words_and_B_got_code]) + S4_input_code_wordlist.upon(connected, enter=S4_input_code_wordlist, + outputs=[]) + S4_input_code_wordlist.upon(lost, enter=S4_input_code_wordlist, + outputs=[]) S5_known.upon(connected, enter=S5_known, outputs=[]) S5_known.upon(lost, enter=S5_known, outputs=[]) From 79d38da49787e585db1634d4d3af0be53965003a Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 12 Mar 2017 13:03:44 +0100 Subject: [PATCH 120/176] split Code into Code/Input/Allocator, ostensibly simpler --- docs/state-machines/allocator.dot | 25 +++++++++++++ docs/state-machines/code.dot | 59 ++++++++++------------------- docs/state-machines/input.dot | 32 ++++++++++++++++ docs/state-machines/machines.dot | 62 +++++++++++++++++++------------ 4 files changed, 115 insertions(+), 63 deletions(-) create mode 100644 docs/state-machines/allocator.dot create mode 100644 docs/state-machines/input.dot diff --git a/docs/state-machines/allocator.dot b/docs/state-machines/allocator.dot new file mode 100644 index 0000000..eb0b9d3 --- /dev/null +++ b/docs/state-machines/allocator.dot @@ -0,0 +1,25 @@ +digraph { + + start [label="A:\nNameplate\nAllocation" style="dotted"] + {rank=same; start S0A S0B} + start -> S0A [style="invis"] + S0A [label="S0A:\nidle\ndisconnected" color="orange"] + S0A -> S0B [label="connected"] + S0B -> S0A [label="lost"] + S0B [label="S0B:\nidle\nconnected"] + S0A -> S1A [label="allocate" color="orange"] + S0B -> P_allocate [label="allocate"] + P_allocate [shape="box" label="RC.tx_allocate" color="orange"] + P_allocate -> S1B [color="orange"] + S1B [label="S1B:\nallocating" color="orange"] + S1B -> S1A [label="lost"] + S1A [label="S1A:\nallocating\ndisconnected" color="orange"] + S1A -> P_allocate [label="connected" color="orange"] + + S1B -> P_allocated [label="rx_allocated" color="orange"] + P_allocated [shape="box" label="C.allocated" color="orange"] + P_allocated -> S2 [color="orange"] + + S2 [label="S2:\ndone" color="orange"] + +} diff --git a/docs/state-machines/code.dot b/docs/state-machines/code.dot index 53d3cca..35940f8 100644 --- a/docs/state-machines/code.dot +++ b/docs/state-machines/code.dot @@ -1,48 +1,27 @@ digraph { - start [label="Wormhole Code\nMachine" style="dotted"] - {rank=same; start S0A S0B} - start -> S0A [style="invis"] - S0A [label="S0A:\nunknown\ndisconnected"] - S0A -> S0B [label="connected"] - S0B -> S0A [label="lost"] - S0B [label="S0B:\nunknown\nconnected"] - S0A -> P0_got_code [label="set_code"] - S0B -> P0_got_code [label="set_code"] + start [label="C:\nCode\nManagement" style="dotted"] + {rank=same; start S0} + start -> S0 [style="invis"] + S0 [label="S0:\nidle"] + S0 -> P0_got_code [label="set_code"] P0_got_code [shape="box" label="B.got_code"] - P0_got_code -> S5 - S5 [label="S5: known" color="green"] + P0_got_code -> S3 + S3 [label="S3: known" color="green"] - S0A -> P0_list_nameplates [label="input_code"] - S0B -> P0_list_nameplates [label="input_code"] - S2 [label="S2: typing\nnameplate"] + {rank=same; S1_inputting S2_allocating} + S0 -> P_input [label="input_code"] + P_input [shape="box" label="I.start"] + P_input -> S1_inputting + S1_inputting [label="S1:\ninputting"] + S1_inputting -> P0_got_code [label="finished_input"] - S2 -> P0_list_nameplates [label="update_nameplates"] - P0_list_nameplates [shape="box" label="L.refresh_nameplates"] - P0_list_nameplates -> S2 - - S2 -> P2_claim [label="claim_nameplate" color="orange" fontcolor="orange"] - P2_claim [shape="box" label="N.set_nameplate"] - P2_claim -> S3 - S3 [label="S3: typing\ncode\n(no wordlist)"] - S3 -> P3_stash_wordlist [label="got_nameplates" color="orange"] - P3_stash_wordlist [shape="box" label="stash\nwordlist" color="orange"] - P3_stash_wordlist -> S4 [color="orange"] - S3 -> P0_got_code [label="submit_words"] - S4 [label="S4: typing\ncode\n(yes wordlist)" color="orange"] - S4 -> P0_got_code [label="submit_words" color="orange" fontcolor="orange"] - - S0A -> S1A [label="allocate_code"] - S1A [label="S1A:\nconnecting"] - S1A -> P1_allocate [label="connected"] - P1_allocate [shape="box" label="RC.tx_allocate"] - P1_allocate -> S1B - S1B [label="S1B:\nallocating"] - S1B -> P1_generate [label="rx_allocated"] - S1B -> S1A [label="lost"] - P1_generate [shape="box" label="generate\nrandom code"] + S0 -> P_allocate [label="allocate_code"] + P_allocate [shape="box" label="A.allocate"] + P_allocate -> S2_allocating + S2_allocating [label="S2:\nallocating"] + S2_allocating -> P1_generate [label="allocated_nameplate"] + P1_generate [shape="box" label="generate\nrandom words"] P1_generate -> P0_got_code - S0B -> P1_allocate [label="allocate_code"] - } diff --git a/docs/state-machines/input.dot b/docs/state-machines/input.dot new file mode 100644 index 0000000..3a68ac4 --- /dev/null +++ b/docs/state-machines/input.dot @@ -0,0 +1,32 @@ +digraph { + + start [label="I:\nCode\nInput" style="dotted"] + {rank=same; start S0} + start -> S0 [style="invis"] + S0 [label="S0:\nidle"] + + S0 -> P0_list_nameplates [label="start" color="orange"] + P0_list_nameplates [shape="box" label="L.refresh_nameplates" color="orange"] + P0_list_nameplates -> S1 [color="orange"] + S1 [label="S1: typing\nnameplate" color="orange"] + + {rank=same; foo P0_list_nameplates} + S1 -> foo [label="update_nameplates"] + foo [style="dashed" label=""] + foo -> P0_list_nameplates + + S1 -> P1_claim [label="claim_nameplate" color="orange" fontcolor="orange"] + P1_claim [shape="box" label="N.set_nameplate" color="orange"] + P1_claim -> S2 [color="orange"] + S2 [label="S2: typing\ncode\n(no wordlist)" color="orange"] + S2 -> P2_stash_wordlist [label="got_nameplates" color="orange"] + P2_stash_wordlist [shape="box" label="stash\nwordlist" color="orange"] + P2_stash_wordlist -> S3 [color="orange"] + S2 -> P_done [label="submit_words"] + S3 [label="S3: typing\ncode\n(yes wordlist)" color="orange"] + S3 -> P_done [label="submit_words" color="orange" fontcolor="orange"] + P_done [shape="box" label="C.finished_input" color="orange"] + P_done -> S4 [color="orange"] + S4 [label="S4: known" color="green"] + +} diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index c1a6b6c..c84e9cb 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -6,30 +6,38 @@ digraph { Mailbox [shape="box" color="blue" fontcolor="blue"] Connection [label="Rendezvous\nConnector" shape="oval" color="blue" fontcolor="blue"] - websocket [color="blue" fontcolor="blue"] + #websocket [color="blue" fontcolor="blue"] Order [shape="box" label="Ordering" color="blue" fontcolor="blue"] Key [shape="box" label="Key" color="blue" fontcolor="blue"] Send [shape="box" label="Send" color="blue" fontcolor="blue"] Receive [shape="box" label="Receive" color="blue" fontcolor="blue"] Code [shape="box" label="Code" color="blue" fontcolor="blue"] - Lister [shape="box" label="(Nameplate)\nLister" + Lister [shape="box" label="(nameplate)\nLister" color="blue" fontcolor="blue"] + Allocator [shape="box" label="(nameplate)\nAllocator" + color="blue" fontcolor="blue"] + Input [shape="box" label="(code)\nInput helper" + color="blue" fontcolor="blue"] Terminator [shape="box" color="blue" fontcolor="blue"] - Connection -> websocket [color="blue"] + #Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] - Wormhole -> Boss [style="dashed" label="allocate_code\ninput_code\nset_code\nsend\nclose\n(once)"] + Wormhole -> Boss [style="dashed" + label="allocate_code\ninput_code\nset_code\nsend\nclose\n(once)" + color="red" fontcolor="red"] #Wormhole -> Boss [color="blue"] Boss -> Wormhole [style="dashed" label="got_code\ngot_key\ngot_verifier\ngot_version\nreceived (seq)\nclosed\n(once)"] #Boss -> Connection [color="blue"] - Boss -> Connection [style="dashed" label="start"] + Boss -> Connection [style="dashed" label="start" + color="red" fontcolor="red"] Connection -> Boss [style="dashed" label="rx_welcome\nrx_error\nerror"] - Boss -> Send [style="dashed" label="send"] + Boss -> Send [style="dashed" color="red" fontcolor="red" label="send"] - Boss -> Nameplate [style="dashed" label="set_nameplate"] + Boss -> Nameplate [style="dashed" color="red" fontcolor="red" + label="set_nameplate"] #Boss -> Mailbox [color="blue"] Mailbox -> Order [style="dashed" label="got_message (once)"] Boss -> Key [style="dashed" label="got_code"] @@ -40,7 +48,8 @@ digraph { Key -> Mailbox [style="dashed" label="add_message (pake)\nadd_message (version)"] Receive -> Send [style="dashed" label="got_verified_key"] - Send -> Mailbox [style="dashed" label="add_message (phase)"] + Send -> Mailbox [style="dashed" color="red" fontcolor="red" + label="add_message (phase)"] Key -> Receive [style="dashed" label="got_key"] Receive -> Boss [style="dashed" @@ -52,7 +61,7 @@ digraph { Mailbox -> Nameplate [style="dashed" label="release"] Nameplate -> Mailbox [style="dashed" label="got_mailbox"] - Mailbox -> Connection [style="dashed" + Mailbox -> Connection [style="dashed" color="red" fontcolor="red" label="tx_open\ntx_add\ntx_close" ] Connection -> Mailbox [style="dashed" @@ -66,23 +75,29 @@ digraph { ] #Boss -> Code [color="blue"] - Connection -> Code [style="dashed" - label="connected\nlost\nrx_allocated"] - Code -> Connection [style="dashed" - label="tx_allocate" - ] - Lister -> Code [style="dashed" - label="got_nameplates" - ] + Connection -> Allocator [style="dashed" + label="connected\nlost\nrx_allocated"] + Allocator -> Connection [style="dashed" color="red" fontcolor="red" + label="tx_allocate" + ] + Lister -> Input [style="dashed" + label="got_nameplates" + ] #Code -> Lister [color="blue"] - Code -> Lister [style="dashed" - label="refresh_nameplates" - ] - Boss -> Code [style="dashed" - label="allocate_code\ninput_code\nset_code_code"] + Input -> Lister [style="dashed" color="red" fontcolor="red" + label="refresh_nameplates" + ] + Boss -> Code [style="dashed" color="red" fontcolor="red" + label="allocate_code\ninput_code\nset_code"] Code -> Boss [style="dashed" label="got_code"] + Code -> Input [style="dashed" color="red" fontcolor="red" label="start"] + Input -> Code [style="dashed" label="finished_input"] + Code -> Allocator [style="dashed" color="red" fontcolor="red" + label="allocate"] + Allocator -> Code [style="dashed" label="allocated"] + Nameplate -> Terminator [style="dashed" label="nameplate_done"] Mailbox -> Terminator [style="dashed" label="mailbox_done"] Terminator -> Nameplate [style="dashed" label="close"] @@ -90,5 +105,6 @@ digraph { Terminator -> Connection [style="dashed" label="stop"] Connection -> Terminator [style="dashed" label="stopped"] Terminator -> Boss [style="dashed" label="closed\n(once)"] - Boss -> Terminator [style="dashed" label="close"] + Boss -> Terminator [style="dashed" color="red" fontcolor="red" + label="close"] } From 4f1b352b2a5830b62c39a1632772dbb22386a58b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 12 Mar 2017 18:38:48 +0100 Subject: [PATCH 121/176] more work: allocator, input, shift responsibilities --- docs/api.md | 68 +++++++++---------- docs/state-machines/allocator.dot | 8 ++- docs/state-machines/boss.dot | 6 +- docs/state-machines/code.dot | 38 +++++++---- docs/state-machines/input.dot | 45 ++++++++----- docs/state-machines/lister.dot | 9 +-- docs/state-machines/machines.dot | 24 ++++--- src/wormhole/_allocator.py | 65 ++++++++++++++++++ src/wormhole/_boss.py | 10 ++- src/wormhole/_code.py | 9 ++- src/wormhole/_input.py | 106 ++++++++++++++++++++++++++++++ src/wormhole/_interfaces.py | 4 ++ src/wormhole/_lister.py | 12 ++-- 13 files changed, 305 insertions(+), 99 deletions(-) create mode 100644 src/wormhole/_allocator.py create mode 100644 src/wormhole/_input.py diff --git a/docs/api.md b/docs/api.md index b8c6a15..6417406 100644 --- a/docs/api.md +++ b/docs/api.md @@ -176,46 +176,46 @@ for consistency. The code-entry Helper object has the following API: -* `update_nameplates()`: requests an updated list of nameplates from the +* `refresh_nameplates()`: requests an updated list of nameplates from the Rendezvous Server. These form the first portion of the wormhole code (e.g. "4" in "4-purple-sausages"). Note that they are unicode strings (so "4", not 4). The Helper will get the response in the background, and calls to - `complete_nameplate()` after the response will use the new list. -* `completions = h.complete_nameplate(prefix)`: returns (synchronously) a - list of suffixes for the given nameplate prefix. For example, if the server - reports nameplates 1, 12, 13, 24, and 170 are in use, - `complete_nameplate("1")` will return `["", "2", "3", "70"]`. Raises - `AlreadyClaimedNameplateError` if called after `h.claim_nameplate`. -* `d = h.claim_nameplate(nameplate)`: accepts a string with the chosen - nameplate. May only be called once, after which `OnlyOneNameplateError` is - raised. Returns a Deferred that fires (with None) when the nameplate's - wordlist is known (which happens after the nameplate is claimed, requiring - a roundtrip to the server). -* `completions = h.complete_words(prefix)`: return (synchronously) a list of - suffixes for the given words prefix. The possible completions depend upon - the wordlist in use for the previously-claimed nameplate, so calling this - before `claim_nameplate` will raise `MustClaimNameplateFirstError`. Given a - prefix like "su", this returns a list of strings which are appropriate to - append to the prefix (e.g. `["pportive", "rrender", "spicious"]`, for - expansion into "supportive", "surrender", and "suspicious". The prefix - should not include the nameplate, but *should* include whatever words and - hyphens have been typed so far (the default wordlist uses alternate lists, - where even numbered words have three syllables, and odd numbered words have - two, so the completions depend upon how many words are present, not just - the partial last word). E.g. `complete_words("pr")` will return - `["ocessor", "ovincial", "oximate"]`, while `complete_words("opulent-pr")` - will return `["eclude", "efer", "eshrunk", "inter", "owler"]`. - If the wordlist is not yet known (i.e. the Deferred from `claim_nameplate` - has not yet fired), this returns an empty list. It will also return an - empty list if the prefix is complete (the last word matches something in - the completion list, and there are no longer extension words), although the - code may not yet be complete if there are additional words. The completions - will never include a hyphen: the UI frontend must supply these if desired. -* `h.submit_words(words)`: call this when the user is finished typing in the + `get_nameplate_completions()` after the response will use the new list. +* `completions = h.get_nameplate_completions(prefix)`: returns + (synchronously) a list of suffixes for the given nameplate prefix. For + example, if the server reports nameplates 1, 12, 13, 24, and 170 are in + use, `get_nameplate_completions("1")` will return `["", "2", "3", "70"]`. + Raises `AlreadyClaimedNameplateError` if called after `h.choose_nameplate`. +* `h.choose_nameplate(nameplate)`: accepts a string with the chosen nameplate. + May only be called once, after which `OnlyOneNameplateError` is raised. (in + this future, this might return a Deferred that fires (with None) when the + nameplate's wordlist is known (which happens after the nameplate is + claimed, requiring a roundtrip to the server)). +* `completions = h.get_word_completions(prefix)`: return (synchronously) a + list of suffixes for the given words prefix. The possible completions + depend upon the wordlist in use for the previously-claimed nameplate, so + calling this before `choose_nameplate` will raise + `MustClaimNameplateFirstError`. Given a prefix like "su", this returns a + list of strings which are appropriate to append to the prefix (e.g. + `["pportive", "rrender", "spicious"]`, for expansion into "supportive", + "surrender", and "suspicious". The prefix should not include the nameplate, + but *should* include whatever words and hyphens have been typed so far (the + default wordlist uses alternate lists, where even numbered words have three + syllables, and odd numbered words have two, so the completions depend upon + how many words are present, not just the partial last word). E.g. + `get_word_completions("pr")` will return `["ocessor", "ovincial", + "oximate"]`, while `get_word_completions("opulent-pr")` will return + `["eclude", "efer", "eshrunk", "inter", "owler"]`. If the wordlist is not + yet known, this returns an empty list. It will also return an empty list if + the prefix is complete (the last word matches something in the completion + list, and there are no longer extension words), although the code may not + yet be complete if there are additional words. The completions will never + include a hyphen: the UI frontend must supply these if desired. +* `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 wormhole connection process. This accepts a string like "purple-sausages", - without the nameplate. It must be called after `h.claim_nameplate()` or + without the nameplate. It must be called after `h.choose_nameplate()` or `MustClaimNameplateFirstError` will be raised. The `rlcompleter` wrapper is a function that knows how to use the code-entry diff --git a/docs/state-machines/allocator.dot b/docs/state-machines/allocator.dot index eb0b9d3..e0bd542 100644 --- a/docs/state-machines/allocator.dot +++ b/docs/state-machines/allocator.dot @@ -11,8 +11,12 @@ digraph { S0B -> P_allocate [label="allocate"] P_allocate [shape="box" label="RC.tx_allocate" color="orange"] P_allocate -> S1B [color="orange"] - S1B [label="S1B:\nallocating" color="orange"] - S1B -> S1A [label="lost"] + {rank=same; S1A P_allocate S1B} + S0B -> S1B [style="invis"] + S1B [label="S1B:\nallocating\nconnected" color="orange"] + S1B -> foo [label="lost"] + foo [style="dotted" label=""] + foo -> S1A S1A [label="S1A:\nallocating\ndisconnected" color="orange"] S1A -> P_allocate [label="connected" color="orange"] diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index d8e3380..43e770e 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -12,7 +12,7 @@ digraph { {rank=same; P0_code S0} P0_code [shape="box" style="dashed" - label="input -> Code.input\n or allocate -> Code.allocate\n or set_code -> Code.set_code"] + label="C.input_code\n or C.allocate_code\n or C.set_code"] P0_code -> S0 S0 [label="S0: empty"] S0 -> P0_build [label="set_code"] @@ -22,7 +22,7 @@ digraph { P_close_error -> S_closing S0 -> P_close_lonely [label="close"] - P0_build [shape="box" label="W.got_code\nN.set_nameplate\nK.got_code"] + P0_build [shape="box" label="W.got_code"] P0_build -> S1 S1 [label="S1: lonely" color="orange"] @@ -67,7 +67,7 @@ digraph { {rank=same; Other S_closed} 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 -> C.allocate\ninput -> C.input\nset_code -> C.set_code" + 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" ] diff --git a/docs/state-machines/code.dot b/docs/state-machines/code.dot index 35940f8..dcb66d3 100644 --- a/docs/state-machines/code.dot +++ b/docs/state-machines/code.dot @@ -1,27 +1,39 @@ digraph { - start [label="C:\nCode\nManagement" style="dotted"] + start [label="C:\nCode\n(management)" style="dotted"] {rank=same; start S0} start -> S0 [style="invis"] S0 [label="S0:\nidle"] S0 -> P0_got_code [label="set_code"] - P0_got_code [shape="box" label="B.got_code"] - P0_got_code -> S3 - S3 [label="S3: known" color="green"] + P0_got_code [shape="box" label="N.set_nameplate"] + P0_got_code -> P_done + P_done [shape="box" label="K.got_code\nB.got_code"] + P_done -> S5 + S5 [label="S5: known" color="green"] - {rank=same; S1_inputting S2_allocating} + {rank=same; S1_inputting_nameplate S3_allocating} + {rank=same; P0_got_code P1_set_nameplate P3_got_nameplate} S0 -> P_input [label="input_code"] P_input [shape="box" label="I.start"] - P_input -> S1_inputting - S1_inputting [label="S1:\ninputting"] - S1_inputting -> P0_got_code [label="finished_input"] + P_input -> S1_inputting_nameplate + S1_inputting_nameplate [label="S1:\ninputting\nnameplate"] + S1_inputting_nameplate -> P1_set_nameplate [label="got_nameplate"] + P1_set_nameplate [shape="box" label="N.set_nameplate"] + P1_set_nameplate -> S2_inputting_words + S2_inputting_words [label="S2:\ninputting\nwords"] + S2_inputting_words -> P1_got_words [label="finished_input"] + P1_got_words [shape="box" label="assemble\ncode"] + P1_got_words -> P_done + P_done S0 -> P_allocate [label="allocate_code"] P_allocate [shape="box" label="A.allocate"] - P_allocate -> S2_allocating - S2_allocating [label="S2:\nallocating"] - S2_allocating -> P1_generate [label="allocated_nameplate"] - P1_generate [shape="box" label="generate\nrandom words"] - P1_generate -> P0_got_code + P_allocate -> S3_allocating + S3_allocating [label="S3:\nallocating"] + S3_allocating -> P3_got_nameplate [label="allocated_nameplate"] + P3_got_nameplate [shape="box" label="N.set_nameplate"] + P3_got_nameplate -> P3_generate + P3_generate [shape="box" label="append\nrandom words"] + P3_generate -> P_done } diff --git a/docs/state-machines/input.dot b/docs/state-machines/input.dot index 3a68ac4..06013fc 100644 --- a/docs/state-machines/input.dot +++ b/docs/state-machines/input.dot @@ -5,28 +5,39 @@ digraph { start -> S0 [style="invis"] S0 [label="S0:\nidle"] - S0 -> P0_list_nameplates [label="start" color="orange"] - P0_list_nameplates [shape="box" label="L.refresh_nameplates" color="orange"] - P0_list_nameplates -> S1 [color="orange"] + S0 -> P0_list_nameplates [label="start"] + P0_list_nameplates [shape="box" label="L.refresh"] + P0_list_nameplates -> S1 S1 [label="S1: typing\nnameplate" color="orange"] {rank=same; foo P0_list_nameplates} - S1 -> foo [label="update_nameplates"] + S1 -> foo [label="refresh_nameplates" color="orange" fontcolor="orange"] foo [style="dashed" label=""] foo -> P0_list_nameplates - S1 -> P1_claim [label="claim_nameplate" color="orange" fontcolor="orange"] - P1_claim [shape="box" label="N.set_nameplate" color="orange"] - P1_claim -> S2 [color="orange"] - S2 [label="S2: typing\ncode\n(no wordlist)" color="orange"] - S2 -> P2_stash_wordlist [label="got_nameplates" color="orange"] - P2_stash_wordlist [shape="box" label="stash\nwordlist" color="orange"] - P2_stash_wordlist -> S3 [color="orange"] - S2 -> P_done [label="submit_words"] - S3 [label="S3: typing\ncode\n(yes wordlist)" color="orange"] - S3 -> P_done [label="submit_words" color="orange" fontcolor="orange"] - P_done [shape="box" label="C.finished_input" color="orange"] - P_done -> S4 [color="orange"] - S4 [label="S4: known" color="green"] + S1 -> P1_record [label="got_nameplates"] + P1_record [shape="box" label="record\nnameplates"] + P1_record -> S1 + S1 -> P1_claim [label="choose_nameplate" color="orange" fontcolor="orange"] + P1_claim [shape="box" label="stash nameplate\nC.got_nameplate"] + P1_claim -> S2 + S2 [label="S2: typing\ncode\n(no wordlist)"] + S2 -> S2 [label="got_nameplates"] + S2 -> P2_stash_wordlist [label="got_wordlist"] + P2_stash_wordlist [shape="box" label="stash wordlist"] + P2_stash_wordlist -> S3 + S2 -> P_done [label="choose_words" color="orange" fontcolor="orange"] + S3 [label="S3: typing\ncode\n(yes wordlist)"] + S3 -> S3 [label="got_nameplates"] + S3 -> P_done [label="choose_words" color="orange" fontcolor="orange"] + P_done [shape="box" label="build code\nC.finished_input"] + P_done -> S4 + S4 [label="S4: done" color="green"] + S4 -> S4 [label="got_nameplates\ngot_wordlist"] + + other [shape="box" style="dotted" + label="h.refresh_nameplates()\nh.get_nameplate_completions(prefix)\nh.choose_nameplate(nameplate)\nh.get_word_completions(prefix)\nh.choose_words(words)" + ] + {rank=same; S4 other} } diff --git a/docs/state-machines/lister.dot b/docs/state-machines/lister.dot index c7300e7..03ddd32 100644 --- a/docs/state-machines/lister.dot +++ b/docs/state-machines/lister.dot @@ -31,12 +31,9 @@ digraph { foo2 [label="" style="dashed"] foo2 -> P_tx - S0B -> P_notify [label="rx"] - S1B -> P_notify [label="rx" color="orange" fontcolor="orange"] - P_notify [shape="box" label="C.got_nameplates()"] + S0B -> P_notify [label="rx_nameplates"] + S1B -> P_notify [label="rx_nameplates" color="orange" fontcolor="orange"] + P_notify [shape="box" label="I.got_nameplates()"] P_notify -> S0B - {rank=same; foo foo2 legend} - legend [shape="box" style="dotted" - label="refresh: L.refresh_nameplates()\nrx: L.rx_nameplates()"] } diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index c84e9cb..015b138 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -2,8 +2,10 @@ digraph { Wormhole [shape="oval" color="blue" fontcolor="blue"] Boss [shape="box" label="Boss\n(manager)" color="blue" fontcolor="blue"] - Nameplate [shape="box" color="blue" fontcolor="blue"] - Mailbox [shape="box" color="blue" fontcolor="blue"] + Nameplate [label="Nameplate\n(claimer)" + shape="box" color="blue" fontcolor="blue"] + Mailbox [label="Mailbox\n(opener)" + shape="box" color="blue" fontcolor="blue"] Connection [label="Rendezvous\nConnector" shape="oval" color="blue" fontcolor="blue"] #websocket [color="blue" fontcolor="blue"] @@ -16,9 +18,11 @@ digraph { color="blue" fontcolor="blue"] Allocator [shape="box" label="(nameplate)\nAllocator" color="blue" fontcolor="blue"] - Input [shape="box" label="(code)\nInput helper" + Input [shape="box" label="(interactive\ncode)\nInput" color="blue" fontcolor="blue"] Terminator [shape="box" color="blue" fontcolor="blue"] + InputHelperAPI [shape="oval" label="input\nhelper\nAPI" + color="blue" fontcolor="blue"] #Connection -> websocket [color="blue"] #Connection -> Order [color="blue"] @@ -36,11 +40,8 @@ digraph { Boss -> Send [style="dashed" color="red" fontcolor="red" label="send"] - Boss -> Nameplate [style="dashed" color="red" fontcolor="red" - label="set_nameplate"] #Boss -> Mailbox [color="blue"] Mailbox -> Order [style="dashed" label="got_message (once)"] - Boss -> Key [style="dashed" label="got_code"] Key -> Boss [style="dashed" label="got_key\ngot_verifier\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] @@ -85,15 +86,18 @@ digraph { ] #Code -> Lister [color="blue"] Input -> Lister [style="dashed" color="red" fontcolor="red" - label="refresh_nameplates" + label="refresh" ] Boss -> Code [style="dashed" color="red" fontcolor="red" label="allocate_code\ninput_code\nset_code"] - Code -> Boss [style="dashed" - label="got_code"] + Code -> Boss [style="dashed" label="got_code"] + Code -> Key [style="dashed" label="got_code"] + Code -> Nameplate [style="dashed" label="set_nameplate"] Code -> Input [style="dashed" color="red" fontcolor="red" label="start"] - Input -> Code [style="dashed" label="finished_input"] + Input -> Code [style="dashed" label="got_nameplate\nfinished_input"] + InputHelperAPI -> Input [label="refresh_nameplates\nget_nameplate_completions\nchoose_nameplate\nget_word_completions\nchoose_words" color="orange" fontcolor="orange"] + Code -> Allocator [style="dashed" color="red" fontcolor="red" label="allocate"] Allocator -> Code [style="dashed" label="allocated"] diff --git a/src/wormhole/_allocator.py b/src/wormhole/_allocator.py new file mode 100644 index 0000000..f07f549 --- /dev/null +++ b/src/wormhole/_allocator.py @@ -0,0 +1,65 @@ +from __future__ import print_function, absolute_import, unicode_literals +from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides +from automat import MethodicalMachine +from . import _interfaces + +@attrs +@implementer(_interfaces.IAllocator) +class Allocator(object): + _timing = attrib(validator=provides(_interfaces.ITiming)) + m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass # pragma: no cover + + def wire(self, rendezvous_connector, code): + self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) + self._C = _interfaces.ICode(code) + + @m.state(initial=True) + def S0A_idle(self): pass # pragma: no cover + @m.state() + def S0B_idle_connected(self): pass # pragma: no cover + @m.state() + def S1A_allocating(self): pass # pragma: no cover + @m.state() + def S1B_allocating_connected(self): pass # pragma: no cover + @m.state() + def S2_done(self): pass # pragma: no cover + + # from Code + @m.input() + def allocate(self): pass + + # from RendezvousConnector + @m.input() + def connected(self): pass + @m.input() + def lost(self): pass + @m.input() + def rx_allocated(self, nameplate): pass + + @m.output() + def RC_tx_allocate(self): + self._RC.tx_allocate() + @m.output() + def C_allocated(self, nameplate): + self._C.allocated(nameplate) + + S0A_idle.upon(connected, enter=S0B_idle_connected, outputs=[]) + S0B_idle_connected.upon(lost, enter=S0A_idle, outputs=[]) + + S0A_idle.upon(allocate, enter=S1A_allocating, outputs=[]) + S0B_idle_connected.upon(allocate, enter=S1B_allocating_connected, + outputs=[RC_tx_allocate]) + + S1A_allocating.upon(connected, enter=S1B_allocating_connected, + outputs=[RC_tx_allocate]) + S1B_allocating_connected.upon(lost, enter=S1A_allocating, outputs=[]) + + S1B_allocating_connected.upon(rx_allocated, enter=S2_done, + outputs=[C_allocated]) + + S2_done.upon(connected, enter=S2_done, outputs=[]) + S2_done.upon(lost, enter=S2_done, outputs=[]) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index f6c89f7..5805dc0 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -15,6 +15,8 @@ from ._key import Key from ._receive import Receive from ._rendezvous import RendezvousConnector from ._lister import Lister +from ._allocator import Allocator +from ._input import Input from ._code import Code from ._terminator import Terminator from .errors import (ServerError, LonelyError, WrongPasswordError, @@ -49,6 +51,8 @@ class Boss(object): self._reactor, self._journal, self._tor_manager, self._timing) self._L = Lister() + self._A = Allocator(self._timing) + self._I = Input(self._timing) self._C = Code(self._timing) self._T = Terminator() @@ -59,7 +63,9 @@ class Boss(object): self._K.wire(self, self._M, self._R) self._R.wire(self, self._S) self._RC.wire(self, self._N, self._M, self._C, self._L, self._T) - self._L.wire(self._RC, self._C) + self._L.wire(self._RC, self._I) + self._A.wire(self._RC, self._C) + self._I.wire(self._C, self._L) self._C.wire(self, self._RC, self._L) self._T.wire(self, self._RC, self._N, self._M) @@ -189,8 +195,6 @@ class Boss(object): @m.output() def do_got_code(self, code): - nameplate = code.split("-")[0] - self._N.set_nameplate(nameplate) self._K.got_code(code) self._W.got_code(code) @m.output() diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 563eca4..caa1a92 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -74,13 +74,11 @@ class Code(object): @m.input() def got_wordlist(self, wordlist): pass - # from CodeInputHelper + # from Input @m.input() - def update_nameplates(self): pass + def got_nameplate(self, nameplate): pass @m.input() - def claim_nameplate(self, nameplate): pass - @m.input() - def submit_words(self, words): pass + def finished_input(self, code): pass @m.output() def L_refresh_nameplates(self): @@ -137,6 +135,7 @@ class Code(object): self._B_got_code() def _B_got_code(self): + self._N.set_nameplate(nameplate) XXX self._B.got_code(self._code) S0A_unknown.upon(connected, enter=S0B_unknown_connected, outputs=[]) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py new file mode 100644 index 0000000..0103e83 --- /dev/null +++ b/src/wormhole/_input.py @@ -0,0 +1,106 @@ +from __future__ import print_function, absolute_import, unicode_literals +from zope.interface import implementer +from attr import attrs, attrib +from attr.validators import provides +from automat import MethodicalMachine +from . import _interfaces + +@attrs +@implementer(_interfaces.IInput) +class Input(object): + _timing = attrib(validator=provides(_interfaces.ITiming)) + m = MethodicalMachine() + @m.setTrace() + def set_trace(): pass # pragma: no cover + + def __attrs_post_init__(self): + self._nameplate = None + self._wordlist = None + self._claimed_waiter = None + + def wire(self, code, lister): + self._C = _interfaces.ICode(code) + self._L = _interfaces.ILister(lister) + + @m.state(initial=True) + def S0_idle(self): pass # pragma: no cover + @m.state() + def S1_nameplate(self): pass # pragma: no cover + @m.state() + def S2_code_no_wordlist(self): pass # pragma: no cover + @m.state() + def S3_code_yes_wordlist(self): pass # pragma: no cover + @m.state(terminal=True) + def S4_done(self): pass # pragma: no cover + + # from Code + @m.input() + def start(self): pass + + # from Lister + @m.input() + def got_nameplates(self, nameplates): pass + + # from Nameplate?? + @m.input() + def got_wordlist(self, wordlist): pass + + # from CodeInputHelper + @m.input() + def refresh_nameplates(self): pass + @m.input() + def _choose_nameplate(self, nameplate): pass + @m.input() + def choose_words(self, words): pass + + @m.output() + def L_refresh_nameplates(self): + self._L.refresh_nameplates() + @m.output() + def start_and_L_refresh_nameplates(self, input_helper): + self._input_helper = input_helper + self._L.refresh_nameplates() + @m.output() + def stash_wordlist_and_notify(self, wordlist): + self._wordlist = wordlist + if self._claimed_waiter: + self._claimed_waiter.callback(None) + del self._claimed_waiter + @m.output() + def stash_nameplate(self, nameplate): + self._nameplate = nameplate + @m.output() + def C_got_nameplate(self, nameplate): + self._C.got_nameplate(nameplate) + + @m.output() + def finished(self, words): + code = self._nameplate + "-" + words + self._C.finished_input(code) + + S0_idle.upon(start, enter=S1_nameplate, outputs=[L_refresh_nameplates]) + S1_nameplate.upon(refresh_nameplates, enter=S1_nameplate, + outputs=[L_refresh_nameplates]) + S1_nameplate.upon(_choose_nameplate, enter=S2_code_no_wordlist, + outputs=[stash_nameplate, C_got_nameplate]) + S2_code_no_wordlist.upon(got_wordlist, enter=S3_code_yes_wordlist, + outputs=[stash_wordlist_and_notify]) + S2_code_no_wordlist.upon(choose_words, enter=S4_done, outputs=[finished]) + S3_code_yes_wordlist.upon(choose_words, enter=S4_done, outputs=[finished]) + + # methods for the CodeInputHelper to use + #refresh_nameplates/_choose_nameplate/choose_words: @m.input methods + + def get_nameplate_completions(self, prefix): + completions = [] + for nameplate in self._nameplates + pass + def choose_nameplate(self, nameplate): + if self._claimed_waiter is not None: + raise X + d = self._claimed_waiter = defer.Deferred() + self._choose_nameplate(nameplate) + + def get_word_completions(self, prefix): + pass + diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index 565924c..f21e0dd 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -22,6 +22,10 @@ class ILister(Interface): pass class ICode(Interface): pass +class IInput(Interface): + pass +class IAllocator(Interface): + pass class ITerminator(Interface): pass diff --git a/src/wormhole/_lister.py b/src/wormhole/_lister.py index c43a927..dbd8d4b 100644 --- a/src/wormhole/_lister.py +++ b/src/wormhole/_lister.py @@ -9,9 +9,9 @@ class Lister(object): @m.setTrace() def set_trace(): pass # pragma: no cover - def wire(self, rendezvous_connector, code): + def wire(self, rendezvous_connector, input): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) - self._C = _interfaces.ICode(code) + self._I = _interfaces.IInput(input) # Ideally, each API request would spawn a new "list_nameplates" message # to the server, so the response would be maximally fresh, but that would @@ -44,8 +44,8 @@ class Lister(object): def RC_tx_list(self): self._RC.tx_list() @m.output() - def C_got_nameplates(self, message): - self._C.got_nameplates(message["nameplates"]) + def I_got_nameplates(self, message): + self._I.got_nameplates(message["nameplates"]) S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) @@ -59,9 +59,9 @@ class Lister(object): S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, outputs=[RC_tx_list]) S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, - outputs=[C_got_nameplates]) + outputs=[I_got_nameplates]) S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, outputs=[RC_tx_list]) S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, - outputs=[C_got_nameplates]) + outputs=[I_got_nameplates]) From ae95948c177876456fc072f4ed4f8810035f94fe Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 15 Mar 2017 08:43:25 +0100 Subject: [PATCH 122/176] more tweaks --- docs/state-machines/allocator.dot | 2 +- docs/state-machines/boss.dot | 2 +- src/wormhole/_allocator.py | 6 +++--- src/wormhole/_boss.py | 5 ++--- src/wormhole/_code.py | 14 ++++---------- src/wormhole/_input.py | 2 +- src/wormhole/_rendezvous.py | 4 ++-- 7 files changed, 14 insertions(+), 21 deletions(-) diff --git a/docs/state-machines/allocator.dot b/docs/state-machines/allocator.dot index e0bd542..a9228bb 100644 --- a/docs/state-machines/allocator.dot +++ b/docs/state-machines/allocator.dot @@ -21,7 +21,7 @@ digraph { S1A -> P_allocate [label="connected" color="orange"] S1B -> P_allocated [label="rx_allocated" color="orange"] - P_allocated [shape="box" label="C.allocated" color="orange"] + P_allocated [shape="box" label="C.allocated_nameplate" color="orange"] P_allocated -> S2 [color="orange"] S2 [label="S2:\ndone" color="orange"] diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index 43e770e..e22414b 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -15,7 +15,7 @@ digraph { label="C.input_code\n or C.allocate_code\n or C.set_code"] P0_code -> S0 S0 [label="S0: empty"] - S0 -> P0_build [label="set_code"] + S0 -> P0_build [label="got_code"] S0 -> P_close_error [label="rx_error"] P_close_error [shape="box" label="T.close(errory)"] diff --git a/src/wormhole/_allocator.py b/src/wormhole/_allocator.py index f07f549..9e3f574 100644 --- a/src/wormhole/_allocator.py +++ b/src/wormhole/_allocator.py @@ -44,8 +44,8 @@ class Allocator(object): def RC_tx_allocate(self): self._RC.tx_allocate() @m.output() - def C_allocated(self, nameplate): - self._C.allocated(nameplate) + def C_allocated_nameplate(self, nameplate): + self._C.allocated_nameplate(nameplate) S0A_idle.upon(connected, enter=S0B_idle_connected, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle, outputs=[]) @@ -59,7 +59,7 @@ class Allocator(object): S1B_allocating_connected.upon(lost, enter=S1A_allocating, outputs=[]) S1B_allocating_connected.upon(rx_allocated, enter=S2_done, - outputs=[C_allocated]) + outputs=[C_allocated_nameplate]) S2_done.upon(connected, enter=S2_done, outputs=[]) S2_done.upon(lost, enter=S2_done, outputs=[]) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 5805dc0..934c298 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -62,11 +62,11 @@ class Boss(object): self._O.wire(self._K, self._R) self._K.wire(self, self._M, self._R) self._R.wire(self, self._S) - self._RC.wire(self, self._N, self._M, self._C, self._L, self._T) + self._RC.wire(self, self._N, self._M, self._A, self._L, self._T) self._L.wire(self._RC, self._I) self._A.wire(self._RC, self._C) self._I.wire(self._C, self._L) - self._C.wire(self, self._RC, self._L) + self._C.wire(self, self._A, self._N, self._K, self._I) self._T.wire(self, self._RC, self._N, self._M) self._did_start_code = False @@ -195,7 +195,6 @@ class Boss(object): @m.output() def do_got_code(self, code): - self._K.got_code(code) self._W.got_code(code) @m.output() def process_version(self, plaintext): diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index caa1a92..4ca2a12 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -30,8 +30,10 @@ class Code(object): def wire(self, boss, rendezvous_connector, lister): self._B = _interfaces.IBoss(boss) - self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) - self._L = _interfaces.ILister(lister) + self._A = _interfaces.IAllocator(allocator) + self._N = _interfaces.INameplate(nameplate) + self._K = _interfaces.IKey(key) + self._I = _interfaces.IInput(input) @m.state(initial=True) def S0A_unknown(self): pass # pragma: no cover @@ -58,14 +60,6 @@ class Code(object): @m.input() def set_code(self, code): pass - # from RendezvousConnector - @m.input() - def connected(self): pass - @m.input() - def lost(self): pass - @m.input() - def rx_allocated(self, nameplate): pass - # from Lister @m.input() def got_nameplates(self, nameplates): pass diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index 0103e83..f742b95 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -45,7 +45,7 @@ class Input(object): @m.input() def got_wordlist(self, wordlist): pass - # from CodeInputHelper + # API provided to app as ICodeInputHelper @m.input() def refresh_nameplates(self): pass @m.input() diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index ebe53f0..0568fac 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -93,11 +93,11 @@ class RendezvousConnector(object): return self._tor_manager.get_endpoint_for(hostname, port) return endpoints.HostnameEndpoint(self._reactor, hostname, port) - def wire(self, boss, nameplate, mailbox, code, lister, terminator): + def wire(self, boss, nameplate, mailbox, allocator, lister, terminator): self._B = _interfaces.IBoss(boss) self._N = _interfaces.INameplate(nameplate) self._M = _interfaces.IMailbox(mailbox) - self._C = _interfaces.ICode(code) + self._A = _interfaces.IAllocator(allocator) self._L = _interfaces.ILister(lister) self._T = _interfaces.ITerminator(terminator) From 175fef2ab49d20e5b474d33fe554aaa5b3ce9400 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sat, 18 Mar 2017 00:50:37 +0100 Subject: [PATCH 123/176] clean up wordlist handling --- docs/state-machines/allocator.dot | 6 +- docs/state-machines/code.dot | 25 ++-- docs/state-machines/input.dot | 2 +- docs/state-machines/machines.dot | 1 + docs/state-machines/nameplate.dot | 2 +- src/wormhole/_allocator.py | 23 +++- src/wormhole/_boss.py | 8 +- src/wormhole/_code.py | 170 ++++++---------------------- src/wormhole/_input.py | 79 ++++++------- src/wormhole/_interfaces.py | 7 ++ src/wormhole/_lister.py | 3 + src/wormhole/_nameplate.py | 11 +- src/wormhole/_wordlist.py | 182 ++++++++++++++++++++++++++++++ 13 files changed, 317 insertions(+), 202 deletions(-) create mode 100644 src/wormhole/_wordlist.py diff --git a/docs/state-machines/allocator.dot b/docs/state-machines/allocator.dot index a9228bb..2e2e280 100644 --- a/docs/state-machines/allocator.dot +++ b/docs/state-machines/allocator.dot @@ -7,8 +7,8 @@ digraph { S0A -> S0B [label="connected"] S0B -> S0A [label="lost"] S0B [label="S0B:\nidle\nconnected"] - S0A -> S1A [label="allocate" color="orange"] - S0B -> P_allocate [label="allocate"] + S0A -> S1A [label="allocate(length, wordlist)" color="orange"] + S0B -> P_allocate [label="allocate(length, wordlist)"] P_allocate [shape="box" label="RC.tx_allocate" color="orange"] P_allocate -> S1B [color="orange"] {rank=same; S1A P_allocate S1B} @@ -21,7 +21,7 @@ digraph { S1A -> P_allocate [label="connected" color="orange"] S1B -> P_allocated [label="rx_allocated" color="orange"] - P_allocated [shape="box" label="C.allocated_nameplate" color="orange"] + P_allocated [shape="box" label="choose words\nC.allocated(nameplate,code)" color="orange"] P_allocated -> S2 [color="orange"] S2 [label="S2:\ndone" color="orange"] diff --git a/docs/state-machines/code.dot b/docs/state-machines/code.dot index dcb66d3..078950c 100644 --- a/docs/state-machines/code.dot +++ b/docs/state-machines/code.dot @@ -4,36 +4,31 @@ digraph { {rank=same; start S0} start -> S0 [style="invis"] S0 [label="S0:\nidle"] - S0 -> P0_got_code [label="set_code"] + S0 -> P0_got_code [label="set_code\n(code)"] P0_got_code [shape="box" label="N.set_nameplate"] P0_got_code -> P_done P_done [shape="box" label="K.got_code\nB.got_code"] - P_done -> S5 - S5 [label="S5: known" color="green"] + P_done -> S4 + S4 [label="S4: known" color="green"] {rank=same; S1_inputting_nameplate S3_allocating} {rank=same; P0_got_code P1_set_nameplate P3_got_nameplate} S0 -> P_input [label="input_code"] - P_input [shape="box" label="I.start"] + P_input [shape="box" label="I.start\n(helper)"] P_input -> S1_inputting_nameplate S1_inputting_nameplate [label="S1:\ninputting\nnameplate"] - S1_inputting_nameplate -> P1_set_nameplate [label="got_nameplate"] + S1_inputting_nameplate -> P1_set_nameplate [label="got_nameplate\n(nameplate)"] P1_set_nameplate [shape="box" label="N.set_nameplate"] P1_set_nameplate -> S2_inputting_words S2_inputting_words [label="S2:\ninputting\nwords"] - S2_inputting_words -> P1_got_words [label="finished_input"] - P1_got_words [shape="box" label="assemble\ncode"] - P1_got_words -> P_done - P_done + S2_inputting_words -> P_done [label="finished_input\n(code)"] - S0 -> P_allocate [label="allocate_code"] - P_allocate [shape="box" label="A.allocate"] + S0 -> P_allocate [label="allocate_code\n(length,\nwordlist)"] + P_allocate [shape="box" label="A.allocate\n(length, wordlist)"] P_allocate -> S3_allocating S3_allocating [label="S3:\nallocating"] - S3_allocating -> P3_got_nameplate [label="allocated_nameplate"] + S3_allocating -> P3_got_nameplate [label="allocated\n(nameplate,\ncode)"] P3_got_nameplate [shape="box" label="N.set_nameplate"] - P3_got_nameplate -> P3_generate - P3_generate [shape="box" label="append\nrandom words"] - P3_generate -> P_done + P3_got_nameplate -> P_done } diff --git a/docs/state-machines/input.dot b/docs/state-machines/input.dot index 06013fc..0902988 100644 --- a/docs/state-machines/input.dot +++ b/docs/state-machines/input.dot @@ -31,7 +31,7 @@ digraph { S3 [label="S3: typing\ncode\n(yes wordlist)"] S3 -> S3 [label="got_nameplates"] S3 -> P_done [label="choose_words" color="orange" fontcolor="orange"] - P_done [shape="box" label="build code\nC.finished_input"] + P_done [shape="box" label="build code\nC.finished_input(code)"] P_done -> S4 S4 [label="S4: done" color="green"] S4 -> S4 [label="got_nameplates\ngot_wordlist"] diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index 015b138..39f7d83 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -61,6 +61,7 @@ digraph { label="connected\nlost\nrx_claimed\nrx_released"] Mailbox -> Nameplate [style="dashed" label="release"] Nameplate -> Mailbox [style="dashed" label="got_mailbox"] + Nameplate -> Input [style="dashed" label="got_wordlist"] Mailbox -> Connection [style="dashed" color="red" fontcolor="red" label="tx_open\ntx_add\ntx_close" diff --git a/docs/state-machines/nameplate.dot b/docs/state-machines/nameplate.dot index 4e6e682..8ddeabd 100644 --- a/docs/state-machines/nameplate.dot +++ b/docs/state-machines/nameplate.dot @@ -38,7 +38,7 @@ digraph { S2A -> S3A [label="(none)" style="invis"] S2B -> P_open [label="rx_claimed" color="orange" fontcolor="orange"] - P_open [shape="box" label="M.got_mailbox" color="orange"] + P_open [shape="box" label="I.got_wordlist\nM.got_mailbox" color="orange"] P_open -> S3B [color="orange"] subgraph {rank=same; S3A S3B} diff --git a/src/wormhole/_allocator.py b/src/wormhole/_allocator.py index 9e3f574..040b913 100644 --- a/src/wormhole/_allocator.py +++ b/src/wormhole/_allocator.py @@ -30,7 +30,7 @@ class Allocator(object): # from Code @m.input() - def allocate(self): pass + def allocate(self, length, wordlist): pass # from RendezvousConnector @m.input() @@ -40,26 +40,37 @@ class Allocator(object): @m.input() def rx_allocated(self, nameplate): pass + @m.output() + def stash(self, length, wordlist): + self._length = length + self._wordlist = _interfaces.IWordlist(wordlist) + @m.output() + def stash_and_RC_rx_allocate(self, length, wordlist): + self._length = length + self._wordlist = _interfaces.IWordlist(wordlist) + self._RC.tx_allocate() @m.output() def RC_tx_allocate(self): self._RC.tx_allocate() @m.output() - def C_allocated_nameplate(self, nameplate): - self._C.allocated_nameplate(nameplate) + def build_and_notify(self, nameplate): + words = self._wordlist.choose_words(self._length) + code = nameplate + "-" + words + self._C.allocated(nameplate, code) S0A_idle.upon(connected, enter=S0B_idle_connected, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle, outputs=[]) - S0A_idle.upon(allocate, enter=S1A_allocating, outputs=[]) + S0A_idle.upon(allocate, enter=S1A_allocating, outputs=[stash]) S0B_idle_connected.upon(allocate, enter=S1B_allocating_connected, - outputs=[RC_tx_allocate]) + outputs=[stash_and_RC_rx_allocate]) S1A_allocating.upon(connected, enter=S1B_allocating_connected, outputs=[RC_tx_allocate]) S1B_allocating_connected.upon(lost, enter=S1A_allocating, outputs=[]) S1B_allocating_connected.upon(rx_allocated, enter=S2_done, - outputs=[C_allocated_nameplate]) + outputs=[build_and_notify]) S2_done.upon(connected, enter=S2_done, outputs=[]) S2_done.upon(lost, enter=S2_done, outputs=[]) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 934c298..fd21b9e 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -19,6 +19,7 @@ from ._allocator import Allocator from ._input import Input from ._code import Code from ._terminator import Terminator +from ._wordlist import PGPWordList from .errors import (ServerError, LonelyError, WrongPasswordError, KeyFormatError, OnlyOneCodeError) from .util import bytes_to_dict @@ -50,13 +51,13 @@ class Boss(object): self._RC = RendezvousConnector(self._url, self._appid, self._side, self._reactor, self._journal, self._tor_manager, self._timing) - self._L = Lister() + self._L = Lister(self._timing) self._A = Allocator(self._timing) self._I = Input(self._timing) self._C = Code(self._timing) self._T = Terminator() - self._N.wire(self._M, self._RC, self._T) + self._N.wire(self._M, self._I, self._RC, self._T) self._M.wire(self._N, self._RC, self._O, self._T) self._S.wire(self._M) self._O.wire(self._K, self._R) @@ -129,7 +130,8 @@ class Boss(object): if self._did_start_code: raise OnlyOneCodeError() self._did_start_code = True - self._C.allocate_code(code_length) + wl = PGPWordList() + self._C.allocate_code(code_length, wl) def set_code(self, code): if ' ' in code: raise KeyFormatError("code (%s) contains spaces." % code) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 4ca2a12..2787e12 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -1,24 +1,9 @@ from __future__ import print_function, absolute_import, unicode_literals -import os from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides from automat import MethodicalMachine from . import _interfaces -from .wordlist import (byte_to_even_word, byte_to_odd_word, - #even_words_lowercase, odd_words_lowercase, - ) - -def make_code(nameplate, code_length): - assert isinstance(nameplate, type("")), type(nameplate) - words = [] - for i in range(code_length): - # we start with an "odd word" - if i % 2 == 0: - words.append(byte_to_odd_word[os.urandom(1)].lower()) - else: - words.append(byte_to_even_word[os.urandom(1)].lower()) - return "%s-%s" % (nameplate, "-".join(words)) @attrs @implementer(_interfaces.ICode) @@ -28,7 +13,7 @@ class Code(object): @m.setTrace() def set_trace(): pass # pragma: no cover - def wire(self, boss, rendezvous_connector, lister): + def wire(self, boss, allocator, nameplate, key, input): self._B = _interfaces.IBoss(boss) self._A = _interfaces.IAllocator(allocator) self._N = _interfaces.INameplate(nameplate) @@ -36,37 +21,27 @@ class Code(object): self._I = _interfaces.IInput(input) @m.state(initial=True) - def S0A_unknown(self): pass # pragma: no cover + def S0_idle(self): pass # pragma: no cover @m.state() - def S0B_unknown_connected(self): pass # pragma: no cover + def S1_inputting_nameplate(self): pass # pragma: no cover @m.state() - def S1A_connecting(self): pass # pragma: no cover + def S2_inputting_words(self): pass # pragma: no cover @m.state() - def S1B_allocating(self): pass # pragma: no cover + def S3_allocating(self): pass # pragma: no cover @m.state() - def S2_input_nameplate(self): pass # pragma: no cover - @m.state() - def S3_input_code_no_wordlist(self): pass # pragma: no cover - @m.state() - def S4_input_code_wordlist(self): pass # pragma: no cover - @m.state() - def S5_known(self): pass # pragma: no cover + def S4_known(self): pass # pragma: no cover # from App @m.input() - def allocate_code(self, code_length): pass + def allocate_code(self, length, wordlist): pass @m.input() def input_code(self, input_helper): pass @m.input() def set_code(self, code): pass - # from Lister + # from Allocator @m.input() - def got_nameplates(self, nameplates): pass - - # from Nameplate - @m.input() - def got_wordlist(self, wordlist): pass + def allocated(self, nameplate, code): pass # from Input @m.input() @@ -75,109 +50,38 @@ class Code(object): def finished_input(self, code): pass @m.output() - def L_refresh_nameplates(self): - self._L.refresh_nameplates() - @m.output() - def start_input_and_L_refresh_nameplates(self, input_helper): - self._input_helper = input_helper - self._L.refresh_nameplates() - @m.output() - def stash_code_length_and_RC_tx_allocate(self, code_length): - self._code_length = code_length - self._RC.tx_allocate() - @m.output() - def stash_code_length(self, code_length): - self._code_length = code_length - @m.output() - def RC_tx_allocate(self): - self._RC.tx_allocate() - @m.output() - def stash_wordlist(self, wordlist): - # TODO - pass - @m.output() - def stash_nameplates(self, nameplates): - self._known_nameplates = nameplates - pass - @m.output() - def lookup_wordlist(self): - pass - @m.output() - def do_completion_code(self): - pass - @m.output() - def record_nameplate(self, nameplate): - self._nameplate = nameplate - @m.output() - def N_set_nameplate(self, nameplate): + def do_set_code(self, code): + nameplate = code.split("-", 2)[0] self._N.set_nameplate(nameplate) + self._K.got_code(code) + self._B.got_code(code) @m.output() - def generate_and_B_got_code(self, nameplate): - self._code = make_code(nameplate, self._code_length) - self._B_got_code() + def do_start_input(self, input_helper): + self._I.start(input_helper) + @m.output() + def do_middle_input(self, nameplate): + self._N.set_nameplate(nameplate) + @m.output() + def do_finish_input(self, code): + self._K.got_code(code) + self._B.got_code(code) @m.output() - def submit_words_and_B_got_code(self, words): - assert self._nameplate - self._code = self._nameplate + "-" + words - self._B_got_code() - + def do_start_allocate(self, length, wordlist): + self._A.allocate(length, wordlist) @m.output() - def B_got_code(self, code): - self._code = code - self._B_got_code() + def do_finish_allocate(self, nameplate, code): + self._N.set_nameplate(nameplate) + self._K.got_code(code) + self._B.got_code(code) - def _B_got_code(self): - self._N.set_nameplate(nameplate) XXX - self._B.got_code(self._code) - - S0A_unknown.upon(connected, enter=S0B_unknown_connected, outputs=[]) - S0B_unknown_connected.upon(lost, enter=S0A_unknown, outputs=[]) - - S0A_unknown.upon(set_code, enter=S5_known, outputs=[B_got_code]) - S0B_unknown_connected.upon(set_code, enter=S5_known, outputs=[B_got_code]) - - S0A_unknown.upon(allocate_code, enter=S1A_connecting, - outputs=[stash_code_length]) - S0B_unknown_connected.upon(allocate_code, enter=S1B_allocating, - outputs=[stash_code_length_and_RC_tx_allocate]) - S1A_connecting.upon(connected, enter=S1B_allocating, - outputs=[RC_tx_allocate]) - S1B_allocating.upon(lost, enter=S1A_connecting, outputs=[]) - S1B_allocating.upon(rx_allocated, enter=S5_known, - outputs=[generate_and_B_got_code]) - - S0A_unknown.upon(input_code, enter=S2_input_nameplate, - outputs=[start_input_and_L_refresh_nameplates]) - S0B_unknown_connected.upon(input_code, enter=S2_input_nameplate, - outputs=[start_input_and_L_refresh_nameplates]) - S2_input_nameplate.upon(update_nameplates, enter=S2_input_nameplate, - outputs=[L_refresh_nameplates]) - S2_input_nameplate.upon(got_nameplates, - enter=S2_input_nameplate, - outputs=[stash_nameplates]) - S2_input_nameplate.upon(claim_nameplate, enter=S3_input_code_no_wordlist, - outputs=[record_nameplate, N_set_nameplate]) - S2_input_nameplate.upon(connected, enter=S2_input_nameplate, outputs=[]) - S2_input_nameplate.upon(lost, enter=S2_input_nameplate, outputs=[]) - - S3_input_code_no_wordlist.upon(got_wordlist, - enter=S4_input_code_wordlist, - outputs=[stash_wordlist]) - S3_input_code_no_wordlist.upon(submit_words, enter=S5_known, - outputs=[submit_words_and_B_got_code]) - S3_input_code_no_wordlist.upon(connected, enter=S3_input_code_no_wordlist, - outputs=[]) - S3_input_code_no_wordlist.upon(lost, enter=S3_input_code_no_wordlist, - outputs=[]) - - S4_input_code_wordlist.upon(submit_words, enter=S5_known, - outputs=[submit_words_and_B_got_code]) - S4_input_code_wordlist.upon(connected, enter=S4_input_code_wordlist, - outputs=[]) - S4_input_code_wordlist.upon(lost, enter=S4_input_code_wordlist, - outputs=[]) - - S5_known.upon(connected, enter=S5_known, outputs=[]) - S5_known.upon(lost, enter=S5_known, outputs=[]) + S0_idle.upon(set_code, enter=S4_known, outputs=[do_set_code]) + S0_idle.upon(input_code, enter=S1_inputting_nameplate, + outputs=[do_start_input]) + S1_inputting_nameplate.upon(got_nameplate, enter=S2_inputting_words, + outputs=[do_middle_input]) + S2_inputting_words.upon(finished_input, enter=S4_known, + outputs=[do_finish_input]) + S0_idle.upon(allocate_code, enter=S3_allocating, outputs=[do_start_allocate]) + S3_allocating.upon(allocated, enter=S4_known, outputs=[do_finish_allocate]) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index f742b95..858ad9a 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -16,7 +16,6 @@ class Input(object): def __attrs_post_init__(self): self._nameplate = None self._wordlist = None - self._claimed_waiter = None def wire(self, code, lister): self._C = _interfaces.ICode(code) @@ -25,23 +24,23 @@ class Input(object): @m.state(initial=True) def S0_idle(self): pass # pragma: no cover @m.state() - def S1_nameplate(self): pass # pragma: no cover + def S1_typing_nameplate(self): pass # pragma: no cover @m.state() - def S2_code_no_wordlist(self): pass # pragma: no cover + def S2_typing_code_no_wordlist(self): pass # pragma: no cover @m.state() - def S3_code_yes_wordlist(self): pass # pragma: no cover + def S3_typing_code_yes_wordlist(self): pass # pragma: no cover @m.state(terminal=True) def S4_done(self): pass # pragma: no cover # from Code @m.input() - def start(self): pass + def start(self, input_helper): pass # from Lister @m.input() def got_nameplates(self, nameplates): pass - # from Nameplate?? + # from Nameplate @m.input() def got_wordlist(self, wordlist): pass @@ -49,58 +48,62 @@ class Input(object): @m.input() def refresh_nameplates(self): pass @m.input() - def _choose_nameplate(self, nameplate): pass + def choose_nameplate(self, nameplate): pass @m.input() def choose_words(self, words): pass @m.output() - def L_refresh_nameplates(self): - self._L.refresh_nameplates() - @m.output() - def start_and_L_refresh_nameplates(self, input_helper): + def do_start(self, input_helper): self._input_helper = input_helper self._L.refresh_nameplates() @m.output() - def stash_wordlist_and_notify(self, wordlist): - self._wordlist = wordlist - if self._claimed_waiter: - self._claimed_waiter.callback(None) - del self._claimed_waiter + def do_refresh(self): + self._L.refresh_nameplates() @m.output() - def stash_nameplate(self, nameplate): + def do_nameplate(self, nameplate): self._nameplate = nameplate - @m.output() - def C_got_nameplate(self, nameplate): self._C.got_nameplate(nameplate) + @m.output() + def do_wordlist(self, wordlist): + self._wordlist = wordlist @m.output() - def finished(self, words): + def do_words(self, words): code = self._nameplate + "-" + words self._C.finished_input(code) - S0_idle.upon(start, enter=S1_nameplate, outputs=[L_refresh_nameplates]) - S1_nameplate.upon(refresh_nameplates, enter=S1_nameplate, - outputs=[L_refresh_nameplates]) - S1_nameplate.upon(_choose_nameplate, enter=S2_code_no_wordlist, - outputs=[stash_nameplate, C_got_nameplate]) - S2_code_no_wordlist.upon(got_wordlist, enter=S3_code_yes_wordlist, - outputs=[stash_wordlist_and_notify]) - S2_code_no_wordlist.upon(choose_words, enter=S4_done, outputs=[finished]) - S3_code_yes_wordlist.upon(choose_words, enter=S4_done, outputs=[finished]) + S0_idle.upon(start, enter=S1_typing_nameplate, outputs=[do_start]) + S1_typing_nameplate.upon(refresh_nameplates, enter=S1_typing_nameplate, + outputs=[do_refresh]) + S1_typing_nameplate.upon(choose_nameplate, enter=S2_typing_code_no_wordlist, + outputs=[do_nameplate]) + S2_typing_code_no_wordlist.upon(got_wordlist, + enter=S3_typing_code_yes_wordlist, + outputs=[do_wordlist]) + S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, + outputs=[do_words]) + S2_typing_code_no_wordlist.upon(got_nameplates, + enter=S2_typing_code_no_wordlist, outputs=[]) + S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, + outputs=[do_words]) + S3_typing_code_yes_wordlist.upon(got_nameplates, + enter=S3_typing_code_yes_wordlist, + outputs=[]) + S4_done.upon(got_nameplates, enter=S4_done, outputs=[]) + S4_done.upon(got_wordlist, enter=S4_done, outputs=[]) # methods for the CodeInputHelper to use #refresh_nameplates/_choose_nameplate/choose_words: @m.input methods def get_nameplate_completions(self, prefix): + lp = len(prefix) completions = [] - for nameplate in self._nameplates - pass - def choose_nameplate(self, nameplate): - if self._claimed_waiter is not None: - raise X - d = self._claimed_waiter = defer.Deferred() - self._choose_nameplate(nameplate) + for nameplate in self._nameplates: + if nameplate.startswith(prefix): + completions.append(nameplate[lp:]) + return completions def get_word_completions(self, prefix): - pass - + if self._wordlist: + return self._wordlist.get_completions(prefix) + return [] diff --git a/src/wormhole/_interfaces.py b/src/wormhole/_interfaces.py index f21e0dd..11b562a 100644 --- a/src/wormhole/_interfaces.py +++ b/src/wormhole/_interfaces.py @@ -33,6 +33,13 @@ class ITiming(Interface): pass class ITorManager(Interface): pass +class IWordlist(Interface): + def choose_words(length): + """Randomly select LENGTH words, join them with hyphens, return the + result.""" + def get_completions(prefix): + """Return a list of all suffixes that could complete the given + prefix.""" class IJournal(Interface): # TODO: this needs to be public pass diff --git a/src/wormhole/_lister.py b/src/wormhole/_lister.py index dbd8d4b..8b49d91 100644 --- a/src/wormhole/_lister.py +++ b/src/wormhole/_lister.py @@ -1,10 +1,13 @@ from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer +from attr import attrib +from attr.validators import provides from automat import MethodicalMachine from . import _interfaces @implementer(_interfaces.ILister) class Lister(object): + _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() @m.setTrace() def set_trace(): pass # pragma: no cover diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 777098d..599573b 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -2,6 +2,7 @@ from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from automat import MethodicalMachine from . import _interfaces +from ._wordlist import PGPWordList @implementer(_interfaces.INameplate) class Nameplate(object): @@ -12,8 +13,9 @@ class Nameplate(object): def __init__(self): self._nameplate = None - def wire(self, mailbox, rendezvous_connector, terminator): + def wire(self, mailbox, input, rendezvous_connector, terminator): self._M = _interfaces.IMailbox(mailbox) + self._I = _interfaces.IInput(input) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._T = _interfaces.ITerminator(terminator) @@ -96,6 +98,11 @@ class Nameplate(object): # when invoked via M.connected(), we must use the stored nameplate self._RC.tx_claim(self._nameplate) @m.output() + def I_got_wordlist(self, mailbox): + # TODO select wordlist based on nameplate properties, in rx_claimed + wordlist = PGPWordList() + self._I.got_wordlist(wordlist) + @m.output() def M_got_mailbox(self, mailbox): self._M.got_mailbox(mailbox) @m.output() @@ -120,7 +127,7 @@ class Nameplate(object): S2A.upon(connected, enter=S2B, outputs=[RC_tx_claim]) S2A.upon(close, enter=S4A, outputs=[]) S2B.upon(lost, enter=S2A, outputs=[]) - S2B.upon(rx_claimed, enter=S3B, outputs=[M_got_mailbox]) + S2B.upon(rx_claimed, enter=S3B, outputs=[I_got_wordlist, M_got_mailbox]) S2B.upon(close, enter=S4B, outputs=[RC_tx_release]) S3A.upon(connected, enter=S3B, outputs=[]) diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py new file mode 100644 index 0000000..6c266a6 --- /dev/null +++ b/src/wormhole/_wordlist.py @@ -0,0 +1,182 @@ +from __future__ import unicode_literals +import os + +# The PGP Word List, which maps bytes to phonetically-distinct words. There +# are two lists, even and odd, and encodings should alternate between then to +# detect dropped words. https://en.wikipedia.org/wiki/PGP_Words + +# Thanks to Warren Guy for transcribing them: +# https://github.com/warrenguy/javascript-pgp-word-list + +from binascii import unhexlify + +raw_words = { +'00': ['aardvark', 'adroitness'], '01': ['absurd', 'adviser'], +'02': ['accrue', 'aftermath'], '03': ['acme', 'aggregate'], +'04': ['adrift', 'alkali'], '05': ['adult', 'almighty'], +'06': ['afflict', 'amulet'], '07': ['ahead', 'amusement'], +'08': ['aimless', 'antenna'], '09': ['Algol', 'applicant'], +'0A': ['allow', 'Apollo'], '0B': ['alone', 'armistice'], +'0C': ['ammo', 'article'], '0D': ['ancient', 'asteroid'], +'0E': ['apple', 'Atlantic'], '0F': ['artist', 'atmosphere'], +'10': ['assume', 'autopsy'], '11': ['Athens', 'Babylon'], +'12': ['atlas', 'backwater'], '13': ['Aztec', 'barbecue'], +'14': ['baboon', 'belowground'], '15': ['backfield', 'bifocals'], +'16': ['backward', 'bodyguard'], '17': ['banjo', 'bookseller'], +'18': ['beaming', 'borderline'], '19': ['bedlamp', 'bottomless'], +'1A': ['beehive', 'Bradbury'], '1B': ['beeswax', 'bravado'], +'1C': ['befriend', 'Brazilian'], '1D': ['Belfast', 'breakaway'], +'1E': ['berserk', 'Burlington'], '1F': ['billiard', 'businessman'], +'20': ['bison', 'butterfat'], '21': ['blackjack', 'Camelot'], +'22': ['blockade', 'candidate'], '23': ['blowtorch', 'cannonball'], +'24': ['bluebird', 'Capricorn'], '25': ['bombast', 'caravan'], +'26': ['bookshelf', 'caretaker'], '27': ['brackish', 'celebrate'], +'28': ['breadline', 'cellulose'], '29': ['breakup', 'certify'], +'2A': ['brickyard', 'chambermaid'], '2B': ['briefcase', 'Cherokee'], +'2C': ['Burbank', 'Chicago'], '2D': ['button', 'clergyman'], +'2E': ['buzzard', 'coherence'], '2F': ['cement', 'combustion'], +'30': ['chairlift', 'commando'], '31': ['chatter', 'company'], +'32': ['checkup', 'component'], '33': ['chisel', 'concurrent'], +'34': ['choking', 'confidence'], '35': ['chopper', 'conformist'], +'36': ['Christmas', 'congregate'], '37': ['clamshell', 'consensus'], +'38': ['classic', 'consulting'], '39': ['classroom', 'corporate'], +'3A': ['cleanup', 'corrosion'], '3B': ['clockwork', 'councilman'], +'3C': ['cobra', 'crossover'], '3D': ['commence', 'crucifix'], +'3E': ['concert', 'cumbersome'], '3F': ['cowbell', 'customer'], +'40': ['crackdown', 'Dakota'], '41': ['cranky', 'decadence'], +'42': ['crowfoot', 'December'], '43': ['crucial', 'decimal'], +'44': ['crumpled', 'designing'], '45': ['crusade', 'detector'], +'46': ['cubic', 'detergent'], '47': ['dashboard', 'determine'], +'48': ['deadbolt', 'dictator'], '49': ['deckhand', 'dinosaur'], +'4A': ['dogsled', 'direction'], '4B': ['dragnet', 'disable'], +'4C': ['drainage', 'disbelief'], '4D': ['dreadful', 'disruptive'], +'4E': ['drifter', 'distortion'], '4F': ['dropper', 'document'], +'50': ['drumbeat', 'embezzle'], '51': ['drunken', 'enchanting'], +'52': ['Dupont', 'enrollment'], '53': ['dwelling', 'enterprise'], +'54': ['eating', 'equation'], '55': ['edict', 'equipment'], +'56': ['egghead', 'escapade'], '57': ['eightball', 'Eskimo'], +'58': ['endorse', 'everyday'], '59': ['endow', 'examine'], +'5A': ['enlist', 'existence'], '5B': ['erase', 'exodus'], +'5C': ['escape', 'fascinate'], '5D': ['exceed', 'filament'], +'5E': ['eyeglass', 'finicky'], '5F': ['eyetooth', 'forever'], +'60': ['facial', 'fortitude'], '61': ['fallout', 'frequency'], +'62': ['flagpole', 'gadgetry'], '63': ['flatfoot', 'Galveston'], +'64': ['flytrap', 'getaway'], '65': ['fracture', 'glossary'], +'66': ['framework', 'gossamer'], '67': ['freedom', 'graduate'], +'68': ['frighten', 'gravity'], '69': ['gazelle', 'guitarist'], +'6A': ['Geiger', 'hamburger'], '6B': ['glitter', 'Hamilton'], +'6C': ['glucose', 'handiwork'], '6D': ['goggles', 'hazardous'], +'6E': ['goldfish', 'headwaters'], '6F': ['gremlin', 'hemisphere'], +'70': ['guidance', 'hesitate'], '71': ['hamlet', 'hideaway'], +'72': ['highchair', 'holiness'], '73': ['hockey', 'hurricane'], +'74': ['indoors', 'hydraulic'], '75': ['indulge', 'impartial'], +'76': ['inverse', 'impetus'], '77': ['involve', 'inception'], +'78': ['island', 'indigo'], '79': ['jawbone', 'inertia'], +'7A': ['keyboard', 'infancy'], '7B': ['kickoff', 'inferno'], +'7C': ['kiwi', 'informant'], '7D': ['klaxon', 'insincere'], +'7E': ['locale', 'insurgent'], '7F': ['lockup', 'integrate'], +'80': ['merit', 'intention'], '81': ['minnow', 'inventive'], +'82': ['miser', 'Istanbul'], '83': ['Mohawk', 'Jamaica'], +'84': ['mural', 'Jupiter'], '85': ['music', 'leprosy'], +'86': ['necklace', 'letterhead'], '87': ['Neptune', 'liberty'], +'88': ['newborn', 'maritime'], '89': ['nightbird', 'matchmaker'], +'8A': ['Oakland', 'maverick'], '8B': ['obtuse', 'Medusa'], +'8C': ['offload', 'megaton'], '8D': ['optic', 'microscope'], +'8E': ['orca', 'microwave'], '8F': ['payday', 'midsummer'], +'90': ['peachy', 'millionaire'], '91': ['pheasant', 'miracle'], +'92': ['physique', 'misnomer'], '93': ['playhouse', 'molasses'], +'94': ['Pluto', 'molecule'], '95': ['preclude', 'Montana'], +'96': ['prefer', 'monument'], '97': ['preshrunk', 'mosquito'], +'98': ['printer', 'narrative'], '99': ['prowler', 'nebula'], +'9A': ['pupil', 'newsletter'], '9B': ['puppy', 'Norwegian'], +'9C': ['python', 'October'], '9D': ['quadrant', 'Ohio'], +'9E': ['quiver', 'onlooker'], '9F': ['quota', 'opulent'], +'A0': ['ragtime', 'Orlando'], 'A1': ['ratchet', 'outfielder'], +'A2': ['rebirth', 'Pacific'], 'A3': ['reform', 'pandemic'], +'A4': ['regain', 'Pandora'], 'A5': ['reindeer', 'paperweight'], +'A6': ['rematch', 'paragon'], 'A7': ['repay', 'paragraph'], +'A8': ['retouch', 'paramount'], 'A9': ['revenge', 'passenger'], +'AA': ['reward', 'pedigree'], 'AB': ['rhythm', 'Pegasus'], +'AC': ['ribcage', 'penetrate'], 'AD': ['ringbolt', 'perceptive'], +'AE': ['robust', 'performance'], 'AF': ['rocker', 'pharmacy'], +'B0': ['ruffled', 'phonetic'], 'B1': ['sailboat', 'photograph'], +'B2': ['sawdust', 'pioneer'], 'B3': ['scallion', 'pocketful'], +'B4': ['scenic', 'politeness'], 'B5': ['scorecard', 'positive'], +'B6': ['Scotland', 'potato'], 'B7': ['seabird', 'processor'], +'B8': ['select', 'provincial'], 'B9': ['sentence', 'proximate'], +'BA': ['shadow', 'puberty'], 'BB': ['shamrock', 'publisher'], +'BC': ['showgirl', 'pyramid'], 'BD': ['skullcap', 'quantity'], +'BE': ['skydive', 'racketeer'], 'BF': ['slingshot', 'rebellion'], +'C0': ['slowdown', 'recipe'], 'C1': ['snapline', 'recover'], +'C2': ['snapshot', 'repellent'], 'C3': ['snowcap', 'replica'], +'C4': ['snowslide', 'reproduce'], 'C5': ['solo', 'resistor'], +'C6': ['southward', 'responsive'], 'C7': ['soybean', 'retraction'], +'C8': ['spaniel', 'retrieval'], 'C9': ['spearhead', 'retrospect'], +'CA': ['spellbind', 'revenue'], 'CB': ['spheroid', 'revival'], +'CC': ['spigot', 'revolver'], 'CD': ['spindle', 'sandalwood'], +'CE': ['spyglass', 'sardonic'], 'CF': ['stagehand', 'Saturday'], +'D0': ['stagnate', 'savagery'], 'D1': ['stairway', 'scavenger'], +'D2': ['standard', 'sensation'], 'D3': ['stapler', 'sociable'], +'D4': ['steamship', 'souvenir'], 'D5': ['sterling', 'specialist'], +'D6': ['stockman', 'speculate'], 'D7': ['stopwatch', 'stethoscope'], +'D8': ['stormy', 'stupendous'], 'D9': ['sugar', 'supportive'], +'DA': ['surmount', 'surrender'], 'DB': ['suspense', 'suspicious'], +'DC': ['sweatband', 'sympathy'], 'DD': ['swelter', 'tambourine'], +'DE': ['tactics', 'telephone'], 'DF': ['talon', 'therapist'], +'E0': ['tapeworm', 'tobacco'], 'E1': ['tempest', 'tolerance'], +'E2': ['tiger', 'tomorrow'], 'E3': ['tissue', 'torpedo'], +'E4': ['tonic', 'tradition'], 'E5': ['topmost', 'travesty'], +'E6': ['tracker', 'trombonist'], 'E7': ['transit', 'truncated'], +'E8': ['trauma', 'typewriter'], 'E9': ['treadmill', 'ultimate'], +'EA': ['Trojan', 'undaunted'], 'EB': ['trouble', 'underfoot'], +'EC': ['tumor', 'unicorn'], 'ED': ['tunnel', 'unify'], +'EE': ['tycoon', 'universe'], 'EF': ['uncut', 'unravel'], +'F0': ['unearth', 'upcoming'], 'F1': ['unwind', 'vacancy'], +'F2': ['uproot', 'vagabond'], 'F3': ['upset', 'vertigo'], +'F4': ['upshot', 'Virginia'], 'F5': ['vapor', 'visitor'], +'F6': ['village', 'vocalist'], 'F7': ['virus', 'voyager'], +'F8': ['Vulcan', 'warranty'], 'F9': ['waffle', 'Waterloo'], +'FA': ['wallet', 'whimsical'], 'FB': ['watchword', 'Wichita'], +'FC': ['wayside', 'Wilmington'], 'FD': ['willow', 'Wyoming'], +'FE': ['woodlark', 'yesteryear'], 'FF': ['Zulu', 'Yucatan'] +}; + +byte_to_even_word = dict([(unhexlify(k.encode("ascii")), both_words[0]) + for k,both_words + in raw_words.items()]) + +byte_to_odd_word = dict([(unhexlify(k.encode("ascii")), both_words[1]) + for k,both_words + in raw_words.items()]) + +even_words_lowercase, odd_words_lowercase = set(), set() + +for k,both_words in raw_words.items(): + even_word, odd_word = both_words + even_words_lowercase.add(even_word.lower()) + odd_words_lowercase.add(odd_word.lower()) + +class PGPWordList(object): + def get_completions(self, prefix): + # start with the odd words + if prefix.count("-") % 2 == 0: + words = odd_words_lowercase + else: + words = even_words_lowercase + last_partial_word = prefix.split("-")[-1] + lp = len(last_partial_word) + completions = [] + for word in words: + if word.startswith(prefix): + completions.append(word[lp:]) + return completions + + def choose_words(self, length): + words = [] + for i in range(length): + # we start with an "odd word" + if i % 2 == 0: + words.append(byte_to_odd_word[os.urandom(1)].lower()) + else: + words.append(byte_to_even_word[os.urandom(1)].lower()) + return "-".join(words) From 3873f55d64b9ba41547acf4e3257baec721a5990 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 17:35:05 +0100 Subject: [PATCH 124/176] make Input tests pass, clarify error cases, cleanups --- docs/api.md | 53 ++--- docs/state-machines/input.dot | 2 +- src/wormhole/_code.py | 7 +- src/wormhole/_input.py | 165 +++++++++++++--- src/wormhole/_lister.py | 22 ++- src/wormhole/_rendezvous.py | 10 +- src/wormhole/_wordlist.py | 4 +- src/wormhole/errors.py | 10 + src/wormhole/test/test_machines.py | 301 +++++++++++++++++++++++++---- 9 files changed, 461 insertions(+), 113 deletions(-) diff --git a/docs/api.md b/docs/api.md index 6417406..656a94f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -181,42 +181,49 @@ The code-entry Helper object has the following API: "4" in "4-purple-sausages"). Note that they are unicode strings (so "4", not 4). The Helper will get the response in the background, and calls to `get_nameplate_completions()` after the response will use the new list. + Calling this after `h.choose_nameplate` will raise + `AlreadyChoseNameplateError`. * `completions = h.get_nameplate_completions(prefix)`: returns - (synchronously) a list of suffixes for the given nameplate prefix. For + (synchronously) a set of suffixes for the given nameplate prefix. For example, if the server reports nameplates 1, 12, 13, 24, and 170 are in - use, `get_nameplate_completions("1")` will return `["", "2", "3", "70"]`. - Raises `AlreadyClaimedNameplateError` if called after `h.choose_nameplate`. -* `h.choose_nameplate(nameplate)`: accepts a string with the chosen nameplate. - May only be called once, after which `OnlyOneNameplateError` is raised. (in - this future, this might return a Deferred that fires (with None) when the - nameplate's wordlist is known (which happens after the nameplate is - claimed, requiring a roundtrip to the server)). + use, `get_nameplate_completions("1")` will return `{"", "2", "3", "70"}`. + You may want to sort these before displaying them to the user. Raises + `AlreadyChoseNameplateError` if called after `h.choose_nameplate`. +* `h.choose_nameplate(nameplate)`: accepts a string with the chosen + nameplate. May only be called once, after which + `AlreadyChoseNameplateError` is raised. (in this future, this might + return a Deferred that fires (with None) when the nameplate's wordlist is + known (which happens after the nameplate is claimed, requiring a roundtrip + to the server)). * `completions = h.get_word_completions(prefix)`: return (synchronously) a - list of suffixes for the given words prefix. The possible completions - depend upon the wordlist in use for the previously-claimed nameplate, so - calling this before `choose_nameplate` will raise - `MustClaimNameplateFirstError`. Given a prefix like "su", this returns a - list of strings which are appropriate to append to the prefix (e.g. - `["pportive", "rrender", "spicious"]`, for expansion into "supportive", - "surrender", and "suspicious". The prefix should not include the nameplate, - but *should* include whatever words and hyphens have been typed so far (the - default wordlist uses alternate lists, where even numbered words have three + set of suffixes for the given words prefix. The possible completions depend + upon the wordlist in use for the previously-claimed nameplate, so calling + this before `choose_nameplate` will raise `MustChooseNameplateFirstError`. + Calling this after `h.choose_words()` will raise `AlreadyChoseWordsError`. + Given a prefix like "su", this returns a set of strings which are + appropriate to append to the prefix (e.g. `{"pportive", "rrender", + "spicious"}`, for expansion into "supportive", "surrender", and + "suspicious". The prefix should not include the nameplate, but *should* + include whatever words and hyphens have been typed so far (the default + wordlist uses alternate lists, where even numbered words have three syllables, and odd numbered words have two, so the completions depend upon how many words are present, not just the partial last word). E.g. - `get_word_completions("pr")` will return `["ocessor", "ovincial", - "oximate"]`, while `get_word_completions("opulent-pr")` will return - `["eclude", "efer", "eshrunk", "inter", "owler"]`. If the wordlist is not - yet known, this returns an empty list. It will also return an empty list if + `get_word_completions("pr")` will return `{"ocessor", "ovincial", + "oximate"}`, while `get_word_completions("opulent-pr")` will return + `{"eclude", "efer", "eshrunk", "inter", "owler"}`. If the wordlist is not + yet known, this returns an empty set. It will also return an empty set if the prefix is complete (the last word matches something in the completion list, and there are no longer extension words), although the code may not yet be complete if there are additional words. The completions will never - include a hyphen: the UI frontend must supply these if desired. + include a hyphen: the UI frontend must supply these if desired. The + frontend is also responsible for sorting the results 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 wormhole connection process. This accepts a string like "purple-sausages", without the nameplate. It must be called after `h.choose_nameplate()` or - `MustClaimNameplateFirstError` will be raised. + `MustChooseNameplateFirstError` will be raised. May only be called once, + after which `AlreadyChoseWordsError` is raised. The `rlcompleter` wrapper is a function that knows how to use the code-entry helper to do tab completion of wormhole codes: diff --git a/docs/state-machines/input.dot b/docs/state-machines/input.dot index 0902988..580d2b9 100644 --- a/docs/state-machines/input.dot +++ b/docs/state-machines/input.dot @@ -36,7 +36,7 @@ digraph { S4 [label="S4: done" color="green"] S4 -> S4 [label="got_nameplates\ngot_wordlist"] - other [shape="box" style="dotted" + other [shape="box" style="dotted" color="orange" fontcolor="orange" label="h.refresh_nameplates()\nh.get_nameplate_completions(prefix)\nh.choose_nameplate(nameplate)\nh.get_word_completions(prefix)\nh.choose_words(words)" ] {rank=same; S4 other} diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 2787e12..d19a9ee 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -35,7 +35,7 @@ class Code(object): @m.input() def allocate_code(self, length, wordlist): pass @m.input() - def input_code(self, input_helper): pass + def input_code(self): pass @m.input() def set_code(self, code): pass @@ -57,8 +57,8 @@ class Code(object): self._B.got_code(code) @m.output() - def do_start_input(self, input_helper): - self._I.start(input_helper) + def do_start_input(self): + return self._I.start() @m.output() def do_middle_input(self, nameplate): self._N.set_nameplate(nameplate) @@ -72,6 +72,7 @@ class Code(object): self._A.allocate(length, wordlist) @m.output() def do_finish_allocate(self, nameplate, code): + assert code.startswith(nameplate+"-"), (nameplate, code) self._N.set_nameplate(nameplate) self._K.got_code(code) self._B.got_code(code) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index 858ad9a..bd16a43 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -3,7 +3,10 @@ from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides from automat import MethodicalMachine -from . import _interfaces +from . import _interfaces, errors + +def first(outputs): + return list(outputs)[0] @attrs @implementer(_interfaces.IInput) @@ -14,6 +17,7 @@ class Input(object): def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): + self._all_nameplates = set() self._nameplate = None self._wordlist = None @@ -34,11 +38,11 @@ class Input(object): # from Code @m.input() - def start(self, input_helper): pass + def start(self): pass # from Lister @m.input() - def got_nameplates(self, nameplates): pass + def got_nameplates(self, all_nameplates): pass # from Nameplate @m.input() @@ -48,62 +52,163 @@ class Input(object): @m.input() def refresh_nameplates(self): pass @m.input() + def get_nameplate_completions(self, prefix): pass + @m.input() def choose_nameplate(self, nameplate): pass @m.input() + def get_word_completions(self, prefix): pass + @m.input() def choose_words(self, words): pass @m.output() - def do_start(self, input_helper): - self._input_helper = input_helper - self._L.refresh_nameplates() + def do_start(self): + self._L.refresh() + return Helper(self) @m.output() def do_refresh(self): - self._L.refresh_nameplates() + self._L.refresh() @m.output() - def do_nameplate(self, nameplate): + def record_nameplates(self, all_nameplates): + # we get a set of nameplate id strings + self._all_nameplates = all_nameplates + @m.output() + def _get_nameplate_completions(self, prefix): + lp = len(prefix) + completions = set() + for nameplate in self._all_nameplates: + if nameplate.startswith(prefix): + completions.add(nameplate[lp:]) + return completions + @m.output() + def record_all_nameplates(self, nameplate): self._nameplate = nameplate self._C.got_nameplate(nameplate) @m.output() - def do_wordlist(self, wordlist): + def record_wordlist(self, wordlist): self._wordlist = wordlist + @m.output() + def no_word_completions(self, prefix): + return set() + @m.output() + def _get_word_completions(self, prefix): + assert self._wordlist + return self._wordlist.get_completions(prefix) + + @m.output() + def raise_must_choose_nameplate1(self, prefix): + raise errors.MustChooseNameplateFirstError() + @m.output() + def raise_must_choose_nameplate2(self, words): + raise errors.MustChooseNameplateFirstError() + @m.output() + def raise_already_chose_nameplate1(self): + raise errors.AlreadyChoseNameplateError() + @m.output() + def raise_already_chose_nameplate2(self, prefix): + raise errors.AlreadyChoseNameplateError() + @m.output() + def raise_already_chose_nameplate3(self, nameplate): + raise errors.AlreadyChoseNameplateError() + @m.output() + def raise_already_chose_words1(self, prefix): + raise errors.AlreadyChoseWordsError() + @m.output() + def raise_already_chose_words2(self, words): + raise errors.AlreadyChoseWordsError() + @m.output() def do_words(self, words): code = self._nameplate + "-" + words self._C.finished_input(code) - S0_idle.upon(start, enter=S1_typing_nameplate, outputs=[do_start]) + S0_idle.upon(start, enter=S1_typing_nameplate, + outputs=[do_start], collector=first) + S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate, + outputs=[record_nameplates]) + # too early for got_wordlist, should never happen S1_typing_nameplate.upon(refresh_nameplates, enter=S1_typing_nameplate, outputs=[do_refresh]) + S1_typing_nameplate.upon(get_nameplate_completions, + enter=S1_typing_nameplate, + outputs=[_get_nameplate_completions], + collector=first) S1_typing_nameplate.upon(choose_nameplate, enter=S2_typing_code_no_wordlist, - outputs=[do_nameplate]) - S2_typing_code_no_wordlist.upon(got_wordlist, - enter=S3_typing_code_yes_wordlist, - outputs=[do_wordlist]) - S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, - outputs=[do_words]) + outputs=[record_all_nameplates]) + S1_typing_nameplate.upon(get_word_completions, + enter=S1_typing_nameplate, + outputs=[raise_must_choose_nameplate1]) + S1_typing_nameplate.upon(choose_words, enter=S1_typing_nameplate, + outputs=[raise_must_choose_nameplate2]) + S2_typing_code_no_wordlist.upon(got_nameplates, enter=S2_typing_code_no_wordlist, outputs=[]) - S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, - outputs=[do_words]) + S2_typing_code_no_wordlist.upon(got_wordlist, + enter=S3_typing_code_yes_wordlist, + outputs=[record_wordlist]) + S2_typing_code_no_wordlist.upon(refresh_nameplates, + enter=S2_typing_code_no_wordlist, + outputs=[raise_already_chose_nameplate1]) + S2_typing_code_no_wordlist.upon(get_nameplate_completions, + enter=S2_typing_code_no_wordlist, + outputs=[raise_already_chose_nameplate2]) + S2_typing_code_no_wordlist.upon(choose_nameplate, + enter=S2_typing_code_no_wordlist, + outputs=[raise_already_chose_nameplate3]) + S2_typing_code_no_wordlist.upon(get_word_completions, + enter=S2_typing_code_no_wordlist, + outputs=[no_word_completions], + collector=first) + S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, + outputs=[do_words]) + S3_typing_code_yes_wordlist.upon(got_nameplates, enter=S3_typing_code_yes_wordlist, outputs=[]) + # got_wordlist: should never happen + S3_typing_code_yes_wordlist.upon(refresh_nameplates, + enter=S3_typing_code_yes_wordlist, + outputs=[raise_already_chose_nameplate1]) + S3_typing_code_yes_wordlist.upon(get_nameplate_completions, + enter=S3_typing_code_yes_wordlist, + outputs=[raise_already_chose_nameplate2]) + S3_typing_code_yes_wordlist.upon(choose_nameplate, + enter=S3_typing_code_yes_wordlist, + outputs=[raise_already_chose_nameplate3]) + S3_typing_code_yes_wordlist.upon(get_word_completions, + enter=S3_typing_code_yes_wordlist, + outputs=[_get_word_completions], + collector=first) + S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, + outputs=[do_words]) + S4_done.upon(got_nameplates, enter=S4_done, outputs=[]) S4_done.upon(got_wordlist, enter=S4_done, outputs=[]) + S4_done.upon(refresh_nameplates, + enter=S4_done, + outputs=[raise_already_chose_nameplate1]) + S4_done.upon(get_nameplate_completions, + enter=S4_done, + outputs=[raise_already_chose_nameplate2]) + S4_done.upon(choose_nameplate, enter=S4_done, + outputs=[raise_already_chose_nameplate3]) + S4_done.upon(get_word_completions, enter=S4_done, + outputs=[raise_already_chose_words1]) + S4_done.upon(choose_words, enter=S4_done, + outputs=[raise_already_chose_words2]) - # methods for the CodeInputHelper to use - #refresh_nameplates/_choose_nameplate/choose_words: @m.input methods +# we only expose the Helper to application code, not _Input +@attrs +class Helper(object): + _input = attrib() + def refresh_nameplates(self): + self._input.refresh_nameplates() def get_nameplate_completions(self, prefix): - lp = len(prefix) - completions = [] - for nameplate in self._nameplates: - if nameplate.startswith(prefix): - completions.append(nameplate[lp:]) - return completions - + return self._input.get_nameplate_completions(prefix) + def choose_nameplate(self, nameplate): + self._input.choose_nameplate(nameplate) def get_word_completions(self, prefix): - if self._wordlist: - return self._wordlist.get_completions(prefix) - return [] + return self._input.get_word_completions(prefix) + def choose_words(self, words): + self._input.choose_words(words) diff --git a/src/wormhole/_lister.py b/src/wormhole/_lister.py index 8b49d91..a58ecf6 100644 --- a/src/wormhole/_lister.py +++ b/src/wormhole/_lister.py @@ -1,10 +1,11 @@ from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer -from attr import attrib +from attr import attrs, attrib from attr.validators import provides from automat import MethodicalMachine from . import _interfaces +@attrs @implementer(_interfaces.ILister) class Lister(object): _timing = attrib(validator=provides(_interfaces.ITiming)) @@ -39,32 +40,35 @@ class Lister(object): @m.input() def lost(self): pass @m.input() - def refresh_nameplates(self): pass + def refresh(self): pass @m.input() - def rx_nameplates(self, message): pass + def rx_nameplates(self, all_nameplates): pass @m.output() def RC_tx_list(self): self._RC.tx_list() @m.output() - def I_got_nameplates(self, message): - self._I.got_nameplates(message["nameplates"]) + def I_got_nameplates(self, all_nameplates): + # We get a set of nameplate ids. There may be more attributes in the + # future: change RendezvousConnector._response_handle_nameplates to + # get them + self._I.got_nameplates(all_nameplates) S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) - S0A_idle_disconnected.upon(refresh_nameplates, + S0A_idle_disconnected.upon(refresh, enter=S1A_wanting_disconnected, outputs=[]) - S1A_wanting_disconnected.upon(refresh_nameplates, + S1A_wanting_disconnected.upon(refresh, enter=S1A_wanting_disconnected, outputs=[]) S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, outputs=[RC_tx_list]) - S0B_idle_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + S0B_idle_connected.upon(refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list]) S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates]) S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) - S1B_wanting_connected.upon(refresh_nameplates, enter=S1B_wanting_connected, + S1B_wanting_connected.upon(refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list]) S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 0568fac..2ed6192 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -147,6 +147,7 @@ class RendezvousConnector(object): self._N.connected() self._M.connected() self._L.connected() + self._A.connected() except Exception as e: self._B.error(e) raise @@ -186,6 +187,7 @@ class RendezvousConnector(object): self._N.lost() self._M.lost() self._L.lost() + self._A.lost() # internal def _stopped(self, res): @@ -207,17 +209,19 @@ class RendezvousConnector(object): def _response_handle_allocated(self, msg): nameplate = msg["nameplate"] assert isinstance(nameplate, type("")), type(nameplate) - self._C.rx_allocated(nameplate) + self._A.rx_allocated(nameplate) def _response_handle_nameplates(self, msg): + # we get list of {id: ID}, with maybe more attributes in the future nameplates = msg["nameplates"] assert isinstance(nameplates, list), type(nameplates) - nids = [] + nids = set() for n in nameplates: assert isinstance(n, dict), type(n) nameplate_id = n["id"] assert isinstance(nameplate_id, type("")), type(nameplate_id) - nids.append(nameplate_id) + nids.add(nameplate_id) + # deliver a set of nameplate ids self._L.rx_nameplates(nids) def _response_handle_ack(self, msg): diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index 6c266a6..71d3f1a 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -165,10 +165,10 @@ class PGPWordList(object): words = even_words_lowercase last_partial_word = prefix.split("-")[-1] lp = len(last_partial_word) - completions = [] + completions = set() for word in words: if word.startswith(prefix): - completions.append(word[lp:]) + completions.add(word[lp:]) return completions def choose_words(self, length): diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index d8f8fee..419605f 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -57,6 +57,16 @@ class NoKeyError(WormholeError): class OnlyOneCodeError(WormholeError): """Only one w.generate_code/w.set_code/w.input_code may be called""" +class MustChooseNameplateFirstError(WormholeError): + """The InputHelper was asked to do get_word_completions() or + choose_words() before the nameplate was chosen.""" +class AlreadyChoseNameplateError(WormholeError): + """The InputHelper was asked to do get_nameplate_completions() after + choose_nameplate() was called, or choose_nameplate() was called a second + time.""" +class AlreadyChoseWordsError(WormholeError): + """The InputHelper was asked to do get_word_completions() after + choose_words() was called, or choose_words() was called a second time.""" class WormholeClosed(Exception): """Deferred-returning API calls errback with WormholeClosed if the wormhole was already closed, or if it closes before a real result can be diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index b412344..3380b86 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -1,14 +1,23 @@ from __future__ import print_function, unicode_literals import json -from zope.interface import directlyProvides +from zope.interface import directlyProvides, implementer from twisted.trial import unittest -from .. import timing, _order, _receive, _key, _code +from .. import errors, timing, _order, _receive, _key, _code, _lister, _input, _allocator from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, - IRendezvousConnector, ILister) + IRendezvousConnector, ILister, IInput, IAllocator, + INameplate, ICode, IWordlist) from .._key import derive_key, derive_phase_key, encrypt_data from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes from spake2 import SPAKE2_Symmetric +@implementer(IWordlist) +class FakeWordList(object): + def choose_words(self, length): + return "-".join(["word"] * length) + def get_completions(self, prefix): + self._get_completions_prefix = prefix + return self._completions + class Dummy: def __init__(self, name, events, iface, *meths): self.name = name @@ -194,52 +203,260 @@ class Code(unittest.TestCase): events = [] c = _code.Code(timing.DebugTiming()) b = Dummy("b", events, IBoss, "got_code") + a = Dummy("a", events, IAllocator, "allocate") + n = Dummy("n", events, INameplate, "set_nameplate") + k = Dummy("k", events, IKey, "got_code") + i = Dummy("i", events, IInput, "start") + c.wire(b, a, n, k, i) + return c, b, a, n, k, i, events + + def test_set_code(self): + c, b, a, n, k, i, events = self.build() + c.set_code(u"1-code") + self.assertEqual(events, [("n.set_nameplate", u"1"), + ("k.got_code", u"1-code"), + ("b.got_code", u"1-code"), + ]) + + def test_allocate_code(self): + c, b, a, n, k, i, events = self.build() + wl = FakeWordList() + c.allocate_code(2, wl) + self.assertEqual(events, [("a.allocate", 2, wl)]) + events[:] = [] + c.allocated("1", "1-code") + self.assertEqual(events, [("n.set_nameplate", u"1"), + ("k.got_code", u"1-code"), + ("b.got_code", u"1-code"), + ]) + + def test_input_code(self): + c, b, a, n, k, i, events = self.build() + c.input_code() + self.assertEqual(events, [("i.start",)]) + events[:] = [] + c.got_nameplate("1") + self.assertEqual(events, [("n.set_nameplate", u"1"), + ]) + events[:] = [] + c.finished_input("1-code") + self.assertEqual(events, [("k.got_code", u"1-code"), + ("b.got_code", u"1-code"), + ]) + +class Input(unittest.TestCase): + def build(self): + events = [] + i = _input.Input(timing.DebugTiming()) + c = Dummy("c", events, ICode, "got_nameplate", "finished_input") + l = Dummy("l", events, ILister, "refresh") + i.wire(c, l) + return i, c, l, events + + def test_ignore_completion(self): + i, c, l, events = self.build() + helper = i.start() + self.assertIsInstance(helper, _input.Helper) + self.assertEqual(events, [("l.refresh",)]) + events[:] = [] + with self.assertRaises(errors.MustChooseNameplateFirstError): + helper.choose_words("word-word") + helper.choose_nameplate("1") + self.assertEqual(events, [("c.got_nameplate", "1")]) + events[:] = [] + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.choose_nameplate("2") + helper.choose_words("word-word") + with self.assertRaises(errors.AlreadyChoseWordsError): + helper.choose_words("word-word") + self.assertEqual(events, [("c.finished_input", "1-word-word")]) + + def test_with_completion(self): + i, c, l, events = self.build() + helper = i.start() + self.assertIsInstance(helper, _input.Helper) + self.assertEqual(events, [("l.refresh",)]) + events[:] = [] + helper.refresh_nameplates() + self.assertEqual(events, [("l.refresh",)]) + events[:] = [] + with self.assertRaises(errors.MustChooseNameplateFirstError): + helper.get_word_completions("prefix") + i.got_nameplates({"1", "12", "34", "35", "367"}) + self.assertEqual(helper.get_nameplate_completions(""), + {"1", "12", "34", "35", "367"}) + self.assertEqual(helper.get_nameplate_completions("1"), + {"", "2"}) + self.assertEqual(helper.get_nameplate_completions("2"), set()) + self.assertEqual(helper.get_nameplate_completions("3"), + {"4", "5", "67"}) + helper.choose_nameplate("34") + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.refresh_nameplates() + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.get_nameplate_completions("1") + self.assertEqual(events, [("c.got_nameplate", "34")]) + events[:] = [] + # no wordlist yet + self.assertEqual(helper.get_word_completions(""), set()) + wl = FakeWordList() + i.got_wordlist(wl) + wl._completions = {"bc", "bcd", "e"} + self.assertEqual(helper.get_word_completions("a"), wl._completions) + self.assertEqual(wl._get_completions_prefix, "a") + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.refresh_nameplates() + with self.assertRaises(errors.AlreadyChoseNameplateError): + helper.get_nameplate_completions("1") + helper.choose_words("word-word") + with self.assertRaises(errors.AlreadyChoseWordsError): + helper.get_word_completions("prefix") + with self.assertRaises(errors.AlreadyChoseWordsError): + helper.choose_words("word-word") + self.assertEqual(events, [("c.finished_input", "34-word-word")]) + + + +class Lister(unittest.TestCase): + def build(self): + events = [] + l = _lister.Lister(timing.DebugTiming()) + rc = Dummy("rc", events, IRendezvousConnector, "tx_list") + i = Dummy("i", events, IInput, "got_nameplates") + l.wire(rc, i) + return l, rc, i, events + + def test_connect_first(self): + l, rc, i, events = self.build() + l.connected() + l.lost() + l.connected() + self.assertEqual(events, []) + l.refresh() + self.assertEqual(events, [("rc.tx_list",), + ]) + events[:] = [] + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("i.got_nameplates", {"1", "2", "3"}), + ]) + events[:] = [] + # now we're satisfied: disconnecting and reconnecting won't ask again + l.lost() + l.connected() + self.assertEqual(events, []) + + # but if we're told to refresh, we'll do so + l.refresh() + self.assertEqual(events, [("rc.tx_list",), + ]) + + def test_connect_first_ask_twice(self): + l, rc, i, events = self.build() + l.connected() + self.assertEqual(events, []) + l.refresh() + l.refresh() + self.assertEqual(events, [("rc.tx_list",), + ("rc.tx_list",), + ]) + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("rc.tx_list",), + ("rc.tx_list",), + ("i.got_nameplates", {"1", "2", "3"}), + ]) + l.rx_nameplates({"1" ,"2", "3", "4"}) + self.assertEqual(events, [("rc.tx_list",), + ("rc.tx_list",), + ("i.got_nameplates", {"1", "2", "3"}), + ("i.got_nameplates", {"1", "2", "3", "4"}), + ]) + + def test_reconnect(self): + l, rc, i, events = self.build() + l.refresh() + l.connected() + self.assertEqual(events, [("rc.tx_list",), + ]) + events[:] = [] + l.lost() + l.connected() + self.assertEqual(events, [("rc.tx_list",), + ]) + + def test_refresh_first(self): + l, rc, i, events = self.build() + l.refresh() + self.assertEqual(events, []) + l.connected() + self.assertEqual(events, [("rc.tx_list",), + ]) + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("rc.tx_list",), + ("i.got_nameplates", {"1", "2", "3"}), + ]) + + def test_unrefreshed(self): + l, rc, i, events = self.build() + self.assertEqual(events, []) + # we receive a spontaneous rx_nameplates, without asking + l.connected() + self.assertEqual(events, []) + l.rx_nameplates({"1", "2", "3"}) + self.assertEqual(events, [("i.got_nameplates", {"1", "2", "3"}), + ]) + +class Allocator(unittest.TestCase): + def build(self): + events = [] + a = _allocator.Allocator(timing.DebugTiming()) rc = Dummy("rc", events, IRendezvousConnector, "tx_allocate") - l = Dummy("l", events, ILister, "refresh_nameplates") - c.wire(b, rc, l) - return c, b, rc, l, events + c = Dummy("c", events, ICode, "allocated") + a.wire(rc, c) + return a, rc, c, events - def test_set_disconnected(self): - c, b, rc, l, events = self.build() - c.set_code(u"code") - self.assertEqual(events, [("b.got_code", u"code")]) - - def test_set_connected(self): - c, b, rc, l, events = self.build() - c.connected() - c.set_code(u"code") - self.assertEqual(events, [("b.got_code", u"code")]) - - def test_allocate_disconnected(self): - c, b, rc, l, events = self.build() - c.allocate_code(2) + def test_no_allocation(self): + a, rc, c, events = self.build() + a.connected() self.assertEqual(events, []) - c.connected() - self.assertEqual(events, [("rc.tx_allocate",)]) - events[:] = [] - c.lost() + + def test_allocate_first(self): + a, rc, c, events = self.build() + a.allocate(2, FakeWordList()) self.assertEqual(events, []) - c.connected() + a.connected() self.assertEqual(events, [("rc.tx_allocate",)]) events[:] = [] - c.rx_allocated("4") - self.assertEqual(len(events), 1, events) - self.assertEqual(events[0][0], "b.got_code") - code = events[0][1] - self.assert_(code.startswith("4-"), code) + a.lost() + a.connected() + self.assertEqual(events, [("rc.tx_allocate",), + ]) + events[:] = [] + a.rx_allocated("1") + self.assertEqual(events, [("c.allocated", "1", "1-word-word"), + ]) - def test_allocate_connected(self): - c, b, rc, l, events = self.build() - c.connected() - c.allocate_code(2) + def test_connect_first(self): + a, rc, c, events = self.build() + a.connected() + self.assertEqual(events, []) + a.allocate(2, FakeWordList()) self.assertEqual(events, [("rc.tx_allocate",)]) events[:] = [] - c.rx_allocated("4") - self.assertEqual(len(events), 1, events) - self.assertEqual(events[0][0], "b.got_code") - code = events[0][1] - self.assert_(code.startswith("4-"), code) - - # TODO: input_code - + a.lost() + a.connected() + self.assertEqual(events, [("rc.tx_allocate",), + ]) + events[:] = [] + a.rx_allocated("1") + self.assertEqual(events, [("c.allocated", "1", "1-word-word"), + ]) +# TODO +# Send +# Mailbox +# Nameplate +# Terminator +# Boss +# RendezvousConnector (not a state machine) +# #Input: exercise helper methods +# wordlist From e66d2df9f149bcc06dee4c071350c33f65a04f8f Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 18:38:14 +0100 Subject: [PATCH 125/176] test and fix wordlist methods --- docs/api.md | 13 +++++++------ src/wormhole/_wordlist.py | 2 +- src/wormhole/test/test_wordlist.py | 20 ++++++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 src/wormhole/test/test_wordlist.py diff --git a/docs/api.md b/docs/api.md index 656a94f..2299a21 100644 --- a/docs/api.md +++ b/docs/api.md @@ -211,12 +211,13 @@ The code-entry Helper object has the following API: `get_word_completions("pr")` will return `{"ocessor", "ovincial", "oximate"}`, while `get_word_completions("opulent-pr")` will return `{"eclude", "efer", "eshrunk", "inter", "owler"}`. If the wordlist is not - yet known, this returns an empty set. It will also return an empty set if - the prefix is complete (the last word matches something in the completion - list, and there are no longer extension words), although the code may not - yet be complete if there are additional words. The completions will never - include a hyphen: the UI frontend must supply these if desired. The - frontend is also responsible for sorting the results before display. + yet known, this returns an empty set. It will include an empty string in + the returned set if the prefix is complete (the last word is an exact match + for something in the completion list), but will include additional strings + if the completion list includes extensions of the last word. The + completions will never include a hyphen: the UI frontend must supply these + if desired. The frontend is also responsible for sorting the results 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 diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index 71d3f1a..3da0e15 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -167,7 +167,7 @@ class PGPWordList(object): lp = len(last_partial_word) completions = set() for word in words: - if word.startswith(prefix): + if word.startswith(last_partial_word): completions.add(word[lp:]) return completions diff --git a/src/wormhole/test/test_wordlist.py b/src/wormhole/test/test_wordlist.py new file mode 100644 index 0000000..56cac4b --- /dev/null +++ b/src/wormhole/test/test_wordlist.py @@ -0,0 +1,20 @@ +from __future__ import print_function, unicode_literals +import mock +from twisted.trial import unittest +from .._wordlist import PGPWordList + +class Completions(unittest.TestCase): + def test_completions(self): + wl = PGPWordList() + gc = wl.get_completions + self.assertEqual(gc("ar"), {"mistice", "ticle"}) + self.assertEqual(gc("armis"), {"tice"}) + self.assertEqual(gc("armistice-ba"), + {"boon", "ckfield", "ckward", "njo"}) + self.assertEqual(gc("armistice-baboon"), {""}) + +class Choose(unittest.TestCase): + def test_choose_words(self): + wl = PGPWordList() + with mock.patch("os.urandom", side_effect=[b"\x04", b"\x10"]): + self.assertEqual(wl.choose_words(2), "alkali-assume") From 3a289f891272f644c08fd023b31055f25b5f31be Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 18:38:35 +0100 Subject: [PATCH 126/176] add tests for Send and Terminator --- src/wormhole/_terminator.py | 2 +- src/wormhole/test/test_machines.py | 121 ++++++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py index f7da3cc..1468c07 100644 --- a/src/wormhole/_terminator.py +++ b/src/wormhole/_terminator.py @@ -9,7 +9,7 @@ class Terminator(object): @m.setTrace() def set_trace(): pass # pragma: no cover - def __attrs_post_init__(self): + def __init__(self): self._mood = None def wire(self, boss, rendezvous_connector, nameplate, mailbox): diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 3380b86..55461c4 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -1,14 +1,17 @@ from __future__ import print_function, unicode_literals import json +import mock from zope.interface import directlyProvides, implementer from twisted.trial import unittest -from .. import errors, timing, _order, _receive, _key, _code, _lister, _input, _allocator +from .. import (errors, timing, _order, _receive, _key, _code, _lister, + _input, _allocator, _send, _terminator) from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, IRendezvousConnector, ILister, IInput, IAllocator, INameplate, ICode, IWordlist) from .._key import derive_key, derive_phase_key, encrypt_data from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes from spake2 import SPAKE2_Symmetric +from nacl.secret import SecretBox @implementer(IWordlist) class FakeWordList(object): @@ -30,6 +33,58 @@ class Dummy: self.events.append(("%s.%s" % (self.name, meth),) + args) setattr(self, meth, log) +class Send(unittest.TestCase): + def build(self): + events = [] + s = _send.Send(u"side", timing.DebugTiming()) + m = Dummy("m", events, IMailbox, "add_message") + s.wire(m) + return s, m, events + + def test_send_first(self): + s, m, events = self.build() + s.send("phase1", b"msg") + self.assertEqual(events, []) + key = b"\x00" * 32 + nonce1 = b"\x00" * SecretBox.NONCE_SIZE + with mock.patch("nacl.utils.random", side_effect=[nonce1]) as r: + s.got_verified_key(key) + self.assertEqual(r.mock_calls, [mock.call(SecretBox.NONCE_SIZE)]) + #print(bytes_to_hexstr(events[0][2])) + enc1 = hexstr_to_bytes("00000000000000000000000000000000000000000000000022f1a46c3c3496423c394621a2a5a8cf275b08") + self.assertEqual(events, [("m.add_message", "phase1", enc1)]) + events[:] = [] + + nonce2 = b"\x02" * SecretBox.NONCE_SIZE + with mock.patch("nacl.utils.random", side_effect=[nonce2]) as r: + s.send("phase2", b"msg") + self.assertEqual(r.mock_calls, [mock.call(SecretBox.NONCE_SIZE)]) + enc2 = hexstr_to_bytes("0202020202020202020202020202020202020202020202026660337c3eac6513c0dac9818b62ef16d9cd7e") + self.assertEqual(events, [("m.add_message", "phase2", enc2)]) + + def test_key_first(self): + s, m, events = self.build() + key = b"\x00" * 32 + s.got_verified_key(key) + self.assertEqual(events, []) + + nonce1 = b"\x00" * SecretBox.NONCE_SIZE + with mock.patch("nacl.utils.random", side_effect=[nonce1]) as r: + s.send("phase1", b"msg") + self.assertEqual(r.mock_calls, [mock.call(SecretBox.NONCE_SIZE)]) + enc1 = hexstr_to_bytes("00000000000000000000000000000000000000000000000022f1a46c3c3496423c394621a2a5a8cf275b08") + self.assertEqual(events, [("m.add_message", "phase1", enc1)]) + events[:] = [] + + nonce2 = b"\x02" * SecretBox.NONCE_SIZE + with mock.patch("nacl.utils.random", side_effect=[nonce2]) as r: + s.send("phase2", b"msg") + self.assertEqual(r.mock_calls, [mock.call(SecretBox.NONCE_SIZE)]) + enc2 = hexstr_to_bytes("0202020202020202020202020202020202020202020202026660337c3eac6513c0dac9818b62ef16d9cd7e") + self.assertEqual(events, [("m.add_message", "phase2", enc2)]) + + + class Order(unittest.TestCase): def build(self): events = [] @@ -451,12 +506,70 @@ class Allocator(unittest.TestCase): self.assertEqual(events, [("c.allocated", "1", "1-word-word"), ]) + +class Terminator(unittest.TestCase): + def build(self): + events = [] + t = _terminator.Terminator() + b = Dummy("b", events, IBoss, "closed") + rc = Dummy("rc", events, IRendezvousConnector, "stop") + n = Dummy("n", events, INameplate, "close") + m = Dummy("m", events, IMailbox, "close") + t.wire(b, rc, n, m) + return t, b, rc, n, m, events + + # there are three events, and we need to test all orderings of them + def _do_test(self, ev1, ev2, ev3): + t, b, rc, n, m, events = self.build() + input_events = {"mailbox": lambda: t.mailbox_done(), + "nameplate": lambda: t.nameplate_done(), + "close": lambda: t.close("happy"), + } + close_events = [("n.close",), + ("m.close", "happy"), + ] + + input_events[ev1]() + expected = [] + if ev1 == "close": + expected.extend(close_events) + self.assertEqual(events, expected) + events[:] = [] + + input_events[ev2]() + expected = [] + if ev2 == "close": + expected.extend(close_events) + self.assertEqual(events, expected) + events[:] = [] + + input_events[ev3]() + expected = [] + if ev3 == "close": + expected.extend(close_events) + expected.append(("rc.stop",)) + self.assertEqual(events, expected) + events[:] = [] + + t.stopped() + self.assertEqual(events, [("b.closed",)]) + + def test_terminate(self): + self._do_test("mailbox", "nameplate", "close") + self._do_test("mailbox", "close", "nameplate") + self._do_test("nameplate", "mailbox", "close") + self._do_test("nameplate", "close", "mailbox") + self._do_test("close", "nameplate", "mailbox") + self._do_test("close", "mailbox", "nameplate") + + + # TODO -# Send +# #Send # Mailbox # Nameplate -# Terminator +# #Terminator # Boss # RendezvousConnector (not a state machine) # #Input: exercise helper methods -# wordlist +# #wordlist From bd974f380161026385b91c81b24a24a44667f4fb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 20:03:00 +0100 Subject: [PATCH 127/176] test Nameplate, Mailbox. refactor a little bit --- docs/state-machines/mailbox.dot | 7 +- src/wormhole/_mailbox.py | 14 +- src/wormhole/test/test_machines.py | 545 ++++++++++++++++++++++++++++- 3 files changed, 550 insertions(+), 16 deletions(-) diff --git a/docs/state-machines/mailbox.dot b/docs/state-machines/mailbox.dot index 712fcd7..9bcd964 100644 --- a/docs/state-machines/mailbox.dot +++ b/docs/state-machines/mailbox.dot @@ -1,7 +1,7 @@ digraph { /* new idea */ - title [label="Message\nMachine" style="dotted"] + title [label="Mailbox\nMachine" style="dotted"] {rank=same; S0A S0B} S0A [label="S0A:\nunknown"] @@ -21,6 +21,9 @@ digraph { S0A -> S1A [label="got_mailbox"] S1A [label="S1A:\nknown"] S1A -> P_open [label="connected"] + S1A -> P1A_queue [label="add_message" style="dotted"] + P1A_queue [shape="box" label="queue" style="dotted"] + P1A_queue -> S1A [style="dotted"] S1A -> S2A [style="invis"] P_open -> P2_connected [style="invis"] @@ -85,7 +88,7 @@ digraph { S4B [label="S4B:\nclosed"] S4A -> S4B [label="connected"] S4B -> S4A [label="lost"] - S4B -> S4B [label="add_message\nrx_message\nclose"] + S4B -> S4B [label="add_message\nrx_message\nclose"] # is "close" needed? S0A -> P3A_done [label="close" color="red"] S0B -> P3B_done [label="close" color="red"] diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 958d6f8..90f9d95 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -131,20 +131,16 @@ class Mailbox(object): @m.output() def N_release_and_accept(self, side, phase, body): self._N.release() - self._accept(side, phase, body) + if phase not in self._processed: + self._processed.add(phase) + self._O.got_message(side, phase, body) @m.output() def RC_tx_close(self): assert self._mood self._RC_tx_close() def _RC_tx_close(self): self._RC.tx_close(self._mailbox, self._mood) - @m.output() - def accept(self, side, phase, body): - self._accept(side, phase, body) - def _accept(self, side, phase, body): - if phase not in self._processed: - self._processed.add(phase) - self._O.got_message(side, phase, body) + @m.output() def dequeue(self, phase, body): self._pending_outbound.pop(phase, None) @@ -191,10 +187,12 @@ class Mailbox(object): S3B.upon(add_message, enter=S3B, outputs=[]) S3B.upon(rx_message_theirs, enter=S3B, outputs=[]) S3B.upon(rx_message_ours, enter=S3B, outputs=[]) + S3B.upon(close, enter=S3B, outputs=[]) S4A.upon(connected, enter=S4B, outputs=[]) S4B.upon(lost, enter=S4A, outputs=[]) S4.upon(add_message, enter=S4, outputs=[]) S4.upon(rx_message_theirs, enter=S4, outputs=[]) S4.upon(rx_message_ours, enter=S4, outputs=[]) + S4.upon(close, enter=S4, outputs=[]) diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 55461c4..c569165 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -4,10 +4,10 @@ import mock from zope.interface import directlyProvides, implementer from twisted.trial import unittest from .. import (errors, timing, _order, _receive, _key, _code, _lister, - _input, _allocator, _send, _terminator) -from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, + _input, _allocator, _send, _terminator, _nameplate, _mailbox) +from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, IOrder, IRendezvousConnector, ILister, IInput, IAllocator, - INameplate, ICode, IWordlist) + INameplate, ICode, IWordlist, ITerminator) from .._key import derive_key, derive_phase_key, encrypt_data from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes from spake2 import SPAKE2_Symmetric @@ -506,6 +506,538 @@ class Allocator(unittest.TestCase): self.assertEqual(events, [("c.allocated", "1", "1-word-word"), ]) +class Nameplate(unittest.TestCase): + def build(self): + events = [] + n = _nameplate.Nameplate() + m = Dummy("m", events, IMailbox, "got_mailbox") + i = Dummy("i", events, IInput, "got_wordlist") + rc = Dummy("rc", events, IRendezvousConnector, "tx_claim", "tx_release") + t = Dummy("t", events, ITerminator, "nameplate_done") + n.wire(m, i, rc, t) + return n, m, i, rc, t, events + + def test_set_first(self): + # connection remains up throughout + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_connect_first(self): + # connection remains up throughout + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_reconnect_while_claiming(self): + # connection bounced while waiting for rx_claimed + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + n.lost() + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + + def test_reconnect_while_claimed(self): + # connection bounced while claimed: no retransmits should be sent + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.lost() + n.connected() + self.assertEqual(events, []) + + def test_reconnect_while_releasing(self): + # connection bounced while waiting for rx_released + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.lost() + n.connected() + self.assertEqual(events, [("rc.tx_release", "1")]) + + def test_reconnect_while_done(self): + # connection bounces after we're done + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + events[:] = [] + + n.lost() + n.connected() + self.assertEqual(events, []) + + def test_close_while_idle(self): + n, m, i, rc, t, events = self.build() + n.close() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_idle_connected(self): + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + n.close() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_unclaimed(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + n.close() # before ever being connected + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_claiming(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + n.close() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_claiming_but_disconnected(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + n.lost() + n.close() + self.assertEqual(events, []) + # we're now waiting for a connection, so we can release the nameplate + n.connected() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_claimed(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.close() + # this path behaves just like a deliberate release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_claimed_but_disconnected(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.lost() + n.close() + # we're now waiting for a connection, so we can release the nameplate + n.connected() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_releasing(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.close() # ignored, we're already on our way out the door + self.assertEqual(events, []) + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_releasing_but_disconnecteda(self): + n, m, i, rc, t, events = self.build() + n.set_nameplate("1") + self.assertEqual(events, []) + n.connected() + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.lost() + n.close() + # we must retransmit the tx_release when we reconnect + self.assertEqual(events, []) + + n.connected() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + + def test_close_while_done(self): + # connection remains up throughout + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + events[:] = [] + + n.close() # NOP + self.assertEqual(events, []) + + def test_close_while_done_but_disconnected(self): + # connection remains up throughout + n, m, i, rc, t, events = self.build() + n.connected() + self.assertEqual(events, []) + + n.set_nameplate("1") + self.assertEqual(events, [("rc.tx_claim", "1")]) + events[:] = [] + + wl = object() + with mock.patch("wormhole._nameplate.PGPWordList", return_value=wl): + n.rx_claimed("mbox1") + self.assertEqual(events, [("i.got_wordlist", wl), + ("m.got_mailbox", "mbox1"), + ]) + events[:] = [] + + n.release() + self.assertEqual(events, [("rc.tx_release", "1")]) + events[:] = [] + + n.rx_released() + self.assertEqual(events, [("t.nameplate_done",)]) + events[:] = [] + + n.lost() + n.close() # NOP + self.assertEqual(events, []) + +class Mailbox(unittest.TestCase): + def build(self): + events = [] + m = _mailbox.Mailbox("side1") + n = Dummy("n", events, INameplate, "release") + rc = Dummy("rc", events, IRendezvousConnector, + "tx_add", "tx_open", "tx_close") + o = Dummy("o", events, IOrder, "got_message") + t = Dummy("t", events, ITerminator, "mailbox_done") + m.wire(n, rc, o, t) + return m, n, rc, o, t, events + + # TODO: test moods + + def assert_events(self, events, initial_events, tx_add_events): + self.assertEqual(len(events), len(initial_events)+len(tx_add_events), + events) + self.assertEqual(events[:len(initial_events)], initial_events) + self.assertEqual(set(events[len(initial_events):]), tx_add_events) + + def test_connect_first(self): # connect before got_mailbox + m, n, rc, o, t, events = self.build() + m.add_message("phase1", b"msg1") + self.assertEqual(events, []) + + m.connected() + self.assertEqual(events, []) + + m.got_mailbox("mbox1") + self.assertEqual(events, [("rc.tx_open", "mbox1"), + ("rc.tx_add", "phase1", b"msg1")]) + events[:] = [] + + m.add_message("phase2", b"msg2") + self.assertEqual(events, [("rc.tx_add", "phase2", b"msg2")]) + events[:] = [] + + # bouncing the connection should retransmit everything, even the open() + m.lost() + self.assertEqual(events, []) + # and messages sent while here should be queued + m.add_message("phase3", b"msg3") + self.assertEqual(events, []) + + m.connected() + # the other messages are allowed to be sent in any order + self.assert_events(events, [("rc.tx_open", "mbox1")], + { ("rc.tx_add", "phase1", b"msg1"), + ("rc.tx_add", "phase2", b"msg2"), + ("rc.tx_add", "phase3", b"msg3"), + }) + events[:] = [] + + m.rx_message("side1", "phase1", b"msg1") # echo of our message, dequeue + self.assertEqual(events, []) + + m.lost() + m.connected() + self.assert_events(events, [("rc.tx_open", "mbox1")], + {("rc.tx_add", "phase2", b"msg2"), + ("rc.tx_add", "phase3", b"msg3"), + }) + events[:] = [] + + # a new message from the peer gets delivered, and the Nameplate is + # released since the message proves that our peer opened the Mailbox + # and therefore no longer needs the Nameplate + m.rx_message("side2", "phase1", b"msg1them") # new message from peer + self.assertEqual(events, [("n.release",), + ("o.got_message", "side2", "phase1", b"msg1them"), + ]) + events[:] = [] + + # we de-duplicate peer messages, but still re-release the nameplate + # since Nameplate is smart enough to ignore that + m.rx_message("side2", "phase1", b"msg1them") + self.assertEqual(events, [("n.release",), + ]) + events[:] = [] + + m.close("happy") + self.assertEqual(events, [("rc.tx_close", "mbox1", "happy")]) + events[:] = [] + + # while closing, we ignore a lot + m.add_message("phase-late", b"late") + m.rx_message("side1", "phase2", b"msg2") + m.close("happy") + self.assertEqual(events, []) + + # bouncing the connection forces a retransmit of the tx_close + m.lost() + self.assertEqual(events, []) + m.connected() + self.assertEqual(events, [("rc.tx_close", "mbox1", "happy")]) + events[:] = [] + + m.rx_closed() + self.assertEqual(events, [("t.mailbox_done",)]) + events[:] = [] + + # while closed, we ignore everything + m.add_message("phase-late", b"late") + m.rx_message("side1", "phase2", b"msg2") + m.close("happy") + m.lost() + m.connected() + self.assertEqual(events, []) + + def test_mailbox_first(self): # got_mailbox before connect + m, n, rc, o, t, events = self.build() + m.add_message("phase1", b"msg1") + self.assertEqual(events, []) + + m.got_mailbox("mbox1") + m.add_message("phase2", b"msg2") + self.assertEqual(events, []) + + m.connected() + + self.assert_events(events, [("rc.tx_open", "mbox1")], + { ("rc.tx_add", "phase1", b"msg1"), + ("rc.tx_add", "phase2", b"msg2"), + }) + + def test_close_while_idle(self): + m, n, rc, o, t, events = self.build() + m.close("happy") + self.assertEqual(events, [("t.mailbox_done",)]) + + def test_close_while_idle_but_connected(self): + m, n, rc, o, t, events = self.build() + m.connected() + m.close("happy") + self.assertEqual(events, [("t.mailbox_done",)]) + + def test_close_while_mailbox_disconnected(self): + m, n, rc, o, t, events = self.build() + m.got_mailbox("mbox1") + m.close("happy") + self.assertEqual(events, [("t.mailbox_done",)]) + + def test_close_while_reconnecting(self): + m, n, rc, o, t, events = self.build() + m.got_mailbox("mbox1") + m.connected() + self.assertEqual(events, [("rc.tx_open", "mbox1")]) + events[:] = [] + + m.lost() + self.assertEqual(events, []) + m.close("happy") + self.assertEqual(events, []) + # we now wait to connect, so we can send the tx_close + + m.connected() + self.assertEqual(events, [("rc.tx_close", "mbox1", "happy")]) + events[:] = [] + + m.rx_closed() + self.assertEqual(events, [("t.mailbox_done",)]) + events[:] = [] class Terminator(unittest.TestCase): def build(self): @@ -562,14 +1094,15 @@ class Terminator(unittest.TestCase): self._do_test("close", "nameplate", "mailbox") self._do_test("close", "mailbox", "nameplate") - + # TODO: test moods # TODO # #Send -# Mailbox -# Nameplate +# #Mailbox +# #Nameplate # #Terminator # Boss # RendezvousConnector (not a state machine) # #Input: exercise helper methods # #wordlist +# test idempotency / at-most-once where applicable From d8d305407b7deb3c62688c3fb0206d15fcbdafc6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 13:03:48 -0700 Subject: [PATCH 128/176] start on Boss tests --- src/wormhole/_boss.py | 5 +++ src/wormhole/test/test_machines.py | 59 +++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index fd21b9e..37868d7 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -42,6 +42,10 @@ class Boss(object): def set_trace(): pass # pragma: no cover def __attrs_post_init__(self): + self._build_workers() + self._init_other_state() + + def _build_workers(self): self._N = Nameplate() self._M = Mailbox(self._side) self._S = Send(self._side, self._timing) @@ -70,6 +74,7 @@ class Boss(object): self._C.wire(self, self._A, self._N, self._K, self._I) self._T.wire(self, self._RC, self._N, self._M) + def _init_other_state(self): self._did_start_code = False self._next_tx_phase = 0 self._next_rx_phase = 0 diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index c569165..3359958 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -3,12 +3,13 @@ import json import mock from zope.interface import directlyProvides, implementer from twisted.trial import unittest -from .. import (errors, timing, _order, _receive, _key, _code, _lister, +from .. import (errors, timing, _order, _receive, _key, _code, _lister, _boss, _input, _allocator, _send, _terminator, _nameplate, _mailbox) from .._interfaces import (IKey, IReceive, IBoss, ISend, IMailbox, IOrder, IRendezvousConnector, ILister, IInput, IAllocator, INameplate, ICode, IWordlist, ITerminator) from .._key import derive_key, derive_phase_key, encrypt_data +from ..journal import ImmediateJournal from ..util import dict_to_bytes, hexstr_to_bytes, bytes_to_hexstr, to_bytes from spake2 import SPAKE2_Symmetric from nacl.secret import SecretBox @@ -25,7 +26,8 @@ class Dummy: def __init__(self, name, events, iface, *meths): self.name = name self.events = events - directlyProvides(self, iface) + if iface: + directlyProvides(self, iface) for meth in meths: self.mock(meth) def mock(self, meth): @@ -1096,6 +1098,59 @@ class Terminator(unittest.TestCase): # TODO: test moods +class MockBoss(_boss.Boss): + def __attrs_post_init__(self): + #self._build_workers() + self._init_other_state() + +class Boss(unittest.TestCase): + def build(self): + events = [] + wormhole = Dummy("w", events, None, + "got_code", "got_key", "got_verifier", "got_version", + "received", "closed") + welcome_handler = mock.Mock() + versions = {"app": "version1"} + reactor = None + journal = ImmediateJournal() + tor_manager = None + b = MockBoss(wormhole, "side", "url", "appid", versions, + welcome_handler, reactor, journal, tor_manager, + timing.DebugTiming()) + t = b._T = Dummy("t", events, ITerminator, "close") + s = b._S = Dummy("s", events, ISend, "send") + rc = b._RC = Dummy("rc", events, IRendezvousConnector, "start") + c = b._C = Dummy("c", events, ICode, + "allocate_code", "input_code", "set_code") + return b, events + + def test_basic(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.got_code("1-code") + self.assertEqual(events, [("w.got_code", "1-code")]) + events[:] = [] + + # pretend a peer message was correctly decrypted + b.happy() + b.got_version({}) + b.got_phase("phase1", b"msg1") + self.assertEqual(events, [("w.got_version", {}), + ("w.received", b"msg1"), + ]) + events[:] = [] + b.close() + self.assertEqual(events, [("t.close", "happy")]) + events[:] = [] + + b.closed() + self.assertEqual(events, [("w.closed", "reasons")]) + + + # TODO # #Send # #Mailbox From 53a911cc806abf547e6c4b7e49c1d55cfa1f82bc Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 15:09:26 -0700 Subject: [PATCH 129/176] finish Boss tests --- src/wormhole/_boss.py | 30 ++--- src/wormhole/_rendezvous.py | 4 +- src/wormhole/errors.py | 4 + src/wormhole/test/test_machines.py | 195 +++++++++++++++++++++++++++-- src/wormhole/wormhole.py | 8 +- 5 files changed, 212 insertions(+), 29 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 37868d7..a101cb3 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -21,7 +21,7 @@ from ._code import Code from ._terminator import Terminator from ._wordlist import PGPWordList from .errors import (ServerError, LonelyError, WrongPasswordError, - KeyFormatError, OnlyOneCodeError) + KeyFormatError, OnlyOneCodeError, _UnknownPhaseError) from .util import bytes_to_dict @attrs @@ -126,11 +126,11 @@ class Boss(object): # would require the Wormhole to be aware of Code (whereas right now # Wormhole only knows about this Boss instance, and everything else is # hidden away). - def input_code(self, helper): + def input_code(self): if self._did_start_code: raise OnlyOneCodeError() self._did_start_code = True - self._C.input_code(helper) + return self._C.input_code() def allocate_code(self, code_length): if self._did_start_code: raise OnlyOneCodeError() @@ -175,17 +175,17 @@ class Boss(object): assert isinstance(phase, type("")), type(phase) assert isinstance(plaintext, type(b"")), type(plaintext) if phase == "version": - self.got_version(plaintext) + self._got_version(plaintext) elif re.search(r'^\d+$', phase): - self.got_phase(int(phase), plaintext) + self._got_phase(int(phase), plaintext) else: # Ignore unrecognized phases, for forwards-compatibility. Use # log.err so tests will catch surprises. - log.err("received unknown phase '%s'" % phase) + log.err(_UnknownPhaseError("received unknown phase '%s'" % phase)) @m.input() - def got_version(self, plaintext): pass + def _got_version(self, plaintext): pass @m.input() - def got_phase(self, phase, plaintext): pass + def _got_phase(self, phase, plaintext): pass @m.input() def got_key(self, key): pass @m.input() @@ -210,7 +210,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_versions(app_versions) + self._W.got_version(app_versions) @m.output() def S_send(self, plaintext): @@ -279,8 +279,8 @@ class Boss(object): 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(got_phase, enter=S2_happy, outputs=[W_received]) - S2_happy.upon(got_version, enter=S2_happy, outputs=[process_version]) + 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(scared, enter=S3_closing, outputs=[close_scared]) S2_happy.upon(close, enter=S3_closing, outputs=[close_happy]) S2_happy.upon(send, enter=S2_happy, outputs=[S_send]) @@ -289,8 +289,8 @@ class Boss(object): S3_closing.upon(rx_welcome, 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_version, 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(happy, enter=S3_closing, outputs=[]) S3_closing.upon(scared, enter=S3_closing, outputs=[]) S3_closing.upon(close, enter=S3_closing, outputs=[]) @@ -299,8 +299,8 @@ class Boss(object): S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error]) S4_closed.upon(rx_welcome, 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_phase, enter=S4_closed, outputs=[]) + S4_closed.upon(_got_version, enter=S4_closed, outputs=[]) S4_closed.upon(happy, enter=S4_closed, outputs=[]) S4_closed.upon(scared, enter=S4_closed, outputs=[]) S4_closed.upon(close, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 2ed6192..a257b78 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -8,7 +8,7 @@ from twisted.python import log from twisted.internet import defer, endpoints from twisted.application import internet from autobahn.twisted import websocket -from . import _interfaces +from . import _interfaces, errors from .util import (bytes_to_hexstr, hexstr_to_bytes, bytes_to_dict, dict_to_bytes) @@ -171,7 +171,7 @@ class RendezvousConnector(object): meth = getattr(self, "_response_handle_"+mtype, None) if not meth: # make tests fail, but real application will ignore it - log.err(ValueError("Unknown inbound message type %r" % (msg,))) + log.err(errors._UnknownMessageTypeError("Unknown inbound message type %r" % (msg,))) return try: return meth(msg) diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index 419605f..1197c26 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -72,3 +72,7 @@ class WormholeClosed(Exception): wormhole was already closed, or if it closes before a real result can be obtained.""" +class _UnknownPhaseError(Exception): + """internal exception type, for tests.""" +class _UnknownMessageTypeError(Exception): + """internal exception type, for tests.""" diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 3359958..c77bae6 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -30,9 +30,11 @@ class Dummy: directlyProvides(self, iface) for meth in meths: self.mock(meth) + self.retval = None def mock(self, meth): def log(*args): self.events.append(("%s.%s" % (self.name, meth),) + args) + return self.retval setattr(self, meth, log) class Send(unittest.TestCase): @@ -1109,13 +1111,13 @@ class Boss(unittest.TestCase): wormhole = Dummy("w", events, None, "got_code", "got_key", "got_verifier", "got_version", "received", "closed") - welcome_handler = mock.Mock() + self._welcome_handler = mock.Mock() versions = {"app": "version1"} reactor = None journal = ImmediateJournal() tor_manager = None b = MockBoss(wormhole, "side", "url", "appid", versions, - welcome_handler, reactor, journal, tor_manager, + self._welcome_handler, reactor, journal, tor_manager, timing.DebugTiming()) t = b._T = Dummy("t", events, ITerminator, "close") s = b._S = Dummy("s", events, ISend, "send") @@ -1134,22 +1136,199 @@ 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")]) + # pretend a peer message was correctly decrypted + b.got_key(b"key") + b.got_verifier(b"verifier") b.happy() - b.got_version({}) - b.got_phase("phase1", b"msg1") - self.assertEqual(events, [("w.got_version", {}), + b.got_message("version", b"{}") + b.got_message("0", b"msg1") + self.assertEqual(events, [("w.got_key", b"key"), + ("w.got_verifier", b"verifier"), + ("w.got_version", {}), ("w.received", b"msg1"), ]) events[:] = [] + + b.send(b"msg2") + self.assertEqual(events, [("s.send", "0", b"msg2")]) + events[:] = [] + b.close() self.assertEqual(events, [("t.close", "happy")]) events[:] = [] b.closed() - self.assertEqual(events, [("w.closed", "reasons")]) - - + self.assertEqual(events, [("w.closed", "happy")]) + + def test_lonely(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.got_code("1-code") + self.assertEqual(events, [("w.got_code", "1-code")]) + events[:] = [] + + b.close() + self.assertEqual(events, [("t.close", "lonely")]) + events[:] = [] + + b.closed() + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], errors.LonelyError) + + def test_server_error(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + orig = {} + b.rx_error("server-error-msg", orig) + self.assertEqual(events, [("t.close", "errory")]) + events[:] = [] + + b.closed() + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], errors.ServerError) + self.assertEqual(events[0][1].args[0], "server-error-msg") + + def test_internal_error(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.error(ValueError("catch me")) + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], ValueError) + self.assertEqual(events[0][1].args[0], "catch me") + + def test_close_early(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.close() # before even w.got_code + self.assertEqual(events, [("t.close", "lonely")]) + events[:] = [] + + b.closed() + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], errors.LonelyError) + + def test_error_while_closing(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.close() + self.assertEqual(events, [("t.close", "lonely")]) + events[:] = [] + + b.error(ValueError("oops")) + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], ValueError) + + def test_scary_version(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.got_code("1-code") + self.assertEqual(events, [("w.got_code", "1-code")]) + events[:] = [] + + b.scared() + self.assertEqual(events, [("t.close", "scary")]) + events[:] = [] + + b.closed() + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], errors.WrongPasswordError) + + def test_scary_phase(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.got_code("1-code") + self.assertEqual(events, [("w.got_code", "1-code")]) + events[:] = [] + + b.happy() # phase=version + + b.scared() # phase=0 + self.assertEqual(events, [("t.close", "scary")]) + events[:] = [] + + b.closed() + self.assertEqual(len(events), 1, events) + self.assertEqual(events[0][0], "w.closed") + self.assertIsInstance(events[0][1], errors.WrongPasswordError) + + def test_unknown_phase(self): + b, events = self.build() + b.set_code("1-code") + self.assertEqual(events, [("c.set_code", "1-code")]) + events[:] = [] + + b.got_code("1-code") + self.assertEqual(events, [("w.got_code", "1-code")]) + events[:] = [] + + b.happy() # phase=version + + b.got_message("unknown-phase", b"spooky") + self.assertEqual(events, []) + + self.flushLoggedErrors(errors._UnknownPhaseError) + + def test_set_code_bad_format(self): + b, events = self.build() + with self.assertRaises(errors.KeyFormatError): + b.set_code("1 code") + + def test_set_code_bad_twice(self): + b, events = self.build() + b.set_code("1-code") + with self.assertRaises(errors.OnlyOneCodeError): + b.set_code("1-code") + + def test_input_code(self): + b, events = self.build() + b._C.retval = "helper" + helper = b.input_code() + self.assertEqual(events, [("c.input_code",)]) + self.assertEqual(helper, "helper") + with self.assertRaises(errors.OnlyOneCodeError): + b.input_code() + + def test_allocate_code(self): + b, events = self.build() + wl = object() + with mock.patch("wormhole._boss.PGPWordList", return_value=wl): + b.allocate_code(3) + self.assertEqual(events, [("c.allocate_code", 3, wl)]) + with self.assertRaises(errors.OnlyOneCodeError): + b.allocate_code(3) + + + # TODO # #Send diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index f730195..dc3daf9 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -124,7 +124,7 @@ class _DelegatedWormhole(object): self._key = key # for derive_key() def got_verifier(self, verifier): self._delegate.wormhole_verified(verifier) - def got_versions(self, versions): + def got_version(self, versions): self._delegate.wormhole_version(versions) def received(self, plaintext): self._delegate.wormhole_received(plaintext) @@ -191,8 +191,8 @@ class _DeferredWormhole(object): def allocate_code(self, code_length=2): self._boss.allocate_code(code_length) - def input_code(self, stdio): # TODO - self._boss.input_code(stdio) + def input_code(self): + return self._boss.input_code() def set_code(self, code): self._boss.set_code(code) @@ -241,7 +241,7 @@ class _DeferredWormhole(object): for d in self._verifier_observers: d.callback(verifier) self._verifier_observers[:] = [] - def got_versions(self, versions): + def got_version(self, versions): self._versions = versions for d in self._version_observers: d.callback(versions) From e82d705764ea900d5a336f690912ffe86582ca03 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 15:23:25 -0700 Subject: [PATCH 130/176] unbreak other tests --- src/wormhole/_input.py | 8 +++++++- src/wormhole/_rendezvous.py | 2 -- src/wormhole/_wordlist.py | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index bd16a43..3c03dd2 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -124,9 +124,15 @@ class Input(object): S0_idle.upon(start, enter=S1_typing_nameplate, outputs=[do_start], collector=first) + # wormholes that don't use input_code (i.e. they use allocate_code or + # generate_code) will never start() us, but Nameplate will give us a + # wordlist anyways (as soon as the nameplate is claimed), so handle it. + S0_idle.upon(got_wordlist, enter=S0_idle, outputs=[record_wordlist]) S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate, outputs=[record_nameplates]) - # too early for got_wordlist, should never happen + # but wormholes that *do* use input_code should not get got_wordlist + # until after we tell Code that we got_nameplate, which is the earliest + # it can be claimed S1_typing_nameplate.upon(refresh_nameplates, enter=S1_typing_nameplate, outputs=[do_refresh]) S1_typing_nameplate.upon(get_nameplate_completions, diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index a257b78..9af0edb 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -143,7 +143,6 @@ class RendezvousConnector(object): self._ws = proto try: self._tx("bind", appid=self._appid, side=self._side) - self._C.connected() self._N.connected() self._M.connected() self._L.connected() @@ -183,7 +182,6 @@ class RendezvousConnector(object): def ws_close(self, wasClean, code, reason): self._debug("R.lost") self._ws = None - self._C.lost() self._N.lost() self._M.lost() self._L.lost() diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index 3da0e15..ac48e78 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals import os +from zope.interface import implementer +from ._interfaces import IWordlist # The PGP Word List, which maps bytes to phonetically-distinct words. There # are two lists, even and odd, and encodings should alternate between then to @@ -156,6 +158,7 @@ for k,both_words in raw_words.items(): even_words_lowercase.add(even_word.lower()) odd_words_lowercase.add(odd_word.lower()) +@implementer(IWordlist) class PGPWordList(object): def get_completions(self, prefix): # start with the odd words From f0cab020f4ac8502d1e7955b8c8f02d6fb9f7831 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 15:24:10 -0700 Subject: [PATCH 131/176] hush some pyflakes --- src/wormhole/test/test_machines.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index c77bae6..0054833 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -1119,11 +1119,11 @@ class Boss(unittest.TestCase): b = MockBoss(wormhole, "side", "url", "appid", versions, self._welcome_handler, reactor, journal, tor_manager, timing.DebugTiming()) - t = b._T = Dummy("t", events, ITerminator, "close") - s = b._S = Dummy("s", events, ISend, "send") - rc = b._RC = Dummy("rc", events, IRendezvousConnector, "start") - c = b._C = Dummy("c", events, ICode, - "allocate_code", "input_code", "set_code") + b._T = Dummy("t", events, ITerminator, "close") + b._S = Dummy("s", events, ISend, "send") + b._RC = Dummy("rc", events, IRendezvousConnector, "start") + b._C = Dummy("c", events, ICode, + "allocate_code", "input_code", "set_code") return b, events def test_basic(self): From a446d4333e465c5aef2199c429716df51f6f06e1 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 16:15:46 -0700 Subject: [PATCH 132/176] start on new rlcompleter wrapper function --- src/wormhole/_rlcompleter.py | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/wormhole/_rlcompleter.py diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py new file mode 100644 index 0000000..f72451c --- /dev/null +++ b/src/wormhole/_rlcompleter.py @@ -0,0 +1,100 @@ +from __future__ import print_function, unicode_literals +import six +from attr import attrs, attrib +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.threads import deferToThread, blockingCallFromThread + +@attrs +class CodeInputter: + _input_helper = attrib() + _reactor = attrib() + def __attrs_post_init__(self): + self.used_completion = False + self._matches = None + self._committed_nameplate = None + + def bcft(self, f, *a, **kw): + return blockingCallFromThread(self._reactor, f, *a, **kw) + + def wrap_completer(self, text, state): + try: + return self.completer(text, state) + except Exception as e: + # completer exceptions are normally silently discarded, which + # makes debugging challenging + print("completer exception: %s" % e) + raise e + + def completer(self, text, state): + self.used_completion = True + #if state == 0: + # print("", file=sys.stderr) + #print("completer: '%s' %d '%d'" % (text, state, + # readline.get_completion_type()), + # file=sys.stderr) + #sys.stderr.flush() + + if state > 0: + # just use the values we decided last time + if state >= len(self._matches): + return None + return self._matches[state] + + if not self._committed_nameplate: + self.bcft(self._input_helper.refresh_nameplates) + + # now figure out new matches + if not "-" in text: + completions = self.bcft(self._input_helper.get_nameplate_completions, + text) + # TODO: does rlcompleter want full strings, or the next suffix? + self._matches = sorted(completions) + else: + nameplate, words = text.split("-", 1) + if self._committed_nameplate: + if nameplate != self._committed_nameplate: + # they deleted past the committment point: we can't use + # this. For now, bail, but in the future let's find a + # gentler way to encourage them to not do that. + raise ValueError("nameplate (NN-) already entered, cannot go back") + # they've just committed to this nameplate + self.bcft(self._input_helper.choose_nameplate, nameplate) + self._committed_nameplate = nameplate + completions = self.bcft(self._input_helper.get_word_completions, + words) + self._matches = sorted(completions) + + #print(" match: '%s'" % self._matches[state], file=sys.stderr) + #sys.stderr.flush() + return self._matches[state] + +def input_code_with_completion(prompt, input_helper, code_length): + try: + import readline + c = CodeInputter(input_helper) + if readline.__doc__ and "libedit" in readline.__doc__: + readline.parse_and_bind("bind ^I rl_complete") + else: + readline.parse_and_bind("tab: complete") + readline.set_completer(c.wrap_completer) + readline.set_completer_delims("") + except ImportError: + c = None + code = six.moves.input(prompt) + # Code is str(bytes) on py2, and str(unicode) on py3. We want unicode. + if isinstance(code, bytes): + code = code.decode("utf-8") + nameplate, words = code.split("-", 1) + input_helper.choose_words(words) + used_completion = c.used_completion if c else False + return (code, used_completion) + +@inlineCallbacks +def rlcompleter_helper(prompt, input_helper, reactor): + def warn_readline(): + pass + t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline) + res = yield deferToThread(input_code_with_completion, prompt, input_helper) + (code, used_completion) = res + reactor.removeSystemEventTrigger(t) + returnValue(used_completion) From 1b5a0289a8ef3cf3c3f00e6ae5f242816daace47 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 16:20:13 -0700 Subject: [PATCH 133/176] cmd_send: finally fix when_verified call --- src/wormhole/cli/cmd_send.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index 99052a9..3c9e814 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -125,7 +125,7 @@ class Sender: #finally: # if not notify.called: # notify.cancel() - yield w.when_verified() + verifier_bytes = yield w.when_verified() if args.verify: verifier = bytes_to_hexstr(verifier_bytes) From 07a49bfacabeae8b141e4556243f3222d582c9d0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Sun, 19 Mar 2017 16:40:16 -0700 Subject: [PATCH 134/176] make progress on rlcompleter, still broken --- docs/api.md | 2 +- src/wormhole/_code.py | 5 +++- src/wormhole/_rlcompleter.py | 50 +++++++++++++++++++++++++++++---- src/wormhole/cli/cmd_receive.py | 10 +++++-- 4 files changed, 56 insertions(+), 11 deletions(-) diff --git a/docs/api.md b/docs/api.md index 2299a21..47fae44 100644 --- a/docs/api.md +++ b/docs/api.md @@ -232,7 +232,7 @@ helper to do tab completion of wormhole codes: ```python from wormhole import create, rlcompleter_helper w = create(appid, relay_url, reactor) -rlcompleter_helper("Wormhole code:", w.input_code()) +rlcompleter_helper("Wormhole code:", w.input_code(), reactor) d = w.when_code() ``` diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index d19a9ee..edcac03 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -5,6 +5,9 @@ from attr.validators import provides from automat import MethodicalMachine from . import _interfaces +def first(outputs): + return list(outputs)[0] + @attrs @implementer(_interfaces.ICode) class Code(object): @@ -79,7 +82,7 @@ class Code(object): S0_idle.upon(set_code, enter=S4_known, outputs=[do_set_code]) S0_idle.upon(input_code, enter=S1_inputting_nameplate, - outputs=[do_start_input]) + outputs=[do_start_input], collector=first) S1_inputting_nameplate.upon(got_nameplate, enter=S2_inputting_words, outputs=[do_middle_input]) S2_inputting_words.upon(finished_input, enter=S4_known, diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index f72451c..1e6db1b 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -1,11 +1,12 @@ from __future__ import print_function, unicode_literals +import sys import six from attr import attrs, attrib from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.threads import deferToThread, blockingCallFromThread @attrs -class CodeInputter: +class CodeInputter(object): _input_helper = attrib() _reactor = attrib() def __attrs_post_init__(self): @@ -23,10 +24,14 @@ class CodeInputter: # completer exceptions are normally silently discarded, which # makes debugging challenging print("completer exception: %s" % e) + import traceback + traceback.print_exc() raise e def completer(self, text, state): self.used_completion = True + # debug + #import readline #if state == 0: # print("", file=sys.stderr) #print("completer: '%s' %d '%d'" % (text, state, @@ -68,10 +73,10 @@ class CodeInputter: #sys.stderr.flush() return self._matches[state] -def input_code_with_completion(prompt, input_helper, code_length): +def input_code_with_completion(prompt, input_helper, reactor): try: import readline - c = CodeInputter(input_helper) + c = CodeInputter(input_helper, reactor) if readline.__doc__ and "libedit" in readline.__doc__: readline.parse_and_bind("bind ^I rl_complete") else: @@ -89,12 +94,45 @@ def input_code_with_completion(prompt, input_helper, code_length): used_completion = c.used_completion if c else False return (code, used_completion) +def warn_readline(): + # When our process receives a SIGINT, Twisted's SIGINT handler will + # stop the reactor and wait for all threads to terminate before the + # process exits. However, if we were waiting for + # input_code_with_completion() when SIGINT happened, the readline + # thread will be blocked waiting for something on stdin. Trick the + # user into satisfying the blocking read so we can exit. + print("\nCommand interrupted: please press Return to quit", + file=sys.stderr) + + # Other potential approaches to this problem: + # * hard-terminate our process with os._exit(1), but make sure the + # tty gets reset to a normal mode ("cooked"?) first, so that the + # next shell command the user types is echoed correctly + # * track down the thread (t.p.threadable.getThreadID from inside the + # thread), get a cffi binding to pthread_kill, deliver SIGINT to it + # * allocate a pty pair (pty.openpty), replace sys.stdin with the + # slave, build a pty bridge that copies bytes (and other PTY + # things) from the real stdin to the master, then close the slave + # at shutdown, so readline sees EOF + # * write tab-completion and basic editing (TTY raw mode, + # backspace-is-erase) without readline, probably with curses or + # twisted.conch.insults + # * write a separate program to get codes (maybe just "wormhole + # --internal-get-code"), run it as a subprocess, let it inherit + # stdin/stdout, send it SIGINT when we receive SIGINT ourselves. It + # needs an RPC mechanism (over some extra file descriptors) to ask + # us to fetch the current nameplate_id list. + # + # Note that hard-terminating our process with os.kill(os.getpid(), + # signal.SIGKILL), or SIGTERM, doesn't seem to work: the thread + # doesn't see the signal, and we must still wait for stdin to make + # readline finish. + @inlineCallbacks def rlcompleter_helper(prompt, input_helper, reactor): - def warn_readline(): - pass t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline) - res = yield deferToThread(input_code_with_completion, prompt, input_helper) + res = yield deferToThread(input_code_with_completion, prompt, input_helper, + reactor) (code, used_completion) = res reactor.removeSystemEventTrigger(t) returnValue(used_completion) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index c6ec093..a15a62a 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -164,9 +164,13 @@ class TwistedReceiver: if code: w.set_code(code) else: - raise NotImplemented - yield w.input_code("Enter receive wormhole code: ", # TODO - self.args.code_length) + from .._rlcompleter import rlcompleter_helper + used_completion = yield rlcompleter_helper("Enter receive wormhole code: ", + w.input_code(), + self._reactor) + if not used_completion: + print(" (note: you can use to complete words)", + file=self.args.stderr) yield w.when_code() def _show_verifier(self, verifier): From 9267c204e9da9dd3ef8e46de621c43c415655740 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Mar 2017 10:31:08 -0700 Subject: [PATCH 135/176] test_wormhole_new: oops, forgot yield on assertFailure This was breaking the last test that gets run (test_xfer_util) because of the lingering failures. --- src/wormhole/test/test_wormhole_new.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 7455acb..92333b6 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -36,7 +36,7 @@ class New(ServerBase, unittest.TestCase): mo = re.search(r"^\d+-\w+-\w+$", code) self.assert_(mo, code) # w.close() fails because we closed before connecting - self.assertFailure(w.close(), errors.LonelyError) + yield self.assertFailure(w.close(), errors.LonelyError) def test_delegated(self): dg = Delegate() @@ -94,6 +94,12 @@ class New(ServerBase, unittest.TestCase): w1.send(b"data") - self.assertFailure(w2.when_received(), errors.WrongPasswordError) - self.assertFailure(w1.close(), errors.WrongPasswordError) - self.assertFailure(w2.close(), errors.WrongPasswordError) + yield self.assertFailure(w2.when_received(), errors.WrongPasswordError) + # wait for w1.when_received, because if we close w1 before it has + # seen the VERSION message, we could legitimately get LonelyError + # instead of WrongPasswordError. w2 didn't send anything, so + # w1.when_received wouldn't ever callback, but it will errback when + # w1 gets the undecryptable VERSION. + yield self.assertFailure(w1.when_received(), errors.WrongPasswordError) + yield self.assertFailure(w1.close(), errors.WrongPasswordError) + yield self.assertFailure(w2.close(), errors.WrongPasswordError) From 351a523d0b6e2df88b50ad75e62a7f4552c402f7 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Mar 2017 10:31:41 -0700 Subject: [PATCH 136/176] disable test_slow_text for now --- src/wormhole/test/test_scripts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index db52693..6568bf9 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -536,6 +536,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): def test_slow_text(self): return self._do_test(mode="slow-text") + test_slow_text.skip = "pending rethink" @inlineCallbacks def _do_test_fail(self, mode, failmode): From 17b4ff9893ee942aa5fc5d5cd9e10e9532a71737 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Mar 2017 16:07:53 -0700 Subject: [PATCH 137/176] rework completion, vaguely works --- src/wormhole/_rlcompleter.py | 134 ++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index 1e6db1b..9191029 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -5,6 +5,16 @@ from attr import attrs, attrib from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.threads import deferToThread, blockingCallFromThread +import os +errf = None +if os.path.exists("err"): + errf = open("err", "w") +def debug(*args, **kwargs): + if errf: + kwargs["file"] = errf + print(*args, **kwargs) + errf.flush() + @attrs class CodeInputter(object): _input_helper = attrib() @@ -12,7 +22,8 @@ class CodeInputter(object): def __attrs_post_init__(self): self.used_completion = False self._matches = None - self._committed_nameplate = None + # once we've claimed the nameplate, we can't go back + self._committed_nameplate = None # or string def bcft(self, f, *a, **kw): return blockingCallFromThread(self._reactor, f, *a, **kw) @@ -30,69 +41,94 @@ class CodeInputter(object): def completer(self, text, state): self.used_completion = True - # debug - #import readline - #if state == 0: - # print("", file=sys.stderr) - #print("completer: '%s' %d '%d'" % (text, state, - # readline.get_completion_type()), - # file=sys.stderr) - #sys.stderr.flush() - - if state > 0: - # just use the values we decided last time - if state >= len(self._matches): - return None - return self._matches[state] - - if not self._committed_nameplate: - self.bcft(self._input_helper.refresh_nameplates) - - # now figure out new matches - if not "-" in text: - completions = self.bcft(self._input_helper.get_nameplate_completions, - text) - # TODO: does rlcompleter want full strings, or the next suffix? - self._matches = sorted(completions) + import readline + ct = readline.get_completion_type() + if state == 0: + debug("completer starting (%s) (state=0) (ct=%d)" % (text, ct)) + self._matches = self._commit_and_build_completions(text) + debug(" matches:", " ".join(["'%s'" % m for m in self._matches])) else: - nameplate, words = text.split("-", 1) - if self._committed_nameplate: - if nameplate != self._committed_nameplate: - # they deleted past the committment point: we can't use - # this. For now, bail, but in the future let's find a - # gentler way to encourage them to not do that. - raise ValueError("nameplate (NN-) already entered, cannot go back") - # they've just committed to this nameplate - self.bcft(self._input_helper.choose_nameplate, nameplate) - self._committed_nameplate = nameplate - completions = self.bcft(self._input_helper.get_word_completions, - words) - self._matches = sorted(completions) + debug(" s%d t'%s' ct=%d" % (state, text, ct)) - #print(" match: '%s'" % self._matches[state], file=sys.stderr) - #sys.stderr.flush() + if state >= len(self._matches): + debug(" returning None") + return None + debug(" returning '%s'" % self._matches[state]) return self._matches[state] + def _commit_and_build_completions(self, text): + ih = self._input_helper + if "-" in text: + got_nameplate = True + nameplate, words = text.split("-", 1) + else: + got_nameplate = False + nameplate = text # partial + + # 'text' is one of these categories: + # "": complete on nameplates + # "12": complete on nameplates + # "123-": commit to nameplate (if not already), complete on words + + if self._committed_nameplate: + if not got_nameplate or nameplate != self._committed_nameplate: + # they deleted past the committment point: we can't use + # this. For now, bail, but in the future let's find a + # gentler way to encourage them to not do that. + raise ValueError("nameplate (NN-) already entered, cannot go back") + if not got_nameplate: + # we're completing on nameplates + self.bcft(ih.refresh_nameplates) # results arrive later + debug(" getting nameplates") + completions = self.bcft(ih.get_nameplate_completions, nameplate) + else: + # time to commit to this nameplate, if they haven't already + if not self._committed_nameplate: + debug(" chose_nameplate", nameplate) + self.bcft(ih.choose_nameplate, nameplate) + self._committed_nameplate = nameplate + # and we're completing on words now + debug(" getting words") + completions = self.bcft(ih.get_word_completions, words) + + # rlcompleter wants full strings + return sorted([text+c for c in completions]) + + def finish(self, text): + if "-" not in text: + raise ValueError("incomplete wormhole code") + nameplate, words = text.split("-", 1) + + if self._committed_nameplate: + if nameplate != self._committed_nameplate: + # they deleted past the committment point: we can't use + # this. For now, bail, but in the future let's find a + # gentler way to encourage them to not do that. + raise ValueError("nameplate (NN-) already entered, cannot go back") + else: + self._input_helper.choose_nameplate(nameplate) + self._input_helper.choose_words(words) + def input_code_with_completion(prompt, input_helper, reactor): + c = CodeInputter(input_helper, reactor) try: import readline - c = CodeInputter(input_helper, reactor) if readline.__doc__ and "libedit" in readline.__doc__: readline.parse_and_bind("bind ^I rl_complete") else: readline.parse_and_bind("tab: complete") readline.set_completer(c.wrap_completer) readline.set_completer_delims("") + debug("==== readline-based completion is prepared") except ImportError: - c = None + debug("==== unable to import readline, disabling completion") + pass code = six.moves.input(prompt) # Code is str(bytes) on py2, and str(unicode) on py3. We want unicode. if isinstance(code, bytes): code = code.decode("utf-8") - nameplate, words = code.split("-", 1) - input_helper.choose_words(words) - used_completion = c.used_completion if c else False - return (code, used_completion) + c.finish(code) + return c.used_completion def warn_readline(): # When our process receives a SIGINT, Twisted's SIGINT handler will @@ -131,8 +167,8 @@ def warn_readline(): @inlineCallbacks def rlcompleter_helper(prompt, input_helper, reactor): t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline) - res = yield deferToThread(input_code_with_completion, prompt, input_helper, - reactor) - (code, used_completion) = res + #input_helper.refresh_nameplates() + used_completion = yield deferToThread(input_code_with_completion, + prompt, input_helper, reactor) reactor.removeSystemEventTrigger(t) returnValue(used_completion) From 0ed363c8947849e1e418e104505bfea3baabfd36 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Mar 2017 16:08:18 -0700 Subject: [PATCH 138/176] Key: sort messages to ensure got_code lands before got_pake Since input_code() sets the nameplate before setting the rest of the code, and since the sender's PAKE will arrive as soon as the nameplate is set, we could got_pake before got_code, and Key wasn't prepared to handle that. --- docs/state-machines/key.dot | 28 ++++++++++++++++--- src/wormhole/_boss.py | 2 +- src/wormhole/_key.py | 54 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/docs/state-machines/key.dot b/docs/state-machines/key.dot index b6d49f8..01f5f2d 100644 --- a/docs/state-machines/key.dot +++ b/docs/state-machines/key.dot @@ -10,6 +10,26 @@ digraph { start [label="Key\nMachine" style="dotted"] + /* two connected state machines: the first just puts the messages in + the right order, the second handles PAKE */ + + {rank=same; SO_00 PO_got_code SO_10} + {rank=same; SO_01 PO_got_both SO_11} + SO_00 [label="S00"] + SO_01 [label="S01: pake"] + SO_10 [label="S10: code"] + SO_11 [label="S11: both"] + SO_00 -> SO_01 [label="got_pake\n(early)"] + SO_00 -> PO_got_code [label="got_code"] + PO_got_code [shape="box" label="K1.got_code"] + PO_got_code -> SO_10 + SO_01 -> PO_got_both [label="got_code"] + PO_got_both [shape="box" label="K1.got_code\nK1.got_pake"] + PO_got_both -> SO_11 + SO_10 -> PO_got_pake [label="got_pake"] + PO_got_pake [shape="box" label="K1.got_pake"] + PO_got_pake -> SO_11 + S0 [label="S0: know\nnothing"] S0 -> P0_build [label="got_code"] @@ -30,14 +50,14 @@ digraph { S1 -> P_mood_scary [label="got_pake\npake bad"] P_mood_scary [shape="box" color="red" label="W.scared"] - P_mood_scary -> S3 [color="red"] - S3 [label="S3:\nscared" color="red"] + P_mood_scary -> S5 [color="red"] + S5 [label="S5:\nscared" color="red"] S1 -> P1_compute [label="got_pake\npake good"] #S1 -> P_mood_lonely [label="close"] P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nB.got_verifier\nR.got_key" shape="box"] - P1_compute -> S2 + P1_compute -> S4 - S2 [label="S2: know_key" color="green"] + S4 [label="S4: know_key" color="green"] } diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index a101cb3..a48ad11 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -88,7 +88,7 @@ class Boss(object): def _set_trace(self, client_name, which, logger): names = {"B": self, "N": self._N, "M": self._M, "S": self._S, - "O": self._O, "K": self._K, "R": self._R, + "O": self._O, "K": self._K, "SK": self._K._SK, "R": self._R, "RC": self._RC, "L": self._L, "C": self._C, "T": self._T} for machine in which.split(): diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 2436de4..330833f 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -52,9 +52,63 @@ def encrypt_data(key, plaintext): nonce = utils.random(SecretBox.NONCE_SIZE) return box.encrypt(plaintext, nonce) +# the Key we expose to callers (Boss, Ordering) is responsible for sorting +# the two messages (got_code and got_pake), then delivering them to +# _SortedKey in the right order. + @attrs @implementer(_interfaces.IKey) class Key(object): + _appid = attrib(validator=instance_of(type(u""))) + _versions = attrib(validator=instance_of(dict)) + _side = attrib(validator=instance_of(type(u""))) + _timing = attrib(validator=provides(_interfaces.ITiming)) + m = MethodicalMachine() + @m.setTrace() + def _set_trace(): pass # pragma: no cover + + def __attrs_post_init__(self): + self._SK = _SortedKey(self._appid, self._versions, self._side, + self._timing) + + def wire(self, boss, mailbox, receive): + self._SK.wire(boss, mailbox, receive) + + @m.state(initial=True) + def S00(self): pass # pragma: no cover + @m.state() + def S01(self): pass # pragma: no cover + @m.state() + def S10(self): pass # pragma: no cover + @m.state() + def S11(self): pass # pragma: no cover + + @m.input() + def got_code(self, code): pass + @m.input() + def got_pake(self, body): pass + + @m.output() + def stash_pake(self, body): + self._pake = body + @m.output() + def deliver_code(self, code): + self._SK.got_code(code) + @m.output() + def deliver_pake(self, body): + self._SK.got_pake(body) + @m.output() + def deliver_code_and_stashed_pake(self, code): + self._SK.got_code(code) + self._SK.got_pake(self._pake) + + S00.upon(got_code, enter=S10, outputs=[deliver_code]) + S10.upon(got_pake, enter=S11, outputs=[deliver_pake]) + S00.upon(got_pake, enter=S01, outputs=[stash_pake]) + S01.upon(got_code, enter=S11, outputs=[deliver_code_and_stashed_pake]) + +@attrs +class _SortedKey(object): _appid = attrib(validator=instance_of(type(u""))) _versions = attrib(validator=instance_of(dict)) _side = attrib(validator=instance_of(type(u""))) From b38d4c94cab64363b296b24796c113d6c24050f5 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Mar 2017 16:09:30 -0700 Subject: [PATCH 139/176] Code: deliver got_code to Boss before Key So when Key sends got_key to Boss, Boss will be ready for it test_machines: match new delivery order --- src/wormhole/_code.py | 6 +++--- src/wormhole/test/test_machines.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index edcac03..e026460 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -56,8 +56,8 @@ class Code(object): def do_set_code(self, code): nameplate = code.split("-", 2)[0] self._N.set_nameplate(nameplate) - self._K.got_code(code) self._B.got_code(code) + self._K.got_code(code) @m.output() def do_start_input(self): @@ -67,8 +67,8 @@ class Code(object): self._N.set_nameplate(nameplate) @m.output() def do_finish_input(self, code): - self._K.got_code(code) self._B.got_code(code) + self._K.got_code(code) @m.output() def do_start_allocate(self, length, wordlist): @@ -77,8 +77,8 @@ class Code(object): def do_finish_allocate(self, nameplate, code): assert code.startswith(nameplate+"-"), (nameplate, code) self._N.set_nameplate(nameplate) - self._K.got_code(code) self._B.got_code(code) + self._K.got_code(code) S0_idle.upon(set_code, enter=S4_known, outputs=[do_set_code]) S0_idle.upon(input_code, enter=S1_inputting_nameplate, diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 0054833..c7b0332 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -273,8 +273,8 @@ class Code(unittest.TestCase): c, b, a, n, k, i, events = self.build() c.set_code(u"1-code") self.assertEqual(events, [("n.set_nameplate", u"1"), - ("k.got_code", u"1-code"), ("b.got_code", u"1-code"), + ("k.got_code", u"1-code"), ]) def test_allocate_code(self): @@ -285,8 +285,8 @@ class Code(unittest.TestCase): events[:] = [] c.allocated("1", "1-code") self.assertEqual(events, [("n.set_nameplate", u"1"), - ("k.got_code", u"1-code"), ("b.got_code", u"1-code"), + ("k.got_code", u"1-code"), ]) def test_input_code(self): @@ -299,8 +299,8 @@ class Code(unittest.TestCase): ]) events[:] = [] c.finished_input("1-code") - self.assertEqual(events, [("k.got_code", u"1-code"), - ("b.got_code", u"1-code"), + self.assertEqual(events, [("b.got_code", u"1-code"), + ("k.got_code", u"1-code"), ]) class Input(unittest.TestCase): From 3ca2720f112b28496abc43cd319a03ea37259e90 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 22 Mar 2017 16:10:02 -0700 Subject: [PATCH 140/176] Input: more debug text --- src/wormhole/_input.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index 3c03dd2..8a8da3a 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -85,6 +85,8 @@ class Input(object): self._C.got_nameplate(nameplate) @m.output() def record_wordlist(self, wordlist): + from ._rlcompleter import debug + debug(" -record_wordlist") self._wordlist = wordlist @m.output() From d44a5335b4ec31ec732bf9381a8da0237e92f1a8 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 30 Mar 2017 13:17:34 -0700 Subject: [PATCH 141/176] _mailbox: new Automat forbids code in Input bodies --- src/wormhole/_mailbox.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 90f9d95..061a39c 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -95,8 +95,6 @@ class Mailbox(object): # from Send or Key @m.input() def add_message(self, phase, body): - assert isinstance(body, type(b"")), type(body) - #print("ADD_MESSAGE", phase, len(body)) pass From 271efb602594ef4b26e5affbcd1fd8667da1f814 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 30 Mar 2017 13:18:49 -0700 Subject: [PATCH 142/176] match new Automat tracing API (in glyph/automat#56 PR) --- src/wormhole/_allocator.py | 3 +-- src/wormhole/_boss.py | 22 +++++++++++++++++----- src/wormhole/_code.py | 3 +-- src/wormhole/_input.py | 3 +-- src/wormhole/_key.py | 6 ++---- src/wormhole/_lister.py | 3 +-- src/wormhole/_mailbox.py | 3 +-- src/wormhole/_nameplate.py | 3 +-- src/wormhole/_order.py | 3 +-- src/wormhole/_receive.py | 3 +-- src/wormhole/_rendezvous.py | 2 +- src/wormhole/_send.py | 3 +-- src/wormhole/_terminator.py | 3 +-- 13 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/wormhole/_allocator.py b/src/wormhole/_allocator.py index 040b913..b671692 100644 --- a/src/wormhole/_allocator.py +++ b/src/wormhole/_allocator.py @@ -10,8 +10,7 @@ from . import _interfaces class Allocator(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def wire(self, rendezvous_connector, code): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index a48ad11..c8a7ab0 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -38,8 +38,7 @@ class Boss(object): _tor_manager = attrib() # TODO: ITorManager or None _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._build_workers() @@ -92,9 +91,22 @@ class Boss(object): "RC": self._RC, "L": self._L, "C": self._C, "T": self._T} for machine in which.split(): - def tracer(old_state, input, new_state, machine=machine): - print("%s.%s[%s].%s -> [%s]" % (client_name, machine, - old_state, input, new_state)) + def tracer(old_state, input, new_state, output, machine=machine): + if output is None: + if new_state: + print("%s.%s[%s].%s -> [%s]" % + (client_name, machine, old_state, input, + new_state)) + else: + # the RendezvousConnector emits message events as if + # they were state transitions, except that old_state + # and new_state are empty strings. "input" is one of + # R.connected, R.rx(type phase+side), R.tx(type + # phase), R.lost . + print("%s.%s.%s" % (client_name, machine, input)) + else: + if new_state: + print(" %s.%s.%s()" % (client_name, machine, output)) names[machine].set_trace(tracer) def serialize(self): diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index e026460..7ff9603 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -13,8 +13,7 @@ def first(outputs): class Code(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def wire(self, boss, allocator, nameplate, key, input): self._B = _interfaces.IBoss(boss) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index 8a8da3a..dab364f 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -13,8 +13,7 @@ def first(outputs): class Input(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._all_nameplates = set() diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 330833f..454808b 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -64,8 +64,7 @@ class Key(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def _set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._SK = _SortedKey(self._appid, self._versions, self._side, @@ -114,8 +113,7 @@ class _SortedKey(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def wire(self, boss, mailbox, receive): self._B = _interfaces.IBoss(boss) diff --git a/src/wormhole/_lister.py b/src/wormhole/_lister.py index a58ecf6..dfca877 100644 --- a/src/wormhole/_lister.py +++ b/src/wormhole/_lister.py @@ -10,8 +10,7 @@ from . import _interfaces class Lister(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def wire(self, rendezvous_connector, input): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 061a39c..3c5413d 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -10,8 +10,7 @@ from . import _interfaces class Mailbox(object): _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._mailbox = None diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 599573b..00f2f7e 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -7,8 +7,7 @@ from ._wordlist import PGPWordList @implementer(_interfaces.INameplate) class Nameplate(object): m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __init__(self): self._nameplate = None diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index 81cb088..26671a1 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -11,8 +11,7 @@ class Order(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._key = None diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 389c0f4..7a78518 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -12,8 +12,7 @@ class Receive(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._key = None diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index 9af0edb..c4e5c45 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -86,7 +86,7 @@ class RendezvousConnector(object): self._trace = f def _debug(self, what): if self._trace: - self._trace(old_state="", input=what, new_state="") + self._trace(old_state="", input=what, new_state="", output=None) def _make_endpoint(self, hostname, port): if self._tor_manager: diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index ccd4699..3c8c722 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -12,8 +12,7 @@ class Send(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __attrs_post_init__(self): self._queue = [] diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py index 1468c07..3a14042 100644 --- a/src/wormhole/_terminator.py +++ b/src/wormhole/_terminator.py @@ -6,8 +6,7 @@ from . import _interfaces @implementer(_interfaces.ITerminator) class Terminator(object): m = MethodicalMachine() - @m.setTrace() - def set_trace(): pass # pragma: no cover + set_trace = m.setTrace def __init__(self): self._mood = None From bbef68c11ed55a81e1be9595ba070d2d30efb4f2 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 30 Mar 2017 17:25:07 -0700 Subject: [PATCH 143/176] remove no-longer-relevant tests --- src/wormhole/test/test_wormhole.py | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 3033903..57c3a77 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -103,40 +103,6 @@ class Welcome(unittest.TestCase): # alas WelcomeError instances don't compare against each other #self.assertEqual(se.mock_calls, [mock.call(WelcomeError("oops"))]) -class InputCode(unittest.TestCase): - def test_list(self): - send_command = mock.Mock() - stderr = io.StringIO() - ic = wormhole._InputCode(None, "prompt", 2, send_command, - DebugTiming(), stderr) - d = ic._list() - self.assertNoResult(d) - self.assertEqual(send_command.mock_calls, [mock.call("list")]) - ic._response_handle_nameplates({"type": "nameplates", - "nameplates": [{"id": "123"}]}) - res = self.successResultOf(d) - self.assertEqual(res, ["123"]) - self.assertEqual(stderr.getvalue(), "") -InputCode.skip = "not yet" - -class GetCode(unittest.TestCase): - def test_get(self): - send_command = mock.Mock() - gc = wormhole._GetCode(2, send_command, DebugTiming()) - d = gc.go() - self.assertNoResult(d) - self.assertEqual(send_command.mock_calls, [mock.call("allocate")]) - # TODO: nameplate attributes get added and checked here - gc._response_handle_allocated({"type": "allocated", - "nameplate": "123"}) - code = self.successResultOf(d) - self.assertIsInstance(code, type("")) - self.assert_(code.startswith("123-")) - pieces = code.split("-") - self.assertEqual(len(pieces), 3) # nameplate plus two words - self.assert_(re.search(r'^\d+-\w+-\w+$', code), code) -GetCode.skip = "not yet" - class Basic(unittest.TestCase): def tearDown(self): # flush out any errorful Deferreds left dangling in cycles From 152775c5c0f1980b1b239353ef30ed118af736d7 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 17:44:00 -0700 Subject: [PATCH 144/176] hush pyflakes --- src/wormhole/test/test_wormhole.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 57c3a77..189edf3 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -1,5 +1,5 @@ from __future__ import print_function, unicode_literals -import os, json, re, gc, io +import os, json, re, gc from binascii import hexlify, unhexlify import mock from twisted.trial import unittest From 76f5960517327abf1dd9242f84fd7ec34d0d7bf2 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 14:23:03 -0700 Subject: [PATCH 145/176] rewrite welcome handler --- docs/api.md | 69 ++++++++++++++++++++++++++++ docs/state-machines/boss.dot | 8 +++- src/wormhole/_boss.py | 48 +++++++++++++------- src/wormhole/cli/cmd_receive.py | 8 +++- src/wormhole/cli/cmd_send.py | 8 +++- src/wormhole/cli/welcome.py | 24 ++++++++++ src/wormhole/test/test_cli.py | 45 +++++++++++++++++++ src/wormhole/test/test_scripts.py | 5 ++- src/wormhole/test/test_wormhole.py | 72 +++++------------------------- src/wormhole/wormhole.py | 37 +++------------ 10 files changed, 211 insertions(+), 113 deletions(-) create mode 100644 src/wormhole/cli/welcome.py create mode 100644 src/wormhole/test/test_cli.py diff --git a/docs/api.md b/docs/api.md index 47fae44..12aad01 100644 --- a/docs/api.md +++ b/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 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 As the wormhole connection is established, several events may be dispatched diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index e22414b..866f0e8 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -22,6 +22,10 @@ digraph { P_close_error -> S_closing 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 -> S1 S1 [label="S1: lonely" color="orange"] @@ -30,6 +34,7 @@ digraph { S1 -> P_close_error [label="rx_error"] S1 -> P_close_scary [label="scared" color="red"] + S1 -> P_close_unwelcome [label="rx_unwelcome"] S1 -> P_close_lonely [label="close"] P_close_lonely [shape="box" label="T.close(lonely)"] P_close_lonely -> S_closing @@ -52,6 +57,7 @@ digraph { S2 -> P_close_error [label="rx_error"] S2 -> P_close_scary [label="scared" color="red"] + S2 -> P_close_unwelcome [label="rx_unwelcome"] S_closing [label="closing"] S_closing -> P_closed [label="closed\nerror"] @@ -67,7 +73,7 @@ digraph { {rank=same; Other S_closed} 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" ] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index c8a7ab0..f10c125 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -21,7 +21,8 @@ from ._code import Code from ._terminator import Terminator from ._wordlist import PGPWordList from .errors import (ServerError, LonelyError, WrongPasswordError, - KeyFormatError, OnlyOneCodeError, _UnknownPhaseError) + KeyFormatError, OnlyOneCodeError, _UnknownPhaseError, + WelcomeError) from .util import bytes_to_dict @attrs @@ -162,11 +163,28 @@ class Boss(object): @m.input() def close(self): pass - # from RendezvousConnector. rx_error an error message from the server - # (probably because of something we did, or due to CrowdedError). error - # is when an exception happened while it tried to deliver something else + # from RendezvousConnector: + # * "rx_welcome" is the Welcome message, which might signal an error, or + # 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() - def rx_welcome(self, welcome): pass + def rx_unwelcome(self, welcome_error): pass @m.input() def rx_error(self, errmsg, orig): pass @m.input() @@ -207,11 +225,6 @@ class Boss(object): @m.input() def closed(self): pass - - @m.output() - def process_welcome(self, welcome): - self._welcome_handler(welcome) - @m.output() def do_got_code(self, code): self._W.got_code(code) @@ -232,6 +245,11 @@ class Boss(object): self._S.send("%d" % phase, plaintext) @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): self._result = ServerError(errmsg) self._T.close("errory") @@ -275,12 +293,12 @@ class Boss(object): S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely]) 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(rx_error, enter=S3_closing, outputs=[close_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(scared, enter=S3_closing, outputs=[close_scared]) 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(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_version, enter=S2_happy, outputs=[process_version]) 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(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(_got_phase, 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(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_version, enter=S4_closed, outputs=[]) S4_closed.upon(happy, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index a15a62a..49b083f 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -5,11 +5,12 @@ from humanize import naturalsize from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue from twisted.python import log -from .. import wormhole +from .. import wormhole, __version__ 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 APPID = u"lothar.com/wormhole/text-or-file-xfer" VERIFY_TIMER = 1 @@ -61,10 +62,13 @@ class TwistedReceiver: # with the user handing off the wormhole code 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, self._reactor, tor_manager=self._tor_manager, - timing=self.args.timing) + timing=self.args.timing, + welcome_handler=wh.handle_welcome) # I wanted to do this instead: # # try: diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index 3c9e814..85877d7 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -7,9 +7,10 @@ from twisted.protocols import basic from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue from ..errors import TransferError, WormholeClosedError, NoTorError -from .. import wormhole +from .. import wormhole, __version__ from ..transit import TransitSender 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" VERIFY_TIMER = 1 @@ -52,10 +53,13 @@ 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 = wormhole.create(self._args.appid or APPID, self._args.relay_url, self._reactor, tor_manager=self._tor_manager, - timing=self._timing) + timing=self._timing, + welcome_handler=wh.handle_welcome) d = self._go(w) # if we succeed, we should close and return the w.close results diff --git a/src/wormhole/cli/welcome.py b/src/wormhole/cli/welcome.py new file mode 100644 index 0000000..f763eae --- /dev/null +++ b/src/wormhole/cli/welcome.py @@ -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) + diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py new file mode 100644 index 0000000..2d8d4db --- /dev/null +++ b/src/wormhole/test/test_cli.py @@ -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") diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 6568bf9..142bd95 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -713,6 +713,9 @@ class NotWelcome(ServerBase, unittest.TestCase): send_d = cmd_send.send(self.cfg) f = yield self.assertFailure(send_d, WelcomeError) 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 def test_receiver(self): @@ -721,7 +724,7 @@ class NotWelcome(ServerBase, unittest.TestCase): receive_d = cmd_receive.receive(self.cfg) f = yield self.assertFailure(receive_d, WelcomeError) self.assertEqual(str(f), "please upgrade XYZ") -NotWelcome.skip = "not yet" + self.flushLoggedErrors(WelcomeError) class Cleanup(ServerBase, unittest.TestCase): diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 189edf3..99dee58 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -1,5 +1,5 @@ from __future__ import print_function, unicode_literals -import os, json, re, gc +import os, json, re, gc, io from binascii import hexlify, unhexlify import mock from twisted.trial import unittest @@ -38,70 +38,20 @@ def response(w, **kwargs): class Welcome(unittest.TestCase): def test_tolerate_no_current_version(self): - w = wormhole._WelcomeHandler("relay_url", "current_cli_version", None) + w = wormhole._WelcomeHandler("relay_url") w.handle_welcome({}) def test_print_motd(self): - w = wormhole._WelcomeHandler("relay_url", "current_cli_version", None) - with mock.patch("sys.stderr") as stderr: - w.handle_welcome({"motd": "message of\nthe day"}) - self.assertEqual(stderr.method_calls, - [mock.call.write("Server (at relay_url) says:\n" - " message of\n the day"), - mock.call.write("\n")]) + 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 - with mock.patch("sys.stderr") as stderr2: - w.handle_welcome({"motd": "second message"}) - self.assertEqual(stderr2.method_calls, - [mock.call.write("Server (at relay_url) says:\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"))]) + 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")) class Basic(unittest.TestCase): def tearDown(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index dc3daf9..7a1c150 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -10,7 +10,7 @@ from .timing import DebugTiming from .journal import ImmediateJournal from ._boss import Boss from ._key import derive_key -from .errors import WelcomeError, NoKeyError, WormholeClosed +from .errors import NoKeyError, WormholeClosed from .util import to_bytes # 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)) class _WelcomeHandler: - def __init__(self, url, current_version, signal_error): - self._ws_url = url - self._version_warning_displayed = False - self._current_version = current_version - self._signal_error = signal_error + 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._ws_url, motd_formatted), file=sys.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") + (self.relay_url, motd_formatted), file=self.stderr) @attrs @implementer(IWormhole) @@ -118,8 +100,6 @@ class _DelegatedWormhole(object): # from below def got_code(self, code): self._delegate.wormhole_code(code) - def got_welcome(self, welcome): - pass # TODO def got_key(self, key): self._key = key # for derive_key() def got_verifier(self, verifier): @@ -232,8 +212,6 @@ class _DeferredWormhole(object): for d in self._code_observers: d.callback(code) self._code_observers[:] = [] - def got_welcome(self, welcome): - pass # TODO def got_key(self, key): self._key = key # for derive_key() def got_verifier(self, verifier): @@ -280,10 +258,7 @@ def create(appid, relay_url, reactor, versions={}, side = bytes_to_hexstr(os.urandom(5)) journal = journal or ImmediateJournal() if not welcome_handler: - from . import __version__ - signal_error = NotImplemented # TODO - wh = _WelcomeHandler(relay_url, __version__, signal_error) - welcome_handler = wh.handle_welcome + welcome_handler = _WelcomeHandler(relay_url).handle_welcome if delegate: w = _DelegatedWormhole(delegate) else: From 228e0ed67183f6b751099ae15e590122406b0ff2 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 18:40:55 -0700 Subject: [PATCH 146/176] set_trace: tolerate an Automat that lacks m.setTrace wormhole.debug_set_trace() won't work until glyph/automat#56 lands, but this should let travis do its job in the meantime. --- src/wormhole/_allocator.py | 2 +- src/wormhole/_boss.py | 2 +- src/wormhole/_code.py | 2 +- src/wormhole/_input.py | 2 +- src/wormhole/_key.py | 4 ++-- src/wormhole/_lister.py | 2 +- src/wormhole/_mailbox.py | 2 +- src/wormhole/_nameplate.py | 2 +- src/wormhole/_order.py | 2 +- src/wormhole/_receive.py | 2 +- src/wormhole/_send.py | 2 +- src/wormhole/_terminator.py | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/wormhole/_allocator.py b/src/wormhole/_allocator.py index b671692..0644c55 100644 --- a/src/wormhole/_allocator.py +++ b/src/wormhole/_allocator.py @@ -10,7 +10,7 @@ from . import _interfaces class Allocator(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def wire(self, rendezvous_connector, code): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index f10c125..74b5f68 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -39,7 +39,7 @@ class Boss(object): _tor_manager = attrib() # TODO: ITorManager or None _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._build_workers() diff --git a/src/wormhole/_code.py b/src/wormhole/_code.py index 7ff9603..b2a9a20 100644 --- a/src/wormhole/_code.py +++ b/src/wormhole/_code.py @@ -13,7 +13,7 @@ def first(outputs): class Code(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def wire(self, boss, allocator, nameplate, key, input): self._B = _interfaces.IBoss(boss) diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index dab364f..82158a3 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -13,7 +13,7 @@ def first(outputs): class Input(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._all_nameplates = set() diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 454808b..67433ec 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -64,7 +64,7 @@ class Key(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._SK = _SortedKey(self._appid, self._versions, self._side, @@ -113,7 +113,7 @@ class _SortedKey(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def wire(self, boss, mailbox, receive): self._B = _interfaces.IBoss(boss) diff --git a/src/wormhole/_lister.py b/src/wormhole/_lister.py index dfca877..cd1a560 100644 --- a/src/wormhole/_lister.py +++ b/src/wormhole/_lister.py @@ -10,7 +10,7 @@ from . import _interfaces class Lister(object): _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def wire(self, rendezvous_connector, input): self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) diff --git a/src/wormhole/_mailbox.py b/src/wormhole/_mailbox.py index 3c5413d..3bca6fb 100644 --- a/src/wormhole/_mailbox.py +++ b/src/wormhole/_mailbox.py @@ -10,7 +10,7 @@ from . import _interfaces class Mailbox(object): _side = attrib(validator=instance_of(type(u""))) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._mailbox = None diff --git a/src/wormhole/_nameplate.py b/src/wormhole/_nameplate.py index 00f2f7e..8ee8025 100644 --- a/src/wormhole/_nameplate.py +++ b/src/wormhole/_nameplate.py @@ -7,7 +7,7 @@ from ._wordlist import PGPWordList @implementer(_interfaces.INameplate) class Nameplate(object): m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __init__(self): self._nameplate = None diff --git a/src/wormhole/_order.py b/src/wormhole/_order.py index 26671a1..5383a14 100644 --- a/src/wormhole/_order.py +++ b/src/wormhole/_order.py @@ -11,7 +11,7 @@ class Order(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._key = None diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 7a78518..1be5220 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -12,7 +12,7 @@ class Receive(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._key = None diff --git a/src/wormhole/_send.py b/src/wormhole/_send.py index 3c8c722..762b2fa 100644 --- a/src/wormhole/_send.py +++ b/src/wormhole/_send.py @@ -12,7 +12,7 @@ class Send(object): _side = attrib(validator=instance_of(type(u""))) _timing = attrib(validator=provides(_interfaces.ITiming)) m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __attrs_post_init__(self): self._queue = [] diff --git a/src/wormhole/_terminator.py b/src/wormhole/_terminator.py index 3a14042..f90f7e5 100644 --- a/src/wormhole/_terminator.py +++ b/src/wormhole/_terminator.py @@ -6,7 +6,7 @@ from . import _interfaces @implementer(_interfaces.ITerminator) class Terminator(object): m = MethodicalMachine() - set_trace = m.setTrace + set_trace = getattr(m, "setTrace", lambda self, f: None) def __init__(self): self._mood = None From b981b4260dd5204a1b5c96dab36dbe98d9393e4d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 18:53:10 -0700 Subject: [PATCH 147/176] docs: reminder that welcome_handler may be called multiple times --- docs/api.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index 12aad01..2b63f55 100644 --- a/docs/api.md +++ b/docs/api.md @@ -352,7 +352,9 @@ 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. +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. The default welcome handler will print `motd` to stderr, and will ignore `current_cli_version`. From d0d2992d4491791fd263517488842b2d32e8aff6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 19:07:53 -0700 Subject: [PATCH 148/176] fix some py2-isms that broke py3 This also changes the can-I-run-wormhole check to use C.UTF-8 instead of en_US.UTF-8, which seems necessary to hush Click on py3. See issue #127 for more discusson. --- src/wormhole/journal.py | 3 ++- src/wormhole/test/test_machines.py | 6 +++--- src/wormhole/test/test_scripts.py | 10 +++++----- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/wormhole/journal.py b/src/wormhole/journal.py index 7d640ea..61d1694 100644 --- a/src/wormhole/journal.py +++ b/src/wormhole/journal.py @@ -1,6 +1,7 @@ +from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer import contextlib -from _interfaces import IJournal +from ._interfaces import IJournal @implementer(IJournal) class Journal(object): diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index c7b0332..998ab47 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -228,7 +228,7 @@ class Key(unittest.TestCase): k.got_code(code) self.assertEqual(len(events), 1) self.assertEqual(events[0][:2], ("m.add_message", "pake")) - msg1_json = events[0][2] + msg1_json = events[0][2].decode("utf-8") events[:] = [] msg1 = json.loads(msg1_json) msg1_bytes = hexstr_to_bytes(msg1["pake_v1"]) @@ -249,9 +249,9 @@ class Key(unittest.TestCase): k.got_code(code) self.assertEqual(len(events), 1) self.assertEqual(events[0][:2], ("m.add_message", "pake")) - pake_1_json = events[0][2] + pake_1_json = events[0][2].decode("utf-8") pake_1 = json.loads(pake_1_json) - self.assertEqual(pake_1.keys(), ["pake_v1"]) # value is PAKE stuff + self.assertEqual(list(pake_1.keys()), ["pake_v1"]) # value is PAKE stuff events[:] = [] bad_pake_d = {"not_pake_v1": "stuff"} k.got_pake(dict_to_bytes(bad_pake_d)) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 142bd95..55b4ae4 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -175,11 +175,11 @@ class ScriptsBase: # Setting LANG/LC_ALL to a unicode-capable locale is necessary to # convince Click to not complain about a forced-ascii locale. My # apologies to folks who want to run tests on a machine that doesn't - # have the en_US.UTF-8 locale installed. + # have the C.UTF-8 locale installed. wormhole = self.find_executable() d = getProcessOutputAndValue(wormhole, ["--version"], - env=dict(LC_ALL="en_US.UTF-8", - LANG="en_US.UTF-8")) + env=dict(LC_ALL="C.UTF-8", + LANG="C.UTF-8")) def _check(res): out, err, rc = res if rc != 0: @@ -335,7 +335,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): send_d = getProcessOutputAndValue( wormhole_bin, send_args, path=send_dir, - env=dict(LC_ALL="en_US.UTF-8", LANG="en_US.UTF-8"), + env=dict(LC_ALL="C.UTF-8", LANG="C.UTF-8"), ) recv_args = [ '--relay-url', self.relayurl, @@ -351,7 +351,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): receive_d = getProcessOutputAndValue( wormhole_bin, recv_args, path=receive_dir, - env=dict(LC_ALL="en_US.UTF-8", LANG="en_US.UTF-8"), + env=dict(LC_ALL="C.UTF-8", LANG="C.UTF-8"), ) (send_res, receive_res) = yield gatherResults([send_d, receive_d], From 6eae5ecf64ef4185d05e9943fdd063f34a6d1149 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 22:52:26 -0700 Subject: [PATCH 149/176] better py2/py3 fix, use locale of C.UTF-8 or en_US.UTF-8 This updates the unit tests to checks the system (by running 'locale -a' just like Click does) to use a UTF-8 -safe locale. It prefers C.UTF-8 if available, then en_US.UTF-8, then will fall back to any UTF-8 it can find. My macOS box has en_US.UTF-8 (but not C.UTF-8), and my linux box has C.UTF-8 (but not en_US.UTF-8). This change doesn't help normal runtime, but ought to allow the unit tests to run on either platform correctly. --- src/wormhole/test/test_scripts.py | 82 +++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 20 deletions(-) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 55b4ae4..25708a8 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -6,7 +6,7 @@ from twisted.trial import unittest from twisted.python import procutils, log from twisted.internet import defer, endpoints, reactor from twisted.internet.utils import getProcessOutputAndValue -from twisted.internet.defer import gatherResults, inlineCallbacks +from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue from .. import __version__ from .common import ServerBase, config from ..cli import cmd_send, cmd_receive @@ -141,6 +141,45 @@ class OfferData(unittest.TestCase): self.assertEqual(str(e), "'%s' is neither file nor directory" % filename) +class LocaleFinder: + def __init__(self): + self._run_once = False + + @inlineCallbacks + def find_utf8_locale(self): + if self._run_once: + returnValue(self._best_locale) + self._best_locale = yield self._find_utf8_locale() + self._run_once = True + returnValue(self._best_locale) + + @inlineCallbacks + def _find_utf8_locale(self): + # Click really wants to be running under a unicode-capable locale, + # especially on python3. macOS has en-US.UTF-8 but not C.UTF-8, and + # most linux boxes have C.UTF-8 but not en-US.UTF-8 . For tests, + # figure out which one is present and use that. For runtime, it's a + # mess, as really the user must take responsibility for setting their + # locale properly. I'm thinking of abandoning Click and going back to + # twisted.python.usage to avoid this problem in the future. + (out, err, rc) = yield getProcessOutputAndValue("locale", ["-a"]) + if rc != 0: + log.msg("error running 'locale -a', rc=%s" % (rc,)) + log.msg("stderr: %s" % (err,)) + returnValue(None) + out = out.decode("utf-8") # make sure we get a string + utf8_locales = {} + for locale in out.splitlines(): + locale = locale.strip() + if locale.lower().endswith((".utf-8", ".utf8")): + utf8_locales[locale.lower()] = locale + for wanted in ["C.utf8", "C.UTF-8", "en_US.utf8", "en_US.UTF-8"]: + if wanted.lower() in utf8_locales: + returnValue(utf8_locales[wanted.lower()]) + if utf8_locales: + returnValue(list(utf8_locales.values())[0]) + returnValue(None) +locale_finder = LocaleFinder() class ScriptsBase: def find_executable(self): @@ -159,6 +198,7 @@ class ScriptsBase: % (wormhole, sys.executable)) return wormhole + @inlineCallbacks def is_runnable(self): # One property of Versioneer is that many changes to the source tree # (making a commit, dirtying a previously-clean tree) will change the @@ -176,20 +216,21 @@ class ScriptsBase: # convince Click to not complain about a forced-ascii locale. My # apologies to folks who want to run tests on a machine that doesn't # have the C.UTF-8 locale installed. + locale = yield locale_finder.find_utf8_locale() + if not locale: + raise unittest.SkipTest("unable to find UTF-8 locale") + locale_env = dict(LC_ALL=locale, LANG=locale) wormhole = self.find_executable() - d = getProcessOutputAndValue(wormhole, ["--version"], - env=dict(LC_ALL="C.UTF-8", - LANG="C.UTF-8")) - def _check(res): - out, err, rc = res - if rc != 0: - log.msg("wormhole not runnable in this tree:") - log.msg("out", out) - log.msg("err", err) - log.msg("rc", rc) - raise unittest.SkipTest("wormhole is not runnable in this tree") - d.addCallback(_check) - return d + res = yield getProcessOutputAndValue(wormhole, ["--version"], + env=locale_env) + out, err, rc = res + if rc != 0: + log.msg("wormhole not runnable in this tree:") + log.msg("out", out) + log.msg("err", err) + log.msg("rc", rc) + raise unittest.SkipTest("wormhole is not runnable in this tree") + returnValue(locale_env) class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase): # we need Twisted to run the server, but we run the sender and receiver @@ -204,7 +245,8 @@ class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase): wormhole = self.find_executable() # we must pass on the environment so that "something" doesn't # get sad about UTF8 vs. ascii encodings - out, err, rc = yield getProcessOutputAndValue(wormhole, ["--version"], env=os.environ) + out, err, rc = yield getProcessOutputAndValue(wormhole, ["--version"], + env=os.environ) err = err.decode("utf-8") if "DistributionNotFound" in err: log.msg("stderr was %s" % err) @@ -230,10 +272,10 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): # we need Twisted to run the server, but we run the sender and receiver # with deferToThread() + @inlineCallbacks def setUp(self): - d = self.is_runnable() - d.addCallback(lambda _: ServerBase.setUp(self)) - return d + self._env = yield self.is_runnable() + yield ServerBase.setUp(self) @inlineCallbacks def _do_test(self, as_subprocess=False, @@ -335,7 +377,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): send_d = getProcessOutputAndValue( wormhole_bin, send_args, path=send_dir, - env=dict(LC_ALL="C.UTF-8", LANG="C.UTF-8"), + env=self._env, ) recv_args = [ '--relay-url', self.relayurl, @@ -351,7 +393,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): receive_d = getProcessOutputAndValue( wormhole_bin, recv_args, path=receive_dir, - env=dict(LC_ALL="C.UTF-8", LANG="C.UTF-8"), + env=self._env, ) (send_res, receive_res) = yield gatherResults([send_d, receive_d], From 580c5a4712fd5d825175fe93db16f1e3d972b79b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 23:50:57 -0700 Subject: [PATCH 150/176] remove unused channel_monitor.py --- src/wormhole/channel_monitor.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/wormhole/channel_monitor.py diff --git a/src/wormhole/channel_monitor.py b/src/wormhole/channel_monitor.py deleted file mode 100644 index f5f4c50..0000000 --- a/src/wormhole/channel_monitor.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import print_function, unicode_literals -import sys -from weakref import ref - -class ChannelMonitor: - def __init__(self): - self._open_channels = set() - def add(self, w): - wr = ref(w, self._lost) - self._open_channels.add(wr) - def _lost(self, wr): - print("Error: a Wormhole instance was not closed", file=sys.stderr) - def close(self, w): - self._open_channels.discard(ref(w)) - -monitor = ChannelMonitor() # singleton From 8d47194612937ab18328243e3db67d7311574e75 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 3 Apr 2017 23:51:07 -0700 Subject: [PATCH 151/176] check when_version() can be called late as well as early --- src/wormhole/test/test_wormhole_new.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py index 92333b6..6e295ba 100644 --- a/src/wormhole/test/test_wormhole_new.py +++ b/src/wormhole/test/test_wormhole_new.py @@ -76,6 +76,9 @@ class New(ServerBase, unittest.TestCase): data2 = yield w1.when_received() self.assertEqual(data2, b"data2") + version1_again = yield w1.when_version() + self.assertEqual(version1, version1_again) + c1 = yield w1.close() self.assertEqual(c1, "happy") c2 = yield w2.close() From 8882e6f64e832c9796660f21050c5839a10c9047 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 4 Apr 2017 00:05:28 -0700 Subject: [PATCH 152/176] merge test_wormhole_new into test_wormhole --- src/wormhole/test/test_wormhole.py | 677 ++++--------------------- src/wormhole/test/test_wormhole_new.py | 108 ---- 2 files changed, 85 insertions(+), 700 deletions(-) delete mode 100644 src/wormhole/test/test_wormhole_new.py diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 99dee58..fc329d6 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -1,20 +1,14 @@ from __future__ import print_function, unicode_literals -import os, json, re, gc, io -from binascii import hexlify, unhexlify +import json, io, re import mock from twisted.trial import unittest from twisted.internet import reactor -from twisted.internet.defer import Deferred, gatherResults, inlineCallbacks +from twisted.internet.defer import gatherResults, inlineCallbacks from .common import ServerBase from .. import wormhole, _rendezvous -from ..errors import (WrongPasswordError, WelcomeError, InternalError, +from ..errors import (WrongPasswordError, KeyFormatError, WormholeClosed, LonelyError, NoKeyError, OnlyOneCodeError) -from spake2 import SPAKE2_Symmetric -from ..timing import DebugTiming -from ..util import (bytes_to_dict, dict_to_bytes, - hexstr_to_bytes, bytes_to_hexstr) -from nacl.secret import SecretBox APPID = "appid" @@ -53,587 +47,6 @@ class Welcome(unittest.TestCase): ("Server (at relay_url) says:\n message of\n the day\n" "Server (at relay_url) says:\n second message\n")) -class Basic(unittest.TestCase): - def tearDown(self): - # flush out any errorful Deferreds left dangling in cycles - gc.collect() - - def check_out(self, out, **kwargs): - # Assert that each kwarg is present in the 'out' dict. Ignore other - # keys ('msgid' in particular) - for key, value in kwargs.items(): - self.assertIn(key, out) - self.assertEqual(out[key], value, (out, key, value)) - - def check_outbound(self, ws, types): - out = ws.outbound() - self.assertEqual(len(out), len(types), (out, types)) - for i,t in enumerate(types): - self.assertEqual(out[i]["type"], t, (i,t,out)) - return out - - def make_pake(self, code, side, msg1): - sp2 = SPAKE2_Symmetric(wormhole.to_bytes(code), - idSymmetric=wormhole.to_bytes(APPID)) - msg2 = sp2.start() - key = sp2.finish(msg1) - return key, msg2 - - def test_create(self): - wormhole._Wormhole(APPID, "relay_url", reactor, None, None, None) - - def test_basic(self): - # We don't call w._start(), so this doesn't create a WebSocket - # connection. We provide a mock connection instead. If we wanted to - # exercise _connect, we'd mock out WSFactory. - # w._connect = lambda self: None - # w._event_connected(mock_ws) - # w._event_ws_opened() - # w._ws_dispatch_response(payload) - - timing = DebugTiming() - with mock.patch("wormhole.wormhole._WelcomeHandler") as wh_c: - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, - None) - wh = wh_c.return_value - self.assertEqual(w._ws_url, "relay_url") - self.assertTrue(w._flag_need_nameplate) - self.assertTrue(w._flag_need_to_build_msg1) - self.assertTrue(w._flag_need_to_send_PAKE) - - v = w.verify() - - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - out = ws.outbound() - self.assertEqual(len(out), 0) - - w._event_ws_opened(None) - out = ws.outbound() - self.assertEqual(len(out), 1) - self.check_out(out[0], type="bind", appid=APPID, side=w._side) - self.assertIn("id", out[0]) - - # WelcomeHandler should get called upon 'welcome' response. Its full - # behavior is exercised in 'Welcome' above. - WELCOME = {"foo": "bar"} - response(w, type="welcome", welcome=WELCOME) - self.assertEqual(wh.mock_calls, [mock.call.handle_welcome(WELCOME)]) - - # because we're connected, setting the code also claims the mailbox - CODE = "123-foo-bar" - w.set_code(CODE) - self.assertFalse(w._flag_need_to_build_msg1) - out = ws.outbound() - self.assertEqual(len(out), 1) - self.check_out(out[0], type="claim", nameplate="123") - - # the server reveals the linked mailbox - response(w, type="claimed", mailbox="mb456") - - # that triggers event_learned_mailbox, which should send open() and - # PAKE - self.assertEqual(w._mailbox_state, wormhole.OPEN) - out = ws.outbound() - self.assertEqual(len(out), 2) - self.check_out(out[0], type="open", mailbox="mb456") - self.check_out(out[1], type="add", phase="pake") - self.assertNoResult(v) - - # server echoes back all "add" messages - response(w, type="message", phase="pake", body=out[1]["body"], - side=w._side) - self.assertNoResult(v) - - # extract our outbound PAKE message - body = bytes_to_dict(hexstr_to_bytes(out[1]["body"])) - msg1 = hexstr_to_bytes(body["pake_v1"]) - - # next we build the simulated peer's PAKE operation - side2 = w._side + "other" - key, msg2 = self.make_pake(CODE, side2, msg1) - payload = {"pake_v1": bytes_to_hexstr(msg2)} - body_hex = bytes_to_hexstr(dict_to_bytes(payload)) - response(w, type="message", phase="pake", body=body_hex, side=side2) - - # hearing the peer's PAKE (msg2) makes us release the nameplate, send - # the confirmation message, and sends any queued phase messages. It - # doesn't deliver the verifier because we're still waiting on the - # confirmation message. - self.assertFalse(w._flag_need_to_see_mailbox_used) - self.assertEqual(w._key, key) - out = ws.outbound() - self.assertEqual(len(out), 2, out) - self.check_out(out[0], type="release") - self.check_out(out[1], type="add", phase="version") - self.assertNoResult(v) - - # hearing a valid confirmation message doesn't throw an error - plaintext = json.dumps({}).encode("utf-8") - data_key = w._derive_phase_key(side2, "version") - confmsg = w._encrypt_data(data_key, plaintext) - version2_hex = hexlify(confmsg).decode("ascii") - response(w, type="message", phase="version", body=version2_hex, - side=side2) - - # and it releases the verifier - verifier = self.successResultOf(v) - self.assertEqual(verifier, - w.derive_key("wormhole:verifier", SecretBox.KEY_SIZE)) - - # an outbound message can now be sent immediately - w.send(b"phase0-outbound") - out = ws.outbound() - self.assertEqual(len(out), 1) - self.check_out(out[0], type="add", phase="0") - # decrypt+check the outbound message - p0_outbound = unhexlify(out[0]["body"].encode("ascii")) - msgkey0 = w._derive_phase_key(w._side, "0") - p0_plaintext = w._decrypt_data(msgkey0, p0_outbound) - self.assertEqual(p0_plaintext, b"phase0-outbound") - - # get() waits for the inbound message to arrive - md = w.get() - self.assertNoResult(md) - self.assertIn("0", w._receive_waiters) - self.assertNotIn("0", w._received_messages) - msgkey1 = w._derive_phase_key(side2, "0") - p0_inbound = w._encrypt_data(msgkey1, b"phase0-inbound") - p0_inbound_hex = hexlify(p0_inbound).decode("ascii") - response(w, type="message", phase="0", body=p0_inbound_hex, - side=side2) - p0_in = self.successResultOf(md) - self.assertEqual(p0_in, b"phase0-inbound") - self.assertNotIn("0", w._receive_waiters) - self.assertIn("0", w._received_messages) - - # receiving an inbound message will queue it until get() is called - msgkey2 = w._derive_phase_key(side2, "1") - p1_inbound = w._encrypt_data(msgkey2, b"phase1-inbound") - p1_inbound_hex = hexlify(p1_inbound).decode("ascii") - response(w, type="message", phase="1", body=p1_inbound_hex, - side=side2) - self.assertIn("1", w._received_messages) - self.assertNotIn("1", w._receive_waiters) - p1_in = self.successResultOf(w.get()) - self.assertEqual(p1_in, b"phase1-inbound") - self.assertIn("1", w._received_messages) - self.assertNotIn("1", w._receive_waiters) - - d = w.close() - self.assertNoResult(d) - out = ws.outbound() - self.assertEqual(len(out), 1) - self.check_out(out[0], type="close", mood="happy") - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="released") - self.assertEqual(w._drop_connection.mock_calls, []) - response(w, type="closed") - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - w._ws_closed(True, None, None) - self.assertEqual(self.successResultOf(d), None) - - def test_close_wait_0(self): - # Close before the connection is established. The connection still - # gets established, but it is then torn down before sending anything. - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - - d = w.close() - self.assertNoResult(d) - - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - self.assertNoResult(d) - - w._ws_closed(True, None, None) - self.successResultOf(d) - - def test_close_wait_1(self): - # close before even claiming the nameplate - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - - d = w.close() - self.check_outbound(ws, ["bind"]) - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - self.assertNoResult(d) - - w._ws_closed(True, None, None) - self.successResultOf(d) - - def test_close_wait_2(self): - # Close after claiming the nameplate, but before opening the mailbox. - # The 'claimed' response arrives before we close. - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - CODE = "123-foo-bar" - w.set_code(CODE) - self.check_outbound(ws, ["bind", "claim"]) - - response(w, type="claimed", mailbox="mb123") - - d = w.close() - self.check_outbound(ws, ["open", "add", "release", "close"]) - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="released") - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="closed") - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - self.assertNoResult(d) - - w._ws_closed(True, None, None) - self.successResultOf(d) - - def test_close_wait_3(self): - # close after claiming the nameplate, but before opening the mailbox - # The 'claimed' response arrives after we start to close. - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - CODE = "123-foo-bar" - w.set_code(CODE) - self.check_outbound(ws, ["bind", "claim"]) - - d = w.close() - response(w, type="claimed", mailbox="mb123") - self.check_outbound(ws, ["release"]) - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="released") - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - self.assertNoResult(d) - - w._ws_closed(True, None, None) - self.successResultOf(d) - - def test_close_wait_4(self): - # close after both claiming the nameplate and opening the mailbox - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - CODE = "123-foo-bar" - w.set_code(CODE) - response(w, type="claimed", mailbox="mb456") - self.check_outbound(ws, ["bind", "claim", "open", "add"]) - - d = w.close() - self.check_outbound(ws, ["release", "close"]) - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="released") - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="closed") - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - - w._ws_closed(True, None, None) - self.successResultOf(d) - - def test_close_wait_5(self): - # close after claiming the nameplate, opening the mailbox, then - # releasing the nameplate - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - CODE = "123-foo-bar" - w.set_code(CODE) - response(w, type="claimed", mailbox="mb456") - - w._key = b"" - msgkey = w._derive_phase_key("side2", "misc") - p1_inbound = w._encrypt_data(msgkey, b"") - p1_inbound_hex = hexlify(p1_inbound).decode("ascii") - response(w, type="message", phase="misc", side="side2", - body=p1_inbound_hex) - self.check_outbound(ws, ["bind", "claim", "open", "add", - "release"]) - - d = w.close() - self.check_outbound(ws, ["close"]) - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="released") - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, []) - - response(w, type="closed") - self.assertNoResult(d) - self.assertEqual(w._drop_connection.mock_calls, [mock.call()]) - - w._ws_closed(True, None, None) - self.successResultOf(d) - - def test_close_errbacks(self): - # make sure the Deferreds returned by verify() and get() are properly - # errbacked upon close - pass - - def test_get_code_mock(self): - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - ws = MockWebSocket() # TODO: mock w._ws_send_command instead - w._event_connected(ws) - w._event_ws_opened(None) - self.check_outbound(ws, ["bind"]) - - gc_c = mock.Mock() - gc = gc_c.return_value = mock.Mock() - gc_d = gc.go.return_value = Deferred() - with mock.patch("wormhole.wormhole._GetCode", gc_c): - d = w.get_code() - self.assertNoResult(d) - - gc_d.callback("123-foo-bar") - code = self.successResultOf(d) - self.assertEqual(code, "123-foo-bar") - - def test_get_code_real(self): - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - self.check_outbound(ws, ["bind"]) - - d = w.get_code() - - out = ws.outbound() - self.assertEqual(len(out), 1) - self.check_out(out[0], type="allocate") - # TODO: nameplate attributes go here - self.assertNoResult(d) - - response(w, type="allocated", nameplate="123") - code = self.successResultOf(d) - self.assertIsInstance(code, type("")) - self.assert_(code.startswith("123-")) - pieces = code.split("-") - self.assertEqual(len(pieces), 3) # nameplate plus two words - self.assert_(re.search(r'^\d+-\w+-\w+$', code), code) - - def _test_establish_key_hook(self, established, before): - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - - if before: - d = w.establish_key() - - if established is True: - w._key = b"key" - elif established is False: - w._key = None - else: - w._key = b"key" - w._error = WelcomeError() - - if not before: - d = w.establish_key() - else: - w._maybe_notify_key() - - if w._key is not None and established is True: - self.successResultOf(d) - elif established is False: - self.assertNot(d.called) - else: - self.failureResultOf(d) - - def test_establish_key_hook(self): - for established in (True, False, "error"): - for before in (True, False): - self._test_establish_key_hook(established, before) - - def test_establish_key_twice(self): - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - d = w.establish_key() - self.assertRaises(InternalError, w.establish_key) - del d - - # make sure verify() can be called both before and after the verifier is - # computed - - def _test_verifier(self, when, order, success): - assert when in ("early", "middle", "late") - assert order in ("key-then-version", "version-then-key") - assert isinstance(success, bool) - #print(when, order, success) - - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - w._ws_send_command = mock.Mock() - w._mailbox_state = wormhole.OPEN - side2 = "side2" - d = None - - if success: - w._key = b"key" - else: - w._key = b"wrongkey" - plaintext = json.dumps({}).encode("utf-8") - data_key = w._derive_phase_key(side2, "version") - confmsg = w._encrypt_data(data_key, plaintext) - w._key = None - - if when == "early": - d = w.verify() - self.assertNoResult(d) - - if order == "key-then-version": - w._key = b"key" - w._event_established_key() - else: - w._event_received_version(side2, confmsg) - - if when == "middle": - d = w.verify() - if d: - self.assertNoResult(d) # still waiting for other msg - - if order == "version-then-key": - w._key = b"key" - w._event_established_key() - else: - w._event_received_version(side2, confmsg) - - if when == "late": - d = w.verify() - if success: - self.successResultOf(d) - else: - self.assertFailure(d, wormhole.WrongPasswordError) - self.flushLoggedErrors(WrongPasswordError) - - def test_verifier(self): - for when in ("early", "middle", "late"): - for order in ("key-then-version", "version-then-key"): - for success in (False, True): - self._test_verifier(when, order, success) - - - def test_api_errors(self): - # doing things you're not supposed to do - pass - - def test_welcome_error(self): - # A welcome message could arrive at any time, with an [error] key - # that should make us halt. In practice, though, this gets sent as - # soon as the connection is established, which limits the possible - # states in which we might see it. - - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - self.check_outbound(ws, ["bind"]) - - d1 = w.get() - d2 = w.verify() - d3 = w.get_code() - # TODO (tricky): test w.input_code - - self.assertNoResult(d1) - self.assertNoResult(d2) - self.assertNoResult(d3) - - w._signal_error(WelcomeError("you are not actually welcome"), "pouty") - self.failureResultOf(d1, WelcomeError) - self.failureResultOf(d2, WelcomeError) - self.failureResultOf(d3, WelcomeError) - - # once the error is signalled, all API calls should fail - self.assertRaises(WelcomeError, w.send, "foo") - self.assertRaises(WelcomeError, - w.derive_key, "foo", SecretBox.KEY_SIZE) - self.failureResultOf(w.get(), WelcomeError) - self.failureResultOf(w.verify(), WelcomeError) - - def test_version_error(self): - # we should only receive the "version" message after we receive the - # PAKE message, by which point we should know the key. If the - # confirmation message doesn't decrypt, we signal an error. - timing = DebugTiming() - w = wormhole._Wormhole(APPID, "relay_url", reactor, None, timing, None) - w._drop_connection = mock.Mock() - ws = MockWebSocket() - w._event_connected(ws) - w._event_ws_opened(None) - w.set_code("123-foo-bar") - response(w, type="claimed", mailbox="mb456") - - d1 = w.get() - d2 = w.verify() - self.assertNoResult(d1) - self.assertNoResult(d2) - - out = ws.outbound() - # ["bind", "claim", "open", "add"] - self.assertEqual(len(out), 4) - self.assertEqual(out[3]["type"], "add") - - sp2 = SPAKE2_Symmetric(b"", idSymmetric=wormhole.to_bytes(APPID)) - msg2 = sp2.start() - payload = {"pake_v1": bytes_to_hexstr(msg2)} - body_hex = bytes_to_hexstr(dict_to_bytes(payload)) - response(w, type="message", phase="pake", body=body_hex, side="s2") - self.assertNoResult(d1) - self.assertNoResult(d2) # verify() waits for confirmation - - # sending a random version message will cause a confirmation error - confkey = w.derive_key("WRONG", SecretBox.KEY_SIZE) - nonce = os.urandom(wormhole.CONFMSG_NONCE_LENGTH) - badversion = wormhole.make_confmsg(confkey, nonce) - badversion_hex = hexlify(badversion).decode("ascii") - response(w, type="message", phase="version", body=badversion_hex, - side="s2") - - self.failureResultOf(d1, WrongPasswordError) - self.failureResultOf(d2, WrongPasswordError) - - # once the error is signalled, all API calls should fail - self.assertRaises(WrongPasswordError, w.send, "foo") - self.assertRaises(WrongPasswordError, - w.derive_key, "foo", SecretBox.KEY_SIZE) - self.failureResultOf(w.get(), WrongPasswordError) - self.failureResultOf(w.verify(), WrongPasswordError) -Basic.skip = "being replaced by test_wormhole_new" - # event orderings to exercise: # # * normal sender: set_code, send_phase1, connected, claimed, learn_msg2, @@ -645,27 +58,105 @@ Basic.skip = "being replaced by test_wormhole_new" # * set_code, then connected # * connected, receive_pake, send_phase, set_code +class Delegate: + def __init__(self): + self.code = None + self.verifier = None + self.messages = [] + self.closed = None + def wormhole_got_code(self, code): + self.code = code + def wormhole_got_verifier(self, verifier): + self.verifier = verifier + def wormhole_receive(self, data): + self.messages.append(data) + def wormhole_closed(self, result): + self.closed = result + +class Delegated(ServerBase, unittest.TestCase): + + def test_delegated(self): + dg = Delegate() + w = wormhole.create(APPID, self.relayurl, reactor, delegate=dg) + w.close() + class Wormholes(ServerBase, unittest.TestCase): # integration test, with a real server def doBoth(self, d1, d2): return gatherResults([d1, d2], True) + @inlineCallbacks + def test_allocate_default(self): + w1 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code() + code = yield w1.when_code() + mo = re.search(r"^\d+-\w+-\w+$", code) + self.assert_(mo, code) + # w.close() fails because we closed before connecting + yield self.assertFailure(w1.close(), LonelyError) + + @inlineCallbacks + def test_allocate_more_words(self): + w1 = wormhole.create(APPID, self.relayurl, reactor) + w1.allocate_code(3) + code = yield w1.when_code() + mo = re.search(r"^\d+-\w+-\w+-\w+$", code) + self.assert_(mo, code) + yield self.assertFailure(w1.close(), LonelyError) + @inlineCallbacks def test_basic(self): w1 = wormhole.create(APPID, self.relayurl, reactor) + #w1.debug_set_trace("W1") w2 = wormhole.create(APPID, self.relayurl, reactor) + #w2.debug_set_trace(" W2") w1.allocate_code() code = yield w1.when_code() w2.set_code(code) + + verifier1 = yield w1.when_verified() + verifier2 = yield w2.when_verified() + self.assertEqual(verifier1, verifier2) + + version1 = yield w1.when_version() + version2 = yield w2.when_version() + # TODO: add the ability to set app-versions + self.assertEqual(version1, {}) + self.assertEqual(version2, {}) + w1.send(b"data1") w2.send(b"data2") dataX = yield w1.when_received() dataY = yield w2.when_received() self.assertEqual(dataX, b"data2") self.assertEqual(dataY, b"data1") - yield w1.close() - yield w2.close() + + version1_again = yield w1.when_version() + self.assertEqual(version1, version1_again) + + c1 = yield w1.close() + self.assertEqual(c1, "happy") + c2 = yield w2.close() + self.assertEqual(c2, "happy") + + @inlineCallbacks + def test_when_code_early(self): + w1 = wormhole.create(APPID, self.relayurl, reactor) + d = w1.when_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): + w1 = wormhole.create(APPID, self.relayurl, reactor) + w1.set_code("1-abc") + d = w1.when_code() + code = self.successResultOf(d) + self.assertEqual(code, "1-abc") + yield self.assertFailure(w1.close(), LonelyError) @inlineCallbacks def test_same_message(self): @@ -793,6 +284,8 @@ class Wormholes(ServerBase, unittest.TestCase): w1.allocate_code() code = yield w1.when_code() w2.set_code(code+"not") + code2 = yield w2.when_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 diff --git a/src/wormhole/test/test_wormhole_new.py b/src/wormhole/test/test_wormhole_new.py deleted file mode 100644 index 6e295ba..0000000 --- a/src/wormhole/test/test_wormhole_new.py +++ /dev/null @@ -1,108 +0,0 @@ -from __future__ import print_function, unicode_literals -import re -from twisted.trial import unittest -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks -from .common import ServerBase -from .. import wormhole, errors - -APPID = "appid" - -class Delegate: - def __init__(self): - self.code = None - self.verifier = None - self.messages = [] - self.closed = None - def wormhole_got_code(self, code): - self.code = code - def wormhole_got_verifier(self, verifier): - self.verifier = verifier - def wormhole_receive(self, data): - self.messages.append(data) - def wormhole_closed(self, result): - self.closed = result - -class New(ServerBase, unittest.TestCase): - timeout = 2 - - @inlineCallbacks - def test_allocate(self): - w = wormhole.create(APPID, self.relayurl, reactor) - #w.debug_set_trace("W1") - w.allocate_code(2) - code = yield w.when_code() - self.assertEqual(type(code), type("")) - mo = re.search(r"^\d+-\w+-\w+$", code) - self.assert_(mo, code) - # w.close() fails because we closed before connecting - yield self.assertFailure(w.close(), errors.LonelyError) - - def test_delegated(self): - dg = Delegate() - w = wormhole.create(APPID, self.relayurl, reactor, delegate=dg) - w.close() - - @inlineCallbacks - def test_basic(self): - w1 = wormhole.create(APPID, self.relayurl, reactor) - #w1.debug_set_trace("W1") - w1.allocate_code(2) - code = yield w1.when_code() - mo = re.search(r"^\d+-\w+-\w+$", code) - self.assert_(mo, code) - w2 = wormhole.create(APPID, self.relayurl, reactor) - #w2.debug_set_trace(" W2") - w2.set_code(code) - code2 = yield w2.when_code() - self.assertEqual(code, code2) - - verifier1 = yield w1.when_verified() - verifier2 = yield w2.when_verified() - self.assertEqual(verifier1, verifier2) - - version1 = yield w1.when_version() - version2 = yield w2.when_version() - # TODO: add the ability to set app-versions - self.assertEqual(version1, {}) - self.assertEqual(version2, {}) - - w1.send(b"data") - - data = yield w2.when_received() - self.assertEqual(data, b"data") - - w2.send(b"data2") - data2 = yield w1.when_received() - self.assertEqual(data2, b"data2") - - version1_again = yield w1.when_version() - self.assertEqual(version1, version1_again) - - c1 = yield w1.close() - self.assertEqual(c1, "happy") - c2 = yield w2.close() - self.assertEqual(c2, "happy") - - @inlineCallbacks - def test_wrong_password(self): - w1 = wormhole.create(APPID, self.relayurl, reactor) - #w1.debug_set_trace("W1") - w1.allocate_code(2) - code = yield w1.when_code() - w2 = wormhole.create(APPID, self.relayurl, reactor) - w2.set_code(code+"NOT") - code2 = yield w2.when_code() - self.assertNotEqual(code, code2) - - w1.send(b"data") - - yield self.assertFailure(w2.when_received(), errors.WrongPasswordError) - # wait for w1.when_received, because if we close w1 before it has - # seen the VERSION message, we could legitimately get LonelyError - # instead of WrongPasswordError. w2 didn't send anything, so - # w1.when_received wouldn't ever callback, but it will errback when - # w1 gets the undecryptable VERSION. - yield self.assertFailure(w1.when_received(), errors.WrongPasswordError) - yield self.assertFailure(w1.close(), errors.WrongPasswordError) - yield self.assertFailure(w2.close(), errors.WrongPasswordError) From bdef446ad41436232f9726d3d1b64b6137efdfc0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 4 Apr 2017 19:51:59 -0700 Subject: [PATCH 153/176] get mostly-full coverage for rlcompleter, rename, export --- docs/api.md | 12 +- src/wormhole/__init__.py | 5 + src/wormhole/_rlcompleter.py | 53 ++-- src/wormhole/cli/cmd_receive.py | 20 +- src/wormhole/cli/cmd_send.py | 12 +- src/wormhole/test/test_rlcompleter.py | 336 ++++++++++++++++++++++++++ 6 files changed, 391 insertions(+), 47 deletions(-) create mode 100644 src/wormhole/test/test_rlcompleter.py diff --git a/docs/api.md b/docs/api.md index 2b63f55..c2385d6 100644 --- a/docs/api.md +++ b/docs/api.md @@ -226,18 +226,18 @@ The code-entry Helper object has the following API: `MustChooseNameplateFirstError` will be raised. May only be called once, after which `AlreadyChoseWordsError` is raised. -The `rlcompleter` wrapper is a function that knows how to use the code-entry -helper to do tab completion of wormhole codes: +The `input_with_completion` wrapper is a function that knows how to use the +code-entry helper to do tab completion of wormhole codes: ```python -from wormhole import create, rlcompleter_helper +from wormhole import create, input_with_completion w = create(appid, relay_url, reactor) -rlcompleter_helper("Wormhole code:", w.input_code(), reactor) +input_with_completion("Wormhole code:", w.input_code(), reactor) d = w.when_code() ``` -This helper runs python's `rawinput()` function inside a thread, since -`rawinput()` normally blocks. +This helper runs python's (raw) `input()` function inside a thread, since +`input()` normally blocks. The two machines participating in the wormhole setup are not distinguished: it doesn't matter which one goes first, and both use the same Wormhole diff --git a/src/wormhole/__init__.py b/src/wormhole/__init__.py index 74f4e66..c00af56 100644 --- a/src/wormhole/__init__.py +++ b/src/wormhole/__init__.py @@ -2,3 +2,8 @@ from ._version import get_versions __version__ = get_versions()['version'] del get_versions + +from .wormhole import create +from ._rlcompleter import input_with_completion + +__all__ = ["create", "input_with_completion", "__version__"] diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index 9191029..d4a6e67 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -1,19 +1,25 @@ from __future__ import print_function, unicode_literals -import sys -import six +import traceback +from sys import stderr +try: + import readline +except ImportError: + readline = None +from six.moves import input from attr import attrs, attrib from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.threads import deferToThread, blockingCallFromThread -import os -errf = None -if os.path.exists("err"): - errf = open("err", "w") +#import os +#errf = None +#if os.path.exists("err"): +# errf = open("err", "w") def debug(*args, **kwargs): - if errf: - kwargs["file"] = errf - print(*args, **kwargs) - errf.flush() +# if errf: +# kwargs["file"] = errf +# print(*args, **kwargs) +# errf.flush() + pass @attrs class CodeInputter(object): @@ -28,20 +34,19 @@ class CodeInputter(object): def bcft(self, f, *a, **kw): return blockingCallFromThread(self._reactor, f, *a, **kw) - def wrap_completer(self, text, state): + def completer(self, text, state): try: - return self.completer(text, state) + return self._wrapped_completer(text, state) except Exception as e: # completer exceptions are normally silently discarded, which # makes debugging challenging print("completer exception: %s" % e) - import traceback traceback.print_exc() raise e - def completer(self, text, state): + def _wrapped_completer(self, text, state): self.used_completion = True - import readline + # if we get here, then readline must be active ct = readline.get_completion_type() if state == 0: debug("completer starting (%s) (state=0) (ct=%d)" % (text, ct)) @@ -109,21 +114,20 @@ class CodeInputter(object): self._input_helper.choose_nameplate(nameplate) self._input_helper.choose_words(words) -def input_code_with_completion(prompt, input_helper, reactor): +def _input_code_with_completion(prompt, input_helper, reactor): c = CodeInputter(input_helper, reactor) - try: - import readline + if readline is not None: if readline.__doc__ and "libedit" in readline.__doc__: readline.parse_and_bind("bind ^I rl_complete") else: readline.parse_and_bind("tab: complete") - readline.set_completer(c.wrap_completer) + readline.set_completer(c.completer) readline.set_completer_delims("") debug("==== readline-based completion is prepared") - except ImportError: + else: debug("==== unable to import readline, disabling completion") pass - code = six.moves.input(prompt) + code = input(prompt) # Code is str(bytes) on py2, and str(unicode) on py3. We want unicode. if isinstance(code, bytes): code = code.decode("utf-8") @@ -137,8 +141,7 @@ def warn_readline(): # input_code_with_completion() when SIGINT happened, the readline # thread will be blocked waiting for something on stdin. Trick the # user into satisfying the blocking read so we can exit. - print("\nCommand interrupted: please press Return to quit", - file=sys.stderr) + print("\nCommand interrupted: please press Return to quit", file=stderr) # Other potential approaches to this problem: # * hard-terminate our process with os._exit(1), but make sure the @@ -165,10 +168,10 @@ def warn_readline(): # readline finish. @inlineCallbacks -def rlcompleter_helper(prompt, input_helper, reactor): +def input_with_completion(prompt, input_helper, reactor): t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline) #input_helper.refresh_nameplates() - used_completion = yield deferToThread(input_code_with_completion, + used_completion = yield deferToThread(_input_code_with_completion, prompt, input_helper, reactor) reactor.removeSystemEventTrigger(t) returnValue(used_completion) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index 49b083f..bc1e1b6 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -5,7 +5,7 @@ from humanize import naturalsize from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue from twisted.python import log -from .. import wormhole, __version__ +from wormhole import create, input_with_completion, __version__ from ..transit import TransitReceiver from ..errors import TransferError, WormholeClosedError, NoTorError from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, @@ -64,11 +64,11 @@ class TwistedReceiver: wh = CLIWelcomeHandler(self.args.relay_url, __version__, self.args.stderr) - w = wormhole.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) + 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) # I wanted to do this instead: # # try: @@ -168,10 +168,10 @@ class TwistedReceiver: if code: w.set_code(code) else: - from .._rlcompleter import rlcompleter_helper - used_completion = yield rlcompleter_helper("Enter receive wormhole code: ", - w.input_code(), - self._reactor) + prompt = "Enter receive wormhole code: " + used_completion = yield input_with_completion(prompt, + w.input_code(), + self._reactor) if not used_completion: print(" (note: you can use to complete words)", file=self.args.stderr) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index 85877d7..cb8bf22 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -7,7 +7,7 @@ from twisted.protocols import basic from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, returnValue from ..errors import TransferError, WormholeClosedError, NoTorError -from .. import wormhole, __version__ +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 @@ -55,11 +55,11 @@ class Sender: wh = CLIWelcomeHandler(self._args.relay_url, __version__, self._args.stderr) - w = wormhole.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) + 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) d = self._go(w) # if we succeed, we should close and return the w.close results diff --git a/src/wormhole/test/test_rlcompleter.py b/src/wormhole/test/test_rlcompleter.py new file mode 100644 index 0000000..238f18b --- /dev/null +++ b/src/wormhole/test/test_rlcompleter.py @@ -0,0 +1,336 @@ +from __future__ import print_function, absolute_import, unicode_literals +import mock +from itertools import count +from twisted.trial import unittest +from twisted.internet import reactor +from twisted.internet.defer import inlineCallbacks +from twisted.internet.threads import deferToThread +from .._rlcompleter import (input_with_completion, + _input_code_with_completion, + CodeInputter, warn_readline) +APPID = "appid" + +class Input(unittest.TestCase): + @inlineCallbacks + def test_wrapper(self): + helper = object() + trueish = object() + with mock.patch("wormhole._rlcompleter._input_code_with_completion", + return_value=trueish) as m: + used_completion = yield input_with_completion("prompt:", helper, + reactor) + self.assertIs(used_completion, trueish) + self.assertEqual(m.mock_calls, + [mock.call("prompt:", helper, reactor)]) + # note: if this test fails, the warn_readline() message will probably + # get written to stderr + +class Sync(unittest.TestCase): + # exercise _input_code_with_completion, which uses the blocking builtin + # "input()" function, hence _input_code_with_completion is usually in a + # thread with deferToThread + + @mock.patch("wormhole._rlcompleter.CodeInputter") + @mock.patch("wormhole._rlcompleter.readline", + __doc__="I am GNU readline") + @mock.patch("wormhole._rlcompleter.input", return_value="code") + def test_readline(self, input, readline, ci): + c = mock.Mock(name="inhibit parenting") + c.completer = object() + trueish = object() + c.used_completion = trueish + ci.configure_mock(return_value=c) + prompt = object() + input_helper = object() + reactor = object() + used = _input_code_with_completion(prompt, input_helper, reactor) + self.assertIs(used, trueish) + self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) + self.assertEqual(c.mock_calls, [mock.call.finish("code")]) + self.assertEqual(input.mock_calls, [mock.call(prompt)]) + self.assertEqual(readline.mock_calls, + [mock.call.parse_and_bind("tab: complete"), + mock.call.set_completer(c.completer), + mock.call.set_completer_delims(""), + ]) + + @mock.patch("wormhole._rlcompleter.CodeInputter") + @mock.patch("wormhole._rlcompleter.readline") + @mock.patch("wormhole._rlcompleter.input", return_value="code") + def test_readline_no_docstring(self, input, readline, ci): + del readline.__doc__ # when in doubt, it assumes GNU readline + c = mock.Mock(name="inhibit parenting") + c.completer = object() + trueish = object() + c.used_completion = trueish + ci.configure_mock(return_value=c) + prompt = object() + input_helper = object() + reactor = object() + used = _input_code_with_completion(prompt, input_helper, reactor) + self.assertIs(used, trueish) + self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) + self.assertEqual(c.mock_calls, [mock.call.finish("code")]) + self.assertEqual(input.mock_calls, [mock.call(prompt)]) + self.assertEqual(readline.mock_calls, + [mock.call.parse_and_bind("tab: complete"), + mock.call.set_completer(c.completer), + mock.call.set_completer_delims(""), + ]) + + @mock.patch("wormhole._rlcompleter.CodeInputter") + @mock.patch("wormhole._rlcompleter.readline", + __doc__="I am libedit") + @mock.patch("wormhole._rlcompleter.input", return_value="code") + def test_libedit(self, input, readline, ci): + c = mock.Mock(name="inhibit parenting") + c.completer = object() + trueish = object() + c.used_completion = trueish + ci.configure_mock(return_value=c) + prompt = object() + input_helper = object() + reactor = object() + used = _input_code_with_completion(prompt, input_helper, reactor) + self.assertIs(used, trueish) + self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) + self.assertEqual(c.mock_calls, [mock.call.finish("code")]) + self.assertEqual(input.mock_calls, [mock.call(prompt)]) + self.assertEqual(readline.mock_calls, + [mock.call.parse_and_bind("bind ^I rl_complete"), + mock.call.set_completer(c.completer), + mock.call.set_completer_delims(""), + ]) + + @mock.patch("wormhole._rlcompleter.CodeInputter") + @mock.patch("wormhole._rlcompleter.readline", None) + @mock.patch("wormhole._rlcompleter.input", return_value="code") + def test_no_readline(self, input, ci): + c = mock.Mock(name="inhibit parenting") + c.completer = object() + trueish = object() + c.used_completion = trueish + ci.configure_mock(return_value=c) + prompt = object() + input_helper = object() + reactor = object() + used = _input_code_with_completion(prompt, input_helper, reactor) + self.assertIs(used, trueish) + self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) + self.assertEqual(c.mock_calls, [mock.call.finish("code")]) + self.assertEqual(input.mock_calls, [mock.call(prompt)]) + + @mock.patch("wormhole._rlcompleter.CodeInputter") + @mock.patch("wormhole._rlcompleter.readline", None) + @mock.patch("wormhole._rlcompleter.input", return_value=b"code") + def test_bytes(self, input, ci): + c = mock.Mock(name="inhibit parenting") + c.completer = object() + trueish = object() + c.used_completion = trueish + ci.configure_mock(return_value=c) + prompt = object() + input_helper = object() + reactor = object() + used = _input_code_with_completion(prompt, input_helper, reactor) + self.assertIs(used, trueish) + self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) + self.assertEqual(c.mock_calls, [mock.call.finish(u"code")]) + self.assertEqual(input.mock_calls, [mock.call(prompt)]) + +def get_completions(c, prefix): + completions = [] + for state in count(0): + text = c.completer(prefix, state) + if text is None: + return completions + completions.append(text) + +class Completion(unittest.TestCase): + def test_simple(self): + # no actual completion + helper = mock.Mock() + c = CodeInputter(helper, "reactor") + c.finish("1-code-ghost") + self.assertFalse(c.used_completion) + self.assertEqual(helper.mock_calls, + [mock.call.choose_nameplate("1"), + mock.call.choose_words("code-ghost")]) + + @mock.patch("wormhole._rlcompleter.readline", + get_completion_type=mock.Mock(return_value=0)) + def test_call(self, readline): + # check that it calls _commit_and_build_completions correctly + helper = mock.Mock() + c = CodeInputter(helper, "reactor") + + # pretend nameplates: 1, 12, 34 + + # first call will be with "1" + cabc = mock.Mock(return_value=["1", "12"]) + c._commit_and_build_completions = cabc + + self.assertEqual(get_completions(c, "1"), ["1", "12"]) + self.assertEqual(cabc.mock_calls, [mock.call("1")]) + + # then "12" + cabc.reset_mock() + cabc.configure_mock(return_value=["12"]) + self.assertEqual(get_completions(c, "12"), ["12"]) + self.assertEqual(cabc.mock_calls, [mock.call("12")]) + + # now we have three "a" words: "and", "ark", "aaah!zombies!!" + cabc.reset_mock() + cabc.configure_mock(return_value=["aargh", "ark", "aaah!zombies!!"]) + self.assertEqual(get_completions(c, "12-a"), + ["aargh", "ark", "aaah!zombies!!"]) + self.assertEqual(cabc.mock_calls, [mock.call("12-a")]) + + cabc.reset_mock() + cabc.configure_mock(return_value=["aargh", "aaah!zombies!!"]) + self.assertEqual(get_completions(c, "12-aa"), + ["aargh", "aaah!zombies!!"]) + self.assertEqual(cabc.mock_calls, [mock.call("12-aa")]) + + cabc.reset_mock() + cabc.configure_mock(return_value=["aaah!zombies!!"]) + self.assertEqual(get_completions(c, "12-aaa"), ["aaah!zombies!!"]) + self.assertEqual(cabc.mock_calls, [mock.call("12-aaa")]) + + c.finish("1-code") + self.assert_(c.used_completion) + + def test_wrap_error(self): + helper = mock.Mock() + c = CodeInputter(helper, "reactor") + c._wrapped_completer = mock.Mock(side_effect=ValueError("oops")) + with mock.patch("wormhole._rlcompleter.traceback") as traceback: + with mock.patch("wormhole._rlcompleter.print") as mock_print: + with self.assertRaises(ValueError) as e: + c.completer("text", 0) + self.assertEqual(traceback.mock_calls, [mock.call.print_exc()]) + self.assertEqual(mock_print.mock_calls, + [mock.call("completer exception: oops")]) + self.assertEqual(str(e.exception), "oops") + + @inlineCallbacks + def test_build_completions(self): + rn = mock.Mock() + # InputHelper.get_nameplate_completions returns just the suffixes + gnc = mock.Mock() # get_nameplate_completions + cn = mock.Mock() # choose_nameplate + gwc = mock.Mock() # get_word_completions + cw = mock.Mock() # choose_words + helper = mock.Mock(refresh_nameplates=rn, + get_nameplate_completions=gnc, + choose_nameplate=cn, + get_word_completions=gwc, + choose_words=cw, + ) + # this needs a real reactor, for blockingCallFromThread + c = CodeInputter(helper, reactor) + cabc = c._commit_and_build_completions + + # 1 TAB -> 1, 12 (and refresh_nameplates) + gnc.configure_mock(return_value=["", "2"]) + matches = yield deferToThread(cabc, "1") + self.assertEqual(matches, ["1", "12"]) + self.assertEqual(rn.mock_calls, [mock.call()]) + self.assertEqual(gnc.mock_calls, [mock.call("1")]) + self.assertEqual(cn.mock_calls, []) + rn.reset_mock() + gnc.reset_mock() + + # current: 12 TAB -> (and refresh_nameplates) + # want: 12 TAB -> 12- (and choose_nameplate) + gnc.configure_mock(return_value=[""]) + matches = yield deferToThread(cabc, "12") + self.assertEqual(matches, ["12"]) # 12- + self.assertEqual(rn.mock_calls, [mock.call()]) + self.assertEqual(gnc.mock_calls, [mock.call("12")]) + self.assertEqual(cn.mock_calls, []) # 12 + rn.reset_mock() + gnc.reset_mock() + + # current: 12-a TAB -> and ark aaah!zombies!! (and choose nameplate) + gnc.configure_mock(side_effect=ValueError) + gwc.configure_mock(return_value=["nd", "rk", "aah!zombies!!"]) + matches = yield deferToThread(cabc, "12-a") + # matches are always sorted + self.assertEqual(matches, ["12-aaah!zombies!!", "12-and", "12-ark"]) + self.assertEqual(rn.mock_calls, []) + self.assertEqual(gnc.mock_calls, []) + self.assertEqual(cn.mock_calls, [mock.call("12")]) + self.assertEqual(gwc.mock_calls, [mock.call("a")]) + cn.reset_mock() + gwc.reset_mock() + + # current: 12-and-b TAB -> bat bet but + gnc.configure_mock(side_effect=ValueError) + gwc.configure_mock(return_value=["at", "et", "ut"]) + matches = yield deferToThread(cabc, "12-and-b") + self.assertEqual(matches, ["12-and-bat", "12-and-bet", "12-and-but"]) + self.assertEqual(rn.mock_calls, []) + self.assertEqual(gnc.mock_calls, []) + self.assertEqual(cn.mock_calls, []) + self.assertEqual(gwc.mock_calls, [mock.call("and-b")]) + cn.reset_mock() + gwc.reset_mock() + + c.finish("12-and-bat") + self.assertEqual(cw.mock_calls, [mock.call("and-bat")]) + + def test_incomplete_code(self): + helper = mock.Mock() + c = CodeInputter(helper, "reactor") + with self.assertRaises(ValueError) as e: + c.finish("1") + self.assertEqual(str(e.exception), "incomplete wormhole code") + + @inlineCallbacks + def test_rollback_nameplate_during_completion(self): + helper = mock.Mock() + gwc = helper.get_word_completions = mock.Mock() + gwc.configure_mock(return_value=["de", "urt"]) + c = CodeInputter(helper, reactor) + cabc = c._commit_and_build_completions + matches = yield deferToThread(cabc, "1-co") # this commits us to 1- + self.assertEqual(helper.mock_calls, + [mock.call.choose_nameplate("1"), + mock.call.get_word_completions("co")]) + self.assertEqual(matches, ["1-code", "1-court"]) + helper.reset_mock() + with self.assertRaises(ValueError) as e: + yield deferToThread(cabc, "2-co") + self.assertEqual(str(e.exception), + "nameplate (NN-) already entered, cannot go back") + self.assertEqual(helper.mock_calls, []) + + @inlineCallbacks + def test_rollback_nameplate_during_finish(self): + helper = mock.Mock() + gwc = helper.get_word_completions = mock.Mock() + gwc.configure_mock(return_value=["de", "urt"]) + c = CodeInputter(helper, reactor) + cabc = c._commit_and_build_completions + matches = yield deferToThread(cabc, "1-co") # this commits us to 1- + self.assertEqual(helper.mock_calls, + [mock.call.choose_nameplate("1"), + mock.call.get_word_completions("co")]) + self.assertEqual(matches, ["1-code", "1-court"]) + helper.reset_mock() + with self.assertRaises(ValueError) as e: + c.finish("2-code") + self.assertEqual(str(e.exception), + "nameplate (NN-) already entered, cannot go back") + self.assertEqual(helper.mock_calls, []) + + @mock.patch("wormhole._rlcompleter.stderr") + def test_warn_readline(self, stderr): + # there is no good way to test that this function gets used at the + # right time, since it involves a reactor and a "system event + # trigger", but let's at least make sure it's invocable + warn_readline() + expected ="\nCommand interrupted: please press Return to quit" + self.assertEqual(stderr.mock_calls, [mock.call.write(expected), + mock.call.write("\n")]) From a1f0d1bbf7c3a4854995939a147a6a9b515e9ead Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Tue, 4 Apr 2017 21:03:34 -0700 Subject: [PATCH 154/176] debug_set_trace(): cleanups, remove dead code --- src/wormhole/_boss.py | 11 +++++++---- src/wormhole/wormhole.py | 14 +++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 74b5f68..1784d3c 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -86,7 +86,7 @@ class Boss(object): def start(self): self._RC.start() - def _set_trace(self, client_name, which, logger): + def _set_trace(self, client_name, which, file): names = {"B": self, "N": self._N, "M": self._M, "S": self._S, "O": self._O, "K": self._K, "SK": self._K._SK, "R": self._R, "RC": self._RC, "L": self._L, "C": self._C, @@ -97,17 +97,20 @@ class Boss(object): if new_state: print("%s.%s[%s].%s -> [%s]" % (client_name, machine, old_state, input, - new_state)) + new_state), file=file) else: # the RendezvousConnector emits message events as if # they were state transitions, except that old_state # and new_state are empty strings. "input" is one of # R.connected, R.rx(type phase+side), R.tx(type # phase), R.lost . - print("%s.%s.%s" % (client_name, machine, input)) + print("%s.%s.%s" % (client_name, machine, input), + file=file) else: if new_state: - print(" %s.%s.%s()" % (client_name, machine, output)) + print(" %s.%s.%s()" % (client_name, machine, output), + file=file) + file.flush() names[machine].set_trace(tracer) def serialize(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 7a1c150..9f38c40 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -34,10 +34,6 @@ from .util import to_bytes # wormhole(delegate=app, delegate_prefix="wormhole_", # delegate_args=(args, kwargs)) -def _log(client_name, machine_name, old_state, input, new_state): - print("%s.%s[%s].%s -> [%s]" % (client_name, machine_name, - old_state, input, new_state)) - class _WelcomeHandler: def __init__(self, url, stderr=sys.stderr): self.relay_url = url @@ -93,9 +89,9 @@ class _DelegatedWormhole(object): def close(self): self._boss.close() - def debug_set_trace(self, client_name, which="B N M S O K R RC NL C T", - logger=_log): - self._boss.set_trace(client_name, which, logger) + def debug_set_trace(self, client_name, which="B N M S O K R RC L C T", + file=sys.stderr): + self._boss._set_trace(client_name, which, file) # from below def got_code(self, code): @@ -203,8 +199,8 @@ class _DeferredWormhole(object): return d def debug_set_trace(self, client_name, which="B N M S O K R RC L C T", - logger=_log): - self._boss._set_trace(client_name, which, logger) + file=sys.stderr): + self._boss._set_trace(client_name, which, file) # from below def got_code(self, code): From 7699ed2291e8f73fc23410099b2f4058946c450b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 5 Apr 2017 12:50:52 -0700 Subject: [PATCH 155/176] tell wordlist how many words to expect, add hyphens to matches I'm still undecided about whether to add this to the mailbox properties (revealing it to attackers) or continue to require non-default wordcounts to be provided as a --code-length= argument to the receiver. So for now the only place that says count=2 is in the default argument on get_completions(). --- src/wormhole/_wordlist.py | 13 +++++++++---- src/wormhole/test/test_wordlist.py | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index ac48e78..cf801c5 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function import os from zope.interface import implementer from ._interfaces import IWordlist @@ -160,9 +160,10 @@ for k,both_words in raw_words.items(): @implementer(IWordlist) class PGPWordList(object): - def get_completions(self, prefix): + def get_completions(self, prefix, num_words=2): # start with the odd words - if prefix.count("-") % 2 == 0: + count = prefix.count("-") + if count % 2 == 0: words = odd_words_lowercase else: words = even_words_lowercase @@ -171,7 +172,11 @@ class PGPWordList(object): completions = set() for word in words: if word.startswith(last_partial_word): - completions.add(word[lp:]) + suffix = word[lp:] + # append a hyphen if we expect more words + if count+1 < num_words: + suffix += "-" + completions.add(suffix) return completions def choose_words(self, length): diff --git a/src/wormhole/test/test_wordlist.py b/src/wormhole/test/test_wordlist.py index 56cac4b..c86fbdb 100644 --- a/src/wormhole/test/test_wordlist.py +++ b/src/wormhole/test/test_wordlist.py @@ -7,11 +7,16 @@ class Completions(unittest.TestCase): def test_completions(self): wl = PGPWordList() gc = wl.get_completions - self.assertEqual(gc("ar"), {"mistice", "ticle"}) - self.assertEqual(gc("armis"), {"tice"}) - self.assertEqual(gc("armistice-ba"), + self.assertEqual(gc("ar", 2), {"mistice-", "ticle-"}) + self.assertEqual(gc("armis", 2), {"tice-"}) + self.assertEqual(gc("armistice", 2), {"-"}) + self.assertEqual(gc("armistice-ba", 2), {"boon", "ckfield", "ckward", "njo"}) - self.assertEqual(gc("armistice-baboon"), {""}) + self.assertEqual(gc("armistice-ba", 3), + {"boon-", "ckfield-", "ckward-", "njo-"}) + self.assertEqual(gc("armistice-baboon", 2), {""}) + self.assertEqual(gc("armistice-baboon", 3), {"-"}) + self.assertEqual(gc("armistice-baboon", 4), {"-"}) class Choose(unittest.TestCase): def test_choose_words(self): From 4c7b9080247fb4ebd624346d3b702d438d70778e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 5 Apr 2017 13:35:27 -0700 Subject: [PATCH 156/176] _rlcompleter: improve debug messages and comments --- src/wormhole/_rlcompleter.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index d4a6e67..e7d859c 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -1,5 +1,5 @@ from __future__ import print_function, unicode_literals -import traceback +import os, traceback from sys import stderr try: import readline @@ -10,16 +10,13 @@ from attr import attrs, attrib from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.threads import deferToThread, blockingCallFromThread -#import os -#errf = None -#if os.path.exists("err"): -# errf = open("err", "w") +errf = None +if 0: + errf = open("err", "w") if os.path.exists("err") else None def debug(*args, **kwargs): -# if errf: -# kwargs["file"] = errf -# print(*args, **kwargs) -# errf.flush() - pass + if errf: + print(*args, file=errf, **kwargs) + errf.flush() @attrs class CodeInputter(object): @@ -72,7 +69,10 @@ class CodeInputter(object): # 'text' is one of these categories: # "": complete on nameplates - # "12": complete on nameplates + # "12" (multiple matches): complete on nameplates + # "123" (single match): return "123-" (no commit, no refresh) + # nope: need to let readline add letters + # so: return "123-" # "123-": commit to nameplate (if not already), complete on words if self._committed_nameplate: @@ -82,14 +82,14 @@ class CodeInputter(object): # gentler way to encourage them to not do that. raise ValueError("nameplate (NN-) already entered, cannot go back") if not got_nameplate: - # we're completing on nameplates + # we're completing on nameplates: "" or "12" or "123" self.bcft(ih.refresh_nameplates) # results arrive later debug(" getting nameplates") completions = self.bcft(ih.get_nameplate_completions, nameplate) - else: + else: # "123-" # time to commit to this nameplate, if they haven't already if not self._committed_nameplate: - debug(" chose_nameplate", nameplate) + debug(" choose_nameplate(%s)" % nameplate) self.bcft(ih.choose_nameplate, nameplate) self._committed_nameplate = nameplate # and we're completing on words now @@ -111,7 +111,9 @@ class CodeInputter(object): # gentler way to encourage them to not do that. raise ValueError("nameplate (NN-) already entered, cannot go back") else: + debug(" choose_nameplate(%s)" % nameplate) self._input_helper.choose_nameplate(nameplate) + debug(" choose_words(%s)" % words) self._input_helper.choose_words(words) def _input_code_with_completion(prompt, input_helper, reactor): From 04926d0be884675f63a8c23d007392bd7f76e3f6 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 5 Apr 2017 13:41:09 -0700 Subject: [PATCH 157/176] minor test improvement --- src/wormhole/test/test_rlcompleter.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/wormhole/test/test_rlcompleter.py b/src/wormhole/test/test_rlcompleter.py index 238f18b..d35c773 100644 --- a/src/wormhole/test/test_rlcompleter.py +++ b/src/wormhole/test/test_rlcompleter.py @@ -231,6 +231,18 @@ class Completion(unittest.TestCase): c = CodeInputter(helper, reactor) cabc = c._commit_and_build_completions + # in this test, we pretend that nameplates 1 and 12 are active. + + # 43 TAB -> nothing (and refresh_nameplates) + gnc.configure_mock(return_value=[]) + matches = yield deferToThread(cabc, "43") + self.assertEqual(matches, []) + self.assertEqual(rn.mock_calls, [mock.call()]) + self.assertEqual(gnc.mock_calls, [mock.call("43")]) + self.assertEqual(cn.mock_calls, []) + rn.reset_mock() + gnc.reset_mock() + # 1 TAB -> 1, 12 (and refresh_nameplates) gnc.configure_mock(return_value=["", "2"]) matches = yield deferToThread(cabc, "1") From d331c51c037edc131bb2b25323c8731cd8a853bb Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 5 Apr 2017 18:26:28 -0700 Subject: [PATCH 158/176] change completion API * InputHelper returns full words, not just suffixes. I liked the fact that suffixes made it impossible to violate the "all matches will start with your prefix" invariant, but in practice it was fiddly to work with. * add ih.when_wordlist_is_available(), so the frontend can block (after claiming the nameplate) until it can return a complete wordlist to readline. This helps the user experience, because readline wasn't really built to work with completions that change over time * make the Wordlist responsible for appending hyphens to all non-final word completions. InputHelper remains responsible for hyphens on nameplates. This makes the frontend simpler, but I may change it again in the future if it helps non-readline GUI frontends. * CodeInputter: after claiming, wait for the wordlist rather than returning an empty list * PGPWordList: change to match This has the unfortunate side-effect that e.g. typing "3-yucatan-tu TAB" shows you completions that include the entire phrase: "3-yucatan-tumor 3-yucatan-tunnel", rather than only mentioning the final word. I'd like to fix this eventually. --- docs/api.md | 60 ++++++++++++----------- src/wormhole/_input.py | 27 +++++++++-- src/wormhole/_rlcompleter.py | 46 +++++++++++++----- src/wormhole/_wordlist.py | 2 +- src/wormhole/errors.py | 10 ++-- src/wormhole/test/test_machines.py | 17 +++++-- src/wormhole/test/test_rlcompleter.py | 69 +++++++++++++++++---------- src/wormhole/test/test_wordlist.py | 18 +++---- 8 files changed, 163 insertions(+), 86 deletions(-) diff --git a/docs/api.md b/docs/api.md index c2385d6..7709719 100644 --- a/docs/api.md +++ b/docs/api.md @@ -183,41 +183,45 @@ The code-entry Helper object has the following API: `get_nameplate_completions()` after the response will use the new list. Calling this after `h.choose_nameplate` will raise `AlreadyChoseNameplateError`. -* `completions = h.get_nameplate_completions(prefix)`: returns - (synchronously) a set of suffixes for the given nameplate prefix. For - example, if the server reports nameplates 1, 12, 13, 24, and 170 are in - use, `get_nameplate_completions("1")` will return `{"", "2", "3", "70"}`. - You may want to sort these before displaying them to the user. Raises - `AlreadyChoseNameplateError` if called after `h.choose_nameplate`. +* `matches = h.get_nameplate_completions(prefix)`: returns (synchronously) a + set of completions for the given nameplate prefix, along with the hyphen + that always follows the nameplate (and separates the nameplate from the + rest of the code). For example, if the server reports nameplates 1, 12, 13, + 24, and 170 are in use, `get_nameplate_completions("1")` will return + `{"1-", "12-", "13-", "170-"}`. You may want to sort these before + displaying them to the user. Raises `AlreadyChoseNameplateError` if called + after `h.choose_nameplate`. * `h.choose_nameplate(nameplate)`: accepts a string with the chosen nameplate. May only be called once, after which `AlreadyChoseNameplateError` is raised. (in this future, this might return a Deferred that fires (with None) when the nameplate's wordlist is known (which happens after the nameplate is claimed, requiring a roundtrip to the server)). -* `completions = h.get_word_completions(prefix)`: return (synchronously) a - set of suffixes for the given words prefix. The possible completions depend - upon the wordlist in use for the previously-claimed nameplate, so calling - this before `choose_nameplate` will raise `MustChooseNameplateFirstError`. +* `d = h.when_wordlist_is_available()`: return a Deferred that fires (with + None) when the wordlist is known. This can be used to block a readline + frontend which has just called `h.choose_nameplate()` until the resulting + wordlist is known, which can improve the tab-completion behavior. +* `matches = h.get_word_completions(prefix)`: return (synchronously) a set of + completions for the given words prefix. This will include a trailing hyphen + if more words are expected. The possible completions depend upon the + wordlist in use for the previously-claimed nameplate, so calling this + before `choose_nameplate` will raise `MustChooseNameplateFirstError`. Calling this after `h.choose_words()` will raise `AlreadyChoseWordsError`. - Given a prefix like "su", this returns a set of strings which are - appropriate to append to the prefix (e.g. `{"pportive", "rrender", - "spicious"}`, for expansion into "supportive", "surrender", and - "suspicious". The prefix should not include the nameplate, but *should* - include whatever words and hyphens have been typed so far (the default - wordlist uses alternate lists, where even numbered words have three - syllables, and odd numbered words have two, so the completions depend upon - how many words are present, not just the partial last word). E.g. - `get_word_completions("pr")` will return `{"ocessor", "ovincial", - "oximate"}`, while `get_word_completions("opulent-pr")` will return - `{"eclude", "efer", "eshrunk", "inter", "owler"}`. If the wordlist is not - yet known, this returns an empty set. It will include an empty string in - the returned set if the prefix is complete (the last word is an exact match - for something in the completion list), but will include additional strings - if the completion list includes extensions of the last word. The - completions will never include a hyphen: the UI frontend must supply these - if desired. The frontend is also responsible for sorting the results before - display. + Given a prefix like "su", this returns a set of strings which are potential + matches (e.g. `{"supportive-", "surrender-", "suspicious-"}`. The prefix + should not include the nameplate, but *should* include whatever words and + hyphens have been typed so far (the default wordlist uses alternate lists, + where even numbered words have three syllables, and odd numbered words have + two, so the completions depend upon how many words are present, not just + the partial last word). E.g. `get_word_completions("pr")` will return + `{"processor-", "provincial-", "proximate-"}`, while + `get_word_completions("opulent-pr")` will return `{"opulent-preclude", + "opulent-prefer", "opulent-preshrunk", "opulent-printer", + "opulent-prowler"}` (note the lack of a trailing hyphen, because the + wordlist is expecting a code of length two). If the wordlist is not yet + known, this returns an empty set. All return values will + `.startwith(prefix)`. The frontend is responsible for sorting the results + 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 diff --git a/src/wormhole/_input.py b/src/wormhole/_input.py index 82158a3..6985253 100644 --- a/src/wormhole/_input.py +++ b/src/wormhole/_input.py @@ -2,6 +2,7 @@ from __future__ import print_function, absolute_import, unicode_literals from zope.interface import implementer from attr import attrs, attrib from attr.validators import provides +from twisted.internet import defer from automat import MethodicalMachine from . import _interfaces, errors @@ -19,11 +20,19 @@ class Input(object): self._all_nameplates = set() self._nameplate = None self._wordlist = None + self._wordlist_waiters = [] def wire(self, code, lister): self._C = _interfaces.ICode(code) self._L = _interfaces.ILister(lister) + def when_wordlist_is_available(self): + if self._wordlist: + return defer.succeed(None) + d = defer.Deferred() + self._wordlist_waiters.append(d) + return d + @m.state(initial=True) def S0_idle(self): pass # pragma: no cover @m.state() @@ -72,11 +81,12 @@ class Input(object): self._all_nameplates = all_nameplates @m.output() def _get_nameplate_completions(self, prefix): - lp = len(prefix) completions = set() for nameplate in self._all_nameplates: if nameplate.startswith(prefix): - completions.add(nameplate[lp:]) + # TODO: it's a little weird that Input is responsible for the + # hyphen on nameplates, but WordList owns it for words + completions.add(nameplate+"-") return completions @m.output() def record_all_nameplates(self, nameplate): @@ -87,6 +97,11 @@ class Input(object): from ._rlcompleter import debug debug(" -record_wordlist") self._wordlist = wordlist + @m.output() + def notify_wordlist_waiters(self, wordlist): + while self._wordlist_waiters: + d = self._wordlist_waiters.pop() + d.callback(None) @m.output() def no_word_completions(self, prefix): @@ -128,7 +143,8 @@ class Input(object): # wormholes that don't use input_code (i.e. they use allocate_code or # generate_code) will never start() us, but Nameplate will give us a # wordlist anyways (as soon as the nameplate is claimed), so handle it. - S0_idle.upon(got_wordlist, enter=S0_idle, outputs=[record_wordlist]) + S0_idle.upon(got_wordlist, enter=S0_idle, outputs=[record_wordlist, + notify_wordlist_waiters]) S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate, outputs=[record_nameplates]) # but wormholes that *do* use input_code should not get got_wordlist @@ -152,7 +168,8 @@ class Input(object): enter=S2_typing_code_no_wordlist, outputs=[]) S2_typing_code_no_wordlist.upon(got_wordlist, enter=S3_typing_code_yes_wordlist, - outputs=[record_wordlist]) + outputs=[record_wordlist, + notify_wordlist_waiters]) S2_typing_code_no_wordlist.upon(refresh_nameplates, enter=S2_typing_code_no_wordlist, outputs=[raise_already_chose_nameplate1]) @@ -215,6 +232,8 @@ class Helper(object): return self._input.get_nameplate_completions(prefix) def choose_nameplate(self, nameplate): self._input.choose_nameplate(nameplate) + def when_wordlist_is_available(self): + return self._input.when_wordlist_is_available() def get_word_completions(self, prefix): return self._input.get_word_completions(prefix) def choose_words(self, words): diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index e7d859c..5be03fa 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -9,6 +9,7 @@ from six.moves import input from attr import attrs, attrib from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.threads import deferToThread, blockingCallFromThread +from .errors import KeyFormatError, AlreadyInputNameplateError errf = None if 0: @@ -68,40 +69,61 @@ class CodeInputter(object): nameplate = text # partial # 'text' is one of these categories: - # "": complete on nameplates - # "12" (multiple matches): complete on nameplates - # "123" (single match): return "123-" (no commit, no refresh) - # nope: need to let readline add letters - # so: return "123-" - # "123-": commit to nameplate (if not already), complete on words + # "" or "12": complete on nameplates (all that match, maybe just one) + + # "123-": if we haven't already committed to a nameplate, commit and + # wait for the wordlist. Then (either way) return the whole wordlist. + + # "123-supp": if we haven't already committed to a nameplate, commit + # and wait for the wordlist. Then (either way) return all current + # matches. if self._committed_nameplate: if not got_nameplate or nameplate != self._committed_nameplate: # they deleted past the committment point: we can't use # this. For now, bail, but in the future let's find a # gentler way to encourage them to not do that. - raise ValueError("nameplate (NN-) already entered, cannot go back") + raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate) if not got_nameplate: # we're completing on nameplates: "" or "12" or "123" self.bcft(ih.refresh_nameplates) # results arrive later debug(" getting nameplates") completions = self.bcft(ih.get_nameplate_completions, nameplate) - else: # "123-" + else: # "123-" or "123-supp" # time to commit to this nameplate, if they haven't already if not self._committed_nameplate: debug(" choose_nameplate(%s)" % nameplate) self.bcft(ih.choose_nameplate, nameplate) self._committed_nameplate = nameplate + + # Now we want to wait for the wordlist to be available. If + # the user just typed "12-supp TAB", we'll claim "12" but + # will need a server roundtrip to discover that "supportive" + # is the only match. If we don't block, we'd return an empty + # wordlist to readline (which will beep and show no + # completions). *Then* when the user hits TAB again a moment + # later (after the wordlist has arrived, but the user hasn't + # modified the input line since the previous empty response), + # readline would show one match but not complete anything. + + # In general we want to avoid returning empty lists to + # readline. If the user hits TAB when typing in the nameplate + # (before the sender has established one, or before we're + # heard about it from the server), it can't be helped. But + # for the rest of the code, a simple wait-for-wordlist will + # improve the user experience. + self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM # and we're completing on words now debug(" getting words") - completions = self.bcft(ih.get_word_completions, words) + completions = [nameplate+"-"+c + for c in self.bcft(ih.get_word_completions, words)] # rlcompleter wants full strings - return sorted([text+c for c in completions]) + return sorted(completions) def finish(self, text): if "-" not in text: - raise ValueError("incomplete wormhole code") + raise KeyFormatError("incomplete wormhole code") nameplate, words = text.split("-", 1) if self._committed_nameplate: @@ -109,7 +131,7 @@ class CodeInputter(object): # they deleted past the committment point: we can't use # this. For now, bail, but in the future let's find a # gentler way to encourage them to not do that. - raise ValueError("nameplate (NN-) already entered, cannot go back") + raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate) else: debug(" choose_nameplate(%s)" % nameplate) self._input_helper.choose_nameplate(nameplate) diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index cf801c5..f966971 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -172,7 +172,7 @@ class PGPWordList(object): completions = set() for word in words: if word.startswith(last_partial_word): - suffix = word[lp:] + suffix = prefix[:-lp] + word # append a hyphen if we expect more words if count+1 < num_words: suffix += "-" diff --git a/src/wormhole/errors.py b/src/wormhole/errors.py index 1197c26..06f74d3 100644 --- a/src/wormhole/errors.py +++ b/src/wormhole/errors.py @@ -31,9 +31,10 @@ class WrongPasswordError(WormholeError): class KeyFormatError(WormholeError): """ - The key you entered contains spaces. Magic-wormhole expects keys to be - separated by dashes. Please reenter the key you were given separating the - words with dashes. + The key you entered contains spaces or was missing a dash. Magic-wormhole + expects the numerical nameplate and the code words to be separated by + dashes. Please reenter the key you were given separating the words with + dashes. """ class ReflectionAttack(WormholeError): @@ -67,6 +68,9 @@ class AlreadyChoseNameplateError(WormholeError): class AlreadyChoseWordsError(WormholeError): """The InputHelper was asked to do get_word_completions() after choose_words() was called, or choose_words() was called a second time.""" +class AlreadyInputNameplateError(WormholeError): + """The CodeInputter was asked to do completion on a nameplate, when we + had already committed to a different one.""" class WormholeClosed(Exception): """Deferred-returning API calls errback with WormholeClosed if the wormhole was already closed, or if it closes before a real result can be diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 998ab47..3e45ab7 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -336,19 +336,22 @@ class Input(unittest.TestCase): self.assertIsInstance(helper, _input.Helper) self.assertEqual(events, [("l.refresh",)]) events[:] = [] + d = helper.when_wordlist_is_available() + self.assertNoResult(d) helper.refresh_nameplates() self.assertEqual(events, [("l.refresh",)]) events[:] = [] with self.assertRaises(errors.MustChooseNameplateFirstError): helper.get_word_completions("prefix") i.got_nameplates({"1", "12", "34", "35", "367"}) + self.assertNoResult(d) self.assertEqual(helper.get_nameplate_completions(""), - {"1", "12", "34", "35", "367"}) + {"1-", "12-", "34-", "35-", "367-"}) self.assertEqual(helper.get_nameplate_completions("1"), - {"", "2"}) + {"1-", "12-"}) self.assertEqual(helper.get_nameplate_completions("2"), set()) self.assertEqual(helper.get_nameplate_completions("3"), - {"4", "5", "67"}) + {"34-", "35-", "367-"}) helper.choose_nameplate("34") with self.assertRaises(errors.AlreadyChoseNameplateError): helper.refresh_nameplates() @@ -357,10 +360,16 @@ class Input(unittest.TestCase): self.assertEqual(events, [("c.got_nameplate", "34")]) events[:] = [] # no wordlist yet + self.assertNoResult(d) self.assertEqual(helper.get_word_completions(""), set()) wl = FakeWordList() i.got_wordlist(wl) - wl._completions = {"bc", "bcd", "e"} + self.assertEqual(self.successResultOf(d), None) + # a new Deferred should fire right away + d = helper.when_wordlist_is_available() + self.assertEqual(self.successResultOf(d), None) + + wl._completions = {"abc-", "abcd-", "ae-"} self.assertEqual(helper.get_word_completions("a"), wl._completions) self.assertEqual(wl._get_completions_prefix, "a") with self.assertRaises(errors.AlreadyChoseNameplateError): diff --git a/src/wormhole/test/test_rlcompleter.py b/src/wormhole/test/test_rlcompleter.py index d35c773..f21e55f 100644 --- a/src/wormhole/test/test_rlcompleter.py +++ b/src/wormhole/test/test_rlcompleter.py @@ -8,6 +8,7 @@ from twisted.internet.threads import deferToThread from .._rlcompleter import (input_with_completion, _input_code_with_completion, CodeInputter, warn_readline) +from ..errors import KeyFormatError, AlreadyInputNameplateError APPID = "appid" class Input(unittest.TestCase): @@ -231,7 +232,7 @@ class Completion(unittest.TestCase): c = CodeInputter(helper, reactor) cabc = c._commit_and_build_completions - # in this test, we pretend that nameplates 1 and 12 are active. + # in this test, we pretend that nameplates 1,12,34 are active. # 43 TAB -> nothing (and refresh_nameplates) gnc.configure_mock(return_value=[]) @@ -243,50 +244,64 @@ class Completion(unittest.TestCase): rn.reset_mock() gnc.reset_mock() - # 1 TAB -> 1, 12 (and refresh_nameplates) - gnc.configure_mock(return_value=["", "2"]) + # 1 TAB -> 1-, 12- (and refresh_nameplates) + gnc.configure_mock(return_value=["1-", "12-"]) matches = yield deferToThread(cabc, "1") - self.assertEqual(matches, ["1", "12"]) + self.assertEqual(matches, ["1-", "12-"]) self.assertEqual(rn.mock_calls, [mock.call()]) self.assertEqual(gnc.mock_calls, [mock.call("1")]) self.assertEqual(cn.mock_calls, []) rn.reset_mock() gnc.reset_mock() - # current: 12 TAB -> (and refresh_nameplates) - # want: 12 TAB -> 12- (and choose_nameplate) - gnc.configure_mock(return_value=[""]) + # 12 TAB -> 12- (and refresh_nameplates) + # I wouldn't mind if it didn't refresh the nameplates here, but meh + gnc.configure_mock(return_value=["12-"]) matches = yield deferToThread(cabc, "12") - self.assertEqual(matches, ["12"]) # 12- + self.assertEqual(matches, ["12-"]) self.assertEqual(rn.mock_calls, [mock.call()]) self.assertEqual(gnc.mock_calls, [mock.call("12")]) - self.assertEqual(cn.mock_calls, []) # 12 + self.assertEqual(cn.mock_calls, []) rn.reset_mock() gnc.reset_mock() - # current: 12-a TAB -> and ark aaah!zombies!! (and choose nameplate) - gnc.configure_mock(side_effect=ValueError) - gwc.configure_mock(return_value=["nd", "rk", "aah!zombies!!"]) - matches = yield deferToThread(cabc, "12-a") - # matches are always sorted - self.assertEqual(matches, ["12-aaah!zombies!!", "12-and", "12-ark"]) + # 12- TAB -> 12- {all words} (claim, no refresh) + gnc.configure_mock(return_value=["12-"]) + gwc.configure_mock(return_value=["and-", "ark-", "aaah!zombies!!-"]) + matches = yield deferToThread(cabc, "12-") + self.assertEqual(matches, ["12-aaah!zombies!!-", "12-and-", "12-ark-"]) self.assertEqual(rn.mock_calls, []) self.assertEqual(gnc.mock_calls, []) self.assertEqual(cn.mock_calls, [mock.call("12")]) - self.assertEqual(gwc.mock_calls, [mock.call("a")]) + self.assertEqual(gwc.mock_calls, [mock.call("")]) cn.reset_mock() gwc.reset_mock() - # current: 12-and-b TAB -> bat bet but + # TODO: another path with "3 TAB" then "34-an TAB", so the claim + # happens in the second call (and it waits for the wordlist) + + # 12-a TAB -> 12-and- 12-ark- 12-aaah!zombies!!- gnc.configure_mock(side_effect=ValueError) - gwc.configure_mock(return_value=["at", "et", "ut"]) + gwc.configure_mock(return_value=["and-", "ark-", "aaah!zombies!!-"]) + matches = yield deferToThread(cabc, "12-a") + # matches are always sorted + self.assertEqual(matches, ["12-aaah!zombies!!-", "12-and-", "12-ark-"]) + self.assertEqual(rn.mock_calls, []) + self.assertEqual(gnc.mock_calls, []) + self.assertEqual(cn.mock_calls, []) + self.assertEqual(gwc.mock_calls, [mock.call("a")]) + gwc.reset_mock() + + # 12-and-b TAB -> 12-and-bat 12-and-bet 12-and-but + gnc.configure_mock(side_effect=ValueError) + # wordlist knows the code length, so doesn't add hyphens to these + gwc.configure_mock(return_value=["and-bat", "and-bet", "and-but"]) matches = yield deferToThread(cabc, "12-and-b") self.assertEqual(matches, ["12-and-bat", "12-and-bet", "12-and-but"]) self.assertEqual(rn.mock_calls, []) self.assertEqual(gnc.mock_calls, []) self.assertEqual(cn.mock_calls, []) self.assertEqual(gwc.mock_calls, [mock.call("and-b")]) - cn.reset_mock() gwc.reset_mock() c.finish("12-and-bat") @@ -295,7 +310,7 @@ class Completion(unittest.TestCase): def test_incomplete_code(self): helper = mock.Mock() c = CodeInputter(helper, "reactor") - with self.assertRaises(ValueError) as e: + with self.assertRaises(KeyFormatError) as e: c.finish("1") self.assertEqual(str(e.exception), "incomplete wormhole code") @@ -303,38 +318,40 @@ class Completion(unittest.TestCase): def test_rollback_nameplate_during_completion(self): helper = mock.Mock() gwc = helper.get_word_completions = mock.Mock() - gwc.configure_mock(return_value=["de", "urt"]) + gwc.configure_mock(return_value=["code", "court"]) c = CodeInputter(helper, reactor) cabc = c._commit_and_build_completions matches = yield deferToThread(cabc, "1-co") # this commits us to 1- self.assertEqual(helper.mock_calls, [mock.call.choose_nameplate("1"), + mock.call.when_wordlist_is_available(), mock.call.get_word_completions("co")]) self.assertEqual(matches, ["1-code", "1-court"]) helper.reset_mock() - with self.assertRaises(ValueError) as e: + with self.assertRaises(AlreadyInputNameplateError) as e: yield deferToThread(cabc, "2-co") self.assertEqual(str(e.exception), - "nameplate (NN-) already entered, cannot go back") + "nameplate (1-) already entered, cannot go back") self.assertEqual(helper.mock_calls, []) @inlineCallbacks def test_rollback_nameplate_during_finish(self): helper = mock.Mock() gwc = helper.get_word_completions = mock.Mock() - gwc.configure_mock(return_value=["de", "urt"]) + gwc.configure_mock(return_value=["code", "court"]) c = CodeInputter(helper, reactor) cabc = c._commit_and_build_completions matches = yield deferToThread(cabc, "1-co") # this commits us to 1- self.assertEqual(helper.mock_calls, [mock.call.choose_nameplate("1"), + mock.call.when_wordlist_is_available(), mock.call.get_word_completions("co")]) self.assertEqual(matches, ["1-code", "1-court"]) helper.reset_mock() - with self.assertRaises(ValueError) as e: + with self.assertRaises(AlreadyInputNameplateError) as e: c.finish("2-code") self.assertEqual(str(e.exception), - "nameplate (NN-) already entered, cannot go back") + "nameplate (1-) already entered, cannot go back") self.assertEqual(helper.mock_calls, []) @mock.patch("wormhole._rlcompleter.stderr") diff --git a/src/wormhole/test/test_wordlist.py b/src/wormhole/test/test_wordlist.py index c86fbdb..b165fc3 100644 --- a/src/wormhole/test/test_wordlist.py +++ b/src/wormhole/test/test_wordlist.py @@ -7,16 +7,18 @@ class Completions(unittest.TestCase): def test_completions(self): wl = PGPWordList() gc = wl.get_completions - self.assertEqual(gc("ar", 2), {"mistice-", "ticle-"}) - self.assertEqual(gc("armis", 2), {"tice-"}) - self.assertEqual(gc("armistice", 2), {"-"}) + self.assertEqual(gc("ar", 2), {"armistice-", "article-"}) + self.assertEqual(gc("armis", 2), {"armistice-"}) + self.assertEqual(gc("armistice", 2), {"armistice-"}) self.assertEqual(gc("armistice-ba", 2), - {"boon", "ckfield", "ckward", "njo"}) + {"armistice-baboon", "armistice-backfield", + "armistice-backward", "armistice-banjo"}) self.assertEqual(gc("armistice-ba", 3), - {"boon-", "ckfield-", "ckward-", "njo-"}) - self.assertEqual(gc("armistice-baboon", 2), {""}) - self.assertEqual(gc("armistice-baboon", 3), {"-"}) - self.assertEqual(gc("armistice-baboon", 4), {"-"}) + {"armistice-baboon-", "armistice-backfield-", + "armistice-backward-", "armistice-banjo-"}) + self.assertEqual(gc("armistice-baboon", 2), {"armistice-baboon"}) + self.assertEqual(gc("armistice-baboon", 3), {"armistice-baboon-"}) + self.assertEqual(gc("armistice-baboon", 4), {"armistice-baboon-"}) class Choose(unittest.TestCase): def test_choose_words(self): From 0da9cbdeeb49c2900059f78ad7afb2aef8ba069d Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 5 Apr 2017 21:43:20 -0700 Subject: [PATCH 159/176] remove unused code --- src/wormhole/_rendezvous.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/wormhole/_rendezvous.py b/src/wormhole/_rendezvous.py index c4e5c45..4029e3f 100644 --- a/src/wormhole/_rendezvous.py +++ b/src/wormhole/_rendezvous.py @@ -57,10 +57,6 @@ class WSFactory(websocket.WebSocketClientFactory): #proto.wormhole_open = False return proto -def dmsg(side, text): - offset = int(side, 16) % 20 - print(" "*offset, text) - @attrs @implementer(_interfaces.IRendezvousConnector) class RendezvousConnector(object): @@ -154,11 +150,6 @@ class RendezvousConnector(object): def ws_message(self, payload): msg = bytes_to_dict(payload) - #if self.DEBUG and msg["type"]!="ack": - # dmsg(self._side, "R.rx(%s %s%s)" % - # (msg["type"], msg.get("phase",""), - # "[mine]" if msg.get("side","") == self._side else "", - # )) if msg["type"] != "ack": self._debug("R.rx(%s %s%s)" % (msg["type"], msg.get("phase",""), From df1b2338b1a6d69e8ff6f82d41d2c20600e0a103 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 10:44:04 -0700 Subject: [PATCH 160/176] tests: exercise Key receiving PAKE before set_code --- src/wormhole/_key.py | 2 ++ src/wormhole/test/common.py | 9 ++++++++- src/wormhole/test/test_machines.py | 28 ++++++++++++++++++++++++++++ src/wormhole/test/test_wormhole.py | 24 +++++++++++++++++++++++- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 67433ec..622af65 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -69,6 +69,7 @@ class Key(object): def __attrs_post_init__(self): self._SK = _SortedKey(self._appid, self._versions, self._side, self._timing) + self._debug_pake_stashed = False # for tests def wire(self, boss, mailbox, receive): self._SK.wire(boss, mailbox, receive) @@ -90,6 +91,7 @@ class Key(object): @m.output() def stash_pake(self, body): self._pake = body + self._debug_pake_stashed = True @m.output() def deliver_code(self, code): self._SK.got_code(code) diff --git a/src/wormhole/test/common.py b/src/wormhole/test/common.py index f3b57ad..dd1d490 100644 --- a/src/wormhole/test/common.py +++ b/src/wormhole/test/common.py @@ -1,6 +1,6 @@ # no unicode_literals untill twisted update from twisted.application import service -from twisted.internet import defer, task +from twisted.internet import defer, task, reactor from twisted.python import log from click.testing import CliRunner import mock @@ -84,3 +84,10 @@ def config(*argv): cfg = go.call_args[0][1] return cfg +@defer.inlineCallbacks +def poll_until(predicate): + # return a Deferred that won't fire until the predicate is True + while not predicate(): + d = defer.Deferred() + reactor.callLater(0.001, d.callback, None) + yield d diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index 3e45ab7..df002b7 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -257,6 +257,34 @@ class Key(unittest.TestCase): k.got_pake(dict_to_bytes(bad_pake_d)) self.assertEqual(events, [("b.scared",)]) + def test_reversed(self): + # A receiver using input_code() will choose the nameplate first, then + # the rest of the code. Once the nameplate is selected, we'll claim + # it and open the mailbox, which will cause the senders PAKE to + # arrive before the code has been set. Key() is supposed to stash the + # PAKE message until the code is set (allowing the PAKE computation + # to finish). This test exercises that PAKE-then-code sequence. + k, b, m, r, events = self.build() + code = u"1-foo" + + sp = SPAKE2_Symmetric(to_bytes(code), idSymmetric=to_bytes(u"appid")) + msg2_bytes = sp.start() + msg2 = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg2_bytes)}) + k.got_pake(msg2) + self.assertEqual(len(events), 0) + + k.got_code(code) + self.assertEqual(len(events), 5) + self.assertEqual(events[0][:2], ("m.add_message", "pake")) + msg1_json = events[0][2].decode("utf-8") + msg1 = json.loads(msg1_json) + msg1_bytes = hexstr_to_bytes(msg1["pake_v1"]) + key2 = sp.finish(msg1_bytes) + self.assertEqual(events[1], ("b.got_key", key2)) + self.assertEqual(events[2][0], "b.got_verifier") + self.assertEqual(events[3][:2], ("m.add_message", "version")) + self.assertEqual(events[4], ("r.got_key", key2)) + class Code(unittest.TestCase): def build(self): events = [] diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index fc329d6..dd2a552 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -4,7 +4,7 @@ import mock from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import gatherResults, inlineCallbacks -from .common import ServerBase +from .common import ServerBase, poll_until from .. import wormhole, _rendezvous from ..errors import (WrongPasswordError, KeyFormatError, WormholeClosed, LonelyError, @@ -234,6 +234,28 @@ class Wormholes(ServerBase, unittest.TestCase): yield w1.close() yield w2.close() + @inlineCallbacks + def test_input_code(self): + w1 = wormhole.create(APPID, self.relayurl, reactor) + w2 = wormhole.create(APPID, self.relayurl, reactor) + w1.set_code("123-purple-elephant") + h = w2.input_code() + h.choose_nameplate("123") + # Pause to allow some messages to get delivered. Specifically we want + # to wait until w2 claims the nameplate, opens the mailbox, and + # receives the PAKE message, to exercise the PAKE-before-CODE path in + # Key. + 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()) + (dataX, dataY) = dl + self.assertEqual(dataX, b"data2") + self.assertEqual(dataY, b"data1") + yield w1.close() + yield w2.close() + @inlineCallbacks def test_multiple_messages(self): From 3cd4d31c0b6ca5fae63b0146bf5a2440895cbbb0 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:07:31 -0700 Subject: [PATCH 161/176] journal: add test coverage --- src/wormhole/journal.py | 2 ++ src/wormhole/test/test_journal.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/wormhole/test/test_journal.py diff --git a/src/wormhole/journal.py b/src/wormhole/journal.py index 61d1694..f7bf0f3 100644 --- a/src/wormhole/journal.py +++ b/src/wormhole/journal.py @@ -29,6 +29,8 @@ class Journal(object): @implementer(IJournal) class ImmediateJournal(object): + def __init__(self): + pass def queue_outbound(self, fn, *args, **kwargs): fn(*args, **kwargs) @contextlib.contextmanager diff --git a/src/wormhole/test/test_journal.py b/src/wormhole/test/test_journal.py new file mode 100644 index 0000000..96b9319 --- /dev/null +++ b/src/wormhole/test/test_journal.py @@ -0,0 +1,28 @@ +from __future__ import print_function, absolute_import, unicode_literals +from twisted.trial import unittest +from .. import journal +from .._interfaces import IJournal + +class Journal(unittest.TestCase): + def test_journal(self): + events = [] + j = journal.Journal(lambda: events.append("checkpoint")) + self.assert_(IJournal.providedBy(j)) + + with j.process(): + j.queue_outbound(events.append, "message1") + j.queue_outbound(events.append, "message2") + self.assertEqual(events, []) + self.assertEqual(events, ["checkpoint", "message1", "message2"]) + + def test_immediate(self): + events = [] + j = journal.ImmediateJournal() + self.assert_(IJournal.providedBy(j)) + + with j.process(): + j.queue_outbound(events.append, "message1") + self.assertEqual(events, ["message1"]) + j.queue_outbound(events.append, "message2") + self.assertEqual(events, ["message1", "message2"]) + self.assertEqual(events, ["message1", "message2"]) From 7c6332b770b2ae086b49f70b1d79695d8470c4af Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:09:24 -0700 Subject: [PATCH 162/176] wormhole: comments --- src/wormhole/wormhole.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 9f38c40..b3cf73d 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -246,7 +246,8 @@ class _DeferredWormhole(object): d.callback(self._closed_result) -def create(appid, relay_url, reactor, versions={}, +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, stderr=sys.stderr): @@ -280,15 +281,3 @@ def from_serialized(serialized, reactor, delegate, b.start() # ?? raise NotImplemented # should the new Wormhole call got_code? only if it wasn't called before. - -# after creating the wormhole object, app must call exactly one of: -# set_code(code), generate_code(), helper=input_code(), and then (if they need -# to know the code) wait for delegate.got_code() or d=w.when_code() - -# the helper for input_code() can be asked for completions: -# d=helper.get_completions(text_so_far), which will fire with a list of -# strings that could usefully be appended to text_so_far. - -# wormhole.input_code_readline(w) is a wrapper that knows how to use -# w.input_code() to drive rlcompleter - From a063ed2b3b9662a5b7f1ead2ee7126205e866e92 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:23:25 -0700 Subject: [PATCH 163/176] remove unused wordlist.py (now lives in _wordlist.py) --- src/wormhole/wordlist.py | 158 --------------------------------------- 1 file changed, 158 deletions(-) delete mode 100644 src/wormhole/wordlist.py diff --git a/src/wormhole/wordlist.py b/src/wormhole/wordlist.py deleted file mode 100644 index fe6c50c..0000000 --- a/src/wormhole/wordlist.py +++ /dev/null @@ -1,158 +0,0 @@ -from __future__ import unicode_literals -# The PGP Word List, which maps bytes to phonetically-distinct words. There -# are two lists, even and odd, and encodings should alternate between then to -# detect dropped words. https://en.wikipedia.org/wiki/PGP_Words - -# Thanks to Warren Guy for transcribing them: -# https://github.com/warrenguy/javascript-pgp-word-list - -from binascii import unhexlify - -raw_words = { -'00': ['aardvark', 'adroitness'], '01': ['absurd', 'adviser'], -'02': ['accrue', 'aftermath'], '03': ['acme', 'aggregate'], -'04': ['adrift', 'alkali'], '05': ['adult', 'almighty'], -'06': ['afflict', 'amulet'], '07': ['ahead', 'amusement'], -'08': ['aimless', 'antenna'], '09': ['Algol', 'applicant'], -'0A': ['allow', 'Apollo'], '0B': ['alone', 'armistice'], -'0C': ['ammo', 'article'], '0D': ['ancient', 'asteroid'], -'0E': ['apple', 'Atlantic'], '0F': ['artist', 'atmosphere'], -'10': ['assume', 'autopsy'], '11': ['Athens', 'Babylon'], -'12': ['atlas', 'backwater'], '13': ['Aztec', 'barbecue'], -'14': ['baboon', 'belowground'], '15': ['backfield', 'bifocals'], -'16': ['backward', 'bodyguard'], '17': ['banjo', 'bookseller'], -'18': ['beaming', 'borderline'], '19': ['bedlamp', 'bottomless'], -'1A': ['beehive', 'Bradbury'], '1B': ['beeswax', 'bravado'], -'1C': ['befriend', 'Brazilian'], '1D': ['Belfast', 'breakaway'], -'1E': ['berserk', 'Burlington'], '1F': ['billiard', 'businessman'], -'20': ['bison', 'butterfat'], '21': ['blackjack', 'Camelot'], -'22': ['blockade', 'candidate'], '23': ['blowtorch', 'cannonball'], -'24': ['bluebird', 'Capricorn'], '25': ['bombast', 'caravan'], -'26': ['bookshelf', 'caretaker'], '27': ['brackish', 'celebrate'], -'28': ['breadline', 'cellulose'], '29': ['breakup', 'certify'], -'2A': ['brickyard', 'chambermaid'], '2B': ['briefcase', 'Cherokee'], -'2C': ['Burbank', 'Chicago'], '2D': ['button', 'clergyman'], -'2E': ['buzzard', 'coherence'], '2F': ['cement', 'combustion'], -'30': ['chairlift', 'commando'], '31': ['chatter', 'company'], -'32': ['checkup', 'component'], '33': ['chisel', 'concurrent'], -'34': ['choking', 'confidence'], '35': ['chopper', 'conformist'], -'36': ['Christmas', 'congregate'], '37': ['clamshell', 'consensus'], -'38': ['classic', 'consulting'], '39': ['classroom', 'corporate'], -'3A': ['cleanup', 'corrosion'], '3B': ['clockwork', 'councilman'], -'3C': ['cobra', 'crossover'], '3D': ['commence', 'crucifix'], -'3E': ['concert', 'cumbersome'], '3F': ['cowbell', 'customer'], -'40': ['crackdown', 'Dakota'], '41': ['cranky', 'decadence'], -'42': ['crowfoot', 'December'], '43': ['crucial', 'decimal'], -'44': ['crumpled', 'designing'], '45': ['crusade', 'detector'], -'46': ['cubic', 'detergent'], '47': ['dashboard', 'determine'], -'48': ['deadbolt', 'dictator'], '49': ['deckhand', 'dinosaur'], -'4A': ['dogsled', 'direction'], '4B': ['dragnet', 'disable'], -'4C': ['drainage', 'disbelief'], '4D': ['dreadful', 'disruptive'], -'4E': ['drifter', 'distortion'], '4F': ['dropper', 'document'], -'50': ['drumbeat', 'embezzle'], '51': ['drunken', 'enchanting'], -'52': ['Dupont', 'enrollment'], '53': ['dwelling', 'enterprise'], -'54': ['eating', 'equation'], '55': ['edict', 'equipment'], -'56': ['egghead', 'escapade'], '57': ['eightball', 'Eskimo'], -'58': ['endorse', 'everyday'], '59': ['endow', 'examine'], -'5A': ['enlist', 'existence'], '5B': ['erase', 'exodus'], -'5C': ['escape', 'fascinate'], '5D': ['exceed', 'filament'], -'5E': ['eyeglass', 'finicky'], '5F': ['eyetooth', 'forever'], -'60': ['facial', 'fortitude'], '61': ['fallout', 'frequency'], -'62': ['flagpole', 'gadgetry'], '63': ['flatfoot', 'Galveston'], -'64': ['flytrap', 'getaway'], '65': ['fracture', 'glossary'], -'66': ['framework', 'gossamer'], '67': ['freedom', 'graduate'], -'68': ['frighten', 'gravity'], '69': ['gazelle', 'guitarist'], -'6A': ['Geiger', 'hamburger'], '6B': ['glitter', 'Hamilton'], -'6C': ['glucose', 'handiwork'], '6D': ['goggles', 'hazardous'], -'6E': ['goldfish', 'headwaters'], '6F': ['gremlin', 'hemisphere'], -'70': ['guidance', 'hesitate'], '71': ['hamlet', 'hideaway'], -'72': ['highchair', 'holiness'], '73': ['hockey', 'hurricane'], -'74': ['indoors', 'hydraulic'], '75': ['indulge', 'impartial'], -'76': ['inverse', 'impetus'], '77': ['involve', 'inception'], -'78': ['island', 'indigo'], '79': ['jawbone', 'inertia'], -'7A': ['keyboard', 'infancy'], '7B': ['kickoff', 'inferno'], -'7C': ['kiwi', 'informant'], '7D': ['klaxon', 'insincere'], -'7E': ['locale', 'insurgent'], '7F': ['lockup', 'integrate'], -'80': ['merit', 'intention'], '81': ['minnow', 'inventive'], -'82': ['miser', 'Istanbul'], '83': ['Mohawk', 'Jamaica'], -'84': ['mural', 'Jupiter'], '85': ['music', 'leprosy'], -'86': ['necklace', 'letterhead'], '87': ['Neptune', 'liberty'], -'88': ['newborn', 'maritime'], '89': ['nightbird', 'matchmaker'], -'8A': ['Oakland', 'maverick'], '8B': ['obtuse', 'Medusa'], -'8C': ['offload', 'megaton'], '8D': ['optic', 'microscope'], -'8E': ['orca', 'microwave'], '8F': ['payday', 'midsummer'], -'90': ['peachy', 'millionaire'], '91': ['pheasant', 'miracle'], -'92': ['physique', 'misnomer'], '93': ['playhouse', 'molasses'], -'94': ['Pluto', 'molecule'], '95': ['preclude', 'Montana'], -'96': ['prefer', 'monument'], '97': ['preshrunk', 'mosquito'], -'98': ['printer', 'narrative'], '99': ['prowler', 'nebula'], -'9A': ['pupil', 'newsletter'], '9B': ['puppy', 'Norwegian'], -'9C': ['python', 'October'], '9D': ['quadrant', 'Ohio'], -'9E': ['quiver', 'onlooker'], '9F': ['quota', 'opulent'], -'A0': ['ragtime', 'Orlando'], 'A1': ['ratchet', 'outfielder'], -'A2': ['rebirth', 'Pacific'], 'A3': ['reform', 'pandemic'], -'A4': ['regain', 'Pandora'], 'A5': ['reindeer', 'paperweight'], -'A6': ['rematch', 'paragon'], 'A7': ['repay', 'paragraph'], -'A8': ['retouch', 'paramount'], 'A9': ['revenge', 'passenger'], -'AA': ['reward', 'pedigree'], 'AB': ['rhythm', 'Pegasus'], -'AC': ['ribcage', 'penetrate'], 'AD': ['ringbolt', 'perceptive'], -'AE': ['robust', 'performance'], 'AF': ['rocker', 'pharmacy'], -'B0': ['ruffled', 'phonetic'], 'B1': ['sailboat', 'photograph'], -'B2': ['sawdust', 'pioneer'], 'B3': ['scallion', 'pocketful'], -'B4': ['scenic', 'politeness'], 'B5': ['scorecard', 'positive'], -'B6': ['Scotland', 'potato'], 'B7': ['seabird', 'processor'], -'B8': ['select', 'provincial'], 'B9': ['sentence', 'proximate'], -'BA': ['shadow', 'puberty'], 'BB': ['shamrock', 'publisher'], -'BC': ['showgirl', 'pyramid'], 'BD': ['skullcap', 'quantity'], -'BE': ['skydive', 'racketeer'], 'BF': ['slingshot', 'rebellion'], -'C0': ['slowdown', 'recipe'], 'C1': ['snapline', 'recover'], -'C2': ['snapshot', 'repellent'], 'C3': ['snowcap', 'replica'], -'C4': ['snowslide', 'reproduce'], 'C5': ['solo', 'resistor'], -'C6': ['southward', 'responsive'], 'C7': ['soybean', 'retraction'], -'C8': ['spaniel', 'retrieval'], 'C9': ['spearhead', 'retrospect'], -'CA': ['spellbind', 'revenue'], 'CB': ['spheroid', 'revival'], -'CC': ['spigot', 'revolver'], 'CD': ['spindle', 'sandalwood'], -'CE': ['spyglass', 'sardonic'], 'CF': ['stagehand', 'Saturday'], -'D0': ['stagnate', 'savagery'], 'D1': ['stairway', 'scavenger'], -'D2': ['standard', 'sensation'], 'D3': ['stapler', 'sociable'], -'D4': ['steamship', 'souvenir'], 'D5': ['sterling', 'specialist'], -'D6': ['stockman', 'speculate'], 'D7': ['stopwatch', 'stethoscope'], -'D8': ['stormy', 'stupendous'], 'D9': ['sugar', 'supportive'], -'DA': ['surmount', 'surrender'], 'DB': ['suspense', 'suspicious'], -'DC': ['sweatband', 'sympathy'], 'DD': ['swelter', 'tambourine'], -'DE': ['tactics', 'telephone'], 'DF': ['talon', 'therapist'], -'E0': ['tapeworm', 'tobacco'], 'E1': ['tempest', 'tolerance'], -'E2': ['tiger', 'tomorrow'], 'E3': ['tissue', 'torpedo'], -'E4': ['tonic', 'tradition'], 'E5': ['topmost', 'travesty'], -'E6': ['tracker', 'trombonist'], 'E7': ['transit', 'truncated'], -'E8': ['trauma', 'typewriter'], 'E9': ['treadmill', 'ultimate'], -'EA': ['Trojan', 'undaunted'], 'EB': ['trouble', 'underfoot'], -'EC': ['tumor', 'unicorn'], 'ED': ['tunnel', 'unify'], -'EE': ['tycoon', 'universe'], 'EF': ['uncut', 'unravel'], -'F0': ['unearth', 'upcoming'], 'F1': ['unwind', 'vacancy'], -'F2': ['uproot', 'vagabond'], 'F3': ['upset', 'vertigo'], -'F4': ['upshot', 'Virginia'], 'F5': ['vapor', 'visitor'], -'F6': ['village', 'vocalist'], 'F7': ['virus', 'voyager'], -'F8': ['Vulcan', 'warranty'], 'F9': ['waffle', 'Waterloo'], -'FA': ['wallet', 'whimsical'], 'FB': ['watchword', 'Wichita'], -'FC': ['wayside', 'Wilmington'], 'FD': ['willow', 'Wyoming'], -'FE': ['woodlark', 'yesteryear'], 'FF': ['Zulu', 'Yucatan'] -}; - -byte_to_even_word = dict([(unhexlify(k.encode("ascii")), both_words[0]) - for k,both_words - in raw_words.items()]) - -byte_to_odd_word = dict([(unhexlify(k.encode("ascii")), both_words[1]) - for k,both_words - in raw_words.items()]) -even_words_lowercase, odd_words_lowercase = set(), set() -even_words_lowercase_to_byte, odd_words_lowercase_to_byte = dict(), dict() -for k,both_words in raw_words.items(): - even_word, odd_word = both_words - - even_words_lowercase.add(even_word.lower()) - even_words_lowercase_to_byte[even_word.lower()] = unhexlify(k.encode("ascii")) - - odd_words_lowercase.add(odd_word.lower()) - odd_words_lowercase_to_byte[odd_word.lower()] = unhexlify(k.encode("ascii")) From f957e9b2fb135ce9f753cf5f0ddfa47daa063f0e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:26:52 -0700 Subject: [PATCH 164/176] test_wormhole: check when_verified() being called late --- src/wormhole/test/test_wormhole.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index dd2a552..e43220a 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -343,7 +343,7 @@ class Wormholes(ServerBase, unittest.TestCase): w1.allocate_code() code = yield w1.when_code() w2.set_code(code) - v1 = yield w1.when_verified() + v1 = yield w1.when_verified() # early v2 = yield w2.when_verified() self.failUnlessEqual(type(v1), type(b"")) self.failUnlessEqual(v1, v2) @@ -353,6 +353,11 @@ class Wormholes(ServerBase, unittest.TestCase): dataY = yield w2.when_received() 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()) + self.assertEqual(v1_late, v1) + yield w1.close() yield w2.close() From 9717b67d1b9be267872e2691086e3422325cdd07 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:27:06 -0700 Subject: [PATCH 165/176] comment out serialize() for now until it's implemented fully --- src/wormhole/_boss.py | 4 ++-- src/wormhole/wormhole.py | 34 +++++++++++++++++----------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 1784d3c..4fa7213 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -113,8 +113,8 @@ class Boss(object): file.flush() names[machine].set_trace(tracer) - def serialize(self): - raise NotImplemented + ## def serialize(self): + ## raise NotImplemented # and these are the state-machine transition functions, which don't take # args diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index b3cf73d..c8ca8b0 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -66,11 +66,11 @@ class _DelegatedWormhole(object): def set_code(self, code): self._boss.set_code(code) - def serialize(self): - s = {"serialized_wormhole_version": 1, - "boss": self._boss.serialize(), - } - return s + ## def serialize(self): + ## s = {"serialized_wormhole_version": 1, + ## "boss": self._boss.serialize(), + ## } + ## return s def send(self, plaintext): self._boss.send(plaintext) @@ -269,15 +269,15 @@ def create(appid, relay_url, reactor, # use keyword args for everything else b.start() return w -def from_serialized(serialized, reactor, delegate, - journal=None, tor_manager=None, - timing=None, stderr=sys.stderr): - assert serialized["serialized_wormhole_version"] == 1 - timing = timing or DebugTiming() - w = _DelegatedWormhole(delegate) - # now unpack state machines, including the SPAKE2 in Key - b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing) - w._set_boss(b) - b.start() # ?? - raise NotImplemented - # should the new Wormhole call got_code? only if it wasn't called before. +## def from_serialized(serialized, reactor, delegate, +## journal=None, tor_manager=None, +## timing=None, stderr=sys.stderr): +## assert serialized["serialized_wormhole_version"] == 1 +## timing = timing or DebugTiming() +## w = _DelegatedWormhole(delegate) +## # now unpack state machines, including the SPAKE2 in Key +## b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing) +## w._set_boss(b) +## b.start() # ?? +## raise NotImplemented +## # should the new Wormhole call got_code? only if it wasn't called before. From e787d0ffc554b8034feb08b17a158f480c0135ff Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:29:58 -0700 Subject: [PATCH 166/176] move Welcome into test_scripts, remove test_cli --- src/wormhole/test/test_cli.py | 45 ------------------------------- src/wormhole/test/test_scripts.py | 43 ++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 46 deletions(-) delete mode 100644 src/wormhole/test/test_cli.py diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py deleted file mode 100644 index 2d8d4db..0000000 --- a/src/wormhole/test/test_cli.py +++ /dev/null @@ -1,45 +0,0 @@ -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") diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_scripts.py index 25708a8..eb9872b 100644 --- a/src/wormhole/test/test_scripts.py +++ b/src/wormhole/test/test_scripts.py @@ -9,7 +9,7 @@ from twisted.internet.utils import getProcessOutputAndValue from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue from .. import __version__ from .common import ServerBase, config -from ..cli import cmd_send, cmd_receive +from ..cli import cmd_send, cmd_receive, welcome from ..errors import TransferError, WrongPasswordError, WelcomeError @@ -888,3 +888,44 @@ class AppID(ServerBase, unittest.TestCase): ).fetchall() self.assertEqual(len(used), 1, used) self.assertEqual(used[0]["app_id"], u"appid2") + +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") From 3f878fb98106f9e9762dbfa4eaa57fd2ceed3400 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:30:56 -0700 Subject: [PATCH 167/176] rename test_scripts to test_cli --- src/wormhole/test/{test_scripts.py => test_cli.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/wormhole/test/{test_scripts.py => test_cli.py} (100%) diff --git a/src/wormhole/test/test_scripts.py b/src/wormhole/test/test_cli.py similarity index 100% rename from src/wormhole/test/test_scripts.py rename to src/wormhole/test/test_cli.py From 194f0be471b8d78a4b268e690bbbcefa9a248307 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 12:46:42 -0700 Subject: [PATCH 168/176] api.md: fix some typos --- docs/api.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/api.md b/docs/api.md index 7709719..b720725 100644 --- a/docs/api.md +++ b/docs/api.md @@ -133,9 +133,9 @@ optional arguments: * `welcome_handler`: this is a function that will be called when the Rendezvous Server's "welcome" message is received. It is used to display important server messages in an application-specific way. -* `app_versions`: this can accept a dictionary (JSON-encodable) of data that - will be made available to the peer via the `got_version` event. This data - is delivered before any data messages, and can be used to indicate peer +* `versions`: this can accept a dictionary (JSON-encodable) of data that will + be made available to the peer via the `got_version` event. This data is + delivered before any data messages, and can be used to indicate peer capabilities. ## Code Management @@ -386,10 +386,10 @@ those Deferreds. 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(version)`: - fired when the VERSION message arrives from the peer. This fires at the - same time as `verified`, but delivers the "app_versions" data (passed into - `wormhole.create`) instead of the verifier string. +* 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 each time a data message arrives from the peer, with the bytestring that the peer passed into `w.send(data)`. @@ -489,19 +489,19 @@ in python3): ## Full API list -action | Deferred-Mode | Delegated-Mode --------------------------- | -------------------- | -------------- -w.generate_code(length=2) | | -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 +------------------ | -------------------- | -------------- +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() | ## Dilation From ba365624829925751cbd0d06ffb77c32b5e58e30 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 13:02:27 -0700 Subject: [PATCH 169/176] docs: move Dilation up next to Serialization, as both are speculative --- docs/api.md | 102 ++++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/docs/api.md b/docs/api.md index b720725..9041d72 100644 --- a/docs/api.md +++ b/docs/api.md @@ -443,7 +443,7 @@ method to be called. ## Serialization -(this section is speculative: this code has not yet been written) +(NOTE: this section is speculative: this code has not yet been written) Wormhole objects can be serialized. This can be useful for apps which save their own state before shutdown, and restore it when they next start up @@ -468,6 +468,56 @@ To ensure correct behavior, serialization should probably only be done in If you use serialization, be careful to never use the same partial wormhole object twice. +## Dilation + +(NOTE: this section is speculative: this code has not yet been written) + +In the longer term, the Wormhole object will incorporate the "Transit" +functionality (see transit.md) directly, removing the need to instantiate a +second object. A Wormhole can be "dilated" into a form that is suitable for +bulk data transfer. + +All wormholes start out "undilated". In this state, all messages are queued +on the Rendezvous Server for the lifetime of the wormhole, and server-imposed +number/size/rate limits apply. Calling `w.dilate()` initiates the dilation +process, and success is signalled via either `d=w.when_dilated()` firing, or +`dg.wormhole_dilated()` being called. Once dilated, the Wormhole can be used +as an IConsumer/IProducer, and messages will be sent on a direct connection +(if possible) or through the transit relay (if not). + +What's good about a non-dilated wormhole?: + +* setup is faster: no delay while it tries to make a direct connection +* survives temporary network outages, since messages are queued +* works with "journaled mode", allowing progress to be made even when both + sides are never online at the same time, by serializing the wormhole + +What's good about dilated wormholes?: + +* they support bulk data transfer +* you get flow control (backpressure), and IProducer/IConsumer +* throughput is faster: no store-and-forward step + +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. + +Dilated wormholes can provide multiple "channels": these are multiplexed +through the single (encrypted) TCP connection. Each channel is a separate +stream (offering IProducer/IConsumer) + +To create a channel, call `c = w.create_channel()` on a dilated wormhole. The +"channel ID" can be obtained with `c.get_id()`. This ID will be a short +(unicode) string, which can be sent to the other side via a normal +`w.send()`, or any other means. On the other side, use `c = +w.open_channel(channel_id)` to get a matching channel object. + +Then use `c.send(data)` and `d=c.when_received()` to exchange data, or wire +them up with `c.registerProducer()`. Note that channels do not close until +the wormhole connection is closed, so they do not have separate `close()` +methods or events. Therefore if you plan to send files through them, you'll +need to inform the recipient ahead of time about how many bytes to expect. + ## Bytes, Strings, Unicode, and Python 3 All cryptographically-sensitive parameters are passed as bytes ("str" in @@ -503,53 +553,3 @@ key=w.derive_key(purpose, length) | | w.close() | | dg.wormhole_closed(result) . | d=w.close() | - -## Dilation - -(this section is speculative: this code has not yet been written) - -In the longer term, the Wormhole object will incorporate the "Transit" -functionality (see transit.md) directly, removing the need to instantiate a -second object. A Wormhole can be "dilated" into a form that is suitable for -bulk data transfer. - -All wormholes start out "undilated". In this state, all messages are queued -on the Rendezvous Server for the lifetime of the wormhole, and server-imposed -number/size/rate limits apply. Calling `w.dilate()` initiates the dilation -process, and success is signalled via either `d=w.when_dilated()` firing, or -`dg.wormhole_dilated()` being called. Once dilated, the Wormhole can be used -as an IConsumer/IProducer, and messages will be sent on a direct connection -(if possible) or through the transit relay (if not). - -What's good about a non-dilated wormhole?: - -* setup is faster: no delay while it tries to make a direct connection -* survives temporary network outages, since messages are queued -* works with "journaled mode", allowing progress to be made even when both - sides are never online at the same time, by serializing the wormhole - -What's good about dilated wormholes?: - -* they support bulk data transfer -* you get flow control (backpressure), and provide IProducer/IConsumer -* throughput is faster: no store-and-forward step - -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. - -Dilated wormholes can provide multiple "channels": these are multiplexed -through the single (encrypted) TCP connection. Each channel is a separate -stream (offering IProducer/IConsumer) - -To create a channel, call `c = w.create_channel()` on a dilated wormhole. The -"channel ID" can be obtained with `c.get_id()`. This ID will be a short -(unicode) string, which can be sent to the other side via a normal -`w.send()`, or any other means. On the other side, use `c = -w.open_channel(channel_id)` to get a matching channel object. - -Then use `c.send(data)` and `d=c.when_received()` to exchange data, or wire -them up with `c.registerProducer()`. Note that channels do not close until -the wormhole connection is closed, so they do not have separate `close()` -methods or events. Therefore if you plan to send files through them, you'll -need to inform the recipient ahead of time about how many bytes to expect. From af3bb0095d4a8c1bddb38ea5d505b12914ef06b8 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 13:22:15 -0700 Subject: [PATCH 170/176] docs: expand section on close() --- docs/api.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api.md b/docs/api.md index 9041d72..88eede7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -441,6 +441,23 @@ In Deferred mode, this just means waiting for the Deferred returned by doesn't return anything) and waiting for the delegate's `wormhole_closed()` method to be called. +`w.close()` will errback (with some form of `WormholeError`) if anything went +wrong with the process, such as: + +* `WelcomeError`: the server told us to signal an error, probably because the + client is too old understand some new protocol feature +* `ServerError`: the server rejected something we did +* `LonelyError`: we didn't hear from the other side, so no key was + established +* `WrongPasswordError`: we received at least one incorrectly-encrypted + message. This probably indicates that the other side used a different + wormhole code than we did, perhaps because of a typo, or maybe an attacker + tried to guess your code and failed. + +If the wormhole was happy at the time it was closed, the `w.close()` Deferred +will callback (probably with the string "happy", but this may change in the +future). + ## Serialization (NOTE: this section is speculative: this code has not yet been written) From ddc6319bf6d2ffdea211b17a99b7d3193116cea1 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 13:52:26 -0700 Subject: [PATCH 171/176] protocol docs fixups --- docs/client-protocol.md | 20 +++++++++-------- docs/server-protocol.md | 50 +++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/client-protocol.md b/docs/client-protocol.md index 037d69b..331ccf6 100644 --- a/docs/client-protocol.md +++ b/docs/client-protocol.md @@ -31,9 +31,10 @@ similar negotiation. At this stage, the client knows the supposed shared key, but has not yet seen evidence that the peer knows it too. When the first peer message arrives (i.e. the first message with a `.side` that does not equal our own), it will -be decrypted: if this decryption succeeds, then we're confident that -*somebody* used the same wormhole code as us. This event pushes the client -mood from "lonely" to "happy". +be decrypted: we use authenticated encryption (`nacl.SecretBox`), so if this +decryption succeeds, then we're confident that *somebody* used the same +wormhole code as us. This event pushes the client mood from "lonely" to +"happy". This might be triggered by the peer's `version` message, but if we had to re-establish the Rendezvous Server connection, we might get peer messages out @@ -46,14 +47,15 @@ strictly in-order: if we see phases 3 then 2 then 1, all three will be delivered in sequence after phase 1 is received. If any message cannot be successfully decrypted, the mood is set to "scary", -and the wormhole is closed. All pending Deferreds will be errbacked with some -kind of WormholeError, the nameplate/mailbox will be released, and the -WebSocket connection will be dropped. If the application calls `close()`, the -resulting Deferred will not fire until deallocation has finished and the -WebSocket is closed, and then it will fire with an errback. +and the wormhole is closed. All pending Deferreds will be errbacked with a +`WrongPasswordError` (a subclass of `WormholeError`), the nameplate/mailbox +will be released, and the WebSocket connection will be dropped. If the +application calls `close()`, the resulting Deferred will not fire until +deallocation has finished and the WebSocket is closed, and then it will fire +with an errback. Both `version` and all numeric (app-specific) phases are encrypted. The -message body will be the hex-encoded output of a NACL SecretBox, keyed by a +message body will be the hex-encoded output of a NaCl `SecretBox`, keyed by a phase+side -specific key (computed with HKDF-SHA256, using the shared PAKE key as the secret input, and `wormhole:phase:%s%s % (SHA256(side), SHA256(phase))` as the CTXinfo), with a random nonce. diff --git a/docs/server-protocol.md b/docs/server-protocol.md index de52cd5..3afb967 100644 --- a/docs/server-protocol.md +++ b/docs/server-protocol.md @@ -50,23 +50,23 @@ the server and both clients, to identify protocol slowdowns and guide optimization efforts. To support this, the client/server messages include additional keys. Client->Server messages include a random `id` key, which is copied into the `ack` that is immediately sent back to the client for all -commands (and is ignored except for the timing tool). Some client->server -messages (`list`, `allocate`, `claim`, `release`, `close`, `ping`) provoke a -direct response by the server: for these, `id` is copied into the response. -This helps the tool correlate the command and response. All server->client -messages have a `server_tx` timestamp (seconds since epoch, as a float), -which records when the message left the server. Direct responses include a -`server_rx` timestamp, to record when the client's command was received. The -tool combines these with local timestamps (recorded by the client and not -shared with the server) to build a full picture of network delays and -round-trip times. +commands (logged for the timing tool but otherwise ignored). Some +client->server messages (`list`, `allocate`, `claim`, `release`, `close`, +`ping`) provoke a direct response by the server: for these, `id` is copied +into the response. This helps the tool correlate the command and response. +All server->client messages have a `server_tx` timestamp (seconds since +epoch, as a float), which records when the message left the server. Direct +responses include a `server_rx` timestamp, to record when the client's +command was received. The tool combines these with local timestamps (recorded +by the client and not shared with the server) to build a full picture of +network delays and round-trip times. All messages are serialized as JSON, encoded to UTF-8, and the resulting bytes sent as a single "binary-mode" WebSocket payload. Servers can signal `error` for any message type it does not recognize. Clients and Servers must ignore unrecognized keys in otherwise-recognized -messages. +messages. Clients must ignore unrecognized message types from the Server. ## Connection-Specific (Client-to-Server) Messages @@ -74,8 +74,8 @@ The first thing each client sends to the server, immediately after the WebSocket connection is established, is a `bind` message. This specifies the AppID and side (in keys `appid` and `side`, respectively) that all subsequent messages will be scoped to. While technically each message could be -independent, I thought it would be less confusing to use exactly one -WebSocket per logical wormhole connection. +independent (with its own `appid` and `side`), I thought it would be less +confusing to use exactly one WebSocket per logical wormhole connection. The first thing the server sends to each client is the `welcome` message. This is intended to deliver important status information to the client that @@ -123,7 +123,7 @@ all nameplates, even ones which they've allocated themselves. Nameplates (on the server) must live until the second client has learned about the associated mailbox, after which point they can be reused by other clients. So if two clients connect quickly, but then maintain a long-lived -wormhole connection, the do not need to consume the limited spare of short +wormhole connection, the do not need to consume the limited space of short nameplates for that whole time. The `allocate` command allocates a nameplate (the server returns one that is @@ -133,7 +133,9 @@ with all allocated nameplates for the bound AppID: this helps the code-input tab-completion feature know which prefixes to offer. The `nameplates` response returns a list of dictionaries, one per claimed nameplate, with at least an `id` key in each one (with the nameplate string). Future versions -may record additional attributes in the nameplate records. +may record additional attributes in the nameplate records, specifically a +wordlist identifier and a code length (again to help with code-completion on +the receiver). ## Mailboxes @@ -162,17 +164,17 @@ interaction. The server records the mood in its "usage" record, so the server operator can get a sense of how many connections are succeeding and failing. The moods currently recognized by the Rendezvous Server are: -* happy (default): the PAKE key-establishment worked, and the client saw a - valid encrypted message from its peer -* lonely: the client gave up without hearing anything from its peer -* scary: the client saw an invalid encrypted message from its peer, +* `happy` (default): the PAKE key-establishment worked, and the client saw at + least one valid encrypted message from its peer +* `lonely`: the client gave up without hearing anything from its peer +* `scary`: the client saw an invalid encrypted message from its peer, indicating that either the wormhole code was typed in wrong, or an attacker tried (and failed) to guess the code -* errory: the client encountered some other error: protocol problem or +* `errory`: the client encountered some other error: protocol problem or internal error -The server will also record "pruney" if it deleted the mailbox due to -inactivity, or "crowded" if more than two sides tried to access the mailbox. +The server will also record `pruney` if it deleted the mailbox due to +inactivity, or `crowded` if more than two sides tried to access the mailbox. When clients use the `add` command to add a client-to-client message, they will put the body (a bytestring) into the command as a hex-encoded string in @@ -223,7 +225,7 @@ any), and which ones provoke direct responses: The server stores all messages in a database, so it should not lose any information when it is restarted. The server will not send a direct response until any side-effects (such as the message being added to the -mailbox) being safely committed to the database. +mailbox) have been safely committed to the database. The client library knows how to resume the protocol after a reconnection event, assuming the client process itself continues to run. @@ -232,4 +234,4 @@ Clients which terminate entirely between messages (e.g. a secure chat application, which requires multiple wormhole messages to exchange address-book entries, and which must function even if the two apps are never both running at the same time) can use "Journal Mode" to ensure forward -progress is made: see "api.md" (Journal Mode) for details. +progress is made: see "journal.md" for details. From 67d53f138812c03819e3e2b9de2bf162edfd4240 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 15:05:37 -0700 Subject: [PATCH 172/176] wordlist: fix "1-word- TAB" case --- src/wormhole/_rlcompleter.py | 2 +- src/wormhole/_wordlist.py | 5 ++++- src/wormhole/test/test_wordlist.py | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index 5be03fa..a525a06 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -114,7 +114,7 @@ class CodeInputter(object): # improve the user experience. self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM # and we're completing on words now - debug(" getting words") + debug(" getting words (%s)" % (words,)) completions = [nameplate+"-"+c for c in self.bcft(ih.get_word_completions, words)] diff --git a/src/wormhole/_wordlist.py b/src/wormhole/_wordlist.py index f966971..e14972b 100644 --- a/src/wormhole/_wordlist.py +++ b/src/wormhole/_wordlist.py @@ -172,7 +172,10 @@ class PGPWordList(object): completions = set() for word in words: if word.startswith(last_partial_word): - suffix = prefix[:-lp] + word + if lp == 0: + suffix = prefix + word + else: + suffix = prefix[:-lp] + word # append a hyphen if we expect more words if count+1 < num_words: suffix += "-" diff --git a/src/wormhole/test/test_wordlist.py b/src/wormhole/test/test_wordlist.py index b165fc3..6b86cdb 100644 --- a/src/wormhole/test/test_wordlist.py +++ b/src/wormhole/test/test_wordlist.py @@ -10,6 +10,10 @@ class Completions(unittest.TestCase): self.assertEqual(gc("ar", 2), {"armistice-", "article-"}) self.assertEqual(gc("armis", 2), {"armistice-"}) self.assertEqual(gc("armistice", 2), {"armistice-"}) + lots = gc("armistice-", 2) + self.assertEqual(len(lots), 256, lots) + first = list(lots)[0] + self.assert_(first.startswith("armistice-"), first) self.assertEqual(gc("armistice-ba", 2), {"armistice-baboon", "armistice-backfield", "armistice-backward", "armistice-banjo"}) From 83e55f1f3ef6c2494b2a8184443dada771c8fdf1 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 18:27:41 -0700 Subject: [PATCH 173/176] add w.when_key(), fix w.when_verified() to fire later Previously, w.when_verified() was documented to fire only after a valid encrypted message was received, but in fact it fired as soon as the shared key was derived (before any encrypted messages are seen, so no actual "verification" could occur yet). This fixes that, and also adds a new w.when_key() API call which fires at the earlier point. Having something which fires early is useful for the CLI commands that want to print a pacifier message when the peer is responding slowly. In particular it helps detect the case where 'wormhole send' has quit early (after depositing the PAKE message on the server, but before the receiver has started). In this case, the receiver will compute the shared key, but then wait forever hoping for a VERSION that will never come. By starting a timer when w.when_key() fires, and cancelling it when w.when_verified() fires, we have a good place to tell the user that something is taking longer than it should have. This shifts responsibility for notifying Boss.got_verifier, out of Key and into Receive, since Receive is what notices the first valid encrypted message. It also shifts the Boss's ordering expectations: it now receives B.happy() before B.got_verifier(), and consequently got_verifier ought to arrive in the S2_happy state rather than S1_lonely. --- docs/api.md | 24 ++++++++-- docs/state-machines/boss.dot | 2 +- docs/state-machines/key.dot | 2 +- docs/state-machines/machines.dot | 4 +- docs/state-machines/receive.dot | 2 +- src/wormhole/_boss.py | 8 ++-- src/wormhole/_key.py | 1 - src/wormhole/_receive.py | 8 +++- src/wormhole/test/common.py | 7 +++ src/wormhole/test/test_machines.py | 28 ++++++----- src/wormhole/test/test_wormhole.py | 77 +++++++++++++++++++++++++----- src/wormhole/wormhole.py | 20 +++++++- 12 files changed, 143 insertions(+), 40 deletions(-) diff --git a/docs/api.md b/docs/api.md index 88eede7..67ae8ac 100644 --- a/docs/api.md +++ b/docs/api.md @@ -286,11 +286,17 @@ 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. -Once retrieved, you can turn this into hex or Base64 to print it, or render -it as ASCII-art, etc. Once the users are convinced that `verify()` from both -sides are the same, call `send()` to continue the protocol. If you call -`send()` before `verify()`, it will perform the complete protocol without -pausing. +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 @@ -377,6 +383,14 @@ those Deferreds. 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 + 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 diff --git a/docs/state-machines/boss.dot b/docs/state-machines/boss.dot index 866f0e8..8e32899 100644 --- a/docs/state-machines/boss.dot +++ b/docs/state-machines/boss.dot @@ -73,7 +73,7 @@ digraph { {rank=same; Other S_closed} Other [shape="box" style="dashed" - 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" + 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" ] diff --git a/docs/state-machines/key.dot b/docs/state-machines/key.dot index 01f5f2d..feda71b 100644 --- a/docs/state-machines/key.dot +++ b/docs/state-machines/key.dot @@ -55,7 +55,7 @@ digraph { S1 -> P1_compute [label="got_pake\npake good"] #S1 -> P_mood_lonely [label="close"] - P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nB.got_verifier\nR.got_key" shape="box"] + P1_compute [label="compute_key\nM.add_message(version)\nB.got_key\nR.got_key" shape="box"] P1_compute -> S4 S4 [label="S4: know_key" color="green"] diff --git a/docs/state-machines/machines.dot b/docs/state-machines/machines.dot index 39f7d83..eccc96d 100644 --- a/docs/state-machines/machines.dot +++ b/docs/state-machines/machines.dot @@ -42,7 +42,7 @@ digraph { #Boss -> Mailbox [color="blue"] Mailbox -> Order [style="dashed" label="got_message (once)"] - Key -> Boss [style="dashed" label="got_key\ngot_verifier\nscared"] + Key -> Boss [style="dashed" label="got_key\nscared"] Order -> Key [style="dashed" label="got_pake"] Order -> Receive [style="dashed" label="got_message"] #Boss -> Key [color="blue"] @@ -54,7 +54,7 @@ digraph { Key -> Receive [style="dashed" label="got_key"] Receive -> Boss [style="dashed" - label="happy\nscared\ngot_message"] + label="happy\nscared\ngot_verifier\ngot_message"] Nameplate -> Connection [style="dashed" label="tx_claim\ntx_release"] Connection -> Nameplate [style="dashed" diff --git a/docs/state-machines/receive.dot b/docs/state-machines/receive.dot index 3fe136d..ba757e1 100644 --- a/docs/state-machines/receive.dot +++ b/docs/state-machines/receive.dot @@ -19,7 +19,7 @@ digraph { S1 [label="S1:\nunverified key" color="orange"] S1 -> P_mood_scary [label="got_message\n(bad)"] S1 -> P1_accept_msg [label="got_message\n(good)" color="orange"] - P1_accept_msg [shape="box" label="S.got_verified_key\nB.happy\nB.got_message" + P1_accept_msg [shape="box" label="S.got_verified_key\nB.happy\nB.got_verifier\nB.got_message" color="orange"] P1_accept_msg -> S2 [color="orange"] diff --git a/src/wormhole/_boss.py b/src/wormhole/_boss.py index 4fa7213..2c93f0b 100644 --- a/src/wormhole/_boss.py +++ b/src/wormhole/_boss.py @@ -197,8 +197,8 @@ class Boss(object): @m.input() def got_code(self, code): pass - # Key sends (got_key, got_verifier, scared) - # Receive sends (got_message, happy, scared) + # Key sends (got_key, scared) + # Receive sends (got_message, happy, got_verifier, scared) @m.input() def happy(self): pass @m.input() @@ -307,11 +307,11 @@ class Boss(object): S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely]) S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send]) S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key]) - S1_lonely.upon(got_verifier, enter=S1_lonely, outputs=[W_got_verifier]) S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error]) S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error]) S2_happy.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome]) + S2_happy.upon(got_verifier, enter=S2_happy, outputs=[W_got_verifier]) 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(scared, enter=S3_closing, outputs=[close_scared]) @@ -322,6 +322,7 @@ class Boss(object): S3_closing.upon(rx_unwelcome, enter=S3_closing, outputs=[]) S3_closing.upon(rx_error, enter=S3_closing, outputs=[]) + S3_closing.upon(got_verifier, 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(happy, enter=S3_closing, outputs=[]) @@ -332,6 +333,7 @@ class Boss(object): S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error]) S4_closed.upon(rx_unwelcome, enter=S4_closed, outputs=[]) + S4_closed.upon(got_verifier, 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(happy, enter=S4_closed, outputs=[]) diff --git a/src/wormhole/_key.py b/src/wormhole/_key.py index 622af65..91c972f 100644 --- a/src/wormhole/_key.py +++ b/src/wormhole/_key.py @@ -166,7 +166,6 @@ class _SortedKey(object): with self._timing.add("pake2", waiting="crypto"): key = self._sp.finish(msg2) self._B.got_key(key) - self._B.got_verifier(derive_key(key, b"wormhole:verifier")) phase = "version" data_key = derive_phase_key(key, self._side, phase) plaintext = dict_to_bytes(self._versions) diff --git a/src/wormhole/_receive.py b/src/wormhole/_receive.py index 1be5220..7003110 100644 --- a/src/wormhole/_receive.py +++ b/src/wormhole/_receive.py @@ -4,7 +4,7 @@ from attr import attrs, attrib from attr.validators import provides, instance_of from automat import MethodicalMachine from . import _interfaces -from ._key import derive_phase_key, decrypt_data, CryptoError +from ._key import derive_key, derive_phase_key, decrypt_data, CryptoError @attrs @implementer(_interfaces.IReceive) @@ -63,6 +63,9 @@ class Receive(object): def W_happy(self, phase, plaintext): self._B.happy() @m.output() + def W_got_verifier(self, phase, plaintext): + self._B.got_verifier(derive_key(self._key, b"wormhole:verifier")) + @m.output() def W_got_message(self, phase, plaintext): assert isinstance(phase, type("")), type(phase) assert isinstance(plaintext, type(b"")), type(plaintext) @@ -73,7 +76,8 @@ class Receive(object): S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key]) S1_unverified_key.upon(got_message_good, enter=S2_verified_key, - outputs=[S_got_verified_key, W_happy, W_got_message]) + outputs=[S_got_verified_key, + W_happy, W_got_verifier, W_got_message]) S1_unverified_key.upon(got_message_bad, enter=S3_scared, outputs=[W_scared]) S2_verified_key.upon(got_message_bad, enter=S3_scared, diff --git a/src/wormhole/test/common.py b/src/wormhole/test/common.py index dd1d490..f0db4ac 100644 --- a/src/wormhole/test/common.py +++ b/src/wormhole/test/common.py @@ -91,3 +91,10 @@ def poll_until(predicate): d = defer.Deferred() reactor.callLater(0.001, d.callback, None) yield d + +@defer.inlineCallbacks +def pause_one_tick(): + # return a Deferred that won't fire until at least the next reactor tick + d = defer.Deferred() + reactor.callLater(0.001, d.callback, None) + yield d diff --git a/src/wormhole/test/test_machines.py b/src/wormhole/test/test_machines.py index df002b7..233524e 100644 --- a/src/wormhole/test/test_machines.py +++ b/src/wormhole/test/test_machines.py @@ -128,7 +128,8 @@ class Receive(unittest.TestCase): def build(self): events = [] r = _receive.Receive(u"side", timing.DebugTiming()) - b = Dummy("b", events, IBoss, "happy", "scared", "got_message") + b = Dummy("b", events, IBoss, + "happy", "scared", "got_verifier", "got_message") s = Dummy("s", events, ISend, "got_verified_key") r.wire(b, s) return r, b, s, events @@ -138,12 +139,14 @@ class Receive(unittest.TestCase): key = b"key" r.got_key(key) self.assertEqual(events, []) + verifier = derive_key(key, b"wormhole:verifier") phase1_key = derive_phase_key(key, u"side", u"phase1") data1 = b"data1" good_body = encrypt_data(phase1_key, data1) r.got_message(u"side", u"phase1", good_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ]) @@ -153,6 +156,7 @@ class Receive(unittest.TestCase): r.got_message(u"side", u"phase2", good_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ("b.got_message", u"phase2", data2), ]) @@ -181,12 +185,14 @@ class Receive(unittest.TestCase): key = b"key" r.got_key(key) self.assertEqual(events, []) + verifier = derive_key(key, b"wormhole:verifier") phase1_key = derive_phase_key(key, u"side", u"phase1") data1 = b"data1" good_body = encrypt_data(phase1_key, data1) r.got_message(u"side", u"phase1", good_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ]) @@ -196,6 +202,7 @@ class Receive(unittest.TestCase): r.got_message(u"side", u"phase2", bad_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ("b.scared",), ]) @@ -203,6 +210,7 @@ class Receive(unittest.TestCase): r.got_message(u"side", u"phase2", bad_body) self.assertEqual(events, [("s.got_verified_key", key), ("b.happy",), + ("b.got_verifier", verifier), ("b.got_message", u"phase1", data1), ("b.scared",), ]) @@ -216,7 +224,7 @@ class Key(unittest.TestCase): def build(self): events = [] k = _key.Key(u"appid", {}, u"side", timing.DebugTiming()) - b = Dummy("b", events, IBoss, "scared", "got_key", "got_verifier") + b = Dummy("b", events, IBoss, "scared", "got_key") m = Dummy("m", events, IMailbox, "add_message") r = Dummy("r", events, IReceive, "got_key") k.wire(b, m, r) @@ -237,11 +245,10 @@ class Key(unittest.TestCase): key2 = sp.finish(msg1_bytes) msg2 = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg2_bytes)}) k.got_pake(msg2) - self.assertEqual(len(events), 4, events) + self.assertEqual(len(events), 3, events) self.assertEqual(events[0], ("b.got_key", key2)) - self.assertEqual(events[1][0], "b.got_verifier") - self.assertEqual(events[2][:2], ("m.add_message", "version")) - self.assertEqual(events[3], ("r.got_key", key2)) + self.assertEqual(events[1][:2], ("m.add_message", "version")) + self.assertEqual(events[2], ("r.got_key", key2)) def test_bad(self): k, b, m, r, events = self.build() @@ -274,16 +281,15 @@ class Key(unittest.TestCase): self.assertEqual(len(events), 0) k.got_code(code) - self.assertEqual(len(events), 5) + self.assertEqual(len(events), 4) self.assertEqual(events[0][:2], ("m.add_message", "pake")) msg1_json = events[0][2].decode("utf-8") msg1 = json.loads(msg1_json) msg1_bytes = hexstr_to_bytes(msg1["pake_v1"]) key2 = sp.finish(msg1_bytes) self.assertEqual(events[1], ("b.got_key", key2)) - self.assertEqual(events[2][0], "b.got_verifier") - self.assertEqual(events[3][:2], ("m.add_message", "version")) - self.assertEqual(events[4], ("r.got_key", key2)) + self.assertEqual(events[2][:2], ("m.add_message", "version")) + self.assertEqual(events[3], ("r.got_key", key2)) class Code(unittest.TestCase): def build(self): @@ -1178,8 +1184,8 @@ class Boss(unittest.TestCase): # pretend a peer message was correctly decrypted b.got_key(b"key") - b.got_verifier(b"verifier") b.happy() + b.got_verifier(b"verifier") b.got_message("version", b"{}") b.got_message("0", b"msg1") self.assertEqual(events, [("w.got_key", b"key"), diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index e43220a..824f23f 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -4,7 +4,7 @@ import mock from twisted.trial import unittest from twisted.internet import reactor from twisted.internet.defer import gatherResults, inlineCallbacks -from .common import ServerBase, poll_until +from .common import ServerBase, poll_until, pause_one_tick from .. import wormhole, _rendezvous from ..errors import (WrongPasswordError, KeyFormatError, WormholeClosed, LonelyError, @@ -115,10 +115,16 @@ class Wormholes(ServerBase, unittest.TestCase): code = yield w1.when_code() w2.set_code(code) + yield w1.when_key() + yield w2.when_key() + verifier1 = yield w1.when_verified() verifier2 = yield w2.when_verified() self.assertEqual(verifier1, verifier2) + self.successResultOf(w1.when_key()) + self.successResultOf(w2.when_key()) + version1 = yield w1.when_version() version2 = yield w2.when_version() # TODO: add the ability to set app-versions @@ -291,12 +297,13 @@ class Wormholes(ServerBase, unittest.TestCase): yield w1.close() yield w2.close() - # once closed, all Deferred-yielding API calls get an error - e = yield self.assertFailure(w1.when_code(), WormholeClosed) - self.assertEqual(e.args[0], "happy") - yield self.assertFailure(w1.when_verified(), WormholeClosed) - yield self.assertFailure(w1.when_version(), WormholeClosed) - yield self.assertFailure(w1.when_received(), WormholeClosed) + # once closed, all Deferred-yielding API calls get an immediate error + f = self.failureResultOf(w1.when_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) @inlineCallbacks @@ -315,16 +322,64 @@ class Wormholes(ServerBase, unittest.TestCase): w1.send(b"should still work") w2.send(b"should still work") - # API calls that wait (i.e. get) will errback - yield self.assertFailure(w2.when_received(), WrongPasswordError) - yield self.assertFailure(w1.when_received(), WrongPasswordError) + key2 = yield w2.when_key() # should work + # w2 has just received w1.PAKE, and is about to send w2.VERSION + key1 = yield w1.when_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. + self.assertNotEqual(key1, key2) + # 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() + + # wait for each side to notice the failure yield self.assertFailure(w1.when_verified(), WrongPasswordError) - yield self.assertFailure(w1.when_version(), WrongPasswordError) + yield self.assertFailure(w2.when_verified(), 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_received, WrongPasswordError) + self.failureResultOf(d2_verified, WrongPasswordError) + self.failureResultOf(d2_version, 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 + + # 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) 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) @inlineCallbacks def test_wrong_password_with_spaces(self): diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index c8ca8b0..0077020 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -97,6 +97,7 @@ class _DelegatedWormhole(object): def got_code(self, code): self._delegate.wormhole_code(code) def got_key(self, key): + self._delegate.wormhole_key() self._key = key # for derive_key() def got_verifier(self, verifier): self._delegate.wormhole_verified(verifier) @@ -113,6 +114,7 @@ class _DeferredWormhole(object): self._code = None self._code_observers = [] self._key = None + self._key_observers = [] self._verifier = None self._verifier_observers = [] self._versions = None @@ -138,6 +140,15 @@ class _DeferredWormhole(object): self._code_observers.append(d) return d + def when_key(self): + if self._observer_result is not None: + return defer.fail(self._observer_result) + if self._key is not None: + return defer.succeed(self._key) + d = defer.Deferred() + self._key_observers.append(d) + return d + def when_verified(self): if self._observer_result is not None: return defer.fail(self._observer_result) @@ -180,7 +191,7 @@ class _DeferredWormhole(object): """Derive a new key from the established wormhole channel for some other purpose. This is a deterministic randomized function of the session key and the 'purpose' string (unicode/py3-string). This - cannot be called until when_verifier() has fired, nor after close() + cannot be called until when_verified() has fired, nor after close() was called. """ if not isinstance(purpose, type("")): raise TypeError(type(purpose)) @@ -210,6 +221,9 @@ class _DeferredWormhole(object): self._code_observers[:] = [] def got_key(self, key): self._key = key # for derive_key() + 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: @@ -232,10 +246,12 @@ class _DeferredWormhole(object): if isinstance(result, Exception): self._observer_result = self._closed_result = failure.Failure(result) else: - # pending w.verify()/w.version()/w.read() get an error + # pending w.key()/w.verify()/w.version()/w.read() get an error self._observer_result = WormholeClosed(result) # but w.close() only gets error if we're unhappy self._closed_result = result + for d in self._key_observers: + d.errback(self._observer_result) for d in self._verifier_observers: d.errback(self._observer_result) for d in self._version_observers: From 1a7b3baaf2da7e75967a4105f178a8a9cf3b882b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 19:17:11 -0700 Subject: [PATCH 174/176] rewrite waiting-for-sender pacifier messages re-enable the test, and add an extra one The comments in cmd_send/cmd_receive now enumerate the four cases where we might notice that things are taking too long, the three cases where we say something about it, and the two cases where it might be appropriate to give up automatically (although we don't do that anywhere yet). --- src/wormhole/cli/cmd_receive.py | 50 ++++++++++++++++++++++++------ src/wormhole/cli/cmd_send.py | 54 +++++++++++++++++++++------------ src/wormhole/test/test_cli.py | 41 +++++++++++++++---------- 3 files changed, 100 insertions(+), 45 deletions(-) diff --git a/src/wormhole/cli/cmd_receive.py b/src/wormhole/cli/cmd_receive.py index bc1e1b6..eae17a0 100644 --- a/src/wormhole/cli/cmd_receive.py +++ b/src/wormhole/cli/cmd_receive.py @@ -13,7 +13,9 @@ 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" -VERIFY_TIMER = 1 + +KEY_TIMER = 1.0 +VERIFY_TIMER = 1.0 class RespondError(Exception): def __init__(self, response): @@ -106,17 +108,45 @@ class TwistedReceiver: @inlineCallbacks def _go(self, w): yield self._handle_code(w) - verifier = yield w.when_verified() - def on_slow_connection(): - print(u"Key established, waiting for confirmation...", - file=self.args.stderr) - notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) + + def on_slow_key(): + print(u"Waiting for sender...", file=self.args.stderr) + notify = self._reactor.callLater(KEY_TIMER, on_slow_key) try: - yield w.when_version() + # We wait here until we connect to the server and see the senders + # PAKE message. If we used set_code() in the "human-selected + # offline codes" mode, then the sender might not have even + # started yet, so we might be sitting here for a while. Because + # 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() finally: if not notify.called: notify.cancel() - self._show_verifier(verifier) + + def on_slow_verification(): + print(u"Key established, waiting for confirmation...", + file=self.args.stderr) + notify = self._reactor.callLater(VERIFY_TIMER, on_slow_verification) + try: + # We wait here until we've seen their VERSION message (which they + # send after seeing our PAKE message, and has the side-effect of + # verifying that we both share the same key). There is a + # round-trip between these two events, and we could experience a + # significant delay here if: + # * the relay server is being restarted + # * the network is very slow + # * the sender is very slow + # * the sender has quit (in which case we may wait forever) + + # It would be reasonable to give up after waiting here for too + # long. + verifier_bytes = yield w.when_verified() + finally: + if not notify.called: + notify.cancel() + self._show_verifier(verifier_bytes) want_offer = True done = False @@ -177,8 +207,8 @@ class TwistedReceiver: file=self.args.stderr) yield w.when_code() - def _show_verifier(self, verifier): - verifier_hex = bytes_to_hexstr(verifier) + def _show_verifier(self, verifier_bytes): + verifier_hex = bytes_to_hexstr(verifier_bytes) if self.args.verify: self._msg(u"Verifier %s." % verifier_hex) diff --git a/src/wormhole/cli/cmd_send.py b/src/wormhole/cli/cmd_send.py index cb8bf22..c51a115 100644 --- a/src/wormhole/cli/cmd_send.py +++ b/src/wormhole/cli/cmd_send.py @@ -118,30 +118,34 @@ class Sender: 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() + + # TODO: don't stall on w.verify() unless they want it def on_slow_connection(): print(u"Key established, waiting for confirmation...", file=args.stderr) - #notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) - - # TODO: don't stall on w.verify() unless they want it - #try: - # verifier_bytes = yield w.when_verified() # might WrongPasswordError - #finally: - # if not notify.called: - # notify.cancel() - verifier_bytes = yield w.when_verified() + notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) + 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 + # 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 + finally: + if not notify.called: + notify.cancel() if args.verify: - verifier = bytes_to_hexstr(verifier_bytes) - while True: - ok = six.moves.input("Verifier %s. ok? (yes/no): " % verifier) - if ok.lower() == "yes": - break - if ok.lower() == "no": - err = "sender rejected verification check, abandoned transfer" - reject_data = dict_to_bytes({"error": err}) - w.send(reject_data) - raise TransferError(err) + self._check_verifier(w, verifier_bytes) # blocks, can TransferError if self._fd_to_send: ts = TransitSender(args.transit_helper, @@ -197,6 +201,18 @@ class Sender: if not recognized: log.msg("unrecognized message %r" % (them_d,)) + def _check_verifier(self, w, verifier_bytes): + verifier = bytes_to_hexstr(verifier_bytes) + while True: + ok = six.moves.input("Verifier %s. ok? (yes/no): " % verifier) + if ok.lower() == "yes": + break + if ok.lower() == "no": + err = "sender rejected verification check, abandoned transfer" + reject_data = dict_to_bytes({"error": err}) + w.send(reject_data) + raise TransferError(err) + def _handle_transit(self, receiver_transit): ts = self._transit_sender ts.add_connection_hints(receiver_transit.get("hints-v1", [])) diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py index eb9872b..fb8fd10 100644 --- a/src/wormhole/test/test_cli.py +++ b/src/wormhole/test/test_cli.py @@ -281,7 +281,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): def _do_test(self, as_subprocess=False, mode="text", addslash=False, override_filename=False, fake_tor=False, overwrite=False, mock_accept=False): - assert mode in ("text", "file", "empty-file", "directory", "slow-text") + assert mode in ("text", "file", "empty-file", "directory", + "slow-text", "slow-sender-text") if fake_tor: assert not as_subprocess send_cfg = config("send") @@ -302,7 +303,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): receive_dir = self.mktemp() os.mkdir(receive_dir) - if mode in ("text", "slow-text"): + if mode in ("text", "slow-text", "slow-sender-text"): send_cfg.text = message elif mode in ("file", "empty-file"): @@ -428,20 +429,22 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): ) as mrx_tm: receive_d = cmd_receive.receive(recv_cfg) else: - send_d = cmd_send.send(send_cfg) - receive_d = cmd_receive.receive(recv_cfg) + KEY_TIMER = 0 if mode == "slow-sender-text" else 1.0 + with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER): + send_d = cmd_send.send(send_cfg) + receive_d = cmd_receive.receive(recv_cfg) # The sender might fail, leaving the receiver hanging, or vice # versa. Make sure we don't wait on one side exclusively - if mode == "slow-text": - with mock.patch.object(cmd_send, "VERIFY_TIMER", 0), \ - mock.patch.object(cmd_receive, "VERIFY_TIMER", 0): - yield gatherResults([send_d, receive_d], True) - elif mock_accept: - with mock.patch.object(cmd_receive.six.moves, 'input', return_value='y'): - yield gatherResults([send_d, receive_d], True) - else: - yield gatherResults([send_d, receive_d], True) + VERIFY_TIMER = 0 if mode == "slow-text" else 1.0 + with mock.patch.object(cmd_receive, "VERIFY_TIMER", VERIFY_TIMER): + with mock.patch.object(cmd_send, "VERIFY_TIMER", VERIFY_TIMER): + if mock_accept: + with mock.patch.object(cmd_receive.six.moves, + 'input', return_value='y'): + yield gatherResults([send_d, receive_d], True) + else: + yield gatherResults([send_d, receive_d], True) if fake_tor: expected_endpoints = [("127.0.0.1", self.relayport)] @@ -512,9 +515,14 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): .format(NL=NL), send_stderr) # check receiver - if mode == "text" or mode == "slow-text": + if mode in ("text", "slow-text", "slow-sender-text"): self.assertEqual(receive_stdout, message+NL) - self.assertEqual(receive_stderr, key_established) + if mode == "text": + self.assertEqual(receive_stderr, "") + elif mode == "slow-text": + self.assertEqual(receive_stderr, key_established) + elif mode == "slow-sender-text": + self.assertEqual(receive_stderr, "Waiting for sender...\n") elif mode == "file": self.failUnlessEqual(receive_stdout, "") self.failUnlessIn("Receiving file ({size:s}) into: {name}" @@ -578,7 +586,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): def test_slow_text(self): return self._do_test(mode="slow-text") - test_slow_text.skip = "pending rethink" + def test_slow_sender_text(self): + return self._do_test(mode="slow-sender-text") @inlineCallbacks def _do_test_fail(self, mode, failmode): From 6aa7fe7c82f29658ef20f7bd36c32df833e3190b Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 19:32:05 -0700 Subject: [PATCH 175/176] Welcome: handle local dev versions (with +, not -) correctly The Welcome class prints a message if the server recommends a CLI version that's newer than what the client is currently using, but only if the client is running a "release" version, not a "local" development one. "local" versions have a "+" in them (at least when Versioneer creates it), but Welcome was looking for "-" as an indicator. So it was printing the warning when it shouldn't be. --- src/wormhole/cli/welcome.py | 4 ++-- src/wormhole/test/test_cli.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wormhole/cli/welcome.py b/src/wormhole/cli/welcome.py index f763eae..50a60a4 100644 --- a/src/wormhole/cli/welcome.py +++ b/src/wormhole/cli/welcome.py @@ -10,9 +10,9 @@ class CLIWelcomeHandler(_WelcomeHandler): 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. + # 0.0.6+DISTANCE.gHASH). Only warn once. if ("current_cli_version" in welcome - and "-" not in self._current_version + 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) diff --git a/src/wormhole/test/test_cli.py b/src/wormhole/test/test_cli.py index fb8fd10..086f18c 100644 --- a/src/wormhole/test/test_cli.py +++ b/src/wormhole/test/test_cli.py @@ -932,7 +932,7 @@ class Welcome(unittest.TestCase): def test_version_unreleased(self): stderr = self.do({"current_cli_version": "3.0"}, - my_version="2.5-middle-something") + my_version="2.5+middle.something") self.assertEqual(stderr, "") def test_motd(self): From 992db1846c96103a1bc5ba4b6197ddfb497ac28e Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Thu, 6 Apr 2017 19:44:27 -0700 Subject: [PATCH 176/176] minor TODO comments --- src/wormhole/test/test_wormhole.py | 3 ++- src/wormhole/wormhole.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wormhole/test/test_wormhole.py b/src/wormhole/test/test_wormhole.py index 824f23f..0dc8eef 100644 --- a/src/wormhole/test/test_wormhole.py +++ b/src/wormhole/test/test_wormhole.py @@ -127,7 +127,8 @@ class Wormholes(ServerBase, unittest.TestCase): version1 = yield w1.when_version() version2 = yield w2.when_version() - # TODO: add the ability to set app-versions + # app-versions are exercised properly in test_versions, this just + # tests the defaults self.assertEqual(version1, {}) self.assertEqual(version2, {}) diff --git a/src/wormhole/wormhole.py b/src/wormhole/wormhole.py index 0077020..a7a8606 100644 --- a/src/wormhole/wormhole.py +++ b/src/wormhole/wormhole.py @@ -131,7 +131,10 @@ class _DeferredWormhole(object): # from above def when_code(self): # TODO: consider throwing error unless one of allocate/set/input_code - # was called first + # was called first. It's legit to grab the Deferred before triggering + # the process that will cause it to fire, but forbidding that + # ordering would make it easier to cause programming errors that + # forget to trigger it entirely. if self._observer_result is not None: return defer.fail(self._observer_result) if self._code is not None: