magic-wormhole/src/wormhole/wormhole.py

312 lines
11 KiB
Python
Raw Normal View History

from __future__ import print_function, absolute_import, unicode_literals
import os, sys
2017-02-23 23:57:24 +00:00
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
2017-02-23 01:02:01 +00:00
from .timing import DebugTiming
2017-02-22 20:51:53 +00:00
from .journal import ImmediateJournal
from ._boss import Boss
2017-03-04 09:55:42 +00:00
from ._key import derive_key
from .errors import WelcomeError, NoKeyError
2017-03-04 09:55:42 +00:00
from .util import to_bytes
2016-05-21 01:49:20 +00:00
2017-02-23 23:57:24 +00:00
# We can provide different APIs to different apps:
# * Deferreds
# w.when_code().addCallback(print_code)
2017-02-23 23:57:24 +00:00
# w.send(data)
# w.when_received().addCallback(got_data)
2017-02-23 23:57:24 +00:00
# w.close().addCallback(closed)
# * delegate callbacks (better for journaled environments)
# w = wormhole(delegate=app)
# w.send(data)
# app.wormhole_got_code(code)
# app.wormhole_got_verifier(verifier)
2017-03-04 10:36:19 +00:00
# app.wormhole_got_version(version)
2017-02-23 23:57:24 +00:00
# app.wormhole_receive(data)
# w.close()
# app.wormhole_closed()
#
# * potential delegate options
# 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, 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")
2017-02-23 23:57:24 +00:00
@attrs
@implementer(IWormhole)
2017-02-23 23:57:24 +00:00
class _DelegatedWormhole(object):
_delegate = attrib()
2017-03-04 09:55:42 +00:00
def __attrs_post_init__(self):
self._key = None
2017-02-23 23:57:24 +00:00
def _set_boss(self, boss):
self._boss = boss
# from above
2017-02-24 02:23:55 +00:00
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 serialize(self):
s = {"serialized_wormhole_version": 1,
"boss": self._boss.serialize(),
}
return s
def send(self, plaintext):
self._boss.send(plaintext)
2017-03-04 09:55:42 +00:00
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)
2017-02-23 23:57:24 +00:00
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)
2017-02-23 23:57:24 +00:00
# from below
def got_code(self, code):
self._delegate.wormhole_got_code(code)
def got_welcome(self, welcome):
pass # TODO
2017-03-04 09:55:42 +00:00
def got_key(self, key):
self._key = key # for derive_key()
2017-02-23 23:57:24 +00:00
def got_verifier(self, verifier):
self._delegate.wormhole_got_verifier(verifier)
2017-03-04 10:36:19 +00:00
def got_version(self, version):
self._delegate.wormhole_got_version(version)
2017-02-24 02:23:55 +00:00
def received(self, plaintext):
self._delegate.wormhole_received(plaintext)
2017-02-23 23:57:24 +00:00
def closed(self, result):
self._delegate.wormhole_closed(result)
2017-02-25 02:30:00 +00:00
class WormholeClosed(Exception):
pass
@implementer(IWormhole)
2017-02-23 23:57:24 +00:00
class _DeferredWormhole(object):
2017-02-23 02:21:47 +00:00
def __init__(self):
self._code = None
self._code_observers = []
2017-03-04 09:55:42 +00:00
self._key = None
2017-02-23 02:21:47 +00:00
self._verifier = None
self._verifier_observers = []
2017-03-04 10:36:19 +00:00
self._version = None
self._version_observers = []
2017-02-24 02:23:55 +00:00
self._received_data = []
self._received_observers = []
self._closed_result = None
2017-02-25 02:30:00 +00:00
self._closed_observers = []
2017-02-23 02:21:47 +00:00
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
2017-03-04 10:36:19 +00:00
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
2017-02-24 02:23:55 +00:00
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)
2017-03-04 10:36:19 +00:00
def input_code(self, stdio): # TODO
2017-02-24 02:23:55 +00:00
self._boss.input_code(stdio)
def set_code(self, code):
self._boss.set_code(code)
# no .serialize in Deferred-mode
def send(self, plaintext):
self._boss.send(plaintext)
2017-03-04 09:55:42 +00:00
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)
2017-02-23 02:21:47 +00:00
def close(self):
# fails with WormholeError unless we established a connection
# (state=="happy"). Fails with WrongPasswordError (a subclass of
# 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
2017-02-25 02:30:00 +00:00
d = defer.Deferred()
self._closed_observers.append(d)
return d
2017-02-23 02:21:47 +00:00
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)
2017-02-23 02:21:47 +00:00
# from below
def got_code(self, code):
self._code = code
for d in self._code_observers:
d.callback(code)
self._code_observers[:] = []
def got_welcome(self, welcome):
pass # TODO
2017-03-04 09:55:42 +00:00
def got_key(self, key):
self._key = key # for derive_key()
2017-02-23 02:21:47 +00:00
def got_verifier(self, verifier):
self._verifier = verifier
for d in self._verifier_observers:
d.callback(verifier)
self._verifier_observers[:] = []
2017-03-04 10:36:19 +00:00
def got_version(self, version):
self._version = version
for d in self._version_observers:
d.callback(version)
self._version_observers[:] = []
2017-02-23 02:21:47 +00:00
2017-02-24 02:23:55 +00:00
def received(self, plaintext):
if self._received_observers:
self._received_observers.pop(0).callback(plaintext)
return
self._received_data.append(plaintext)
2017-02-23 02:21:47 +00:00
def closed(self, result):
#print("closed", result, type(result))
if isinstance(result, Exception):
observer_result = self._closed_result = failure.Failure(result)
2017-02-25 02:30:00 +00:00
else:
2017-03-04 10:36:19 +00:00
# pending w.verify()/w.version()/w.read() get an error
observer_result = WormholeClosed(result)
# but w.close() only gets error if we're unhappy
self._closed_result = result
2017-02-25 02:30:00 +00:00
for d in self._verifier_observers:
d.errback(observer_result)
2017-03-04 10:36:19 +00:00
for d in self._version_observers:
d.errback(observer_result)
2017-02-25 02:30:00 +00:00
for d in self._received_observers:
d.errback(observer_result)
2017-02-25 02:30:00 +00:00
for d in self._closed_observers:
d.callback(self._closed_result)
2017-02-23 02:21:47 +00:00
def create(appid, relay_url, reactor, delegate=None, journal=None,
tor_manager=None, timing=None, welcome_handler=None,
stderr=sys.stderr):
timing = timing or DebugTiming()
2017-02-23 02:21:47 +00:00
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
2017-02-23 23:57:24 +00:00
if delegate:
w = _DelegatedWormhole(delegate)
else:
w = _DeferredWormhole()
2017-03-04 12:07:31 +00:00
b = Boss(w, side, relay_url, appid, welcome_handler, reactor, journal,
tor_manager, timing)
2017-02-23 02:21:47 +00:00
w._set_boss(b)
b.start()
2016-05-21 01:49:20 +00:00
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.
# 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