magic-wormhole/src/wormhole/_boss.py

445 lines
16 KiB
Python
Raw Normal View History

2018-04-21 07:30:08 +00:00
from __future__ import absolute_import, print_function, unicode_literals
2017-02-25 02:30:00 +00:00
import re
2018-04-21 07:30:08 +00:00
2017-02-25 02:30:00 +00:00
import six
2018-04-21 07:30:08 +00:00
from attr import attrib, attrs
from attr.validators import instance_of, optional, provides
from automat import MethodicalMachine
2018-04-21 07:30:08 +00:00
from twisted.python import log
from zope.interface import implementer
from . import _interfaces
2018-04-21 07:30:08 +00:00
from ._allocator import Allocator
from ._code import Code, validate_code
from ._dilation.manager import Dilator
2018-04-21 07:30:08 +00:00
from ._input import Input
from ._key import Key
from ._lister import Lister
from ._mailbox import Mailbox
2018-04-21 07:30:08 +00:00
from ._nameplate import Nameplate
from ._order import Order
from ._receive import Receive
from ._rendezvous import RendezvousConnector
2018-04-21 07:30:08 +00:00
from ._send import Send
from ._terminator import Terminator
2017-03-17 23:50:37 +00:00
from ._wordlist import PGPWordList
2018-04-21 07:30:08 +00:00
from .errors import (LonelyError, OnlyOneCodeError, ServerError, WelcomeError,
WrongPasswordError, _UnknownPhaseError)
2017-02-22 20:51:53 +00:00
from .util import bytes_to_dict
2018-04-21 07:30:08 +00:00
2017-02-23 02:21:47 +00:00
@attrs
@implementer(_interfaces.IBoss)
2017-02-23 23:57:24 +00:00
class Boss(object):
_W = attrib()
2017-02-23 02:21:47 +00:00
_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))
_client_version = attrib(validator=instance_of(tuple))
2017-02-23 02:21:47 +00:00
_reactor = attrib()
_eventual_queue = attrib()
_cooperator = attrib()
2017-02-23 02:21:47 +00:00
_journal = attrib(validator=provides(_interfaces.IJournal))
_tor = attrib(validator=optional(provides(_interfaces.ITorManager)))
2017-02-23 02:21:47 +00:00
_timing = attrib(validator=provides(_interfaces.ITiming))
2017-02-15 20:11:17 +00:00
m = MethodicalMachine()
2018-04-21 07:30:08 +00:00
set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
2017-02-15 20:11:17 +00:00
def __attrs_post_init__(self):
2017-03-19 20:03:48 +00:00
self._build_workers()
self._init_other_state()
def _build_workers(self):
self._N = Nameplate()
2017-02-23 02:21:47 +00:00
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._versions, self._side, self._timing)
2017-02-23 02:21:47 +00:00
self._R = Receive(self._side, self._timing)
self._RC = RendezvousConnector(self._url, self._appid, self._side,
2018-04-21 07:30:08 +00:00
self._reactor, self._journal, self._tor,
self._timing, self._client_version)
2017-03-17 23:50:37 +00:00
self._L = Lister(self._timing)
self._A = Allocator(self._timing)
self._I = Input(self._timing)
2017-02-23 02:21:47 +00:00
self._C = Code(self._timing)
self._T = Terminator()
2018-06-30 23:19:41 +00:00
self._D = Dilator(self._reactor, self._eventual_queue,
self._cooperator)
2017-03-17 23:50:37 +00:00
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)
self._K.wire(self, self._M, self._R)
2017-03-03 07:59:45 +00:00
self._R.wire(self, self._S)
2017-03-15 07:43:25 +00:00
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)
2017-03-15 07:43:25 +00:00
self._C.wire(self, self._A, self._N, self._K, self._I)
self._T.wire(self, self._RC, self._N, self._M)
self._D.wire(self._S)
2017-02-15 20:11:17 +00:00
2017-03-19 20:03:48 +00:00
def _init_other_state(self):
2017-03-08 07:45:11 +00:00
self._did_start_code = False
self._next_tx_phase = 0
self._next_rx_phase = 0
2018-04-21 07:30:08 +00:00
self._rx_phases = {} # phase -> plaintext
self._next_rx_dilate_seqnum = 0
2018-06-30 23:19:41 +00:00
self._rx_dilate_seqnums = {} # seqnum -> plaintext
2017-02-25 02:30:00 +00:00
self._result = "empty"
2017-02-15 20:11:17 +00:00
# these methods are called from outside
def start(self):
2017-02-22 20:51:53 +00:00
self._RC.start()
2017-02-15 20:11:17 +00:00
2018-04-21 07:30:08 +00:00
def _print_trace(self, old_state, input, new_state, client_name, machine,
file):
2017-04-19 02:49:17 +00:00
if new_state:
2018-04-21 07:30:08 +00:00
print(
"%s.%s[%s].%s -> [%s]" % (client_name, machine, old_state,
input, new_state),
file=file)
2017-04-19 02:49:17 +00:00
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 .
2018-04-21 07:30:08 +00:00
print("%s.%s.%s" % (client_name, machine, input), file=file)
2017-04-19 02:49:17 +00:00
file.flush()
2018-04-21 07:30:08 +00:00
2017-04-19 02:49:17 +00:00
def output_tracer(output):
2018-04-21 07:30:08 +00:00
print(" %s.%s.%s()" % (client_name, machine, output), file=file)
2017-04-19 02:49:17 +00:00
file.flush()
2018-04-21 07:30:08 +00:00
2017-04-19 02:49:17 +00:00
return output_tracer
def _set_trace(self, client_name, which, file):
2018-04-21 07:30:08 +00:00
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,
"A": self._A,
"I": self._I,
"C": self._C,
"T": self._T
}
for machine in which.split():
t = (lambda old_state, input, new_state, machine=machine:
2017-04-19 02:49:17 +00:00
self._print_trace(old_state, input, new_state,
client_name=client_name,
machine=machine, file=file))
names[machine].set_trace(t)
if machine == "I":
self._I.set_debug(t)
2018-04-21 07:30:08 +00:00
# def serialize(self):
# raise NotImplemented
2017-02-15 20:11:17 +00:00
# and these are the state-machine transition functions, which don't take
# args
@m.state(initial=True)
2018-04-21 07:30:08 +00:00
def S0_empty(self):
pass # pragma: no cover
2017-02-15 20:11:17 +00:00
@m.state()
2018-04-21 07:30:08 +00:00
def S1_lonely(self):
pass # pragma: no cover
2017-02-15 20:11:17 +00:00
@m.state()
2018-04-21 07:30:08 +00:00
def S2_happy(self):
pass # pragma: no cover
2017-02-15 20:11:17 +00:00
@m.state()
2018-04-21 07:30:08 +00:00
def S3_closing(self):
pass # pragma: no cover
2017-02-15 20:11:17 +00:00
@m.state(terminal=True)
2018-04-21 07:30:08 +00:00
def S4_closed(self):
pass # pragma: no cover
2017-02-15 20:11:17 +00:00
2017-02-23 00:56:39 +00:00
# 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).
2017-03-19 22:09:26 +00:00
def input_code(self):
2017-03-08 07:45:11 +00:00
if self._did_start_code:
raise OnlyOneCodeError()
self._did_start_code = True
2017-03-19 22:09:26 +00:00
return self._C.input_code()
2018-04-21 07:30:08 +00:00
2017-02-24 02:23:55 +00:00
def allocate_code(self, code_length):
2017-03-08 07:45:11 +00:00
if self._did_start_code:
raise OnlyOneCodeError()
self._did_start_code = True
2017-03-17 23:50:37 +00:00
wl = PGPWordList()
self._C.allocate_code(code_length, wl)
2018-04-21 07:30:08 +00:00
2017-02-23 00:56:39 +00:00
def set_code(self, code):
2018-04-21 07:30:08 +00:00
validate_code(code) # can raise KeyFormatError
2017-03-08 07:45:11 +00:00
if self._did_start_code:
raise OnlyOneCodeError()
self._did_start_code = True
2017-02-23 00:56:39 +00:00
self._C.set_code(code)
def dilate(self):
2018-06-30 23:19:41 +00:00
return self._D.dilate() # fires with endpoints
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def send(self, plaintext):
pass
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def close(self):
pass
2017-02-15 20:11:17 +00:00
2017-04-03 21:23:03 +00:00
# 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
2017-04-03 21:23:03 +00:00
# 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.
2018-04-21 07:30:08 +00:00
self._W.got_welcome(welcome) # TODO: let this raise WelcomeError?
2017-04-03 21:23:03 +00:00
except WelcomeError as welcome_error:
self.rx_unwelcome(welcome_error)
2018-04-21 07:30:08 +00:00
2017-02-24 02:11:07 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def rx_unwelcome(self, welcome_error):
pass
@m.input()
2018-04-21 07:30:08 +00:00
def rx_error(self, errmsg, orig):
pass
@m.input()
2018-04-21 07:30:08 +00:00
def error(self, err):
pass
2017-02-24 02:11:07 +00:00
2017-02-23 00:56:39 +00:00
# from Code (provoked by input/allocate/set_code)
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def got_code(self, code):
pass
2017-02-15 20:11:17 +00:00
# Key sends (got_key, scared)
# Receive sends (got_message, happy, got_verifier, scared)
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def happy(self):
pass
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def scared(self):
pass
2017-02-25 02:30:00 +00:00
def got_message(self, phase, plaintext):
2017-02-22 20:51:53 +00:00
assert isinstance(phase, type("")), type(phase)
assert isinstance(plaintext, type(b"")), type(plaintext)
d_mo = re.search(r'^dilate-(\d+)$', phase)
if phase == "version":
self._got_version(plaintext)
elif d_mo:
self._got_dilate(int(d_mo.group(1)), plaintext)
2017-02-25 02:30:00 +00:00
elif re.search(r'^\d+$', phase):
2017-03-19 22:09:26 +00:00
self._got_phase(int(phase), plaintext)
else:
2017-02-25 02:30:00 +00:00
# Ignore unrecognized phases, for forwards-compatibility. Use
# log.err so tests will catch surprises.
2017-03-19 22:09:26 +00:00
log.err(_UnknownPhaseError("received unknown phase '%s'" % phase))
2018-04-21 07:30:08 +00:00
2017-02-15 20:11:17 +00:00
@m.input()
def _got_version(self, plaintext):
2018-04-21 07:30:08 +00:00
pass
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def _got_phase(self, phase, plaintext):
pass
@m.input()
def _got_dilate(self, seqnum, plaintext):
pass
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def got_key(self, key):
pass
2017-03-04 09:55:42 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def got_verifier(self, verifier):
pass
# Terminator sends closed
2017-02-15 20:11:17 +00:00
@m.input()
2018-04-21 07:30:08 +00:00
def closed(self):
pass
2017-02-15 20:11:17 +00:00
@m.output()
2017-02-23 00:56:39 +00:00
def do_got_code(self, code):
2017-02-23 02:21:47 +00:00
self._W.got_code(code)
2018-04-21 07:30:08 +00:00
2017-02-15 20:11:17 +00:00
@m.output()
def process_version(self, plaintext):
2017-03-04 10:36:19 +00:00
# most of this is wormhole-to-wormhole, ignored for now
# in the future, this is how Dilation is signalled
2017-02-22 20:51:53 +00:00
self._their_versions = bytes_to_dict(plaintext)
self._D.got_wormhole_versions(self._their_versions)
2017-03-04 10:36:19 +00:00
# but this part is app-to-app
app_versions = self._their_versions.get("app_versions", {})
self._W.got_versions(app_versions)
2017-02-15 20:11:17 +00:00
@m.output()
def S_send(self, plaintext):
2017-02-25 02:30:00 +00:00
assert isinstance(plaintext, type(b"")), type(plaintext)
phase = self._next_tx_phase
self._next_tx_phase += 1
2017-02-25 02:30:00 +00:00
self._S.send("%d" % phase, plaintext)
2017-02-15 20:11:17 +00:00
@m.output()
2017-04-03 21:23:03 +00:00
def close_unwelcome(self, welcome_error):
2018-04-21 07:30:08 +00:00
# assert isinstance(err, WelcomeError)
2017-04-03 21:23:03 +00:00
self._result = welcome_error
self._T.close("unwelcome")
2018-04-21 07:30:08 +00:00
2017-04-03 21:23:03 +00:00
@m.output()
def close_error(self, errmsg, orig):
self._result = ServerError(errmsg)
self._T.close("errory")
2018-04-21 07:30:08 +00:00
2017-02-25 02:30:00 +00:00
@m.output()
def close_scared(self):
2017-02-25 02:30:00 +00:00
self._result = WrongPasswordError()
self._T.close("scary")
2018-04-21 07:30:08 +00:00
2017-02-15 20:11:17 +00:00
@m.output()
def close_lonely(self):
self._result = LonelyError()
self._T.close("lonely")
2018-04-21 07:30:08 +00:00
@m.output()
def close_happy(self):
2017-02-25 02:30:00 +00:00
self._result = "happy"
self._T.close("happy")
2017-02-15 20:11:17 +00:00
@m.output()
2017-03-04 09:55:42 +00:00
def W_got_key(self, key):
self._W.got_key(key)
2018-04-21 07:30:08 +00:00
@m.output()
def D_got_key(self, key):
self._D.got_key(key)
2017-03-04 09:55:42 +00:00
@m.output()
2017-02-23 02:21:47 +00:00
def W_got_verifier(self, verifier):
self._W.got_verifier(verifier)
2018-04-21 07:30:08 +00:00
@m.output()
2017-02-23 02:21:47 +00:00
def W_received(self, phase, plaintext):
2017-02-25 02:30:00 +00:00
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:
2017-02-24 02:23:55 +00:00
self._W.received(self._rx_phases.pop(self._next_rx_phase))
self._next_rx_phase += 1
2017-02-15 20:11:17 +00:00
@m.output()
def D_received_dilate(self, seqnum, plaintext):
assert isinstance(seqnum, six.integer_types), type(seqnum)
# strict phase order, no gaps
self._rx_dilate_seqnums[seqnum] = plaintext
while self._next_rx_dilate_seqnum in self._rx_dilate_seqnums:
m = self._rx_dilate_seqnums.pop(self._next_rx_dilate_seqnum)
self._D.received_dilate(m)
self._next_rx_dilate_seqnum += 1
@m.output()
def W_close_with_error(self, err):
2018-04-21 07:30:08 +00:00
self._result = err # exception
self._W.closed(self._result)
2017-02-15 20:11:17 +00:00
@m.output()
2017-02-23 02:21:47 +00:00
def W_closed(self):
# result is either "happy" or a WormholeError of some sort
2017-02-25 02:30:00 +00:00
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])
2017-04-03 21:23:03 +00:00
S0_empty.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
2017-02-23 00:56:39 +00:00
S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code])
2017-02-25 02:30:00 +00:00
S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error])
S0_empty.upon(error, enter=S4_closed, outputs=[W_close_with_error])
2017-04-03 21:23:03 +00:00
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])
S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send])
S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key, D_got_key])
2017-02-25 02:30:00 +00:00
S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error])
S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error])
2017-04-03 21:23:03 +00:00
S2_happy.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
S2_happy.upon(got_verifier, enter=S2_happy, outputs=[W_got_verifier])
2017-03-19 22:09:26 +00:00
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_dilate, enter=S2_happy, outputs=[D_received_dilate])
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])
2017-02-25 02:30:00 +00:00
S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_error])
S2_happy.upon(error, enter=S4_closed, outputs=[W_close_with_error])
2017-04-03 21:23:03 +00:00
S3_closing.upon(rx_unwelcome, enter=S3_closing, outputs=[])
2017-02-25 02:30:00 +00:00
S3_closing.upon(rx_error, enter=S3_closing, outputs=[])
S3_closing.upon(got_verifier, enter=S3_closing, outputs=[])
2017-03-19 22:09:26 +00:00
S3_closing.upon(_got_phase, enter=S3_closing, outputs=[])
S3_closing.upon(_got_version, enter=S3_closing, outputs=[])
S3_closing.upon(_got_dilate, 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=[])
2017-02-23 02:21:47 +00:00
S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed])
S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error])
2017-04-03 21:23:03 +00:00
S4_closed.upon(rx_unwelcome, enter=S4_closed, outputs=[])
S4_closed.upon(got_verifier, enter=S4_closed, outputs=[])
2017-03-19 22:09:26 +00:00
S4_closed.upon(_got_phase, enter=S4_closed, outputs=[])
S4_closed.upon(_got_version, enter=S4_closed, outputs=[])
S4_closed.upon(_got_dilate, 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=[])
S4_closed.upon(error, enter=S4_closed, outputs=[])