Make code pep-8 compliant

This commit is contained in:
Vasudev Kamath 2018-04-21 13:00:08 +05:30
parent 355cc01aee
commit 12dcd6a184
53 changed files with 3260 additions and 1899 deletions

View File

@ -1,9 +1,4 @@
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
from .wormhole import create
from ._rlcompleter import input_with_completion from ._rlcompleter import input_with_completion
from .wormhole import create, __version__
__all__ = ["create", "input_with_completion", "__version__"] __all__ = ["create", "input_with_completion", "__version__"]

View File

@ -1,8 +1,6 @@
from .cli import cli
if __name__ != "__main__": if __name__ != "__main__":
raise ImportError('this module should not be imported') raise ImportError('this module should not be imported')
from .cli import cli
cli.wormhole() cli.wormhole()

View File

@ -1,56 +1,78 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
from attr import attrs, attrib from attr import attrib, attrs
from attr.validators import provides from attr.validators import provides
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
@attrs @attrs
@implementer(_interfaces.IAllocator) @implementer(_interfaces.IAllocator)
class Allocator(object): class Allocator(object):
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def wire(self, rendezvous_connector, code): def wire(self, rendezvous_connector, code):
self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
self._C = _interfaces.ICode(code) self._C = _interfaces.ICode(code)
@m.state(initial=True) @m.state(initial=True)
def S0A_idle(self): pass # pragma: no cover def S0A_idle(self):
pass # pragma: no cover
@m.state() @m.state()
def S0B_idle_connected(self): pass # pragma: no cover def S0B_idle_connected(self):
pass # pragma: no cover
@m.state() @m.state()
def S1A_allocating(self): pass # pragma: no cover def S1A_allocating(self):
pass # pragma: no cover
@m.state() @m.state()
def S1B_allocating_connected(self): pass # pragma: no cover def S1B_allocating_connected(self):
pass # pragma: no cover
@m.state() @m.state()
def S2_done(self): pass # pragma: no cover def S2_done(self):
pass # pragma: no cover
# from Code # from Code
@m.input() @m.input()
def allocate(self, length, wordlist): pass def allocate(self, length, wordlist):
pass
# from RendezvousConnector # from RendezvousConnector
@m.input() @m.input()
def connected(self): pass def connected(self):
pass
@m.input() @m.input()
def lost(self): pass def lost(self):
pass
@m.input() @m.input()
def rx_allocated(self, nameplate): pass def rx_allocated(self, nameplate):
pass
@m.output() @m.output()
def stash(self, length, wordlist): def stash(self, length, wordlist):
self._length = length self._length = length
self._wordlist = _interfaces.IWordlist(wordlist) self._wordlist = _interfaces.IWordlist(wordlist)
@m.output() @m.output()
def stash_and_RC_rx_allocate(self, length, wordlist): def stash_and_RC_rx_allocate(self, length, wordlist):
self._length = length self._length = length
self._wordlist = _interfaces.IWordlist(wordlist) self._wordlist = _interfaces.IWordlist(wordlist)
self._RC.tx_allocate() self._RC.tx_allocate()
@m.output() @m.output()
def RC_tx_allocate(self): def RC_tx_allocate(self):
self._RC.tx_allocate() self._RC.tx_allocate()
@m.output() @m.output()
def build_and_notify(self, nameplate): def build_and_notify(self, nameplate):
words = self._wordlist.choose_words(self._length) words = self._wordlist.choose_words(self._length)
@ -61,15 +83,17 @@ class Allocator(object):
S0B_idle_connected.upon(lost, enter=S0A_idle, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle, outputs=[])
S0A_idle.upon(allocate, enter=S1A_allocating, outputs=[stash]) S0A_idle.upon(allocate, enter=S1A_allocating, outputs=[stash])
S0B_idle_connected.upon(allocate, enter=S1B_allocating_connected, S0B_idle_connected.upon(
outputs=[stash_and_RC_rx_allocate]) allocate,
enter=S1B_allocating_connected,
outputs=[stash_and_RC_rx_allocate])
S1A_allocating.upon(connected, enter=S1B_allocating_connected, S1A_allocating.upon(
outputs=[RC_tx_allocate]) connected, enter=S1B_allocating_connected, outputs=[RC_tx_allocate])
S1B_allocating_connected.upon(lost, enter=S1A_allocating, outputs=[]) S1B_allocating_connected.upon(lost, enter=S1A_allocating, outputs=[])
S1B_allocating_connected.upon(rx_allocated, enter=S2_done, S1B_allocating_connected.upon(
outputs=[build_and_notify]) rx_allocated, enter=S2_done, outputs=[build_and_notify])
S2_done.upon(connected, enter=S2_done, outputs=[]) S2_done.upon(connected, enter=S2_done, outputs=[])
S2_done.upon(lost, enter=S2_done, outputs=[]) S2_done.upon(lost, enter=S2_done, outputs=[])

View File

@ -1,29 +1,33 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import re import re
import six import six
from zope.interface import implementer from attr import attrib, attrs
from attr import attrs, attrib from attr.validators import instance_of, optional, provides
from attr.validators import provides, instance_of, optional
from twisted.python import log
from automat import MethodicalMachine from automat import MethodicalMachine
from twisted.python import log
from zope.interface import implementer
from . import _interfaces from . import _interfaces
from ._nameplate import Nameplate from ._allocator import Allocator
from ._mailbox import Mailbox from ._code import Code, validate_code
from ._send import Send from ._input import Input
from ._order import Order
from ._key import Key from ._key import Key
from ._lister import Lister
from ._mailbox import Mailbox
from ._nameplate import Nameplate
from ._order import Order
from ._receive import Receive from ._receive import Receive
from ._rendezvous import RendezvousConnector from ._rendezvous import RendezvousConnector
from ._lister import Lister from ._send import Send
from ._allocator import Allocator
from ._input import Input
from ._code import Code, validate_code
from ._terminator import Terminator from ._terminator import Terminator
from ._wordlist import PGPWordList from ._wordlist import PGPWordList
from .errors import (ServerError, LonelyError, WrongPasswordError, from .errors import (LonelyError, OnlyOneCodeError, ServerError, WelcomeError,
OnlyOneCodeError, _UnknownPhaseError, WelcomeError) WrongPasswordError, _UnknownPhaseError)
from .util import bytes_to_dict from .util import bytes_to_dict
@attrs @attrs
@implementer(_interfaces.IBoss) @implementer(_interfaces.IBoss)
class Boss(object): class Boss(object):
@ -38,7 +42,8 @@ class Boss(object):
_tor = attrib(validator=optional(provides(_interfaces.ITorManager))) _tor = attrib(validator=optional(provides(_interfaces.ITorManager)))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._build_workers() self._build_workers()
@ -52,9 +57,8 @@ class Boss(object):
self._K = Key(self._appid, self._versions, self._side, self._timing) self._K = Key(self._appid, self._versions, self._side, self._timing)
self._R = Receive(self._side, self._timing) self._R = Receive(self._side, self._timing)
self._RC = RendezvousConnector(self._url, self._appid, self._side, self._RC = RendezvousConnector(self._url, self._appid, self._side,
self._reactor, self._journal, self._reactor, self._journal, self._tor,
self._tor, self._timing, self._timing, self._client_version)
self._client_version)
self._L = Lister(self._timing) self._L = Lister(self._timing)
self._A = Allocator(self._timing) self._A = Allocator(self._timing)
self._I = Input(self._timing) self._I = Input(self._timing)
@ -78,7 +82,7 @@ class Boss(object):
self._did_start_code = False self._did_start_code = False
self._next_tx_phase = 0 self._next_tx_phase = 0
self._next_rx_phase = 0 self._next_rx_phase = 0
self._rx_phases = {} # phase -> plaintext self._rx_phases = {} # phase -> plaintext
self._result = "empty" self._result = "empty"
@ -86,32 +90,45 @@ class Boss(object):
def start(self): def start(self):
self._RC.start() self._RC.start()
def _print_trace(self, old_state, input, new_state, def _print_trace(self, old_state, input, new_state, client_name, machine,
client_name, machine, file): file):
if new_state: if new_state:
print("%s.%s[%s].%s -> [%s]" % print(
(client_name, machine, old_state, input, "%s.%s[%s].%s -> [%s]" % (client_name, machine, old_state,
new_state), file=file) input, new_state),
file=file)
else: else:
# the RendezvousConnector emits message events as if # the RendezvousConnector emits message events as if
# they were state transitions, except that old_state # they were state transitions, except that old_state
# and new_state are empty strings. "input" is one of # and new_state are empty strings. "input" is one of
# R.connected, R.rx(type phase+side), R.tx(type # R.connected, R.rx(type phase+side), R.tx(type
# phase), R.lost . # phase), R.lost .
print("%s.%s.%s" % (client_name, machine, input), print("%s.%s.%s" % (client_name, machine, input), file=file)
file=file)
file.flush() file.flush()
def output_tracer(output): def output_tracer(output):
print(" %s.%s.%s()" % (client_name, machine, output), print(" %s.%s.%s()" % (client_name, machine, output), file=file)
file=file)
file.flush() file.flush()
return output_tracer return output_tracer
def _set_trace(self, client_name, which, file): def _set_trace(self, client_name, which, file):
names = {"B": self, "N": self._N, "M": self._M, "S": self._S, names = {
"O": self._O, "K": self._K, "SK": self._K._SK, "R": self._R, "B": self,
"RC": self._RC, "L": self._L, "A": self._A, "I": self._I, "N": self._N,
"C": self._C, "T": self._T} "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(): for machine in which.split():
t = (lambda old_state, input, new_state, machine=machine: t = (lambda old_state, input, new_state, machine=machine:
self._print_trace(old_state, input, new_state, self._print_trace(old_state, input, new_state,
@ -121,21 +138,30 @@ class Boss(object):
if machine == "I": if machine == "I":
self._I.set_debug(t) self._I.set_debug(t)
## def serialize(self): # def serialize(self):
## raise NotImplemented # raise NotImplemented
# and these are the state-machine transition functions, which don't take # and these are the state-machine transition functions, which don't take
# args # args
@m.state(initial=True) @m.state(initial=True)
def S0_empty(self): pass # pragma: no cover def S0_empty(self):
pass # pragma: no cover
@m.state() @m.state()
def S1_lonely(self): pass # pragma: no cover def S1_lonely(self):
pass # pragma: no cover
@m.state() @m.state()
def S2_happy(self): pass # pragma: no cover def S2_happy(self):
pass # pragma: no cover
@m.state() @m.state()
def S3_closing(self): pass # pragma: no cover def S3_closing(self):
pass # pragma: no cover
@m.state(terminal=True) @m.state(terminal=True)
def S4_closed(self): pass # pragma: no cover def S4_closed(self):
pass # pragma: no cover
# from the Wormhole # from the Wormhole
@ -155,23 +181,28 @@ class Boss(object):
raise OnlyOneCodeError() raise OnlyOneCodeError()
self._did_start_code = True self._did_start_code = True
return self._C.input_code() return self._C.input_code()
def allocate_code(self, code_length): def allocate_code(self, code_length):
if self._did_start_code: if self._did_start_code:
raise OnlyOneCodeError() raise OnlyOneCodeError()
self._did_start_code = True self._did_start_code = True
wl = PGPWordList() wl = PGPWordList()
self._C.allocate_code(code_length, wl) self._C.allocate_code(code_length, wl)
def set_code(self, code): def set_code(self, code):
validate_code(code) # can raise KeyFormatError validate_code(code) # can raise KeyFormatError
if self._did_start_code: if self._did_start_code:
raise OnlyOneCodeError() raise OnlyOneCodeError()
self._did_start_code = True self._did_start_code = True
self._C.set_code(code) self._C.set_code(code)
@m.input() @m.input()
def send(self, plaintext): pass def send(self, plaintext):
pass
@m.input() @m.input()
def close(self): pass def close(self):
pass
# from RendezvousConnector: # from RendezvousConnector:
# * "rx_welcome" is the Welcome message, which might signal an error, or # * "rx_welcome" is the Welcome message, which might signal an error, or
@ -190,26 +221,36 @@ class Boss(object):
# delivering a new input (rx_error or something) while in the # delivering a new input (rx_error or something) while in the
# middle of processing the rx_welcome input, and I wasn't sure # middle of processing the rx_welcome input, and I wasn't sure
# Automat would handle that correctly. # Automat would handle that correctly.
self._W.got_welcome(welcome) # TODO: let this raise WelcomeError? self._W.got_welcome(welcome) # TODO: let this raise WelcomeError?
except WelcomeError as welcome_error: except WelcomeError as welcome_error:
self.rx_unwelcome(welcome_error) self.rx_unwelcome(welcome_error)
@m.input() @m.input()
def rx_unwelcome(self, welcome_error): pass def rx_unwelcome(self, welcome_error):
pass
@m.input() @m.input()
def rx_error(self, errmsg, orig): pass def rx_error(self, errmsg, orig):
pass
@m.input() @m.input()
def error(self, err): pass def error(self, err):
pass
# from Code (provoked by input/allocate/set_code) # from Code (provoked by input/allocate/set_code)
@m.input() @m.input()
def got_code(self, code): pass def got_code(self, code):
pass
# Key sends (got_key, scared) # Key sends (got_key, scared)
# Receive sends (got_message, happy, got_verifier, scared) # Receive sends (got_message, happy, got_verifier, scared)
@m.input() @m.input()
def happy(self): pass def happy(self):
pass
@m.input() @m.input()
def scared(self): pass def scared(self):
pass
def got_message(self, phase, plaintext): def got_message(self, phase, plaintext):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
@ -222,22 +263,32 @@ class Boss(object):
# Ignore unrecognized phases, for forwards-compatibility. Use # Ignore unrecognized phases, for forwards-compatibility. Use
# log.err so tests will catch surprises. # log.err so tests will catch surprises.
log.err(_UnknownPhaseError("received unknown phase '%s'" % phase)) log.err(_UnknownPhaseError("received unknown phase '%s'" % phase))
@m.input() @m.input()
def _got_version(self, plaintext): pass def _got_version(self, plaintext):
pass
@m.input() @m.input()
def _got_phase(self, phase, plaintext): pass def _got_phase(self, phase, plaintext):
pass
@m.input() @m.input()
def got_key(self, key): pass def got_key(self, key):
pass
@m.input() @m.input()
def got_verifier(self, verifier): pass def got_verifier(self, verifier):
pass
# Terminator sends closed # Terminator sends closed
@m.input() @m.input()
def closed(self): pass def closed(self):
pass
@m.output() @m.output()
def do_got_code(self, code): def do_got_code(self, code):
self._W.got_code(code) self._W.got_code(code)
@m.output() @m.output()
def process_version(self, plaintext): def process_version(self, plaintext):
# most of this is wormhole-to-wormhole, ignored for now # most of this is wormhole-to-wormhole, ignored for now
@ -256,21 +307,25 @@ class Boss(object):
@m.output() @m.output()
def close_unwelcome(self, welcome_error): def close_unwelcome(self, welcome_error):
#assert isinstance(err, WelcomeError) # assert isinstance(err, WelcomeError)
self._result = welcome_error self._result = welcome_error
self._T.close("unwelcome") self._T.close("unwelcome")
@m.output() @m.output()
def close_error(self, errmsg, orig): def close_error(self, errmsg, orig):
self._result = ServerError(errmsg) self._result = ServerError(errmsg)
self._T.close("errory") self._T.close("errory")
@m.output() @m.output()
def close_scared(self): def close_scared(self):
self._result = WrongPasswordError() self._result = WrongPasswordError()
self._T.close("scary") self._T.close("scary")
@m.output() @m.output()
def close_lonely(self): def close_lonely(self):
self._result = LonelyError() self._result = LonelyError()
self._T.close("lonely") self._T.close("lonely")
@m.output() @m.output()
def close_happy(self): def close_happy(self):
self._result = "happy" self._result = "happy"
@ -279,9 +334,11 @@ class Boss(object):
@m.output() @m.output()
def W_got_key(self, key): def W_got_key(self, key):
self._W.got_key(key) self._W.got_key(key)
@m.output() @m.output()
def W_got_verifier(self, verifier): def W_got_verifier(self, verifier):
self._W.got_verifier(verifier) self._W.got_verifier(verifier)
@m.output() @m.output()
def W_received(self, phase, plaintext): def W_received(self, phase, plaintext):
assert isinstance(phase, six.integer_types), type(phase) assert isinstance(phase, six.integer_types), type(phase)
@ -293,7 +350,7 @@ class Boss(object):
@m.output() @m.output()
def W_close_with_error(self, err): def W_close_with_error(self, err):
self._result = err # exception self._result = err # exception
self._W.closed(self._result) self._W.closed(self._result)
@m.output() @m.output()

View File

@ -7,21 +7,25 @@ from . import _interfaces
from ._nameplate import validate_nameplate from ._nameplate import validate_nameplate
from .errors import KeyFormatError from .errors import KeyFormatError
def validate_code(code): def validate_code(code):
if ' ' in code: if ' ' in code:
raise KeyFormatError("Code '%s' contains spaces." % code) raise KeyFormatError("Code '%s' contains spaces." % code)
nameplate = code.split("-", 2)[0] nameplate = code.split("-", 2)[0]
validate_nameplate(nameplate) # can raise KeyFormatError validate_nameplate(nameplate) # can raise KeyFormatError
def first(outputs): def first(outputs):
return list(outputs)[0] return list(outputs)[0]
@attrs @attrs
@implementer(_interfaces.ICode) @implementer(_interfaces.ICode)
class Code(object): class Code(object):
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def wire(self, boss, allocator, nameplate, key, input): def wire(self, boss, allocator, nameplate, key, input):
self._B = _interfaces.IBoss(boss) self._B = _interfaces.IBoss(boss)
@ -31,36 +35,55 @@ class Code(object):
self._I = _interfaces.IInput(input) self._I = _interfaces.IInput(input)
@m.state(initial=True) @m.state(initial=True)
def S0_idle(self): pass # pragma: no cover def S0_idle(self):
pass # pragma: no cover
@m.state() @m.state()
def S1_inputting_nameplate(self): pass # pragma: no cover def S1_inputting_nameplate(self):
pass # pragma: no cover
@m.state() @m.state()
def S2_inputting_words(self): pass # pragma: no cover def S2_inputting_words(self):
pass # pragma: no cover
@m.state() @m.state()
def S3_allocating(self): pass # pragma: no cover def S3_allocating(self):
pass # pragma: no cover
@m.state() @m.state()
def S4_known(self): pass # pragma: no cover def S4_known(self):
pass # pragma: no cover
# from App # from App
@m.input() @m.input()
def allocate_code(self, length, wordlist): pass def allocate_code(self, length, wordlist):
pass
@m.input() @m.input()
def input_code(self): pass def input_code(self):
pass
def set_code(self, code): def set_code(self, code):
validate_code(code) # can raise KeyFormatError validate_code(code) # can raise KeyFormatError
self._set_code(code) self._set_code(code)
@m.input() @m.input()
def _set_code(self, code): pass def _set_code(self, code):
pass
# from Allocator # from Allocator
@m.input() @m.input()
def allocated(self, nameplate, code): pass def allocated(self, nameplate, code):
pass
# from Input # from Input
@m.input() @m.input()
def got_nameplate(self, nameplate): pass def got_nameplate(self, nameplate):
pass
@m.input() @m.input()
def finished_input(self, code): pass def finished_input(self, code):
pass
@m.output() @m.output()
def do_set_code(self, code): def do_set_code(self, code):
@ -72,9 +95,11 @@ class Code(object):
@m.output() @m.output()
def do_start_input(self): def do_start_input(self):
return self._I.start() return self._I.start()
@m.output() @m.output()
def do_middle_input(self, nameplate): def do_middle_input(self, nameplate):
self._N.set_nameplate(nameplate) self._N.set_nameplate(nameplate)
@m.output() @m.output()
def do_finish_input(self, code): def do_finish_input(self, code):
self._B.got_code(code) self._B.got_code(code)
@ -83,19 +108,24 @@ class Code(object):
@m.output() @m.output()
def do_start_allocate(self, length, wordlist): def do_start_allocate(self, length, wordlist):
self._A.allocate(length, wordlist) self._A.allocate(length, wordlist)
@m.output() @m.output()
def do_finish_allocate(self, nameplate, code): def do_finish_allocate(self, nameplate, code):
assert code.startswith(nameplate+"-"), (nameplate, code) assert code.startswith(nameplate + "-"), (nameplate, code)
self._N.set_nameplate(nameplate) self._N.set_nameplate(nameplate)
self._B.got_code(code) self._B.got_code(code)
self._K.got_code(code) self._K.got_code(code)
S0_idle.upon(_set_code, enter=S4_known, outputs=[do_set_code]) S0_idle.upon(_set_code, enter=S4_known, outputs=[do_set_code])
S0_idle.upon(input_code, enter=S1_inputting_nameplate, S0_idle.upon(
outputs=[do_start_input], collector=first) input_code,
S1_inputting_nameplate.upon(got_nameplate, enter=S2_inputting_words, enter=S1_inputting_nameplate,
outputs=[do_middle_input]) outputs=[do_start_input],
S2_inputting_words.upon(finished_input, enter=S4_known, collector=first)
outputs=[do_finish_input]) S1_inputting_nameplate.upon(
S0_idle.upon(allocate_code, enter=S3_allocating, outputs=[do_start_allocate]) 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]) S3_allocating.upon(allocated, enter=S4_known, outputs=[do_finish_allocate])

View File

@ -1,25 +1,31 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
# We use 'threading' defensively here, to detect if we're being called from a # We use 'threading' defensively here, to detect if we're being called from a
# non-main thread. _rlcompleter.py is the only internal Wormhole code that # non-main thread. _rlcompleter.py is the only internal Wormhole code that
# deliberately creates a new thread. # deliberately creates a new thread.
import threading import threading
from zope.interface import implementer
from attr import attrs, attrib from attr import attrib, attrs
from attr.validators import provides from attr.validators import provides
from twisted.internet import defer
from automat import MethodicalMachine from automat import MethodicalMachine
from twisted.internet import defer
from zope.interface import implementer
from . import _interfaces, errors from . import _interfaces, errors
from ._nameplate import validate_nameplate from ._nameplate import validate_nameplate
def first(outputs): def first(outputs):
return list(outputs)[0] return list(outputs)[0]
@attrs @attrs
@implementer(_interfaces.IInput) @implementer(_interfaces.IInput)
class Input(object): class Input(object):
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._all_nameplates = set() self._all_nameplates = set()
@ -30,7 +36,8 @@ class Input(object):
def set_debug(self, f): def set_debug(self, f):
self._trace = f self._trace = f
def _debug(self, what): # pragma: no cover
def _debug(self, what): # pragma: no cover
if self._trace: if self._trace:
self._trace(old_state="", input=what, new_state="") self._trace(old_state="", input=what, new_state="")
@ -46,55 +53,80 @@ class Input(object):
return d return d
@m.state(initial=True) @m.state(initial=True)
def S0_idle(self): pass # pragma: no cover def S0_idle(self):
pass # pragma: no cover
@m.state() @m.state()
def S1_typing_nameplate(self): pass # pragma: no cover def S1_typing_nameplate(self):
pass # pragma: no cover
@m.state() @m.state()
def S2_typing_code_no_wordlist(self): pass # pragma: no cover def S2_typing_code_no_wordlist(self):
pass # pragma: no cover
@m.state() @m.state()
def S3_typing_code_yes_wordlist(self): pass # pragma: no cover def S3_typing_code_yes_wordlist(self):
pass # pragma: no cover
@m.state(terminal=True) @m.state(terminal=True)
def S4_done(self): pass # pragma: no cover def S4_done(self):
pass # pragma: no cover
# from Code # from Code
@m.input() @m.input()
def start(self): pass def start(self):
pass
# from Lister # from Lister
@m.input() @m.input()
def got_nameplates(self, all_nameplates): pass def got_nameplates(self, all_nameplates):
pass
# from Nameplate # from Nameplate
@m.input() @m.input()
def got_wordlist(self, wordlist): pass def got_wordlist(self, wordlist):
pass
# API provided to app as ICodeInputHelper # API provided to app as ICodeInputHelper
@m.input() @m.input()
def refresh_nameplates(self): pass def refresh_nameplates(self):
pass
@m.input() @m.input()
def get_nameplate_completions(self, prefix): pass def get_nameplate_completions(self, prefix):
pass
def choose_nameplate(self, nameplate): def choose_nameplate(self, nameplate):
validate_nameplate(nameplate) # can raise KeyFormatError validate_nameplate(nameplate) # can raise KeyFormatError
self._choose_nameplate(nameplate) self._choose_nameplate(nameplate)
@m.input() @m.input()
def _choose_nameplate(self, nameplate): pass def _choose_nameplate(self, nameplate):
pass
@m.input() @m.input()
def get_word_completions(self, prefix): pass def get_word_completions(self, prefix):
pass
@m.input() @m.input()
def choose_words(self, words): pass def choose_words(self, words):
pass
@m.output() @m.output()
def do_start(self): def do_start(self):
self._start_timing = self._timing.add("input code", waiting="user") self._start_timing = self._timing.add("input code", waiting="user")
self._L.refresh() self._L.refresh()
return Helper(self) return Helper(self)
@m.output() @m.output()
def do_refresh(self): def do_refresh(self):
self._L.refresh() self._L.refresh()
@m.output() @m.output()
def record_nameplates(self, all_nameplates): def record_nameplates(self, all_nameplates):
# we get a set of nameplate id strings # we get a set of nameplate id strings
self._all_nameplates = all_nameplates self._all_nameplates = all_nameplates
@m.output() @m.output()
def _get_nameplate_completions(self, prefix): def _get_nameplate_completions(self, prefix):
completions = set() completions = set()
@ -102,17 +134,20 @@ class Input(object):
if nameplate.startswith(prefix): if nameplate.startswith(prefix):
# TODO: it's a little weird that Input is responsible for the # TODO: it's a little weird that Input is responsible for the
# hyphen on nameplates, but WordList owns it for words # hyphen on nameplates, but WordList owns it for words
completions.add(nameplate+"-") completions.add(nameplate + "-")
return completions return completions
@m.output() @m.output()
def record_all_nameplates(self, nameplate): def record_all_nameplates(self, nameplate):
self._nameplate = nameplate self._nameplate = nameplate
self._C.got_nameplate(nameplate) self._C.got_nameplate(nameplate)
@m.output() @m.output()
def record_wordlist(self, wordlist): def record_wordlist(self, wordlist):
from ._rlcompleter import debug from ._rlcompleter import debug
debug(" -record_wordlist") debug(" -record_wordlist")
self._wordlist = wordlist self._wordlist = wordlist
@m.output() @m.output()
def notify_wordlist_waiters(self, wordlist): def notify_wordlist_waiters(self, wordlist):
while self._wordlist_waiters: while self._wordlist_waiters:
@ -122,6 +157,7 @@ class Input(object):
@m.output() @m.output()
def no_word_completions(self, prefix): def no_word_completions(self, prefix):
return set() return set()
@m.output() @m.output()
def _get_word_completions(self, prefix): def _get_word_completions(self, prefix):
assert self._wordlist assert self._wordlist
@ -130,21 +166,27 @@ class Input(object):
@m.output() @m.output()
def raise_must_choose_nameplate1(self, prefix): def raise_must_choose_nameplate1(self, prefix):
raise errors.MustChooseNameplateFirstError() raise errors.MustChooseNameplateFirstError()
@m.output() @m.output()
def raise_must_choose_nameplate2(self, words): def raise_must_choose_nameplate2(self, words):
raise errors.MustChooseNameplateFirstError() raise errors.MustChooseNameplateFirstError()
@m.output() @m.output()
def raise_already_chose_nameplate1(self): def raise_already_chose_nameplate1(self):
raise errors.AlreadyChoseNameplateError() raise errors.AlreadyChoseNameplateError()
@m.output() @m.output()
def raise_already_chose_nameplate2(self, prefix): def raise_already_chose_nameplate2(self, prefix):
raise errors.AlreadyChoseNameplateError() raise errors.AlreadyChoseNameplateError()
@m.output() @m.output()
def raise_already_chose_nameplate3(self, nameplate): def raise_already_chose_nameplate3(self, nameplate):
raise errors.AlreadyChoseNameplateError() raise errors.AlreadyChoseNameplateError()
@m.output() @m.output()
def raise_already_chose_words1(self, prefix): def raise_already_chose_words1(self, prefix):
raise errors.AlreadyChoseWordsError() raise errors.AlreadyChoseWordsError()
@m.output() @m.output()
def raise_already_chose_words2(self, words): def raise_already_chose_words2(self, words):
raise errors.AlreadyChoseWordsError() raise errors.AlreadyChoseWordsError()
@ -155,88 +197,110 @@ class Input(object):
self._start_timing.finish() self._start_timing.finish()
self._C.finished_input(code) self._C.finished_input(code)
S0_idle.upon(start, enter=S1_typing_nameplate, S0_idle.upon(
outputs=[do_start], collector=first) start, enter=S1_typing_nameplate, outputs=[do_start], collector=first)
# wormholes that don't use input_code (i.e. they use allocate_code or # 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 # generate_code) will never start() us, but Nameplate will give us a
# wordlist anyways (as soon as the nameplate is claimed), so handle it. # 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(
notify_wordlist_waiters]) got_wordlist,
S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate, enter=S0_idle,
outputs=[record_nameplates]) 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 # 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 # until after we tell Code that we got_nameplate, which is the earliest
# it can be claimed # it can be claimed
S1_typing_nameplate.upon(refresh_nameplates, enter=S1_typing_nameplate, S1_typing_nameplate.upon(
outputs=[do_refresh]) refresh_nameplates, enter=S1_typing_nameplate, outputs=[do_refresh])
S1_typing_nameplate.upon(get_nameplate_completions, S1_typing_nameplate.upon(
enter=S1_typing_nameplate, get_nameplate_completions,
outputs=[_get_nameplate_completions], enter=S1_typing_nameplate,
collector=first) outputs=[_get_nameplate_completions],
S1_typing_nameplate.upon(_choose_nameplate, enter=S2_typing_code_no_wordlist, collector=first)
outputs=[record_all_nameplates]) S1_typing_nameplate.upon(
S1_typing_nameplate.upon(get_word_completions, _choose_nameplate,
enter=S1_typing_nameplate, enter=S2_typing_code_no_wordlist,
outputs=[raise_must_choose_nameplate1]) outputs=[record_all_nameplates])
S1_typing_nameplate.upon(choose_words, enter=S1_typing_nameplate, S1_typing_nameplate.upon(
outputs=[raise_must_choose_nameplate2]) 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, S2_typing_code_no_wordlist.upon(
enter=S2_typing_code_no_wordlist, outputs=[]) got_nameplates, enter=S2_typing_code_no_wordlist, outputs=[])
S2_typing_code_no_wordlist.upon(got_wordlist, S2_typing_code_no_wordlist.upon(
enter=S3_typing_code_yes_wordlist, got_wordlist,
outputs=[record_wordlist, enter=S3_typing_code_yes_wordlist,
notify_wordlist_waiters]) outputs=[record_wordlist, notify_wordlist_waiters])
S2_typing_code_no_wordlist.upon(refresh_nameplates, S2_typing_code_no_wordlist.upon(
enter=S2_typing_code_no_wordlist, refresh_nameplates,
outputs=[raise_already_chose_nameplate1]) enter=S2_typing_code_no_wordlist,
S2_typing_code_no_wordlist.upon(get_nameplate_completions, outputs=[raise_already_chose_nameplate1])
enter=S2_typing_code_no_wordlist, S2_typing_code_no_wordlist.upon(
outputs=[raise_already_chose_nameplate2]) get_nameplate_completions,
S2_typing_code_no_wordlist.upon(_choose_nameplate, enter=S2_typing_code_no_wordlist,
enter=S2_typing_code_no_wordlist, outputs=[raise_already_chose_nameplate2])
outputs=[raise_already_chose_nameplate3]) S2_typing_code_no_wordlist.upon(
S2_typing_code_no_wordlist.upon(get_word_completions, _choose_nameplate,
enter=S2_typing_code_no_wordlist, enter=S2_typing_code_no_wordlist,
outputs=[no_word_completions], outputs=[raise_already_chose_nameplate3])
collector=first) S2_typing_code_no_wordlist.upon(
S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, get_word_completions,
outputs=[do_words]) 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, S3_typing_code_yes_wordlist.upon(
enter=S3_typing_code_yes_wordlist, got_nameplates, enter=S3_typing_code_yes_wordlist, outputs=[])
outputs=[])
# got_wordlist: should never happen # got_wordlist: should never happen
S3_typing_code_yes_wordlist.upon(refresh_nameplates, S3_typing_code_yes_wordlist.upon(
enter=S3_typing_code_yes_wordlist, refresh_nameplates,
outputs=[raise_already_chose_nameplate1]) enter=S3_typing_code_yes_wordlist,
S3_typing_code_yes_wordlist.upon(get_nameplate_completions, outputs=[raise_already_chose_nameplate1])
enter=S3_typing_code_yes_wordlist, S3_typing_code_yes_wordlist.upon(
outputs=[raise_already_chose_nameplate2]) get_nameplate_completions,
S3_typing_code_yes_wordlist.upon(_choose_nameplate, enter=S3_typing_code_yes_wordlist,
enter=S3_typing_code_yes_wordlist, outputs=[raise_already_chose_nameplate2])
outputs=[raise_already_chose_nameplate3]) S3_typing_code_yes_wordlist.upon(
S3_typing_code_yes_wordlist.upon(get_word_completions, _choose_nameplate,
enter=S3_typing_code_yes_wordlist, enter=S3_typing_code_yes_wordlist,
outputs=[_get_word_completions], outputs=[raise_already_chose_nameplate3])
collector=first) S3_typing_code_yes_wordlist.upon(
S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, get_word_completions,
outputs=[do_words]) 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_nameplates, enter=S4_done, outputs=[])
S4_done.upon(got_wordlist, enter=S4_done, outputs=[]) S4_done.upon(got_wordlist, enter=S4_done, outputs=[])
S4_done.upon(refresh_nameplates, S4_done.upon(
enter=S4_done, refresh_nameplates,
outputs=[raise_already_chose_nameplate1]) enter=S4_done,
S4_done.upon(get_nameplate_completions, outputs=[raise_already_chose_nameplate1])
enter=S4_done, S4_done.upon(
outputs=[raise_already_chose_nameplate2]) get_nameplate_completions,
S4_done.upon(_choose_nameplate, enter=S4_done, enter=S4_done,
outputs=[raise_already_chose_nameplate3]) outputs=[raise_already_chose_nameplate2])
S4_done.upon(get_word_completions, enter=S4_done, S4_done.upon(
outputs=[raise_already_chose_words1]) _choose_nameplate,
S4_done.upon(choose_words, enter=S4_done, enter=S4_done,
outputs=[raise_already_chose_words2]) 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])
# we only expose the Helper to application code, not _Input # we only expose the Helper to application code, not _Input
@attrs @attrs
@ -250,20 +314,25 @@ class Helper(object):
def refresh_nameplates(self): def refresh_nameplates(self):
assert threading.current_thread().ident == self._main_thread assert threading.current_thread().ident == self._main_thread
self._input.refresh_nameplates() self._input.refresh_nameplates()
def get_nameplate_completions(self, prefix): def get_nameplate_completions(self, prefix):
assert threading.current_thread().ident == self._main_thread assert threading.current_thread().ident == self._main_thread
return self._input.get_nameplate_completions(prefix) return self._input.get_nameplate_completions(prefix)
def choose_nameplate(self, nameplate): def choose_nameplate(self, nameplate):
assert threading.current_thread().ident == self._main_thread assert threading.current_thread().ident == self._main_thread
self._input._debug("I.choose_nameplate") self._input._debug("I.choose_nameplate")
self._input.choose_nameplate(nameplate) self._input.choose_nameplate(nameplate)
self._input._debug("I.choose_nameplate finished") self._input._debug("I.choose_nameplate finished")
def when_wordlist_is_available(self): def when_wordlist_is_available(self):
assert threading.current_thread().ident == self._main_thread assert threading.current_thread().ident == self._main_thread
return self._input.when_wordlist_is_available() return self._input.when_wordlist_is_available()
def get_word_completions(self, prefix): def get_word_completions(self, prefix):
assert threading.current_thread().ident == self._main_thread assert threading.current_thread().ident == self._main_thread
return self._input.get_word_completions(prefix) return self._input.get_word_completions(prefix)
def choose_words(self, words): def choose_words(self, words):
assert threading.current_thread().ident == self._main_thread assert threading.current_thread().ident == self._main_thread
self._input._debug("I.choose_words") self._input._debug("I.choose_words")

View File

@ -3,64 +3,105 @@ from zope.interface import Interface
# These interfaces are private: we use them as markers to detect # These interfaces are private: we use them as markers to detect
# swapped argument bugs in the various .wire() calls # swapped argument bugs in the various .wire() calls
class IWormhole(Interface): class IWormhole(Interface):
"""Internal: this contains the methods invoked 'from below'.""" """Internal: this contains the methods invoked 'from below'."""
def got_welcome(welcome): def got_welcome(welcome):
pass pass
def got_code(code): def got_code(code):
pass pass
def got_key(key): def got_key(key):
pass pass
def got_verifier(verifier): def got_verifier(verifier):
pass pass
def got_versions(versions): def got_versions(versions):
pass pass
def received(plaintext): def received(plaintext):
pass pass
def closed(result): def closed(result):
pass pass
class IBoss(Interface): class IBoss(Interface):
pass pass
class INameplate(Interface): class INameplate(Interface):
pass pass
class IMailbox(Interface): class IMailbox(Interface):
pass pass
class ISend(Interface): class ISend(Interface):
pass pass
class IOrder(Interface): class IOrder(Interface):
pass pass
class IKey(Interface): class IKey(Interface):
pass pass
class IReceive(Interface): class IReceive(Interface):
pass pass
class IRendezvousConnector(Interface): class IRendezvousConnector(Interface):
pass pass
class ILister(Interface): class ILister(Interface):
pass pass
class ICode(Interface): class ICode(Interface):
pass pass
class IInput(Interface): class IInput(Interface):
pass pass
class IAllocator(Interface): class IAllocator(Interface):
pass pass
class ITerminator(Interface): class ITerminator(Interface):
pass pass
class ITiming(Interface): class ITiming(Interface):
pass pass
class ITorManager(Interface): class ITorManager(Interface):
pass pass
class IWordlist(Interface): class IWordlist(Interface):
def choose_words(length): def choose_words(length):
"""Randomly select LENGTH words, join them with hyphens, return the """Randomly select LENGTH words, join them with hyphens, return the
result.""" result."""
def get_completions(prefix): def get_completions(prefix):
"""Return a list of all suffixes that could complete the given """Return a list of all suffixes that could complete the given
prefix.""" prefix."""
# These interfaces are public, and are re-exported by __init__.py # These interfaces are public, and are re-exported by __init__.py
class IDeferredWormhole(Interface): class IDeferredWormhole(Interface):
def get_welcome(): def get_welcome():
""" """
@ -277,6 +318,7 @@ class IDeferredWormhole(Interface):
:rtype: ``Deferred`` :rtype: ``Deferred``
""" """
class IInputHelper(Interface): class IInputHelper(Interface):
def refresh_nameplates(): def refresh_nameplates():
""" """
@ -389,5 +431,5 @@ class IInputHelper(Interface):
""" """
class IJournal(Interface): # TODO: this needs to be public class IJournal(Interface): # TODO: this needs to be public
pass pass

View File

@ -1,41 +1,50 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from hashlib import sha256 from hashlib import sha256
import six import six
from zope.interface import implementer from attr import attrib, attrs
from attr import attrs, attrib from attr.validators import instance_of, provides
from attr.validators import provides, instance_of
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 automat import MethodicalMachine
from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes, from hkdf import Hkdf
bytes_to_dict, dict_to_bytes) from nacl import utils
from nacl.exceptions import CryptoError
from nacl.secret import SecretBox
from spake2 import SPAKE2_Symmetric
from zope.interface import implementer
from . import _interfaces from . import _interfaces
from .util import (bytes_to_dict, bytes_to_hexstr, dict_to_bytes,
hexstr_to_bytes, to_bytes)
CryptoError CryptoError
__all__ = ["derive_key", "derive_phase_key", "CryptoError", __all__ = ["derive_key", "derive_phase_key", "CryptoError", "Key"]
"Key"]
def HKDF(skm, outlen, salt=None, CTXinfo=b""): def HKDF(skm, outlen, salt=None, CTXinfo=b""):
return Hkdf(salt, skm).expand(CTXinfo, outlen) return Hkdf(salt, skm).expand(CTXinfo, outlen)
def derive_key(key, purpose, length=SecretBox.KEY_SIZE): def derive_key(key, purpose, length=SecretBox.KEY_SIZE):
if not isinstance(key, type(b"")): raise TypeError(type(key)) if not isinstance(key, type(b"")):
if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) raise TypeError(type(key))
if not isinstance(length, six.integer_types): raise TypeError(type(length)) if not isinstance(purpose, type(b"")):
raise TypeError(type(purpose))
if not isinstance(length, six.integer_types):
raise TypeError(type(length))
return HKDF(key, length, CTXinfo=purpose) return HKDF(key, length, CTXinfo=purpose)
def derive_phase_key(key, side, phase): def derive_phase_key(key, side, phase):
assert isinstance(side, type("")), type(side) assert isinstance(side, type("")), type(side)
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
side_bytes = side.encode("ascii") side_bytes = side.encode("ascii")
phase_bytes = phase.encode("ascii") phase_bytes = phase.encode("ascii")
purpose = (b"wormhole:phase:" purpose = (b"wormhole:phase:" + sha256(side_bytes).digest() +
+ sha256(side_bytes).digest() sha256(phase_bytes).digest())
+ sha256(phase_bytes).digest())
return derive_key(key, purpose) return derive_key(key, purpose)
def decrypt_data(key, encrypted): def decrypt_data(key, encrypted):
assert isinstance(key, type(b"")), type(key) assert isinstance(key, type(b"")), type(key)
assert isinstance(encrypted, type(b"")), type(encrypted) assert isinstance(encrypted, type(b"")), type(encrypted)
@ -44,6 +53,7 @@ def decrypt_data(key, encrypted):
data = box.decrypt(encrypted) data = box.decrypt(encrypted)
return data return data
def encrypt_data(key, plaintext): def encrypt_data(key, plaintext):
assert isinstance(key, type(b"")), type(key) assert isinstance(key, type(b"")), type(key)
assert isinstance(plaintext, type(b"")), type(plaintext) assert isinstance(plaintext, type(b"")), type(plaintext)
@ -52,10 +62,12 @@ def encrypt_data(key, plaintext):
nonce = utils.random(SecretBox.NONCE_SIZE) nonce = utils.random(SecretBox.NONCE_SIZE)
return box.encrypt(plaintext, nonce) return box.encrypt(plaintext, nonce)
# the Key we expose to callers (Boss, Ordering) is responsible for sorting # the Key we expose to callers (Boss, Ordering) is responsible for sorting
# the two messages (got_code and got_pake), then delivering them to # the two messages (got_code and got_pake), then delivering them to
# _SortedKey in the right order. # _SortedKey in the right order.
@attrs @attrs
@implementer(_interfaces.IKey) @implementer(_interfaces.IKey)
class Key(object): class Key(object):
@ -64,40 +76,54 @@ class Key(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._SK = _SortedKey(self._appid, self._versions, self._side, self._SK = _SortedKey(self._appid, self._versions, self._side,
self._timing) self._timing)
self._debug_pake_stashed = False # for tests self._debug_pake_stashed = False # for tests
def wire(self, boss, mailbox, receive): def wire(self, boss, mailbox, receive):
self._SK.wire(boss, mailbox, receive) self._SK.wire(boss, mailbox, receive)
@m.state(initial=True) @m.state(initial=True)
def S00(self): pass # pragma: no cover def S00(self):
pass # pragma: no cover
@m.state() @m.state()
def S01(self): pass # pragma: no cover def S01(self):
pass # pragma: no cover
@m.state() @m.state()
def S10(self): pass # pragma: no cover def S10(self):
pass # pragma: no cover
@m.state() @m.state()
def S11(self): pass # pragma: no cover def S11(self):
pass # pragma: no cover
@m.input() @m.input()
def got_code(self, code): pass def got_code(self, code):
pass
@m.input() @m.input()
def got_pake(self, body): pass def got_pake(self, body):
pass
@m.output() @m.output()
def stash_pake(self, body): def stash_pake(self, body):
self._pake = body self._pake = body
self._debug_pake_stashed = True self._debug_pake_stashed = True
@m.output() @m.output()
def deliver_code(self, code): def deliver_code(self, code):
self._SK.got_code(code) self._SK.got_code(code)
@m.output() @m.output()
def deliver_pake(self, body): def deliver_pake(self, body):
self._SK.got_pake(body) self._SK.got_pake(body)
@m.output() @m.output()
def deliver_code_and_stashed_pake(self, code): def deliver_code_and_stashed_pake(self, code):
self._SK.got_code(code) self._SK.got_code(code)
@ -108,6 +134,7 @@ class Key(object):
S00.upon(got_pake, enter=S01, outputs=[stash_pake]) S00.upon(got_pake, enter=S01, outputs=[stash_pake])
S01.upon(got_code, enter=S11, outputs=[deliver_code_and_stashed_pake]) S01.upon(got_code, enter=S11, outputs=[deliver_code_and_stashed_pake])
@attrs @attrs
class _SortedKey(object): class _SortedKey(object):
_appid = attrib(validator=instance_of(type(u""))) _appid = attrib(validator=instance_of(type(u"")))
@ -115,7 +142,8 @@ class _SortedKey(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def wire(self, boss, mailbox, receive): def wire(self, boss, mailbox, receive):
self._B = _interfaces.IBoss(boss) self._B = _interfaces.IBoss(boss)
@ -123,17 +151,25 @@ class _SortedKey(object):
self._R = _interfaces.IReceive(receive) self._R = _interfaces.IReceive(receive)
@m.state(initial=True) @m.state(initial=True)
def S0_know_nothing(self): pass # pragma: no cover def S0_know_nothing(self):
pass # pragma: no cover
@m.state() @m.state()
def S1_know_code(self): pass # pragma: no cover def S1_know_code(self):
pass # pragma: no cover
@m.state() @m.state()
def S2_know_key(self): pass # pragma: no cover def S2_know_key(self):
pass # pragma: no cover
@m.state(terminal=True) @m.state(terminal=True)
def S3_scared(self): pass # pragma: no cover def S3_scared(self):
pass # pragma: no cover
# from Boss # from Boss
@m.input() @m.input()
def got_code(self, code): pass def got_code(self, code):
pass
# from Ordering # from Ordering
def got_pake(self, body): def got_pake(self, body):
@ -143,16 +179,20 @@ class _SortedKey(object):
self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) self.got_pake_good(hexstr_to_bytes(payload["pake_v1"]))
else: else:
self.got_pake_bad() self.got_pake_bad()
@m.input() @m.input()
def got_pake_good(self, msg2): pass def got_pake_good(self, msg2):
pass
@m.input() @m.input()
def got_pake_bad(self): pass def got_pake_bad(self):
pass
@m.output() @m.output()
def build_pake(self, code): def build_pake(self, code):
with self._timing.add("pake1", waiting="crypto"): with self._timing.add("pake1", waiting="crypto"):
self._sp = SPAKE2_Symmetric(to_bytes(code), self._sp = SPAKE2_Symmetric(
idSymmetric=to_bytes(self._appid)) to_bytes(code), idSymmetric=to_bytes(self._appid))
msg1 = self._sp.start() msg1 = self._sp.start()
body = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg1)}) body = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg1)})
self._M.add_message("pake", body) self._M.add_message("pake", body)
@ -160,6 +200,7 @@ class _SortedKey(object):
@m.output() @m.output()
def scared(self): def scared(self):
self._B.scared() self._B.scared()
@m.output() @m.output()
def compute_key(self, msg2): def compute_key(self, msg2):
assert isinstance(msg2, type(b"")) assert isinstance(msg2, type(b""))

View File

@ -1,16 +1,20 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
from attr import attrs, attrib from attr import attrib, attrs
from attr.validators import provides from attr.validators import provides
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
@attrs @attrs
@implementer(_interfaces.ILister) @implementer(_interfaces.ILister)
class Lister(object): class Lister(object):
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def wire(self, rendezvous_connector, input): def wire(self, rendezvous_connector, input):
self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
@ -26,26 +30,41 @@ class Lister(object):
# request arrives, both requests will be satisfied by the same response. # request arrives, both requests will be satisfied by the same response.
@m.state(initial=True) @m.state(initial=True)
def S0A_idle_disconnected(self): pass # pragma: no cover def S0A_idle_disconnected(self):
pass # pragma: no cover
@m.state() @m.state()
def S1A_wanting_disconnected(self): pass # pragma: no cover def S1A_wanting_disconnected(self):
pass # pragma: no cover
@m.state() @m.state()
def S0B_idle_connected(self): pass # pragma: no cover def S0B_idle_connected(self):
pass # pragma: no cover
@m.state() @m.state()
def S1B_wanting_connected(self): pass # pragma: no cover def S1B_wanting_connected(self):
pass # pragma: no cover
@m.input() @m.input()
def connected(self): pass def connected(self):
pass
@m.input() @m.input()
def lost(self): pass def lost(self):
pass
@m.input() @m.input()
def refresh(self): pass def refresh(self):
pass
@m.input() @m.input()
def rx_nameplates(self, all_nameplates): pass def rx_nameplates(self, all_nameplates):
pass
@m.output() @m.output()
def RC_tx_list(self): def RC_tx_list(self):
self._RC.tx_list() self._RC.tx_list()
@m.output() @m.output()
def I_got_nameplates(self, all_nameplates): def I_got_nameplates(self, all_nameplates):
# We get a set of nameplate ids. There may be more attributes in the # We get a set of nameplate ids. There may be more attributes in the
@ -56,18 +75,19 @@ class Lister(object):
S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[]) S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[])
S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[])
S0A_idle_disconnected.upon(refresh, S0A_idle_disconnected.upon(
enter=S1A_wanting_disconnected, outputs=[]) refresh, enter=S1A_wanting_disconnected, outputs=[])
S1A_wanting_disconnected.upon(refresh, S1A_wanting_disconnected.upon(
enter=S1A_wanting_disconnected, outputs=[]) refresh, enter=S1A_wanting_disconnected, outputs=[])
S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, S1A_wanting_disconnected.upon(
outputs=[RC_tx_list]) connected, enter=S1B_wanting_connected, outputs=[RC_tx_list])
S0B_idle_connected.upon(refresh, enter=S1B_wanting_connected, S0B_idle_connected.upon(
outputs=[RC_tx_list]) refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list])
S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, S0B_idle_connected.upon(
outputs=[I_got_nameplates]) rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates])
S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) S1B_wanting_connected.upon(
S1B_wanting_connected.upon(refresh, enter=S1B_wanting_connected, lost, enter=S1A_wanting_disconnected, outputs=[])
outputs=[RC_tx_list]) S1B_wanting_connected.upon(
S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list])
outputs=[I_got_nameplates]) S1B_wanting_connected.upon(
rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates])

View File

@ -1,16 +1,20 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
from attr import attrs, attrib from attr import attrib, attrs
from attr.validators import instance_of from attr.validators import instance_of
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
@attrs @attrs
@implementer(_interfaces.IMailbox) @implementer(_interfaces.IMailbox)
class Mailbox(object): class Mailbox(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._mailbox = None self._mailbox = None
@ -29,52 +33,68 @@ class Mailbox(object):
# S0: know nothing # S0: know nothing
@m.state(initial=True) @m.state(initial=True)
def S0A(self): pass # pragma: no cover def S0A(self):
pass # pragma: no cover
@m.state() @m.state()
def S0B(self): pass # pragma: no cover def S0B(self):
pass # pragma: no cover
# S1: mailbox known, not opened # S1: mailbox known, not opened
@m.state() @m.state()
def S1A(self): pass # pragma: no cover def S1A(self):
pass # pragma: no cover
# S2: mailbox known, opened # S2: mailbox known, opened
# We've definitely tried to open the mailbox at least once, but it must # 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() # be re-opened with each connection, because open() is also subscribe()
@m.state() @m.state()
def S2A(self): pass # pragma: no cover def S2A(self):
pass # pragma: no cover
@m.state() @m.state()
def S2B(self): pass # pragma: no cover def S2B(self):
pass # pragma: no cover
# S3: closing # S3: closing
@m.state() @m.state()
def S3A(self): pass # pragma: no cover def S3A(self):
pass # pragma: no cover
@m.state() @m.state()
def S3B(self): pass # pragma: no cover def S3B(self):
pass # pragma: no cover
# S4: closed. We no longer care whether we're connected or not # S4: closed. We no longer care whether we're connected or not
#@m.state() # @m.state()
#def S4A(self): pass # def S4A(self): pass
#@m.state() # @m.state()
#def S4B(self): pass # def S4B(self): pass
@m.state(terminal=True) @m.state(terminal=True)
def S4(self): pass # pragma: no cover def S4(self):
pass # pragma: no cover
S4A = S4 S4A = S4
S4B = S4 S4B = S4
# from Terminator # from Terminator
@m.input() @m.input()
def close(self, mood): pass def close(self, mood):
pass
# from Nameplate # from Nameplate
@m.input() @m.input()
def got_mailbox(self, mailbox): pass def got_mailbox(self, mailbox):
pass
# from RendezvousConnector # from RendezvousConnector
@m.input() @m.input()
def connected(self): pass def connected(self):
pass
@m.input() @m.input()
def lost(self): pass def lost(self):
pass
def rx_message(self, side, phase, body): def rx_message(self, side, phase, body):
assert isinstance(side, type("")), type(side) assert isinstance(side, type("")), type(side)
@ -84,73 +104,91 @@ class Mailbox(object):
self.rx_message_ours(phase, body) self.rx_message_ours(phase, body)
else: else:
self.rx_message_theirs(side, phase, body) self.rx_message_theirs(side, phase, body)
@m.input() @m.input()
def rx_message_ours(self, phase, body): pass def rx_message_ours(self, phase, body):
pass
@m.input() @m.input()
def rx_message_theirs(self, side, phase, body): pass def rx_message_theirs(self, side, phase, body):
pass
@m.input() @m.input()
def rx_closed(self): pass def rx_closed(self):
pass
# from Send or Key # from Send or Key
@m.input() @m.input()
def add_message(self, phase, body): def add_message(self, phase, body):
pass pass
@m.output() @m.output()
def record_mailbox(self, mailbox): def record_mailbox(self, mailbox):
self._mailbox = mailbox self._mailbox = mailbox
@m.output() @m.output()
def RC_tx_open(self): def RC_tx_open(self):
assert self._mailbox assert self._mailbox
self._RC.tx_open(self._mailbox) self._RC.tx_open(self._mailbox)
@m.output() @m.output()
def queue(self, phase, body): def queue(self, phase, body):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
assert isinstance(body, type(b"")), (type(body), phase, body) assert isinstance(body, type(b"")), (type(body), phase, body)
self._pending_outbound[phase] = body self._pending_outbound[phase] = body
@m.output() @m.output()
def record_mailbox_and_RC_tx_open_and_drain(self, mailbox): def record_mailbox_and_RC_tx_open_and_drain(self, mailbox):
self._mailbox = mailbox self._mailbox = mailbox
self._RC.tx_open(mailbox) self._RC.tx_open(mailbox)
self._drain() self._drain()
@m.output() @m.output()
def drain(self): def drain(self):
self._drain() self._drain()
def _drain(self): def _drain(self):
for phase, body in self._pending_outbound.items(): for phase, body in self._pending_outbound.items():
self._RC.tx_add(phase, body) self._RC.tx_add(phase, body)
@m.output() @m.output()
def RC_tx_add(self, phase, body): def RC_tx_add(self, phase, body):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
assert isinstance(body, type(b"")), type(body) assert isinstance(body, type(b"")), type(body)
self._RC.tx_add(phase, body) self._RC.tx_add(phase, body)
@m.output() @m.output()
def N_release_and_accept(self, side, phase, body): def N_release_and_accept(self, side, phase, body):
self._N.release() self._N.release()
if phase not in self._processed: if phase not in self._processed:
self._processed.add(phase) self._processed.add(phase)
self._O.got_message(side, phase, body) self._O.got_message(side, phase, body)
@m.output() @m.output()
def RC_tx_close(self): def RC_tx_close(self):
assert self._mood assert self._mood
self._RC_tx_close() self._RC_tx_close()
def _RC_tx_close(self): def _RC_tx_close(self):
self._RC.tx_close(self._mailbox, self._mood) self._RC.tx_close(self._mailbox, self._mood)
@m.output() @m.output()
def dequeue(self, phase, body): def dequeue(self, phase, body):
self._pending_outbound.pop(phase, None) self._pending_outbound.pop(phase, None)
@m.output() @m.output()
def record_mood(self, mood): def record_mood(self, mood):
self._mood = mood self._mood = mood
@m.output() @m.output()
def record_mood_and_RC_tx_close(self, mood): def record_mood_and_RC_tx_close(self, mood):
self._mood = mood self._mood = mood
self._RC_tx_close() self._RC_tx_close()
@m.output() @m.output()
def ignore_mood_and_T_mailbox_done(self, mood): def ignore_mood_and_T_mailbox_done(self, mood):
self._T.mailbox_done() self._T.mailbox_done()
@m.output() @m.output()
def T_mailbox_done(self): def T_mailbox_done(self):
self._T.mailbox_done() self._T.mailbox_done()
@ -162,8 +200,10 @@ class Mailbox(object):
S0B.upon(lost, enter=S0A, outputs=[]) S0B.upon(lost, enter=S0A, outputs=[])
S0B.upon(add_message, enter=S0B, outputs=[queue]) S0B.upon(add_message, enter=S0B, outputs=[queue])
S0B.upon(close, enter=S4B, outputs=[ignore_mood_and_T_mailbox_done]) S0B.upon(close, enter=S4B, outputs=[ignore_mood_and_T_mailbox_done])
S0B.upon(got_mailbox, enter=S2B, S0B.upon(
outputs=[record_mailbox_and_RC_tx_open_and_drain]) got_mailbox,
enter=S2B,
outputs=[record_mailbox_and_RC_tx_open_and_drain])
S1A.upon(connected, enter=S2B, outputs=[RC_tx_open, drain]) S1A.upon(connected, enter=S2B, outputs=[RC_tx_open, drain])
S1A.upon(add_message, enter=S1A, outputs=[queue]) S1A.upon(add_message, enter=S1A, outputs=[queue])
@ -192,4 +232,3 @@ class Mailbox(object):
S4.upon(rx_message_theirs, enter=S4, outputs=[]) S4.upon(rx_message_theirs, enter=S4, outputs=[])
S4.upon(rx_message_ours, enter=S4, outputs=[]) S4.upon(rx_message_ours, enter=S4, outputs=[])
S4.upon(close, enter=S4, outputs=[]) S4.upon(close, enter=S4, outputs=[])

View File

@ -1,7 +1,10 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import re import re
from zope.interface import implementer
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
from ._wordlist import PGPWordList from ._wordlist import PGPWordList
from .errors import KeyFormatError from .errors import KeyFormatError
@ -9,13 +12,15 @@ from .errors import KeyFormatError
def validate_nameplate(nameplate): def validate_nameplate(nameplate):
if not re.search(r'^\d+$', nameplate): if not re.search(r'^\d+$', nameplate):
raise KeyFormatError("Nameplate '%s' must be numeric, with no spaces." raise KeyFormatError(
% nameplate) "Nameplate '%s' must be numeric, with no spaces." % nameplate)
@implementer(_interfaces.INameplate) @implementer(_interfaces.INameplate)
class Nameplate(object): class Nameplate(object):
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __init__(self): def __init__(self):
self._nameplate = None self._nameplate = None
@ -32,94 +37,125 @@ class Nameplate(object):
# S0: know nothing # S0: know nothing
@m.state(initial=True) @m.state(initial=True)
def S0A(self): pass # pragma: no cover def S0A(self):
pass # pragma: no cover
@m.state() @m.state()
def S0B(self): pass # pragma: no cover def S0B(self):
pass # pragma: no cover
# S1: nameplate known, never claimed # S1: nameplate known, never claimed
@m.state() @m.state()
def S1A(self): pass # pragma: no cover def S1A(self):
pass # pragma: no cover
# S2: nameplate known, maybe claimed # S2: nameplate known, maybe claimed
@m.state() @m.state()
def S2A(self): pass # pragma: no cover def S2A(self):
pass # pragma: no cover
@m.state() @m.state()
def S2B(self): pass # pragma: no cover def S2B(self):
pass # pragma: no cover
# S3: nameplate claimed # S3: nameplate claimed
@m.state() @m.state()
def S3A(self): pass # pragma: no cover def S3A(self):
pass # pragma: no cover
@m.state() @m.state()
def S3B(self): pass # pragma: no cover def S3B(self):
pass # pragma: no cover
# S4: maybe released # S4: maybe released
@m.state() @m.state()
def S4A(self): pass # pragma: no cover def S4A(self):
pass # pragma: no cover
@m.state() @m.state()
def S4B(self): pass # pragma: no cover def S4B(self):
pass # pragma: no cover
# S5: released # S5: released
# we no longer care whether we're connected or not # we no longer care whether we're connected or not
#@m.state() # @m.state()
#def S5A(self): pass # def S5A(self): pass
#@m.state() # @m.state()
#def S5B(self): pass # def S5B(self): pass
@m.state() @m.state()
def S5(self): pass # pragma: no cover def S5(self):
pass # pragma: no cover
S5A = S5 S5A = S5
S5B = S5 S5B = S5
# from Boss # from Boss
def set_nameplate(self, nameplate): def set_nameplate(self, nameplate):
validate_nameplate(nameplate) # can raise KeyFormatError validate_nameplate(nameplate) # can raise KeyFormatError
self._set_nameplate(nameplate) self._set_nameplate(nameplate)
@m.input() @m.input()
def _set_nameplate(self, nameplate): pass def _set_nameplate(self, nameplate):
pass
# from Mailbox # from Mailbox
@m.input() @m.input()
def release(self): pass def release(self):
pass
# from Terminator # from Terminator
@m.input() @m.input()
def close(self): pass def close(self):
pass
# from RendezvousConnector # from RendezvousConnector
@m.input() @m.input()
def connected(self): pass def connected(self):
@m.input() pass
def lost(self): pass
@m.input() @m.input()
def rx_claimed(self, mailbox): pass def lost(self):
pass
@m.input() @m.input()
def rx_released(self): pass def rx_claimed(self, mailbox):
pass
@m.input()
def rx_released(self):
pass
@m.output() @m.output()
def record_nameplate(self, nameplate): def record_nameplate(self, nameplate):
validate_nameplate(nameplate) validate_nameplate(nameplate)
self._nameplate = nameplate self._nameplate = nameplate
@m.output() @m.output()
def record_nameplate_and_RC_tx_claim(self, nameplate): def record_nameplate_and_RC_tx_claim(self, nameplate):
validate_nameplate(nameplate) validate_nameplate(nameplate)
self._nameplate = nameplate self._nameplate = nameplate
self._RC.tx_claim(self._nameplate) self._RC.tx_claim(self._nameplate)
@m.output() @m.output()
def RC_tx_claim(self): def RC_tx_claim(self):
# when invoked via M.connected(), we must use the stored nameplate # when invoked via M.connected(), we must use the stored nameplate
self._RC.tx_claim(self._nameplate) self._RC.tx_claim(self._nameplate)
@m.output() @m.output()
def I_got_wordlist(self, mailbox): def I_got_wordlist(self, mailbox):
# TODO select wordlist based on nameplate properties, in rx_claimed # TODO select wordlist based on nameplate properties, in rx_claimed
wordlist = PGPWordList() wordlist = PGPWordList()
self._I.got_wordlist(wordlist) self._I.got_wordlist(wordlist)
@m.output() @m.output()
def M_got_mailbox(self, mailbox): def M_got_mailbox(self, mailbox):
self._M.got_mailbox(mailbox) self._M.got_mailbox(mailbox)
@m.output() @m.output()
def RC_tx_release(self): def RC_tx_release(self):
assert self._nameplate assert self._nameplate
self._RC.tx_release(self._nameplate) self._RC.tx_release(self._nameplate)
@m.output() @m.output()
def T_nameplate_done(self): def T_nameplate_done(self):
self._T.nameplate_done() self._T.nameplate_done()
@ -127,8 +163,8 @@ class Nameplate(object):
S0A.upon(_set_nameplate, enter=S1A, outputs=[record_nameplate]) S0A.upon(_set_nameplate, enter=S1A, outputs=[record_nameplate])
S0A.upon(connected, enter=S0B, outputs=[]) S0A.upon(connected, enter=S0B, outputs=[])
S0A.upon(close, enter=S5A, outputs=[T_nameplate_done]) S0A.upon(close, enter=S5A, outputs=[T_nameplate_done])
S0B.upon(_set_nameplate, enter=S2B, S0B.upon(
outputs=[record_nameplate_and_RC_tx_claim]) _set_nameplate, enter=S2B, outputs=[record_nameplate_and_RC_tx_claim])
S0B.upon(lost, enter=S0A, outputs=[]) S0B.upon(lost, enter=S0A, outputs=[])
S0B.upon(close, enter=S5A, outputs=[T_nameplate_done]) S0B.upon(close, enter=S5A, outputs=[T_nameplate_done])
@ -144,7 +180,7 @@ class Nameplate(object):
S3A.upon(connected, enter=S3B, outputs=[]) S3A.upon(connected, enter=S3B, outputs=[])
S3A.upon(close, enter=S4A, outputs=[]) S3A.upon(close, enter=S4A, outputs=[])
S3B.upon(lost, enter=S3A, outputs=[]) S3B.upon(lost, enter=S3A, outputs=[])
#S3B.upon(rx_claimed, enter=S3B, outputs=[]) # shouldn't happen # S3B.upon(rx_claimed, enter=S3B, outputs=[]) # shouldn't happen
S3B.upon(release, enter=S4B, outputs=[RC_tx_release]) S3B.upon(release, enter=S4B, outputs=[RC_tx_release])
S3B.upon(close, enter=S4B, outputs=[RC_tx_release]) S3B.upon(close, enter=S4B, outputs=[RC_tx_release])
@ -153,7 +189,7 @@ class Nameplate(object):
S4B.upon(lost, enter=S4A, outputs=[]) S4B.upon(lost, enter=S4A, outputs=[])
S4B.upon(rx_claimed, enter=S4B, outputs=[]) S4B.upon(rx_claimed, enter=S4B, outputs=[])
S4B.upon(rx_released, enter=S5B, outputs=[T_nameplate_done]) S4B.upon(rx_released, enter=S5B, outputs=[T_nameplate_done])
S4B.upon(release, enter=S4B, outputs=[]) # mailbox is lazy S4B.upon(release, enter=S4B, outputs=[]) # mailbox is lazy
# Mailbox doesn't remember how many times it's sent a release, and will # 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 # 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. # is easier than adding a new pair of states to Mailbox.
@ -161,5 +197,5 @@ class Nameplate(object):
S5A.upon(connected, enter=S5B, outputs=[]) S5A.upon(connected, enter=S5B, outputs=[])
S5B.upon(lost, enter=S5A, outputs=[]) S5B.upon(lost, enter=S5A, outputs=[])
S5.upon(release, enter=S5, outputs=[]) # mailbox is lazy S5.upon(release, enter=S5, outputs=[]) # mailbox is lazy
S5.upon(close, enter=S5, outputs=[]) S5.upon(close, enter=S5, outputs=[])

View File

@ -1,32 +1,40 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
from attr import attrs, attrib from attr import attrib, attrs
from attr.validators import provides, instance_of from attr.validators import instance_of, provides
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
@attrs @attrs
@implementer(_interfaces.IOrder) @implementer(_interfaces.IOrder)
class Order(object): class Order(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._key = None self._key = None
self._queue = [] self._queue = []
def wire(self, key, receive): def wire(self, key, receive):
self._K = _interfaces.IKey(key) self._K = _interfaces.IKey(key)
self._R = _interfaces.IReceive(receive) self._R = _interfaces.IReceive(receive)
@m.state(initial=True) @m.state(initial=True)
def S0_no_pake(self): pass # pragma: no cover def S0_no_pake(self):
pass # pragma: no cover
@m.state(terminal=True) @m.state(terminal=True)
def S1_yes_pake(self): pass # pragma: no cover def S1_yes_pake(self):
pass # pragma: no cover
def got_message(self, side, phase, body): def got_message(self, side, phase, body):
#print("ORDER[%s].got_message(%s)" % (self._side, phase)) # print("ORDER[%s].got_message(%s)" % (self._side, phase))
assert isinstance(side, type("")), type(phase) assert isinstance(side, type("")), type(phase)
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
assert isinstance(body, type(b"")), type(body) assert isinstance(body, type(b"")), type(body)
@ -36,9 +44,12 @@ class Order(object):
self.got_non_pake(side, phase, body) self.got_non_pake(side, phase, body)
@m.input() @m.input()
def got_pake(self, side, phase, body): pass def got_pake(self, side, phase, body):
pass
@m.input() @m.input()
def got_non_pake(self, side, phase, body): pass def got_non_pake(self, side, phase, body):
pass
@m.output() @m.output()
def queue(self, side, phase, body): def queue(self, side, phase, body):
@ -46,9 +57,11 @@ class Order(object):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
assert isinstance(body, type(b"")), type(body) assert isinstance(body, type(b"")), type(body)
self._queue.append((side, phase, body)) self._queue.append((side, phase, body))
@m.output() @m.output()
def notify_key(self, side, phase, body): def notify_key(self, side, phase, body):
self._K.got_pake(body) self._K.got_pake(body)
@m.output() @m.output()
def drain(self, side, phase, body): def drain(self, side, phase, body):
del phase del phase
@ -56,6 +69,7 @@ class Order(object):
for (side, phase, body) in self._queue: for (side, phase, body) in self._queue:
self._deliver(side, phase, body) self._deliver(side, phase, body)
self._queue[:] = [] self._queue[:] = []
@m.output() @m.output()
def deliver(self, side, phase, body): def deliver(self, side, phase, body):
self._deliver(side, phase, body) self._deliver(side, phase, body)

View File

@ -1,10 +1,13 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
from attr import attrs, attrib from attr import attrib, attrs
from attr.validators import provides, instance_of from attr.validators import instance_of, provides
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
from ._key import derive_key, derive_phase_key, decrypt_data, CryptoError from ._key import CryptoError, decrypt_data, derive_key, derive_phase_key
@attrs @attrs
@implementer(_interfaces.IReceive) @implementer(_interfaces.IReceive)
@ -12,7 +15,8 @@ class Receive(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._key = None self._key = None
@ -22,13 +26,20 @@ class Receive(object):
self._S = _interfaces.ISend(send) self._S = _interfaces.ISend(send)
@m.state(initial=True) @m.state(initial=True)
def S0_unknown_key(self): pass # pragma: no cover def S0_unknown_key(self):
pass # pragma: no cover
@m.state() @m.state()
def S1_unverified_key(self): pass # pragma: no cover def S1_unverified_key(self):
pass # pragma: no cover
@m.state() @m.state()
def S2_verified_key(self): pass # pragma: no cover def S2_verified_key(self):
pass # pragma: no cover
@m.state(terminal=True) @m.state(terminal=True)
def S3_scared(self): pass # pragma: no cover def S3_scared(self):
pass # pragma: no cover
# from Ordering # from Ordering
def got_message(self, side, phase, body): def got_message(self, side, phase, body):
@ -43,47 +54,56 @@ class Receive(object):
self.got_message_bad() self.got_message_bad()
return return
self.got_message_good(phase, plaintext) self.got_message_good(phase, plaintext)
@m.input() @m.input()
def got_message_good(self, phase, plaintext): pass def got_message_good(self, phase, plaintext):
pass
@m.input() @m.input()
def got_message_bad(self): pass def got_message_bad(self):
pass
# from Key # from Key
@m.input() @m.input()
def got_key(self, key): pass def got_key(self, key):
pass
@m.output() @m.output()
def record_key(self, key): def record_key(self, key):
self._key = key self._key = key
@m.output() @m.output()
def S_got_verified_key(self, phase, plaintext): def S_got_verified_key(self, phase, plaintext):
assert self._key assert self._key
self._S.got_verified_key(self._key) self._S.got_verified_key(self._key)
@m.output() @m.output()
def W_happy(self, phase, plaintext): def W_happy(self, phase, plaintext):
self._B.happy() self._B.happy()
@m.output() @m.output()
def W_got_verifier(self, phase, plaintext): def W_got_verifier(self, phase, plaintext):
self._B.got_verifier(derive_key(self._key, b"wormhole:verifier")) self._B.got_verifier(derive_key(self._key, b"wormhole:verifier"))
@m.output() @m.output()
def W_got_message(self, phase, plaintext): def W_got_message(self, phase, plaintext):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
assert isinstance(plaintext, type(b"")), type(plaintext) assert isinstance(plaintext, type(b"")), type(plaintext)
self._B.got_message(phase, plaintext) self._B.got_message(phase, plaintext)
@m.output() @m.output()
def W_scared(self): def W_scared(self):
self._B.scared() self._B.scared()
S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key]) S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key])
S1_unverified_key.upon(got_message_good, enter=S2_verified_key, S1_unverified_key.upon(
outputs=[S_got_verified_key, got_message_good,
W_happy, W_got_verifier, W_got_message]) enter=S2_verified_key,
S1_unverified_key.upon(got_message_bad, enter=S3_scared, outputs=[S_got_verified_key, W_happy, W_got_verifier, W_got_message])
outputs=[W_scared]) S1_unverified_key.upon(
S2_verified_key.upon(got_message_bad, enter=S3_scared, got_message_bad, enter=S3_scared, outputs=[W_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, S2_verified_key.upon(
outputs=[W_got_message]) 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_good, enter=S3_scared, outputs=[])
S3_scared.upon(got_message_bad, enter=S3_scared, outputs=[]) S3_scared.upon(got_message_bad, enter=S3_scared, outputs=[])

View File

@ -9,44 +9,47 @@ from twisted.internet import defer, endpoints, task
from twisted.application import internet from twisted.application import internet
from autobahn.twisted import websocket from autobahn.twisted import websocket
from . import _interfaces, errors from . import _interfaces, errors
from .util import (bytes_to_hexstr, hexstr_to_bytes, from .util import (bytes_to_hexstr, hexstr_to_bytes, bytes_to_dict,
bytes_to_dict, dict_to_bytes) dict_to_bytes)
class WSClient(websocket.WebSocketClientProtocol): class WSClient(websocket.WebSocketClientProtocol):
def onConnect(self, response): def onConnect(self, response):
# this fires during WebSocket negotiation, and isn't very useful # this fires during WebSocket negotiation, and isn't very useful
# unless you want to modify the protocol settings # unless you want to modify the protocol settings
#print("onConnect", response) # print("onConnect", response)
pass pass
def onOpen(self, *args): def onOpen(self, *args):
# this fires when the WebSocket is ready to go. No arguments # this fires when the WebSocket is ready to go. No arguments
#print("onOpen", args) # print("onOpen", args)
#self.wormhole_open = True # self.wormhole_open = True
self._RC.ws_open(self) self._RC.ws_open(self)
def onMessage(self, payload, isBinary): def onMessage(self, payload, isBinary):
assert not isBinary assert not isBinary
try: try:
self._RC.ws_message(payload) self._RC.ws_message(payload)
except: except Exception:
from twisted.python.failure import Failure from twisted.python.failure import Failure
print("LOGGING", Failure()) print("LOGGING", Failure())
log.err() log.err()
raise raise
def onClose(self, wasClean, code, reason): def onClose(self, wasClean, code, reason):
#print("onClose") # print("onClose")
self._RC.ws_close(wasClean, code, reason) self._RC.ws_close(wasClean, code, reason)
#if self.wormhole_open: # if self.wormhole_open:
# self.wormhole._ws_closed(wasClean, code, reason) # self.wormhole._ws_closed(wasClean, code, reason)
#else: # else:
# # we closed before establishing a connection (onConnect) or # # we closed before establishing a connection (onConnect) or
# # finishing WebSocket negotiation (onOpen): errback # # finishing WebSocket negotiation (onOpen): errback
# self.factory.d.errback(error.ConnectError(reason)) # self.factory.d.errback(error.ConnectError(reason))
class WSFactory(websocket.WebSocketClientFactory): class WSFactory(websocket.WebSocketClientFactory):
protocol = WSClient protocol = WSClient
def __init__(self, RC, *args, **kwargs): def __init__(self, RC, *args, **kwargs):
websocket.WebSocketClientFactory.__init__(self, *args, **kwargs) websocket.WebSocketClientFactory.__init__(self, *args, **kwargs)
self._RC = RC self._RC = RC
@ -54,9 +57,10 @@ class WSFactory(websocket.WebSocketClientFactory):
def buildProtocol(self, addr): def buildProtocol(self, addr):
proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) proto = websocket.WebSocketClientFactory.buildProtocol(self, addr)
proto._RC = self._RC proto._RC = self._RC
#proto.wormhole_open = False # proto.wormhole_open = False
return proto return proto
@attrs @attrs
@implementer(_interfaces.IRendezvousConnector) @implementer(_interfaces.IRendezvousConnector)
class RendezvousConnector(object): class RendezvousConnector(object):
@ -90,6 +94,7 @@ class RendezvousConnector(object):
def set_trace(self, f): def set_trace(self, f):
self._trace = f self._trace = f
def _debug(self, what): def _debug(self, what):
if self._trace: if self._trace:
self._trace(old_state="", input=what, new_state="") self._trace(old_state="", input=what, new_state="")
@ -133,14 +138,13 @@ class RendezvousConnector(object):
def stop(self): def stop(self):
# ClientService.stopService is defined to "Stop attempting to # ClientService.stopService is defined to "Stop attempting to
# reconnect and close any existing connections" # reconnect and close any existing connections"
self._stopping = True # to catch _initial_connection_failed error self._stopping = True # to catch _initial_connection_failed error
d = defer.maybeDeferred(self._connector.stopService) d = defer.maybeDeferred(self._connector.stopService)
# ClientService.stopService always fires with None, even if the # ClientService.stopService always fires with None, even if the
# initial connection failed, so log.err just in case # initial connection failed, so log.err just in case
d.addErrback(log.err) d.addErrback(log.err)
d.addBoth(self._stopped) d.addBoth(self._stopped)
# from Lister # from Lister
def tx_list(self): def tx_list(self):
self._tx("list") self._tx("list")
@ -157,7 +161,7 @@ class RendezvousConnector(object):
# this should happen right away: the ClientService ought to be in # this should happen right away: the ClientService ought to be in
# the "_waiting" state, and everything in the _waiting.stop # the "_waiting" state, and everything in the _waiting.stop
# transition is immediate # transition is immediate
d.addErrback(log.err) # just in case something goes wrong d.addErrback(log.err) # just in case something goes wrong
d.addCallback(lambda _: self._B.error(sce)) d.addCallback(lambda _: self._B.error(sce))
# from our WSClient (the WebSocket protocol) # from our WSClient (the WebSocket protocol)
@ -166,8 +170,11 @@ class RendezvousConnector(object):
self._have_made_a_successful_connection = True self._have_made_a_successful_connection = True
self._ws = proto self._ws = proto
try: try:
self._tx("bind", appid=self._appid, side=self._side, self._tx(
client_version=self._client_version) "bind",
appid=self._appid,
side=self._side,
client_version=self._client_version)
self._N.connected() self._N.connected()
self._M.connected() self._M.connected()
self._L.connected() self._L.connected()
@ -180,19 +187,22 @@ class RendezvousConnector(object):
def ws_message(self, payload): def ws_message(self, payload):
msg = bytes_to_dict(payload) msg = bytes_to_dict(payload)
if msg["type"] != "ack": if msg["type"] != "ack":
self._debug("R.rx(%s %s%s)" % self._debug("R.rx(%s %s%s)" % (
(msg["type"], msg.get("phase",""), msg["type"],
"[mine]" if msg.get("side","") == self._side else "", msg.get("phase", ""),
)) "[mine]" if msg.get("side", "") == self._side else "",
))
self._timing.add("ws_receive", _side=self._side, message=msg) self._timing.add("ws_receive", _side=self._side, message=msg)
if self._debug_record_inbound_f: if self._debug_record_inbound_f:
self._debug_record_inbound_f(msg) self._debug_record_inbound_f(msg)
mtype = msg["type"] mtype = msg["type"]
meth = getattr(self, "_response_handle_"+mtype, None) meth = getattr(self, "_response_handle_" + mtype, None)
if not meth: if not meth:
# make tests fail, but real application will ignore it # make tests fail, but real application will ignore it
log.err(errors._UnknownMessageTypeError("Unknown inbound message type %r" % (msg,))) log.err(
errors._UnknownMessageTypeError(
"Unknown inbound message type %r" % (msg, )))
return return
try: try:
return meth(msg) return meth(msg)
@ -229,7 +239,7 @@ class RendezvousConnector(object):
# valid connection # valid connection
sce = errors.ServerConnectionError(self._url, reason) sce = errors.ServerConnectionError(self._url, reason)
d = defer.maybeDeferred(self._connector.stopService) d = defer.maybeDeferred(self._connector.stopService)
d.addErrback(log.err) # just in case something goes wrong d.addErrback(log.err) # just in case something goes wrong
# tell the Boss to quit and inform the user # tell the Boss to quit and inform the user
d.addCallback(lambda _: self._B.error(sce)) d.addCallback(lambda _: self._B.error(sce))
@ -292,7 +302,7 @@ class RendezvousConnector(object):
side = msg["side"] side = msg["side"]
phase = msg["phase"] phase = msg["phase"]
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
body = hexstr_to_bytes(msg["body"]) # bytes body = hexstr_to_bytes(msg["body"]) # bytes
self._M.rx_message(side, phase, body) self._M.rx_message(side, phase, body)
def _response_handle_released(self, msg): def _response_handle_released(self, msg):
@ -301,5 +311,4 @@ class RendezvousConnector(object):
def _response_handle_closed(self, msg): def _response_handle_closed(self, msg):
self._M.rx_closed() self._M.rx_closed()
# record, message, payload, packet, bundle, ciphertext, plaintext # record, message, payload, packet, bundle, ciphertext, plaintext

View File

@ -1,33 +1,41 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import traceback import traceback
from sys import stderr from sys import stderr
from attr import attrib, attrs
from six.moves import input
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.threads import blockingCallFromThread, deferToThread
from .errors import AlreadyInputNameplateError, KeyFormatError
try: try:
import readline import readline
except ImportError: except ImportError:
readline = None 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
from .errors import KeyFormatError, AlreadyInputNameplateError
errf = None errf = None
# uncomment this to enable tab-completion debugging # uncomment this to enable tab-completion debugging
#import os ; errf = open("err", "w") if os.path.exists("err") else None # import os ; errf = open("err", "w") if os.path.exists("err") else None
def debug(*args, **kwargs): # pragma: no cover def debug(*args, **kwargs): # pragma: no cover
if errf: if errf:
print(*args, file=errf, **kwargs) print(*args, file=errf, **kwargs)
errf.flush() errf.flush()
@attrs @attrs
class CodeInputter(object): class CodeInputter(object):
_input_helper = attrib() _input_helper = attrib()
_reactor = attrib() _reactor = attrib()
def __attrs_post_init__(self): def __attrs_post_init__(self):
self.used_completion = False self.used_completion = False
self._matches = None self._matches = None
# once we've claimed the nameplate, we can't go back # once we've claimed the nameplate, we can't go back
self._committed_nameplate = None # or string self._committed_nameplate = None # or string
def bcft(self, f, *a, **kw): def bcft(self, f, *a, **kw):
return blockingCallFromThread(self._reactor, f, *a, **kw) return blockingCallFromThread(self._reactor, f, *a, **kw)
@ -66,7 +74,7 @@ class CodeInputter(object):
nameplate, words = text.split("-", 1) nameplate, words = text.split("-", 1)
else: else:
got_nameplate = False got_nameplate = False
nameplate = text # partial nameplate = text # partial
# 'text' is one of these categories: # 'text' is one of these categories:
# "" or "12": complete on nameplates (all that match, maybe just one) # "" or "12": complete on nameplates (all that match, maybe just one)
@ -83,13 +91,15 @@ class CodeInputter(object):
# they deleted past the committment point: we can't use # they deleted past the committment point: we can't use
# this. For now, bail, but in the future let's find a # this. For now, bail, but in the future let's find a
# gentler way to encourage them to not do that. # gentler way to encourage them to not do that.
raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate) raise AlreadyInputNameplateError(
"nameplate (%s-) already entered, cannot go back" %
self._committed_nameplate)
if not got_nameplate: if not got_nameplate:
# we're completing on nameplates: "" or "12" or "123" # we're completing on nameplates: "" or "12" or "123"
self.bcft(ih.refresh_nameplates) # results arrive later self.bcft(ih.refresh_nameplates) # results arrive later
debug(" getting nameplates") debug(" getting nameplates")
completions = self.bcft(ih.get_nameplate_completions, nameplate) completions = self.bcft(ih.get_nameplate_completions, nameplate)
else: # "123-" or "123-supp" else: # "123-" or "123-supp"
# time to commit to this nameplate, if they haven't already # time to commit to this nameplate, if they haven't already
if not self._committed_nameplate: if not self._committed_nameplate:
debug(" choose_nameplate(%s)" % nameplate) debug(" choose_nameplate(%s)" % nameplate)
@ -112,11 +122,13 @@ class CodeInputter(object):
# heard about it from the server), it can't be helped. But # heard about it from the server), it can't be helped. But
# for the rest of the code, a simple wait-for-wordlist will # for the rest of the code, a simple wait-for-wordlist will
# improve the user experience. # improve the user experience.
self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM
# and we're completing on words now # and we're completing on words now
debug(" getting words (%s)" % (words,)) debug(" getting words (%s)" % (words, ))
completions = [nameplate+"-"+c completions = [
for c in self.bcft(ih.get_word_completions, words)] nameplate + "-" + c
for c in self.bcft(ih.get_word_completions, words)
]
# rlcompleter wants full strings # rlcompleter wants full strings
return sorted(completions) return sorted(completions)
@ -131,13 +143,16 @@ class CodeInputter(object):
# they deleted past the committment point: we can't use # they deleted past the committment point: we can't use
# this. For now, bail, but in the future let's find a # this. For now, bail, but in the future let's find a
# gentler way to encourage them to not do that. # gentler way to encourage them to not do that.
raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate) raise AlreadyInputNameplateError(
"nameplate (%s-) already entered, cannot go back" %
self._committed_nameplate)
else: else:
debug(" choose_nameplate(%s)" % nameplate) debug(" choose_nameplate(%s)" % nameplate)
self.bcft(self._input_helper.choose_nameplate, nameplate) self.bcft(self._input_helper.choose_nameplate, nameplate)
debug(" choose_words(%s)" % words) debug(" choose_words(%s)" % words)
self.bcft(self._input_helper.choose_words, words) self.bcft(self._input_helper.choose_words, words)
def _input_code_with_completion(prompt, input_helper, reactor): def _input_code_with_completion(prompt, input_helper, reactor):
# reminder: this all occurs in a separate thread. All calls to input_helper # reminder: this all occurs in a separate thread. All calls to input_helper
# must go through blockingCallFromThread() # must go through blockingCallFromThread()
@ -159,6 +174,7 @@ def _input_code_with_completion(prompt, input_helper, reactor):
c.finish(code) c.finish(code)
return c.used_completion return c.used_completion
def warn_readline(): def warn_readline():
# When our process receives a SIGINT, Twisted's SIGINT handler will # When our process receives a SIGINT, Twisted's SIGINT handler will
# stop the reactor and wait for all threads to terminate before the # stop the reactor and wait for all threads to terminate before the
@ -192,11 +208,12 @@ def warn_readline():
# doesn't see the signal, and we must still wait for stdin to make # doesn't see the signal, and we must still wait for stdin to make
# readline finish. # readline finish.
@inlineCallbacks @inlineCallbacks
def input_with_completion(prompt, input_helper, reactor): def input_with_completion(prompt, input_helper, reactor):
t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline) t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline)
#input_helper.refresh_nameplates() # input_helper.refresh_nameplates()
used_completion = yield deferToThread(_input_code_with_completion, used_completion = yield deferToThread(_input_code_with_completion, prompt,
prompt, input_helper, reactor) input_helper, reactor)
reactor.removeSystemEventTrigger(t) reactor.removeSystemEventTrigger(t)
returnValue(used_completion) returnValue(used_completion)

View File

@ -1,18 +1,22 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from attr import attrs, attrib
from attr.validators import provides, instance_of from attr import attrib, attrs
from zope.interface import implementer from attr.validators import instance_of, provides
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
from ._key import derive_phase_key, encrypt_data from ._key import derive_phase_key, encrypt_data
@attrs @attrs
@implementer(_interfaces.ISend) @implementer(_interfaces.ISend)
class Send(object): class Send(object):
_side = attrib(validator=instance_of(type(u""))) _side = attrib(validator=instance_of(type(u"")))
_timing = attrib(validator=provides(_interfaces.ITiming)) _timing = attrib(validator=provides(_interfaces.ITiming))
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __attrs_post_init__(self): def __attrs_post_init__(self):
self._queue = [] self._queue = []
@ -21,31 +25,40 @@ class Send(object):
self._M = _interfaces.IMailbox(mailbox) self._M = _interfaces.IMailbox(mailbox)
@m.state(initial=True) @m.state(initial=True)
def S0_no_key(self): pass # pragma: no cover def S0_no_key(self):
pass # pragma: no cover
@m.state(terminal=True) @m.state(terminal=True)
def S1_verified_key(self): pass # pragma: no cover def S1_verified_key(self):
pass # pragma: no cover
# from Receive # from Receive
@m.input() @m.input()
def got_verified_key(self, key): pass def got_verified_key(self, key):
pass
# from Boss # from Boss
@m.input() @m.input()
def send(self, phase, plaintext): pass def send(self, phase, plaintext):
pass
@m.output() @m.output()
def queue(self, phase, plaintext): def queue(self, phase, plaintext):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
assert isinstance(plaintext, type(b"")), type(plaintext) assert isinstance(plaintext, type(b"")), type(plaintext)
self._queue.append((phase, plaintext)) self._queue.append((phase, plaintext))
@m.output() @m.output()
def record_key(self, key): def record_key(self, key):
self._key = key self._key = key
@m.output() @m.output()
def drain(self, key): def drain(self, key):
del key del key
for (phase, plaintext) in self._queue: for (phase, plaintext) in self._queue:
self._encrypt_and_send(phase, plaintext) self._encrypt_and_send(phase, plaintext)
self._queue[:] = [] self._queue[:] = []
@m.output() @m.output()
def deliver(self, phase, plaintext): def deliver(self, phase, plaintext):
assert isinstance(phase, type("")), type(phase) assert isinstance(phase, type("")), type(phase)
@ -59,6 +72,6 @@ class Send(object):
self._M.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(send, enter=S0_no_key, outputs=[queue])
S0_no_key.upon(got_verified_key, enter=S1_verified_key, S0_no_key.upon(
outputs=[record_key, drain]) got_verified_key, enter=S1_verified_key, outputs=[record_key, drain])
S1_verified_key.upon(send, enter=S1_verified_key, outputs=[deliver]) S1_verified_key.upon(send, enter=S1_verified_key, outputs=[deliver])

View File

@ -1,12 +1,16 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
from automat import MethodicalMachine from automat import MethodicalMachine
from zope.interface import implementer
from . import _interfaces from . import _interfaces
@implementer(_interfaces.ITerminator) @implementer(_interfaces.ITerminator)
class Terminator(object): class Terminator(object):
m = MethodicalMachine() m = MethodicalMachine()
set_trace = getattr(m, "_setTrace", lambda self, f: None) # pragma: no cover set_trace = getattr(m, "_setTrace",
lambda self, f: None) # pragma: no cover
def __init__(self): def __init__(self):
self._mood = None self._mood = None
@ -29,48 +33,68 @@ class Terminator(object):
# done, and we're closing, then we stop the RendezvousConnector # done, and we're closing, then we stop the RendezvousConnector
@m.state(initial=True) @m.state(initial=True)
def Snmo(self): pass # pragma: no cover def Snmo(self):
@m.state() pass # pragma: no cover
def Smo(self): pass # pragma: no cover
@m.state()
def Sno(self): pass # pragma: no cover
@m.state()
def S0o(self): pass # pragma: no cover
@m.state() @m.state()
def Snm(self): pass # pragma: no cover def Smo(self):
@m.state() pass # pragma: no cover
def Sm(self): pass # pragma: no cover
@m.state()
def Sn(self): pass # pragma: no cover
#@m.state()
#def S0(self): pass # unused
@m.state() @m.state()
def S_stopping(self): pass # pragma: no cover def Sno(self):
pass # pragma: no cover
@m.state() @m.state()
def S_stopped(self, terminal=True): pass # pragma: no cover def S0o(self):
pass # pragma: no cover
@m.state()
def Snm(self):
pass # pragma: no cover
@m.state()
def Sm(self):
pass # pragma: no cover
@m.state()
def Sn(self):
pass # pragma: no cover
# @m.state()
# def S0(self): pass # unused
@m.state()
def S_stopping(self):
pass # pragma: no cover
@m.state()
def S_stopped(self, terminal=True):
pass # pragma: no cover
# from Boss # from Boss
@m.input() @m.input()
def close(self, mood): pass def close(self, mood):
pass
# from Nameplate # from Nameplate
@m.input() @m.input()
def nameplate_done(self): pass def nameplate_done(self):
pass
# from Mailbox # from Mailbox
@m.input() @m.input()
def mailbox_done(self): pass def mailbox_done(self):
pass
# from RendezvousConnector # from RendezvousConnector
@m.input() @m.input()
def stopped(self): pass def stopped(self):
pass
@m.output() @m.output()
def close_nameplate(self, mood): def close_nameplate(self, mood):
self._N.close() # ignores mood self._N.close() # ignores mood
@m.output() @m.output()
def close_mailbox(self, mood): def close_mailbox(self, mood):
self._M.close(mood) self._M.close(mood)
@ -78,9 +102,11 @@ class Terminator(object):
@m.output() @m.output()
def ignore_mood_and_RC_stop(self, mood): def ignore_mood_and_RC_stop(self, mood):
self._RC.stop() self._RC.stop()
@m.output() @m.output()
def RC_stop(self): def RC_stop(self):
self._RC.stop() self._RC.stop()
@m.output() @m.output()
def B_closed(self): def B_closed(self):
self._B.closed() self._B.closed()
@ -99,8 +125,10 @@ class Terminator(object):
Snm.upon(nameplate_done, enter=Sm, outputs=[]) Snm.upon(nameplate_done, enter=Sm, outputs=[])
Sn.upon(nameplate_done, enter=S_stopping, outputs=[RC_stop]) Sn.upon(nameplate_done, enter=S_stopping, outputs=[RC_stop])
S0o.upon(close, enter=S_stopping, S0o.upon(
outputs=[close_nameplate, close_mailbox, ignore_mood_and_RC_stop]) close,
enter=S_stopping,
outputs=[close_nameplate, close_mailbox, ignore_mood_and_RC_stop])
Sm.upon(mailbox_done, enter=S_stopping, outputs=[RC_stop]) Sm.upon(mailbox_done, enter=S_stopping, outputs=[RC_stop])
S_stopping.upon(stopped, enter=S_stopped, outputs=[B_closed]) S_stopping.upon(stopped, enter=S_stopped, outputs=[B_closed])

View File

@ -1,6 +1,10 @@
from __future__ import unicode_literals, print_function from __future__ import print_function, unicode_literals
import os import os
from binascii import unhexlify
from zope.interface import implementer from zope.interface import implementer
from ._interfaces import IWordlist from ._interfaces import IWordlist
# The PGP Word List, which maps bytes to phonetically-distinct words. There # The PGP Word List, which maps bytes to phonetically-distinct words. There
@ -10,154 +14,280 @@ from ._interfaces import IWordlist
# Thanks to Warren Guy for transcribing them: # Thanks to Warren Guy for transcribing them:
# https://github.com/warrenguy/javascript-pgp-word-list # https://github.com/warrenguy/javascript-pgp-word-list
from binascii import unhexlify
raw_words = { raw_words = {
'00': ['aardvark', 'adroitness'], '01': ['absurd', 'adviser'], '00': ['aardvark', 'adroitness'],
'02': ['accrue', 'aftermath'], '03': ['acme', 'aggregate'], '01': ['absurd', 'adviser'],
'04': ['adrift', 'alkali'], '05': ['adult', 'almighty'], '02': ['accrue', 'aftermath'],
'06': ['afflict', 'amulet'], '07': ['ahead', 'amusement'], '03': ['acme', 'aggregate'],
'08': ['aimless', 'antenna'], '09': ['Algol', 'applicant'], '04': ['adrift', 'alkali'],
'0A': ['allow', 'Apollo'], '0B': ['alone', 'armistice'], '05': ['adult', 'almighty'],
'0C': ['ammo', 'article'], '0D': ['ancient', 'asteroid'], '06': ['afflict', 'amulet'],
'0E': ['apple', 'Atlantic'], '0F': ['artist', 'atmosphere'], '07': ['ahead', 'amusement'],
'10': ['assume', 'autopsy'], '11': ['Athens', 'Babylon'], '08': ['aimless', 'antenna'],
'12': ['atlas', 'backwater'], '13': ['Aztec', 'barbecue'], '09': ['Algol', 'applicant'],
'14': ['baboon', 'belowground'], '15': ['backfield', 'bifocals'], '0A': ['allow', 'Apollo'],
'16': ['backward', 'bodyguard'], '17': ['banjo', 'bookseller'], '0B': ['alone', 'armistice'],
'18': ['beaming', 'borderline'], '19': ['bedlamp', 'bottomless'], '0C': ['ammo', 'article'],
'1A': ['beehive', 'Bradbury'], '1B': ['beeswax', 'bravado'], '0D': ['ancient', 'asteroid'],
'1C': ['befriend', 'Brazilian'], '1D': ['Belfast', 'breakaway'], '0E': ['apple', 'Atlantic'],
'1E': ['berserk', 'Burlington'], '1F': ['billiard', 'businessman'], '0F': ['artist', 'atmosphere'],
'20': ['bison', 'butterfat'], '21': ['blackjack', 'Camelot'], '10': ['assume', 'autopsy'],
'22': ['blockade', 'candidate'], '23': ['blowtorch', 'cannonball'], '11': ['Athens', 'Babylon'],
'24': ['bluebird', 'Capricorn'], '25': ['bombast', 'caravan'], '12': ['atlas', 'backwater'],
'26': ['bookshelf', 'caretaker'], '27': ['brackish', 'celebrate'], '13': ['Aztec', 'barbecue'],
'28': ['breadline', 'cellulose'], '29': ['breakup', 'certify'], '14': ['baboon', 'belowground'],
'2A': ['brickyard', 'chambermaid'], '2B': ['briefcase', 'Cherokee'], '15': ['backfield', 'bifocals'],
'2C': ['Burbank', 'Chicago'], '2D': ['button', 'clergyman'], '16': ['backward', 'bodyguard'],
'2E': ['buzzard', 'coherence'], '2F': ['cement', 'combustion'], '17': ['banjo', 'bookseller'],
'30': ['chairlift', 'commando'], '31': ['chatter', 'company'], '18': ['beaming', 'borderline'],
'32': ['checkup', 'component'], '33': ['chisel', 'concurrent'], '19': ['bedlamp', 'bottomless'],
'34': ['choking', 'confidence'], '35': ['chopper', 'conformist'], '1A': ['beehive', 'Bradbury'],
'36': ['Christmas', 'congregate'], '37': ['clamshell', 'consensus'], '1B': ['beeswax', 'bravado'],
'38': ['classic', 'consulting'], '39': ['classroom', 'corporate'], '1C': ['befriend', 'Brazilian'],
'3A': ['cleanup', 'corrosion'], '3B': ['clockwork', 'councilman'], '1D': ['Belfast', 'breakaway'],
'3C': ['cobra', 'crossover'], '3D': ['commence', 'crucifix'], '1E': ['berserk', 'Burlington'],
'3E': ['concert', 'cumbersome'], '3F': ['cowbell', 'customer'], '1F': ['billiard', 'businessman'],
'40': ['crackdown', 'Dakota'], '41': ['cranky', 'decadence'], '20': ['bison', 'butterfat'],
'42': ['crowfoot', 'December'], '43': ['crucial', 'decimal'], '21': ['blackjack', 'Camelot'],
'44': ['crumpled', 'designing'], '45': ['crusade', 'detector'], '22': ['blockade', 'candidate'],
'46': ['cubic', 'detergent'], '47': ['dashboard', 'determine'], '23': ['blowtorch', 'cannonball'],
'48': ['deadbolt', 'dictator'], '49': ['deckhand', 'dinosaur'], '24': ['bluebird', 'Capricorn'],
'4A': ['dogsled', 'direction'], '4B': ['dragnet', 'disable'], '25': ['bombast', 'caravan'],
'4C': ['drainage', 'disbelief'], '4D': ['dreadful', 'disruptive'], '26': ['bookshelf', 'caretaker'],
'4E': ['drifter', 'distortion'], '4F': ['dropper', 'document'], '27': ['brackish', 'celebrate'],
'50': ['drumbeat', 'embezzle'], '51': ['drunken', 'enchanting'], '28': ['breadline', 'cellulose'],
'52': ['Dupont', 'enrollment'], '53': ['dwelling', 'enterprise'], '29': ['breakup', 'certify'],
'54': ['eating', 'equation'], '55': ['edict', 'equipment'], '2A': ['brickyard', 'chambermaid'],
'56': ['egghead', 'escapade'], '57': ['eightball', 'Eskimo'], '2B': ['briefcase', 'Cherokee'],
'58': ['endorse', 'everyday'], '59': ['endow', 'examine'], '2C': ['Burbank', 'Chicago'],
'5A': ['enlist', 'existence'], '5B': ['erase', 'exodus'], '2D': ['button', 'clergyman'],
'5C': ['escape', 'fascinate'], '5D': ['exceed', 'filament'], '2E': ['buzzard', 'coherence'],
'5E': ['eyeglass', 'finicky'], '5F': ['eyetooth', 'forever'], '2F': ['cement', 'combustion'],
'60': ['facial', 'fortitude'], '61': ['fallout', 'frequency'], '30': ['chairlift', 'commando'],
'62': ['flagpole', 'gadgetry'], '63': ['flatfoot', 'Galveston'], '31': ['chatter', 'company'],
'64': ['flytrap', 'getaway'], '65': ['fracture', 'glossary'], '32': ['checkup', 'component'],
'66': ['framework', 'gossamer'], '67': ['freedom', 'graduate'], '33': ['chisel', 'concurrent'],
'68': ['frighten', 'gravity'], '69': ['gazelle', 'guitarist'], '34': ['choking', 'confidence'],
'6A': ['Geiger', 'hamburger'], '6B': ['glitter', 'Hamilton'], '35': ['chopper', 'conformist'],
'6C': ['glucose', 'handiwork'], '6D': ['goggles', 'hazardous'], '36': ['Christmas', 'congregate'],
'6E': ['goldfish', 'headwaters'], '6F': ['gremlin', 'hemisphere'], '37': ['clamshell', 'consensus'],
'70': ['guidance', 'hesitate'], '71': ['hamlet', 'hideaway'], '38': ['classic', 'consulting'],
'72': ['highchair', 'holiness'], '73': ['hockey', 'hurricane'], '39': ['classroom', 'corporate'],
'74': ['indoors', 'hydraulic'], '75': ['indulge', 'impartial'], '3A': ['cleanup', 'corrosion'],
'76': ['inverse', 'impetus'], '77': ['involve', 'inception'], '3B': ['clockwork', 'councilman'],
'78': ['island', 'indigo'], '79': ['jawbone', 'inertia'], '3C': ['cobra', 'crossover'],
'7A': ['keyboard', 'infancy'], '7B': ['kickoff', 'inferno'], '3D': ['commence', 'crucifix'],
'7C': ['kiwi', 'informant'], '7D': ['klaxon', 'insincere'], '3E': ['concert', 'cumbersome'],
'7E': ['locale', 'insurgent'], '7F': ['lockup', 'integrate'], '3F': ['cowbell', 'customer'],
'80': ['merit', 'intention'], '81': ['minnow', 'inventive'], '40': ['crackdown', 'Dakota'],
'82': ['miser', 'Istanbul'], '83': ['Mohawk', 'Jamaica'], '41': ['cranky', 'decadence'],
'84': ['mural', 'Jupiter'], '85': ['music', 'leprosy'], '42': ['crowfoot', 'December'],
'86': ['necklace', 'letterhead'], '87': ['Neptune', 'liberty'], '43': ['crucial', 'decimal'],
'88': ['newborn', 'maritime'], '89': ['nightbird', 'matchmaker'], '44': ['crumpled', 'designing'],
'8A': ['Oakland', 'maverick'], '8B': ['obtuse', 'Medusa'], '45': ['crusade', 'detector'],
'8C': ['offload', 'megaton'], '8D': ['optic', 'microscope'], '46': ['cubic', 'detergent'],
'8E': ['orca', 'microwave'], '8F': ['payday', 'midsummer'], '47': ['dashboard', 'determine'],
'90': ['peachy', 'millionaire'], '91': ['pheasant', 'miracle'], '48': ['deadbolt', 'dictator'],
'92': ['physique', 'misnomer'], '93': ['playhouse', 'molasses'], '49': ['deckhand', 'dinosaur'],
'94': ['Pluto', 'molecule'], '95': ['preclude', 'Montana'], '4A': ['dogsled', 'direction'],
'96': ['prefer', 'monument'], '97': ['preshrunk', 'mosquito'], '4B': ['dragnet', 'disable'],
'98': ['printer', 'narrative'], '99': ['prowler', 'nebula'], '4C': ['drainage', 'disbelief'],
'9A': ['pupil', 'newsletter'], '9B': ['puppy', 'Norwegian'], '4D': ['dreadful', 'disruptive'],
'9C': ['python', 'October'], '9D': ['quadrant', 'Ohio'], '4E': ['drifter', 'distortion'],
'9E': ['quiver', 'onlooker'], '9F': ['quota', 'opulent'], '4F': ['dropper', 'document'],
'A0': ['ragtime', 'Orlando'], 'A1': ['ratchet', 'outfielder'], '50': ['drumbeat', 'embezzle'],
'A2': ['rebirth', 'Pacific'], 'A3': ['reform', 'pandemic'], '51': ['drunken', 'enchanting'],
'A4': ['regain', 'Pandora'], 'A5': ['reindeer', 'paperweight'], '52': ['Dupont', 'enrollment'],
'A6': ['rematch', 'paragon'], 'A7': ['repay', 'paragraph'], '53': ['dwelling', 'enterprise'],
'A8': ['retouch', 'paramount'], 'A9': ['revenge', 'passenger'], '54': ['eating', 'equation'],
'AA': ['reward', 'pedigree'], 'AB': ['rhythm', 'Pegasus'], '55': ['edict', 'equipment'],
'AC': ['ribcage', 'penetrate'], 'AD': ['ringbolt', 'perceptive'], '56': ['egghead', 'escapade'],
'AE': ['robust', 'performance'], 'AF': ['rocker', 'pharmacy'], '57': ['eightball', 'Eskimo'],
'B0': ['ruffled', 'phonetic'], 'B1': ['sailboat', 'photograph'], '58': ['endorse', 'everyday'],
'B2': ['sawdust', 'pioneer'], 'B3': ['scallion', 'pocketful'], '59': ['endow', 'examine'],
'B4': ['scenic', 'politeness'], 'B5': ['scorecard', 'positive'], '5A': ['enlist', 'existence'],
'B6': ['Scotland', 'potato'], 'B7': ['seabird', 'processor'], '5B': ['erase', 'exodus'],
'B8': ['select', 'provincial'], 'B9': ['sentence', 'proximate'], '5C': ['escape', 'fascinate'],
'BA': ['shadow', 'puberty'], 'BB': ['shamrock', 'publisher'], '5D': ['exceed', 'filament'],
'BC': ['showgirl', 'pyramid'], 'BD': ['skullcap', 'quantity'], '5E': ['eyeglass', 'finicky'],
'BE': ['skydive', 'racketeer'], 'BF': ['slingshot', 'rebellion'], '5F': ['eyetooth', 'forever'],
'C0': ['slowdown', 'recipe'], 'C1': ['snapline', 'recover'], '60': ['facial', 'fortitude'],
'C2': ['snapshot', 'repellent'], 'C3': ['snowcap', 'replica'], '61': ['fallout', 'frequency'],
'C4': ['snowslide', 'reproduce'], 'C5': ['solo', 'resistor'], '62': ['flagpole', 'gadgetry'],
'C6': ['southward', 'responsive'], 'C7': ['soybean', 'retraction'], '63': ['flatfoot', 'Galveston'],
'C8': ['spaniel', 'retrieval'], 'C9': ['spearhead', 'retrospect'], '64': ['flytrap', 'getaway'],
'CA': ['spellbind', 'revenue'], 'CB': ['spheroid', 'revival'], '65': ['fracture', 'glossary'],
'CC': ['spigot', 'revolver'], 'CD': ['spindle', 'sandalwood'], '66': ['framework', 'gossamer'],
'CE': ['spyglass', 'sardonic'], 'CF': ['stagehand', 'Saturday'], '67': ['freedom', 'graduate'],
'D0': ['stagnate', 'savagery'], 'D1': ['stairway', 'scavenger'], '68': ['frighten', 'gravity'],
'D2': ['standard', 'sensation'], 'D3': ['stapler', 'sociable'], '69': ['gazelle', 'guitarist'],
'D4': ['steamship', 'souvenir'], 'D5': ['sterling', 'specialist'], '6A': ['Geiger', 'hamburger'],
'D6': ['stockman', 'speculate'], 'D7': ['stopwatch', 'stethoscope'], '6B': ['glitter', 'Hamilton'],
'D8': ['stormy', 'stupendous'], 'D9': ['sugar', 'supportive'], '6C': ['glucose', 'handiwork'],
'DA': ['surmount', 'surrender'], 'DB': ['suspense', 'suspicious'], '6D': ['goggles', 'hazardous'],
'DC': ['sweatband', 'sympathy'], 'DD': ['swelter', 'tambourine'], '6E': ['goldfish', 'headwaters'],
'DE': ['tactics', 'telephone'], 'DF': ['talon', 'therapist'], '6F': ['gremlin', 'hemisphere'],
'E0': ['tapeworm', 'tobacco'], 'E1': ['tempest', 'tolerance'], '70': ['guidance', 'hesitate'],
'E2': ['tiger', 'tomorrow'], 'E3': ['tissue', 'torpedo'], '71': ['hamlet', 'hideaway'],
'E4': ['tonic', 'tradition'], 'E5': ['topmost', 'travesty'], '72': ['highchair', 'holiness'],
'E6': ['tracker', 'trombonist'], 'E7': ['transit', 'truncated'], '73': ['hockey', 'hurricane'],
'E8': ['trauma', 'typewriter'], 'E9': ['treadmill', 'ultimate'], '74': ['indoors', 'hydraulic'],
'EA': ['Trojan', 'undaunted'], 'EB': ['trouble', 'underfoot'], '75': ['indulge', 'impartial'],
'EC': ['tumor', 'unicorn'], 'ED': ['tunnel', 'unify'], '76': ['inverse', 'impetus'],
'EE': ['tycoon', 'universe'], 'EF': ['uncut', 'unravel'], '77': ['involve', 'inception'],
'F0': ['unearth', 'upcoming'], 'F1': ['unwind', 'vacancy'], '78': ['island', 'indigo'],
'F2': ['uproot', 'vagabond'], 'F3': ['upset', 'vertigo'], '79': ['jawbone', 'inertia'],
'F4': ['upshot', 'Virginia'], 'F5': ['vapor', 'visitor'], '7A': ['keyboard', 'infancy'],
'F6': ['village', 'vocalist'], 'F7': ['virus', 'voyager'], '7B': ['kickoff', 'inferno'],
'F8': ['Vulcan', 'warranty'], 'F9': ['waffle', 'Waterloo'], '7C': ['kiwi', 'informant'],
'FA': ['wallet', 'whimsical'], 'FB': ['watchword', 'Wichita'], '7D': ['klaxon', 'insincere'],
'FC': ['wayside', 'Wilmington'], 'FD': ['willow', 'Wyoming'], '7E': ['locale', 'insurgent'],
'FE': ['woodlark', 'yesteryear'], 'FF': ['Zulu', 'Yucatan'] '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]) byte_to_even_word = dict([(unhexlify(k.encode("ascii")), both_words[0])
for k,both_words for k, both_words in raw_words.items()])
in raw_words.items()])
byte_to_odd_word = dict([(unhexlify(k.encode("ascii")), both_words[1]) byte_to_odd_word = dict([(unhexlify(k.encode("ascii")), both_words[1])
for k,both_words for k, both_words in raw_words.items()])
in raw_words.items()])
even_words_lowercase, odd_words_lowercase = set(), set() even_words_lowercase, odd_words_lowercase = set(), set()
for k,both_words in raw_words.items(): for k, both_words in raw_words.items():
even_word, odd_word = both_words even_word, odd_word = both_words
even_words_lowercase.add(even_word.lower()) even_words_lowercase.add(even_word.lower())
odd_words_lowercase.add(odd_word.lower()) odd_words_lowercase.add(odd_word.lower())
@implementer(IWordlist) @implementer(IWordlist)
class PGPWordList(object): class PGPWordList(object):
def get_completions(self, prefix, num_words=2): def get_completions(self, prefix, num_words=2):
@ -177,7 +307,7 @@ class PGPWordList(object):
else: else:
suffix = prefix[:-lp] + word suffix = prefix[:-lp] + word
# append a hyphen if we expect more words # append a hyphen if we expect more words
if count+1 < num_words: if count + 1 < num_words:
suffix += "-" suffix += "-"
completions.add(suffix) completions.add(suffix)
return completions return completions

View File

@ -2,21 +2,24 @@ from __future__ import print_function
import os import os
import time import time
start = time.time() from sys import stderr, stdout
import six from textwrap import dedent, fill
from textwrap import fill, dedent
from sys import stdout, stderr
from . import public_relay
from .. import __version__
from ..timing import DebugTiming
from ..errors import (WrongPasswordError, WelcomeError, KeyFormatError,
TransferError, NoTorError, UnsendableFileError,
ServerConnectionError)
from twisted.internet.defer import inlineCallbacks, maybeDeferred
from twisted.python.failure import Failure
from twisted.internet.task import react
import click import click
import six
from twisted.internet.defer import inlineCallbacks, maybeDeferred
from twisted.internet.task import react
from twisted.python.failure import Failure
from . import public_relay
from .. import __version__
from ..errors import (KeyFormatError, NoTorError, ServerConnectionError,
TransferError, UnsendableFileError, WelcomeError,
WrongPasswordError)
from ..timing import DebugTiming
start = time.time()
top_import_finish = time.time() top_import_finish = time.time()
@ -24,6 +27,7 @@ class Config(object):
""" """
Union of config options that we pass down to (sub) commands. Union of config options that we pass down to (sub) commands.
""" """
def __init__(self): def __init__(self):
# This only holds attributes which are *not* set by CLI arguments. # This only holds attributes which are *not* set by CLI arguments.
# Everything else comes from Click decorators, so we can be sure # Everything else comes from Click decorators, so we can be sure
@ -34,11 +38,13 @@ class Config(object):
self.stderr = stderr self.stderr = stderr
self.tor = False # XXX? self.tor = False # XXX?
def _compose(*decorators): def _compose(*decorators):
def decorate(f): def decorate(f):
for d in reversed(decorators): for d in reversed(decorators):
f = d(f) f = d(f)
return f return f
return decorate return decorate
@ -48,6 +54,8 @@ ALIASES = {
"recieve": "receive", "recieve": "receive",
"recv": "receive", "recv": "receive",
} }
class AliasedGroup(click.Group): class AliasedGroup(click.Group):
def get_command(self, ctx, cmd_name): def get_command(self, ctx, cmd_name):
cmd_name = ALIASES.get(cmd_name, cmd_name) cmd_name = ALIASES.get(cmd_name, cmd_name)
@ -56,22 +64,24 @@ class AliasedGroup(click.Group):
# top-level command ("wormhole ...") # top-level command ("wormhole ...")
@click.group(cls=AliasedGroup) @click.group(cls=AliasedGroup)
@click.option("--appid", default=None, metavar="APPID", help="appid to use")
@click.option( @click.option(
"--appid", default=None, metavar="APPID", help="appid to use") "--relay-url",
@click.option( default=public_relay.RENDEZVOUS_RELAY,
"--relay-url", default=public_relay.RENDEZVOUS_RELAY,
envvar='WORMHOLE_RELAY_URL', envvar='WORMHOLE_RELAY_URL',
metavar="URL", metavar="URL",
help="rendezvous relay to use", help="rendezvous relay to use",
) )
@click.option( @click.option(
"--transit-helper", default=public_relay.TRANSIT_RELAY, "--transit-helper",
default=public_relay.TRANSIT_RELAY,
envvar='WORMHOLE_TRANSIT_HELPER', envvar='WORMHOLE_TRANSIT_HELPER',
metavar="tcp:HOST:PORT", metavar="tcp:HOST:PORT",
help="transit relay to use", help="transit relay to use",
) )
@click.option( @click.option(
"--dump-timing", type=type(u""), # TODO: hide from --help output "--dump-timing",
type=type(u""), # TODO: hide from --help output
default=None, default=None,
metavar="FILE.json", metavar="FILE.json",
help="(debug) write timing data to file", help="(debug) write timing data to file",
@ -104,7 +114,8 @@ def _dispatch_command(reactor, cfg, command):
errors for the user. errors for the user.
""" """
cfg.timing.add("command dispatch") cfg.timing.add("command dispatch")
cfg.timing.add("import", when=start, which="top").finish(when=top_import_finish) cfg.timing.add(
"import", when=start, which="top").finish(when=top_import_finish)
try: try:
yield maybeDeferred(command) yield maybeDeferred(command)
@ -141,56 +152,89 @@ def _dispatch_command(reactor, cfg, command):
CommonArgs = _compose( CommonArgs = _compose(
click.option("-0", "zeromode", default=False, is_flag=True, click.option(
help="enable no-code anything-goes mode", "-0",
), "zeromode",
click.option("-c", "--code-length", default=2, metavar="NUMWORDS", default=False,
help="length of code (in bytes/words)", is_flag=True,
), help="enable no-code anything-goes mode",
click.option("-v", "--verify", is_flag=True, default=False, ),
help="display verification string (and wait for approval)", click.option(
), "-c",
click.option("--hide-progress", is_flag=True, default=False, "--code-length",
help="supress progress-bar display", default=2,
), metavar="NUMWORDS",
click.option("--listen/--no-listen", default=True, help="length of code (in bytes/words)",
help="(debug) don't open a listening socket for Transit", ),
), click.option(
"-v",
"--verify",
is_flag=True,
default=False,
help="display verification string (and wait for approval)",
),
click.option(
"--hide-progress",
is_flag=True,
default=False,
help="supress progress-bar display",
),
click.option(
"--listen/--no-listen",
default=True,
help="(debug) don't open a listening socket for Transit",
),
) )
TorArgs = _compose( TorArgs = _compose(
click.option("--tor", is_flag=True, default=False, click.option(
help="use Tor when connecting", "--tor",
), is_flag=True,
click.option("--launch-tor", is_flag=True, default=False, default=False,
help="launch Tor, rather than use existing control/socks port", help="use Tor when connecting",
), ),
click.option("--tor-control-port", default=None, metavar="ENDPOINT", click.option(
help="endpoint descriptor for Tor control port", "--launch-tor",
), is_flag=True,
default=False,
help="launch Tor, rather than use existing control/socks port",
),
click.option(
"--tor-control-port",
default=None,
metavar="ENDPOINT",
help="endpoint descriptor for Tor control port",
),
) )
@wormhole.command() @wormhole.command()
@click.pass_context @click.pass_context
def help(context, **kwargs): def help(context, **kwargs):
print(context.find_root().get_help()) print(context.find_root().get_help())
# wormhole send (or "wormhole tx") # wormhole send (or "wormhole tx")
@wormhole.command() @wormhole.command()
@CommonArgs @CommonArgs
@TorArgs @TorArgs
@click.option( @click.option(
"--code", metavar="CODE", "--code",
metavar="CODE",
help="human-generated code phrase", help="human-generated code phrase",
) )
@click.option( @click.option(
"--text", default=None, metavar="MESSAGE", "--text",
help="text message to send, instead of a file. Use '-' to read from stdin.", default=None,
metavar="MESSAGE",
help=("text message to send, instead of a file."
" Use '-' to read from stdin."),
) )
@click.option( @click.option(
"--ignore-unsendable-files", default=False, is_flag=True, "--ignore-unsendable-files",
help="Don't raise an error if a file can't be read." default=False,
) is_flag=True,
help="Don't raise an error if a file can't be read.")
@click.argument("what", required=False, type=click.Path(path_type=type(u""))) @click.argument("what", required=False, type=click.Path(path_type=type(u"")))
@click.pass_obj @click.pass_obj
def send(cfg, **kwargs): def send(cfg, **kwargs):
@ -202,6 +246,7 @@ def send(cfg, **kwargs):
return go(cmd_send.send, cfg) return go(cmd_send.send, cfg)
# this intermediate function can be mocked by tests that need to build a # this intermediate function can be mocked by tests that need to build a
# Config object # Config object
def go(f, cfg): def go(f, cfg):
@ -214,23 +259,29 @@ def go(f, cfg):
@CommonArgs @CommonArgs
@TorArgs @TorArgs
@click.option( @click.option(
"--only-text", "-t", is_flag=True, "--only-text",
"-t",
is_flag=True,
help="refuse file transfers, only accept text transfers", help="refuse file transfers, only accept text transfers",
) )
@click.option( @click.option(
"--accept-file", is_flag=True, "--accept-file",
is_flag=True,
help="accept file transfer without asking for confirmation", help="accept file transfer without asking for confirmation",
) )
@click.option( @click.option(
"--output-file", "-o", "--output-file",
"-o",
metavar="FILENAME|DIRNAME", metavar="FILENAME|DIRNAME",
help=("The file or directory to create, overriding the name suggested" help=("The file or directory to create, overriding the name suggested"
" by the sender."), " by the sender."),
) )
@click.argument( @click.argument(
"code", nargs=-1, default=None, "code",
# help=("The magic-wormhole code, from the sender. If omitted, the" nargs=-1,
# " program will ask for it, using tab-completion."), default=None,
# help=("The magic-wormhole code, from the sender. If omitted, the"
# " program will ask for it, using tab-completion."),
) )
@click.pass_obj @click.pass_obj
def receive(cfg, code, **kwargs): def receive(cfg, code, **kwargs):
@ -244,10 +295,8 @@ def receive(cfg, code, **kwargs):
if len(code) == 1: if len(code) == 1:
cfg.code = code[0] cfg.code = code[0]
elif len(code) > 1: elif len(code) > 1:
print( print("Pass either no code or just one code; you passed"
"Pass either no code or just one code; you passed" " {}: {}".format(len(code), ', '.join(code)))
" {}: {}".format(len(code), ', '.join(code))
)
raise SystemExit(1) raise SystemExit(1)
else: else:
cfg.code = None cfg.code = None
@ -260,17 +309,19 @@ def ssh():
""" """
Facilitate sending/receiving SSH public keys Facilitate sending/receiving SSH public keys
""" """
pass
@ssh.command(name="invite") @ssh.command(name="invite")
@click.option( @click.option(
"-c", "--code-length", default=2, "-c",
"--code-length",
default=2,
metavar="NUMWORDS", metavar="NUMWORDS",
help="length of code (in bytes/words)", help="length of code (in bytes/words)",
) )
@click.option( @click.option(
"--user", "-u", "--user",
"-u",
default=None, default=None,
metavar="USER", metavar="USER",
help="Add to USER's ~/.ssh/authorized_keys", help="Add to USER's ~/.ssh/authorized_keys",
@ -291,15 +342,20 @@ def ssh_invite(ctx, code_length, user, **kwargs):
@ssh.command(name="accept") @ssh.command(name="accept")
@click.argument( @click.argument(
"code", nargs=1, required=True, "code",
nargs=1,
required=True,
) )
@click.option( @click.option(
"--key-file", "-F", "--key-file",
"-F",
default=None, default=None,
type=click.Path(exists=True), type=click.Path(exists=True),
) )
@click.option( @click.option(
"--yes", "-y", is_flag=True, "--yes",
"-y",
is_flag=True,
help="Skip confirmation prompt to send key", help="Skip confirmation prompt to send key",
) )
@TorArgs @TorArgs
@ -318,7 +374,8 @@ def ssh_accept(cfg, code, key_file, yes, **kwargs):
kind, keyid, pubkey = cmd_ssh.find_public_key(key_file) kind, keyid, pubkey = cmd_ssh.find_public_key(key_file)
print("Sending public key type='{}' keyid='{}'".format(kind, keyid)) print("Sending public key type='{}' keyid='{}'".format(kind, keyid))
if yes is not True: if yes is not True:
click.confirm("Really send public key '{}' ?".format(keyid), abort=True) click.confirm(
"Really send public key '{}' ?".format(keyid), abort=True)
cfg.public_key = (kind, keyid, pubkey) cfg.public_key = (kind, keyid, pubkey)
cfg.code = code cfg.code = code

View File

@ -1,14 +1,23 @@
from __future__ import print_function from __future__ import print_function
import os, sys, six, tempfile, zipfile, hashlib, shutil
from tqdm import tqdm import hashlib
import os
import shutil
import sys
import tempfile
import zipfile
import six
from humanize import naturalsize from humanize import naturalsize
from tqdm import tqdm
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python import log from twisted.python import log
from wormhole import create, input_with_completion, __version__ from wormhole import __version__, create, input_with_completion
from ..transit import TransitReceiver
from ..errors import TransferError from ..errors import TransferError
from ..util import (dict_to_bytes, bytes_to_dict, bytes_to_hexstr, from ..transit import TransitReceiver
from ..util import (bytes_to_dict, bytes_to_hexstr, dict_to_bytes,
estimate_free_space) estimate_free_space)
from .welcome import handle_welcome from .welcome import handle_welcome
@ -17,14 +26,17 @@ APPID = u"lothar.com/wormhole/text-or-file-xfer"
KEY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_KEY_TIMER", 1.0)) KEY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_KEY_TIMER", 1.0))
VERIFY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_VERIFY_TIMER", 1.0)) VERIFY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_VERIFY_TIMER", 1.0))
class RespondError(Exception): class RespondError(Exception):
def __init__(self, response): def __init__(self, response):
self.response = response self.response = response
class TransferRejectedError(RespondError): class TransferRejectedError(RespondError):
def __init__(self): def __init__(self):
RespondError.__init__(self, "transfer rejected") RespondError.__init__(self, "transfer rejected")
def receive(args, reactor=reactor, _debug_stash_wormhole=None): def receive(args, reactor=reactor, _debug_stash_wormhole=None):
"""I implement 'wormhole receive'. I return a Deferred that fires with """I implement 'wormhole receive'. I return a Deferred that fires with
None (for success), or signals one of the following errors: None (for success), or signals one of the following errors:
@ -60,16 +72,19 @@ class Receiver:
# tor in parallel with everything else, make sure the Tor object # tor in parallel with everything else, make sure the Tor object
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # with the user handing off the wormhole code
self._tor = yield get_tor(self._reactor, self._tor = yield get_tor(
self.args.launch_tor, self._reactor,
self.args.tor_control_port, self.args.launch_tor,
timing=self.args.timing) self.args.tor_control_port,
timing=self.args.timing)
w = create(self.args.appid or APPID, self.args.relay_url, w = create(
self._reactor, self.args.appid or APPID,
tor=self._tor, self.args.relay_url,
timing=self.args.timing) self._reactor,
self._w = w # so tests can wait on events too tor=self._tor,
timing=self.args.timing)
self._w = w # so tests can wait on events too
# I wanted to do this instead: # I wanted to do this instead:
# #
@ -87,7 +102,7 @@ class Receiver:
# (which might be an error) # (which might be an error)
@inlineCallbacks @inlineCallbacks
def _good(res): def _good(res):
yield w.close() # wait for ack yield w.close() # wait for ack
returnValue(res) returnValue(res)
# if we raise an error, we should close and then return the original # if we raise an error, we should close and then return the original
@ -96,8 +111,8 @@ class Receiver:
@inlineCallbacks @inlineCallbacks
def _bad(f): def _bad(f):
try: try:
yield w.close() # might be an error too yield w.close() # might be an error too
except: except Exception:
pass pass
returnValue(f) returnValue(f)
@ -114,6 +129,7 @@ class Receiver:
def on_slow_key(): def on_slow_key():
print(u"Waiting for sender...", file=self.args.stderr) print(u"Waiting for sender...", file=self.args.stderr)
notify = self._reactor.callLater(KEY_TIMER, on_slow_key) notify = self._reactor.callLater(KEY_TIMER, on_slow_key)
try: try:
# We wait here until we connect to the server and see the senders # We wait here until we connect to the server and see the senders
@ -129,8 +145,10 @@ class Receiver:
notify.cancel() notify.cancel()
def on_slow_verification(): def on_slow_verification():
print(u"Key established, waiting for confirmation...", print(
file=self.args.stderr) u"Key established, waiting for confirmation...",
file=self.args.stderr)
notify = self._reactor.callLater(VERIFY_TIMER, on_slow_verification) notify = self._reactor.callLater(VERIFY_TIMER, on_slow_verification)
try: try:
# We wait here until we've seen their VERSION message (which they # We wait here until we've seen their VERSION message (which they
@ -155,7 +173,7 @@ class Receiver:
while True: while True:
them_d = yield self._get_data(w) them_d = yield self._get_data(w)
#print("GOT", them_d) # print("GOT", them_d)
recognized = False recognized = False
if u"transit" in them_d: if u"transit" in them_d:
recognized = True recognized = True
@ -172,7 +190,7 @@ class Receiver:
raise TransferError(r.response) raise TransferError(r.response)
returnValue(None) returnValue(None)
if not recognized: if not recognized:
log.msg("unrecognized message %r" % (them_d,)) log.msg("unrecognized message %r" % (them_d, ))
def _send_data(self, data, w): def _send_data(self, data, w):
data_bytes = dict_to_bytes(data) data_bytes = dict_to_bytes(data)
@ -197,12 +215,12 @@ class Receiver:
w.set_code(code) w.set_code(code)
else: else:
prompt = "Enter receive wormhole code: " prompt = "Enter receive wormhole code: "
used_completion = yield input_with_completion(prompt, used_completion = yield input_with_completion(
w.input_code(), prompt, w.input_code(), self._reactor)
self._reactor)
if not used_completion: if not used_completion:
print(" (note: you can use <Tab> to complete words)", print(
file=self.args.stderr) " (note: you can use <Tab> to complete words)",
file=self.args.stderr)
yield w.get_code() yield w.get_code()
def _show_verifier(self, verifier_bytes): def _show_verifier(self, verifier_bytes):
@ -220,21 +238,24 @@ class Receiver:
@inlineCallbacks @inlineCallbacks
def _build_transit(self, w, sender_transit): def _build_transit(self, w, sender_transit):
tr = TransitReceiver(self.args.transit_helper, tr = TransitReceiver(
no_listen=(not self.args.listen), self.args.transit_helper,
tor=self._tor, no_listen=(not self.args.listen),
reactor=self._reactor, tor=self._tor,
timing=self.args.timing) reactor=self._reactor,
timing=self.args.timing)
self._transit_receiver = tr self._transit_receiver = tr
transit_key = w.derive_key(APPID+u"/transit-key", tr.TRANSIT_KEY_LENGTH) transit_key = w.derive_key(APPID + u"/transit-key",
tr.TRANSIT_KEY_LENGTH)
tr.set_transit_key(transit_key) tr.set_transit_key(transit_key)
tr.add_connection_hints(sender_transit.get("hints-v1", [])) tr.add_connection_hints(sender_transit.get("hints-v1", []))
receiver_abilities = tr.get_connection_abilities() receiver_abilities = tr.get_connection_abilities()
receiver_hints = yield tr.get_connection_hints() receiver_hints = yield tr.get_connection_hints()
receiver_transit = {"abilities-v1": receiver_abilities, receiver_transit = {
"hints-v1": receiver_hints, "abilities-v1": receiver_abilities,
} "hints-v1": receiver_hints,
}
self._send_data({u"transit": receiver_transit}, w) self._send_data({u"transit": receiver_transit}, w)
# TODO: send more hints as the TransitReceiver produces them # TODO: send more hints as the TransitReceiver produces them
@ -260,7 +281,7 @@ class Receiver:
yield self._close_transit(rp, datahash) yield self._close_transit(rp, datahash)
else: else:
self._msg(u"I don't know what they're offering\n") self._msg(u"I don't know what they're offering\n")
self._msg(u"Offer details: %r" % (them_d,)) self._msg(u"Offer details: %r" % (them_d, ))
raise RespondError("unknown offer type") raise RespondError("unknown offer type")
def _handle_text(self, them_d, w): def _handle_text(self, them_d, w):
@ -276,12 +297,13 @@ class Receiver:
self.xfersize = file_data["filesize"] self.xfersize = file_data["filesize"]
free = estimate_free_space(self.abs_destname) free = estimate_free_space(self.abs_destname)
if free is not None and free < self.xfersize: if free is not None and free < self.xfersize:
self._msg(u"Error: insufficient free space (%sB) for file (%sB)" self._msg(u"Error: insufficient free space (%sB) for file (%sB)" %
% (free, self.xfersize)) (free, self.xfersize))
raise TransferRejectedError() raise TransferRejectedError()
self._msg(u"Receiving file (%s) into: %s" % self._msg(u"Receiving file (%s) into: %s" %
(naturalsize(self.xfersize), os.path.basename(self.abs_destname))) (naturalsize(self.xfersize),
os.path.basename(self.abs_destname)))
self._ask_permission() self._ask_permission()
tmp_destname = self.abs_destname + ".tmp" tmp_destname = self.abs_destname + ".tmp"
return open(tmp_destname, "wb") return open(tmp_destname, "wb")
@ -290,19 +312,22 @@ class Receiver:
file_data = them_d["directory"] file_data = them_d["directory"]
zipmode = file_data["mode"] zipmode = file_data["mode"]
if zipmode != "zipfile/deflated": if zipmode != "zipfile/deflated":
self._msg(u"Error: unknown directory-transfer mode '%s'" % (zipmode,)) self._msg(u"Error: unknown directory-transfer mode '%s'" %
(zipmode, ))
raise RespondError("unknown mode") raise RespondError("unknown mode")
self.abs_destname = self._decide_destname("directory", self.abs_destname = self._decide_destname("directory",
file_data["dirname"]) file_data["dirname"])
self.xfersize = file_data["zipsize"] self.xfersize = file_data["zipsize"]
free = estimate_free_space(self.abs_destname) free = estimate_free_space(self.abs_destname)
if free is not None and free < file_data["numbytes"]: if free is not None and free < file_data["numbytes"]:
self._msg(u"Error: insufficient free space (%sB) for directory (%sB)" self._msg(
% (free, file_data["numbytes"])) u"Error: insufficient free space (%sB) for directory (%sB)" %
(free, file_data["numbytes"]))
raise TransferRejectedError() raise TransferRejectedError()
self._msg(u"Receiving directory (%s) into: %s/" % self._msg(u"Receiving directory (%s) into: %s/" %
(naturalsize(self.xfersize), os.path.basename(self.abs_destname))) (naturalsize(self.xfersize),
os.path.basename(self.abs_destname)))
self._msg(u"%d files, %s (uncompressed)" % self._msg(u"%d files, %s (uncompressed)" %
(file_data["numfiles"], naturalsize(file_data["numbytes"]))) (file_data["numfiles"], naturalsize(file_data["numbytes"])))
self._ask_permission() self._ask_permission()
@ -313,23 +338,26 @@ class Receiver:
# "~/.ssh/authorized_keys" and other attacks # "~/.ssh/authorized_keys" and other attacks
destname = os.path.basename(destname) destname = os.path.basename(destname)
if self.args.output_file: if self.args.output_file:
destname = self.args.output_file # override destname = self.args.output_file # override
abs_destname = os.path.abspath( os.path.join(self.args.cwd, destname) ) abs_destname = os.path.abspath(os.path.join(self.args.cwd, destname))
# get confirmation from the user before writing to the local directory # get confirmation from the user before writing to the local directory
if os.path.exists(abs_destname): if os.path.exists(abs_destname):
if self.args.output_file: # overwrite is intentional if self.args.output_file: # overwrite is intentional
self._msg(u"Overwriting '%s'" % destname) self._msg(u"Overwriting '%s'" % destname)
if self.args.accept_file: if self.args.accept_file:
self._remove_existing(abs_destname) self._remove_existing(abs_destname)
else: else:
self._msg(u"Error: refusing to overwrite existing '%s'" % destname) self._msg(
u"Error: refusing to overwrite existing '%s'" % destname)
raise TransferRejectedError() raise TransferRejectedError()
return abs_destname return abs_destname
def _remove_existing(self, path): def _remove_existing(self, path):
if os.path.isfile(path): os.remove(path) if os.path.isfile(path):
if os.path.isdir(path): shutil.rmtree(path) os.remove(path)
if os.path.isdir(path):
shutil.rmtree(path)
def _ask_permission(self): def _ask_permission(self):
with self.args.timing.add("permission", waiting="user") as t: with self.args.timing.add("permission", waiting="user") as t:
@ -345,7 +373,7 @@ class Receiver:
t.detail(answer="yes") t.detail(answer="yes")
def _send_permission(self, w): def _send_permission(self, w):
self._send_data({"answer": { "file_ack": "ok" }}, w) self._send_data({"answer": {"file_ack": "ok"}}, w)
@inlineCallbacks @inlineCallbacks
def _establish_transit(self): def _establish_transit(self):
@ -359,14 +387,16 @@ class Receiver:
self._msg(u"Receiving (%s).." % record_pipe.describe()) self._msg(u"Receiving (%s).." % record_pipe.describe())
with self.args.timing.add("rx file"): with self.args.timing.add("rx file"):
progress = tqdm(file=self.args.stderr, progress = tqdm(
disable=self.args.hide_progress, file=self.args.stderr,
unit="B", unit_scale=True, total=self.xfersize) disable=self.args.hide_progress,
unit="B",
unit_scale=True,
total=self.xfersize)
hasher = hashlib.sha256() hasher = hashlib.sha256()
with progress: with progress:
received = yield record_pipe.writeToFile(f, self.xfersize, received = yield record_pipe.writeToFile(
progress.update, f, self.xfersize, progress.update, hasher.update)
hasher.update)
datahash = hasher.digest() datahash = hasher.digest()
# except TransitError # except TransitError
@ -382,25 +412,26 @@ class Receiver:
tmp_name = f.name tmp_name = f.name
f.close() f.close()
os.rename(tmp_name, self.abs_destname) os.rename(tmp_name, self.abs_destname)
self._msg(u"Received file written to %s" % self._msg(u"Received file written to %s" % os.path.basename(
os.path.basename(self.abs_destname)) self.abs_destname))
def _extract_file(self, zf, info, extract_dir): def _extract_file(self, zf, info, extract_dir):
""" """
the zipfile module does not restore file permissions the zipfile module does not restore file permissions
so we'll do it manually so we'll do it manually
""" """
out_path = os.path.join( extract_dir, info.filename ) out_path = os.path.join(extract_dir, info.filename)
out_path = os.path.abspath( out_path ) out_path = os.path.abspath(out_path)
if not out_path.startswith( extract_dir ): if not out_path.startswith(extract_dir):
raise ValueError( "malicious zipfile, %s outside of extract_dir %s" raise ValueError(
% (info.filename, extract_dir) ) "malicious zipfile, %s outside of extract_dir %s" %
(info.filename, extract_dir))
zf.extract( info.filename, path=extract_dir ) zf.extract(info.filename, path=extract_dir)
# not sure why zipfiles store the perms 16 bits away but they do # not sure why zipfiles store the perms 16 bits away but they do
perm = info.external_attr >> 16 perm = info.external_attr >> 16
os.chmod( out_path, perm ) os.chmod(out_path, perm)
def _write_directory(self, f): def _write_directory(self, f):
@ -408,10 +439,10 @@ class Receiver:
with self.args.timing.add("unpack zip"): with self.args.timing.add("unpack zip"):
with zipfile.ZipFile(f, "r", zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(f, "r", zipfile.ZIP_DEFLATED) as zf:
for info in zf.infolist(): for info in zf.infolist():
self._extract_file( zf, info, self.abs_destname ) self._extract_file(zf, info, self.abs_destname)
self._msg(u"Received files written to %s/" % self._msg(u"Received files written to %s/" % os.path.basename(
os.path.basename(self.abs_destname)) self.abs_destname))
f.close() f.close()
@inlineCallbacks @inlineCallbacks

View File

@ -1,20 +1,29 @@
from __future__ import print_function from __future__ import print_function
import os, sys, six, tempfile, zipfile, hashlib
from tqdm import tqdm import hashlib
import os
import sys
import tempfile
import zipfile
import six
from humanize import naturalsize from humanize import naturalsize
from twisted.python import log from tqdm import tqdm
from twisted.protocols import basic
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.protocols import basic
from twisted.python import log
from wormhole import __version__, create
from ..errors import TransferError, UnsendableFileError from ..errors import TransferError, UnsendableFileError
from wormhole import create, __version__
from ..transit import TransitSender from ..transit import TransitSender
from ..util import dict_to_bytes, bytes_to_dict, bytes_to_hexstr from ..util import bytes_to_dict, bytes_to_hexstr, dict_to_bytes
from .welcome import handle_welcome from .welcome import handle_welcome
APPID = u"lothar.com/wormhole/text-or-file-xfer" APPID = u"lothar.com/wormhole/text-or-file-xfer"
VERIFY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_VERIFY_TIMER", 1.0)) VERIFY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_VERIFY_TIMER", 1.0))
def send(args, reactor=reactor): def send(args, reactor=reactor):
"""I implement 'wormhole send'. I return a Deferred that fires with None """I implement 'wormhole send'. I return a Deferred that fires with None
(for success), or signals one of the following errors: (for success), or signals one of the following errors:
@ -26,6 +35,7 @@ def send(args, reactor=reactor):
""" """
return Sender(args, reactor).go() return Sender(args, reactor).go()
class Sender: class Sender:
def __init__(self, args, reactor): def __init__(self, args, reactor):
self._args = args self._args = args
@ -45,22 +55,25 @@ class Sender:
# tor in parallel with everything else, make sure the Tor object # tor in parallel with everything else, make sure the Tor object
# can lazy-provide an endpoint, and overlap the startup process # can lazy-provide an endpoint, and overlap the startup process
# with the user handing off the wormhole code # with the user handing off the wormhole code
self._tor = yield get_tor(reactor, self._tor = yield get_tor(
self._args.launch_tor, reactor,
self._args.tor_control_port, self._args.launch_tor,
timing=self._timing) self._args.tor_control_port,
timing=self._timing)
w = create(self._args.appid or APPID, self._args.relay_url, w = create(
self._reactor, self._args.appid or APPID,
tor=self._tor, self._args.relay_url,
timing=self._timing) self._reactor,
tor=self._tor,
timing=self._timing)
d = self._go(w) d = self._go(w)
# if we succeed, we should close and return the w.close results # if we succeed, we should close and return the w.close results
# (which might be an error) # (which might be an error)
@inlineCallbacks @inlineCallbacks
def _good(res): def _good(res):
yield w.close() # wait for ack yield w.close() # wait for ack
returnValue(res) returnValue(res)
# if we raise an error, we should close and then return the original # if we raise an error, we should close and then return the original
@ -69,8 +82,8 @@ class Sender:
@inlineCallbacks @inlineCallbacks
def _bad(f): def _bad(f):
try: try:
yield w.close() # might be an error too yield w.close() # might be an error too
except: except Exception:
pass pass
returnValue(f) returnValue(f)
@ -125,8 +138,10 @@ class Sender:
# TODO: don't stall on w.get_verifier() unless they want it # TODO: don't stall on w.get_verifier() unless they want it
def on_slow_connection(): def on_slow_connection():
print(u"Key established, waiting for confirmation...", print(
file=args.stderr) 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)
try: try:
# The usual sender-chooses-code sequence means the receiver's # The usual sender-chooses-code sequence means the receiver's
@ -137,32 +152,35 @@ class Sender:
# sitting here for a while, so printing the "waiting" message # sitting here for a while, so printing the "waiting" message
# seems like a good idea. It might even be appropriate to give up # seems like a good idea. It might even be appropriate to give up
# after a while. # after a while.
verifier_bytes = yield w.get_verifier() # might WrongPasswordError verifier_bytes = yield w.get_verifier() # might WrongPasswordError
finally: finally:
if not notify.called: if not notify.called:
notify.cancel() notify.cancel()
if args.verify: if args.verify:
self._check_verifier(w, verifier_bytes) # blocks, can TransferError self._check_verifier(w,
verifier_bytes) # blocks, can TransferError
if self._fd_to_send: if self._fd_to_send:
ts = TransitSender(args.transit_helper, ts = TransitSender(
no_listen=(not args.listen), args.transit_helper,
tor=self._tor, no_listen=(not args.listen),
reactor=self._reactor, tor=self._tor,
timing=self._timing) reactor=self._reactor,
timing=self._timing)
self._transit_sender = ts self._transit_sender = ts
# for now, send this before the main offer # for now, send this before the main offer
sender_abilities = ts.get_connection_abilities() sender_abilities = ts.get_connection_abilities()
sender_hints = yield ts.get_connection_hints() sender_hints = yield ts.get_connection_hints()
sender_transit = {"abilities-v1": sender_abilities, sender_transit = {
"hints-v1": sender_hints, "abilities-v1": sender_abilities,
} "hints-v1": sender_hints,
}
self._send_data({u"transit": sender_transit}, w) self._send_data({u"transit": sender_transit}, w)
# TODO: move this down below w.get_message() # TODO: move this down below w.get_message()
transit_key = w.derive_key(APPID+"/transit-key", transit_key = w.derive_key(APPID + "/transit-key",
ts.TRANSIT_KEY_LENGTH) ts.TRANSIT_KEY_LENGTH)
ts.set_transit_key(transit_key) ts.set_transit_key(transit_key)
@ -175,11 +193,11 @@ class Sender:
# TODO: get_message() fired, so get_verifier must have fired, so # TODO: get_message() fired, so get_verifier must have fired, so
# now it's safe to use w.derive_key() # now it's safe to use w.derive_key()
them_d = bytes_to_dict(them_d_bytes) them_d = bytes_to_dict(them_d_bytes)
#print("GOT", them_d) # print("GOT", them_d)
recognized = False recognized = False
if u"error" in them_d: if u"error" in them_d:
raise TransferError("remote error, transfer abandoned: %s" raise TransferError(
% them_d["error"]) "remote error, transfer abandoned: %s" % them_d["error"])
if u"transit" in them_d: if u"transit" in them_d:
recognized = True recognized = True
yield self._handle_transit(them_d[u"transit"]) yield self._handle_transit(them_d[u"transit"])
@ -191,7 +209,7 @@ class Sender:
yield self._handle_answer(them_d[u"answer"]) yield self._handle_answer(them_d[u"answer"])
returnValue(None) returnValue(None)
if not recognized: if not recognized:
log.msg("unrecognized message %r" % (them_d,)) log.msg("unrecognized message %r" % (them_d, ))
def _check_verifier(self, w, verifier_bytes): def _check_verifier(self, w, verifier_bytes):
verifier = bytes_to_hexstr(verifier_bytes) verifier = bytes_to_hexstr(verifier_bytes)
@ -221,9 +239,10 @@ class Sender:
text = six.moves.input("Text to send: ") text = six.moves.input("Text to send: ")
if text is not None: if text is not None:
print(u"Sending text message (%s)" % naturalsize(len(text)), print(
file=args.stderr) u"Sending text message (%s)" % naturalsize(len(text)),
offer = { "message": text } file=args.stderr)
offer = {"message": text}
fd_to_send = None fd_to_send = None
return offer, fd_to_send return offer, fd_to_send
@ -244,7 +263,7 @@ class Sender:
# is a symlink to something with a different name. The normpath() is # is a symlink to something with a different name. The normpath() is
# there to remove trailing slashes. # there to remove trailing slashes.
basename = os.path.basename(os.path.normpath(what)) basename = os.path.basename(os.path.normpath(what))
assert basename != "", what # normpath shouldn't allow this assert basename != "", what # normpath shouldn't allow this
# We use realpath() instead of normpath() to locate the actual # We use realpath() instead of normpath() to locate the actual
# file/directory, because the path might contain symlinks, and # file/directory, because the path might contain symlinks, and
@ -273,8 +292,8 @@ class Sender:
what = os.path.realpath(what) what = os.path.realpath(what)
if not os.path.exists(what): if not os.path.exists(what):
raise TransferError("Cannot send: no file/directory named '%s'" % raise TransferError(
args.what) "Cannot send: no file/directory named '%s'" % args.what)
if os.path.isfile(what): if os.path.isfile(what):
# we're sending a file # we're sending a file
@ -282,10 +301,11 @@ class Sender:
offer["file"] = { offer["file"] = {
"filename": basename, "filename": basename,
"filesize": filesize, "filesize": filesize,
} }
print(u"Sending %s file named '%s'" print(
% (naturalsize(filesize), basename), u"Sending %s file named '%s'" % (naturalsize(filesize),
file=args.stderr) basename),
file=args.stderr)
fd_to_send = open(what, "rb") fd_to_send = open(what, "rb")
return offer, fd_to_send return offer, fd_to_send
@ -297,16 +317,18 @@ class Sender:
num_files = 0 num_files = 0
num_bytes = 0 num_bytes = 0
tostrip = len(what.split(os.sep)) tostrip = len(what.split(os.sep))
with zipfile.ZipFile(fd_to_send, "w", with zipfile.ZipFile(
compression=zipfile.ZIP_DEFLATED, fd_to_send,
allowZip64=True) as zf: "w",
for path,dirs,files in os.walk(what): compression=zipfile.ZIP_DEFLATED,
allowZip64=True) as zf:
for path, dirs, files in os.walk(what):
# path always starts with args.what, then sometimes might # path always starts with args.what, then sometimes might
# have "/subdir" appended. We want the zipfile to contain # have "/subdir" appended. We want the zipfile to contain
# "" or "subdir" # "" or "subdir"
localpath = list(path.split(os.sep)[tostrip:]) localpath = list(path.split(os.sep)[tostrip:])
for fn in files: for fn in files:
archivename = os.path.join(*tuple(localpath+[fn])) archivename = os.path.join(*tuple(localpath + [fn]))
localfilename = os.path.join(path, fn) localfilename = os.path.join(path, fn)
try: try:
zf.write(localfilename, archivename) zf.write(localfilename, archivename)
@ -315,22 +337,25 @@ class Sender:
except OSError as e: except OSError as e:
errmsg = u"{}: {}".format(fn, e.strerror) errmsg = u"{}: {}".format(fn, e.strerror)
if self._args.ignore_unsendable_files: if self._args.ignore_unsendable_files:
print(u"{} (ignoring error)".format(errmsg), print(
file=args.stderr) u"{} (ignoring error)".format(errmsg),
file=args.stderr)
else: else:
raise UnsendableFileError(errmsg) raise UnsendableFileError(errmsg)
fd_to_send.seek(0,2) fd_to_send.seek(0, 2)
filesize = fd_to_send.tell() filesize = fd_to_send.tell()
fd_to_send.seek(0,0) fd_to_send.seek(0, 0)
offer["directory"] = { offer["directory"] = {
"mode": "zipfile/deflated", "mode": "zipfile/deflated",
"dirname": basename, "dirname": basename,
"zipsize": filesize, "zipsize": filesize,
"numbytes": num_bytes, "numbytes": num_bytes,
"numfiles": num_files, "numfiles": num_files,
} }
print(u"Sending directory (%s compressed) named '%s'" print(
% (naturalsize(filesize), basename), file=args.stderr) u"Sending directory (%s compressed) named '%s'" %
(naturalsize(filesize), basename),
file=args.stderr)
return offer, fd_to_send return offer, fd_to_send
raise TypeError("'%s' is neither file nor directory" % args.what) raise TypeError("'%s' is neither file nor directory" % args.what)
@ -340,23 +365,22 @@ class Sender:
if self._fd_to_send is None: if self._fd_to_send is None:
if them_answer["message_ack"] == "ok": if them_answer["message_ack"] == "ok":
print(u"text message sent", file=self._args.stderr) print(u"text message sent", file=self._args.stderr)
returnValue(None) # terminates this function returnValue(None) # terminates this function
raise TransferError("error sending text: %r" % (them_answer,)) raise TransferError("error sending text: %r" % (them_answer, ))
if them_answer.get("file_ack") != "ok": if them_answer.get("file_ack") != "ok":
raise TransferError("ambiguous response from remote, " raise TransferError("ambiguous response from remote, "
"transfer abandoned: %s" % (them_answer,)) "transfer abandoned: %s" % (them_answer, ))
yield self._send_file() yield self._send_file()
@inlineCallbacks @inlineCallbacks
def _send_file(self): def _send_file(self):
ts = self._transit_sender ts = self._transit_sender
self._fd_to_send.seek(0,2) self._fd_to_send.seek(0, 2)
filesize = self._fd_to_send.tell() filesize = self._fd_to_send.tell()
self._fd_to_send.seek(0,0) self._fd_to_send.seek(0, 0)
record_pipe = yield ts.connect() record_pipe = yield ts.connect()
self._timing.add("transit connected") self._timing.add("transit connected")
@ -365,21 +389,28 @@ class Sender:
print(u"Sending (%s).." % record_pipe.describe(), file=stderr) print(u"Sending (%s).." % record_pipe.describe(), file=stderr)
hasher = hashlib.sha256() hasher = hashlib.sha256()
progress = tqdm(file=stderr, disable=self._args.hide_progress, progress = tqdm(
unit="B", unit_scale=True, file=stderr,
total=filesize) disable=self._args.hide_progress,
unit="B",
unit_scale=True,
total=filesize)
def _count_and_hash(data): def _count_and_hash(data):
hasher.update(data) hasher.update(data)
progress.update(len(data)) progress.update(len(data))
return data return data
fs = basic.FileSender() fs = basic.FileSender()
with self._timing.add("tx file"): with self._timing.add("tx file"):
with progress: with progress:
if filesize: if filesize:
# don't send zero-length files # don't send zero-length files
yield fs.beginFileTransfer(self._fd_to_send, record_pipe, yield fs.beginFileTransfer(
transform=_count_and_hash) self._fd_to_send,
record_pipe,
transform=_count_and_hash)
expected_hash = hasher.digest() expected_hash = hasher.digest()
expected_hex = bytes_to_hexstr(expected_hash) expected_hex = bytes_to_hexstr(expected_hash)

View File

@ -1,16 +1,19 @@
from __future__ import print_function from __future__ import print_function
import os import os
from os.path import expanduser, exists, join from os.path import exists, expanduser, join
from twisted.internet.defer import inlineCallbacks
from twisted.internet import reactor
import click import click
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks
from .. import xfer_util from .. import xfer_util
class PubkeyError(Exception): class PubkeyError(Exception):
pass pass
def find_public_key(hint=None): def find_public_key(hint=None):
""" """
This looks for an appropriate SSH key to send, possibly querying This looks for an appropriate SSH key to send, possibly querying
@ -34,8 +37,9 @@ def find_public_key(hint=None):
got_key = False got_key = False
while not got_key: while not got_key:
ans = click.prompt( ans = click.prompt(
"Multiple public-keys found:\n" + \ "Multiple public-keys found:\n" +
"\n".join([" {}: {}".format(a, b) for a, b in enumerate(pubkeys)]) + \ "\n".join([" {}: {}".format(a, b)
for a, b in enumerate(pubkeys)]) +
"\nSend which one?" "\nSend which one?"
) )
try: try:
@ -76,7 +80,6 @@ def accept(cfg, reactor=reactor):
@inlineCallbacks @inlineCallbacks
def invite(cfg, reactor=reactor): def invite(cfg, reactor=reactor):
def on_code_created(code): def on_code_created(code):
print("Now tell the other user to run:") print("Now tell the other user to run:")
print() print()

View File

@ -1,4 +1,3 @@
# This is a relay I run on a personal server. If it gets too expensive to # This is a relay I run on a personal server. If it gets too expensive to
# run, I'll shut it down. # run, I'll shut it down.
RENDEZVOUS_RELAY = u"ws://relay.magic-wormhole.io:4000/v1" RENDEZVOUS_RELAY = u"ws://relay.magic-wormhole.io:4000/v1"

View File

@ -1,18 +1,23 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
def handle_welcome(welcome, relay_url, my_version, stderr): def handle_welcome(welcome, relay_url, my_version, stderr):
if "motd" in welcome: if "motd" in welcome:
motd_lines = welcome["motd"].splitlines() motd_lines = welcome["motd"].splitlines()
motd_formatted = "\n ".join(motd_lines) motd_formatted = "\n ".join(motd_lines)
print("Server (at %s) says:\n %s" % (relay_url, motd_formatted), print(
file=stderr) "Server (at %s) says:\n %s" % (relay_url, motd_formatted),
file=stderr)
# Only warn if we're running a release version (e.g. 0.0.6, not # 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 if ("current_cli_version" in welcome and "+" not in my_version
and "+" not in my_version and welcome["current_cli_version"] != my_version):
and welcome["current_cli_version"] != my_version): print(
print("Warning: errors may occur unless both sides are running the same version", file=stderr) ("Warning: errors may occur unless both sides are running the"
print("Server claims %s is current, but ours is %s" " same version"),
% (welcome["current_cli_version"], my_version), file=stderr)
file=stderr) print(
"Server claims %s is current, but ours is %s" %
(welcome["current_cli_version"], my_version),
file=stderr)

View File

@ -1,8 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
class WormholeError(Exception): class WormholeError(Exception):
"""Parent class for all wormhole-related errors""" """Parent class for all wormhole-related errors"""
class UnsendableFileError(Exception): class UnsendableFileError(Exception):
""" """
A file you wanted to send couldn't be read, maybe because it's not A file you wanted to send couldn't be read, maybe because it's not
@ -13,30 +15,38 @@ class UnsendableFileError(Exception):
--ignore-unsendable-files flag. --ignore-unsendable-files flag.
""" """
class ServerError(WormholeError): class ServerError(WormholeError):
"""The relay server complained about something we did.""" """The relay server complained about something we did."""
class ServerConnectionError(WormholeError): class ServerConnectionError(WormholeError):
"""We had a problem connecting to the relay server:""" """We had a problem connecting to the relay server:"""
def __init__(self, url, reason): def __init__(self, url, reason):
self.url = url self.url = url
self.reason = reason self.reason = reason
def __str__(self): def __str__(self):
return str(self.reason) return str(self.reason)
class Timeout(WormholeError): class Timeout(WormholeError):
pass pass
class WelcomeError(WormholeError): class WelcomeError(WormholeError):
""" """
The relay server told us to signal an error, probably because our version The relay server told us to signal an error, probably because our version
is too old to possibly work. The server said:""" is too old to possibly work. The server said:"""
pass pass
class LonelyError(WormholeError): class LonelyError(WormholeError):
"""wormhole.close() was called before the peer connection could be """wormhole.close() was called before the peer connection could be
established""" established"""
class WrongPasswordError(WormholeError): class WrongPasswordError(WormholeError):
""" """
Key confirmation failed. Either you or your correspondent typed the code Key confirmation failed. Either you or your correspondent typed the code
@ -47,6 +57,7 @@ class WrongPasswordError(WormholeError):
# or the data blob was corrupted, and that's why decrypt failed # or the data blob was corrupted, and that's why decrypt failed
pass pass
class KeyFormatError(WormholeError): class KeyFormatError(WormholeError):
""" """
The key you entered contains spaces or was missing a dash. Magic-wormhole The key you entered contains spaces or was missing a dash. Magic-wormhole
@ -55,43 +66,61 @@ class KeyFormatError(WormholeError):
dashes. dashes.
""" """
class ReflectionAttack(WormholeError): class ReflectionAttack(WormholeError):
"""An attacker (or bug) reflected our outgoing message back to us.""" """An attacker (or bug) reflected our outgoing message back to us."""
class InternalError(WormholeError): class InternalError(WormholeError):
"""The programmer did something wrong.""" """The programmer did something wrong."""
class TransferError(WormholeError): class TransferError(WormholeError):
"""Something bad happened and the transfer failed.""" """Something bad happened and the transfer failed."""
class NoTorError(WormholeError): class NoTorError(WormholeError):
"""--tor was requested, but 'txtorcon' is not installed.""" """--tor was requested, but 'txtorcon' is not installed."""
class NoKeyError(WormholeError): class NoKeyError(WormholeError):
"""w.derive_key() was called before got_verifier() fired""" """w.derive_key() was called before got_verifier() fired"""
class OnlyOneCodeError(WormholeError): class OnlyOneCodeError(WormholeError):
"""Only one w.generate_code/w.set_code/w.input_code may be called""" """Only one w.generate_code/w.set_code/w.input_code may be called"""
class MustChooseNameplateFirstError(WormholeError): class MustChooseNameplateFirstError(WormholeError):
"""The InputHelper was asked to do get_word_completions() or """The InputHelper was asked to do get_word_completions() or
choose_words() before the nameplate was chosen.""" choose_words() before the nameplate was chosen."""
class AlreadyChoseNameplateError(WormholeError): class AlreadyChoseNameplateError(WormholeError):
"""The InputHelper was asked to do get_nameplate_completions() after """The InputHelper was asked to do get_nameplate_completions() after
choose_nameplate() was called, or choose_nameplate() was called a second choose_nameplate() was called, or choose_nameplate() was called a second
time.""" time."""
class AlreadyChoseWordsError(WormholeError): class AlreadyChoseWordsError(WormholeError):
"""The InputHelper was asked to do get_word_completions() after """The InputHelper was asked to do get_word_completions() after
choose_words() was called, or choose_words() was called a second time.""" choose_words() was called, or choose_words() was called a second time."""
class AlreadyInputNameplateError(WormholeError): class AlreadyInputNameplateError(WormholeError):
"""The CodeInputter was asked to do completion on a nameplate, when we """The CodeInputter was asked to do completion on a nameplate, when we
had already committed to a different one.""" had already committed to a different one."""
class WormholeClosed(Exception): class WormholeClosed(Exception):
"""Deferred-returning API calls errback with WormholeClosed if the """Deferred-returning API calls errback with WormholeClosed if the
wormhole was already closed, or if it closes before a real result can be wormhole was already closed, or if it closes before a real result can be
obtained.""" obtained."""
class _UnknownPhaseError(Exception): class _UnknownPhaseError(Exception):
"""internal exception type, for tests.""" """internal exception type, for tests."""
class _UnknownMessageTypeError(Exception): class _UnknownMessageTypeError(Exception):
"""internal exception type, for tests.""" """internal exception type, for tests."""

View File

@ -5,6 +5,7 @@ from twisted.internet.defer import Deferred
from twisted.internet.interfaces import IReactorTime from twisted.internet.interfaces import IReactorTime
from twisted.python import log from twisted.python import log
class EventualQueue(object): class EventualQueue(object):
def __init__(self, clock): def __init__(self, clock):
# pass clock=reactor unless you're testing # pass clock=reactor unless you're testing
@ -14,7 +15,7 @@ class EventualQueue(object):
self._timer = None self._timer = None
def eventually(self, f, *args, **kwargs): def eventually(self, f, *args, **kwargs):
self._calls.append( (f, args, kwargs) ) self._calls.append((f, args, kwargs))
if not self._timer: if not self._timer:
self._timer = self._clock.callLater(0, self._turn) self._timer = self._clock.callLater(0, self._turn)
@ -28,7 +29,7 @@ class EventualQueue(object):
(f, args, kwargs) = self._calls.pop(0) (f, args, kwargs) = self._calls.pop(0)
try: try:
f(*args, **kwargs) f(*args, **kwargs)
except: except Exception:
log.err() log.err()
self._timer = None self._timer = None
d, self._flush_d = self._flush_d, None d, self._flush_d = self._flush_d, None

View File

@ -1,27 +1,37 @@
# no unicode_literals # no unicode_literals
# Find all of our ip addresses. From tahoe's src/allmydata/util/iputil.py # Find all of our ip addresses. From tahoe's src/allmydata/util/iputil.py
import os, re, subprocess, errno import errno
import os
import re
import subprocess
from sys import platform from sys import platform
from twisted.python.procutils import which from twisted.python.procutils import which
# Wow, I'm really amazed at home much mileage we've gotten out of calling # Wow, I'm really amazed at home much mileage we've gotten out of calling
# the external route.exe program on windows... It appears to work on all # the external route.exe program on windows... It appears to work on all
# versions so far. Still, the real system calls would much be preferred... # versions so far. Still, the real system calls would much be preferred...
# ... thus wrote Greg Smith in time immemorial... # ... thus wrote Greg Smith in time immemorial...
_win32_re = re.compile(r'^\s*\d+\.\d+\.\d+\.\d+\s.+\s(?P<address>\d+\.\d+\.\d+\.\d+)\s+(?P<metric>\d+)\s*$', flags=re.M|re.I|re.S) _win32_re = re.compile(
_win32_commands = (('route.exe', ('print',), _win32_re),) (r'^\s*\d+\.\d+\.\d+\.\d+\s.+\s'
r'(?P<address>\d+\.\d+\.\d+\.\d+)\s+(?P<metric>\d+)\s*$'),
flags=re.M | re.I | re.S)
_win32_commands = (('route.exe', ('print', ), _win32_re), )
# These work in most Unices. # These work in most Unices.
_addr_re = re.compile(r'^\s*inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)[\s/].+$', flags=re.M|re.I|re.S) _addr_re = re.compile(
_unix_commands = (('/bin/ip', ('addr',), _addr_re), r'^\s*inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)[\s/].+$',
('/sbin/ip', ('addr',), _addr_re), flags=re.M | re.I | re.S)
('/sbin/ifconfig', ('-a',), _addr_re), _unix_commands = (
('/usr/sbin/ifconfig', ('-a',), _addr_re), ('/bin/ip', ('addr', ), _addr_re),
('/usr/etc/ifconfig', ('-a',), _addr_re), ('/sbin/ip', ('addr', ), _addr_re),
('ifconfig', ('-a',), _addr_re), ('/sbin/ifconfig', ('-a', ), _addr_re),
('/sbin/ifconfig', (), _addr_re), ('/usr/sbin/ifconfig', ('-a', ), _addr_re),
) ('/usr/etc/ifconfig', ('-a', ), _addr_re),
('ifconfig', ('-a', ), _addr_re),
('/sbin/ifconfig', (), _addr_re),
)
def find_addresses(): def find_addresses():
@ -54,17 +64,19 @@ def find_addresses():
return ["127.0.0.1"] return ["127.0.0.1"]
def _query(path, args, regex): def _query(path, args, regex):
env = {'LANG': 'en_US.UTF-8'} env = {'LANG': 'en_US.UTF-8'}
trial = 0 trial = 0
while True: while True:
trial += 1 trial += 1
try: try:
p = subprocess.Popen([path] + list(args), p = subprocess.Popen(
stdout=subprocess.PIPE, [path] + list(args),
stderr=subprocess.PIPE, stdout=subprocess.PIPE,
env=env, stderr=subprocess.PIPE,
universal_newlines=True) env=env,
universal_newlines=True)
(output, err) = p.communicate() (output, err) = p.communicate()
break break
except OSError as e: except OSError as e:

View File

@ -1,8 +1,12 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from zope.interface import implementer
import contextlib import contextlib
from zope.interface import implementer
from ._interfaces import IJournal from ._interfaces import IJournal
@implementer(IJournal) @implementer(IJournal)
class Journal(object): class Journal(object):
def __init__(self, save_checkpoint): def __init__(self, save_checkpoint):
@ -19,7 +23,7 @@ class Journal(object):
assert not self._processing assert not self._processing
assert not self._outbound_queue assert not self._outbound_queue
self._processing = True self._processing = True
yield # process inbound messages, change state, queue outbound yield # process inbound messages, change state, queue outbound
self._save_checkpoint() self._save_checkpoint()
for (fn, args, kwargs) in self._outbound_queue: for (fn, args, kwargs) in self._outbound_queue:
fn(*args, **kwargs) fn(*args, **kwargs)
@ -31,8 +35,10 @@ class Journal(object):
class ImmediateJournal(object): class ImmediateJournal(object):
def __init__(self): def __init__(self):
pass pass
def queue_outbound(self, fn, *args, **kwargs): def queue_outbound(self, fn, *args, **kwargs):
fn(*args, **kwargs) fn(*args, **kwargs)
@contextlib.contextmanager @contextlib.contextmanager
def process(self): def process(self):
yield yield

View File

@ -1,14 +1,16 @@
from __future__ import unicode_literals, print_function from __future__ import print_function, unicode_literals
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from twisted.python.failure import Failure from twisted.python.failure import Failure
NoResult = object() NoResult = object()
class OneShotObserver(object): class OneShotObserver(object):
def __init__(self, eventual_queue): def __init__(self, eventual_queue):
self._eq = eventual_queue self._eq = eventual_queue
self._result = NoResult self._result = NoResult
self._observers = [] # list of Deferreds self._observers = [] # list of Deferreds
def when_fired(self): def when_fired(self):
d = Deferred() d = Deferred()
@ -38,6 +40,7 @@ class OneShotObserver(object):
if self._result is NoResult: if self._result is NoResult:
self.fire(result) self.fire(result)
class SequenceObserver(object): class SequenceObserver(object):
def __init__(self, eventual_queue): def __init__(self, eventual_queue):
self._eq = eventual_queue self._eq = eventual_queue

View File

@ -1,16 +1,19 @@
# no unicode_literals untill twisted update # no unicode_literals untill twisted update
from twisted.application import service, internet
from twisted.internet import defer, task, reactor, endpoints
from twisted.python import log
from click.testing import CliRunner from click.testing import CliRunner
from twisted.application import internet, service
from twisted.internet import defer, endpoints, reactor, task
from twisted.python import log
import mock import mock
from ..cli import cli from wormhole_mailbox_server.database import create_channel_db, create_usage_db
from ..transit import allocate_tcp_port
from wormhole_mailbox_server.server import make_server from wormhole_mailbox_server.server import make_server
from wormhole_mailbox_server.web import make_web_server from wormhole_mailbox_server.web import make_web_server
from wormhole_mailbox_server.database import create_channel_db, create_usage_db
from wormhole_transit_relay.transit_server import Transit from wormhole_transit_relay.transit_server import Transit
from ..cli import cli
from ..transit import allocate_tcp_port
class MyInternetService(service.Service, object): class MyInternetService(service.Service, object):
# like StreamServerEndpointService, but you can retrieve the port # like StreamServerEndpointService, but you can retrieve the port
def __init__(self, endpoint, factory): def __init__(self, endpoint, factory):
@ -22,12 +25,15 @@ class MyInternetService(service.Service, object):
def startService(self): def startService(self):
super(MyInternetService, self).startService() super(MyInternetService, self).startService()
d = self.endpoint.listen(self.factory) d = self.endpoint.listen(self.factory)
def good(lp): def good(lp):
self._lp = lp self._lp = lp
self._port_d.callback(lp.getHost().port) self._port_d.callback(lp.getHost().port)
def bad(f): def bad(f):
log.err(f) log.err(f)
self._port_d.errback(f) self._port_d.errback(f)
d.addCallbacks(good, bad) d.addCallbacks(good, bad)
@defer.inlineCallbacks @defer.inlineCallbacks
@ -35,9 +41,10 @@ class MyInternetService(service.Service, object):
if self._lp: if self._lp:
yield self._lp.stopListening() yield self._lp.stopListening()
def getPort(self): # only call once! def getPort(self): # only call once!
return self._port_d return self._port_d
class ServerBase: class ServerBase:
@defer.inlineCallbacks @defer.inlineCallbacks
def setUp(self): def setUp(self):
@ -51,27 +58,27 @@ class ServerBase:
# endpoints.serverFromString # endpoints.serverFromString
db = create_channel_db(":memory:") db = create_channel_db(":memory:")
self._usage_db = create_usage_db(":memory:") self._usage_db = create_usage_db(":memory:")
self._rendezvous = make_server(db, self._rendezvous = make_server(
advertise_version=advertise_version, db,
signal_error=error, advertise_version=advertise_version,
usage_db=self._usage_db) signal_error=error,
usage_db=self._usage_db)
ep = endpoints.TCP4ServerEndpoint(reactor, 0, interface="127.0.0.1") ep = endpoints.TCP4ServerEndpoint(reactor, 0, interface="127.0.0.1")
site = make_web_server(self._rendezvous, log_requests=False) site = make_web_server(self._rendezvous, log_requests=False)
#self._lp = yield ep.listen(site) # self._lp = yield ep.listen(site)
s = MyInternetService(ep, site) s = MyInternetService(ep, site)
s.setServiceParent(self.sp) s.setServiceParent(self.sp)
self.rdv_ws_port = yield s.getPort() self.rdv_ws_port = yield s.getPort()
self._relay_server = s self._relay_server = s
#self._rendezvous = s._rendezvous # self._rendezvous = s._rendezvous
self.relayurl = u"ws://127.0.0.1:%d/v1" % self.rdv_ws_port self.relayurl = u"ws://127.0.0.1:%d/v1" % self.rdv_ws_port
# ws://127.0.0.1:%d/wormhole-relay/ws # ws://127.0.0.1:%d/wormhole-relay/ws
self.transitport = allocate_tcp_port() self.transitport = allocate_tcp_port()
ep = endpoints.serverFromString(reactor, ep = endpoints.serverFromString(
"tcp:%d:interface=127.0.0.1" % reactor, "tcp:%d:interface=127.0.0.1" % self.transitport)
self.transitport) self._transit_server = f = Transit(
self._transit_server = f = Transit(blur_usage=None, log_file=None, blur_usage=None, log_file=None, usage_db=None)
usage_db=None)
internet.StreamServerEndpointService(ep, f).setServiceParent(self.sp) internet.StreamServerEndpointService(ep, f).setServiceParent(self.sp)
self.transit = u"tcp:127.0.0.1:%d" % self.transitport self.transit = u"tcp:127.0.0.1:%d" % self.transitport
@ -109,6 +116,7 @@ class ServerBase:
" I convinced all threads to exit.") " I convinced all threads to exit.")
yield d yield d
def config(*argv): def config(*argv):
r = CliRunner() r = CliRunner()
with mock.patch("wormhole.cli.cli.go") as go: with mock.patch("wormhole.cli.cli.go") as go:
@ -121,6 +129,7 @@ def config(*argv):
cfg = go.call_args[0][1] cfg = go.call_args[0][1]
return cfg return cfg
@defer.inlineCallbacks @defer.inlineCallbacks
def poll_until(predicate): def poll_until(predicate):
# return a Deferred that won't fire until the predicate is True # return a Deferred that won't fire until the predicate is True

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
# This is a tiny helper module, to let "python -m wormhole.test.run_trial # This is a tiny helper module, to let "python -m wormhole.test.run_trial
# ARGS" does the same thing as running "trial ARGS" (unfortunately # ARGS" does the same thing as running "trial ARGS" (unfortunately
# twisted/scripts/trial.py does not have a '__name__=="__main__"' clause). # twisted/scripts/trial.py does not have a '__name__=="__main__"' clause).

View File

@ -1,15 +1,17 @@
import os import os
import sys import sys
import mock
from twisted.trial import unittest from twisted.trial import unittest
import mock
from ..cli.public_relay import RENDEZVOUS_RELAY, TRANSIT_RELAY from ..cli.public_relay import RENDEZVOUS_RELAY, TRANSIT_RELAY
from .common import config from .common import config
#from pprint import pprint
class Send(unittest.TestCase): class Send(unittest.TestCase):
def test_baseline(self): def test_baseline(self):
cfg = config("send", "--text", "hi") cfg = config("send", "--text", "hi")
#pprint(cfg.__dict__)
self.assertEqual(cfg.what, None) self.assertEqual(cfg.what, None)
self.assertEqual(cfg.code, None) self.assertEqual(cfg.code, None)
self.assertEqual(cfg.code_length, 2) self.assertEqual(cfg.code_length, 2)
@ -32,7 +34,6 @@ class Send(unittest.TestCase):
def test_file(self): def test_file(self):
cfg = config("send", "fn") cfg = config("send", "fn")
#pprint(cfg.__dict__)
self.assertEqual(cfg.what, u"fn") self.assertEqual(cfg.what, u"fn")
self.assertEqual(cfg.text, None) self.assertEqual(cfg.text, None)
@ -101,7 +102,6 @@ class Send(unittest.TestCase):
class Receive(unittest.TestCase): class Receive(unittest.TestCase):
def test_baseline(self): def test_baseline(self):
cfg = config("receive") cfg = config("receive")
#pprint(cfg.__dict__)
self.assertEqual(cfg.accept_file, False) self.assertEqual(cfg.accept_file, False)
self.assertEqual(cfg.code, None) self.assertEqual(cfg.code, None)
self.assertEqual(cfg.code_length, 2) self.assertEqual(cfg.code_length, 2)
@ -191,10 +191,12 @@ class Receive(unittest.TestCase):
cfg = config("--transit-helper", transit_url_2, "receive") cfg = config("--transit-helper", transit_url_2, "receive")
self.assertEqual(cfg.transit_helper, transit_url_2) self.assertEqual(cfg.transit_helper, transit_url_2)
class Config(unittest.TestCase): class Config(unittest.TestCase):
def test_send(self): def test_send(self):
cfg = config("send") cfg = config("send")
self.assertEqual(cfg.stdout, sys.stdout) self.assertEqual(cfg.stdout, sys.stdout)
def test_receive(self): def test_receive(self):
cfg = config("receive") cfg = config("receive")
self.assertEqual(cfg.stdout, sys.stdout) self.assertEqual(cfg.stdout, sys.stdout)

View File

@ -1,22 +1,32 @@
from __future__ import print_function from __future__ import print_function
import os, sys, re, io, zipfile, six, stat
from textwrap import fill, dedent import io
from humanize import naturalsize import os
import mock import re
import stat
import sys
import zipfile
from textwrap import dedent, fill
import six
from click.testing import CliRunner from click.testing import CliRunner
from zope.interface import implementer from humanize import naturalsize
from twisted.trial import unittest
from twisted.python import procutils, log
from twisted.internet import endpoints, reactor from twisted.internet import endpoints, reactor
from twisted.internet.utils import getProcessOutputAndValue
from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue
from twisted.internet.error import ConnectionRefusedError from twisted.internet.error import ConnectionRefusedError
from twisted.internet.utils import getProcessOutputAndValue
from twisted.python import log, procutils
from twisted.trial import unittest
from zope.interface import implementer
import mock
from .. import __version__ from .. import __version__
from .common import ServerBase, config
from ..cli import cmd_send, cmd_receive, welcome, cli
from ..errors import (TransferError, WrongPasswordError, WelcomeError,
UnsendableFileError, ServerConnectionError)
from .._interfaces import ITorManager from .._interfaces import ITorManager
from ..cli import cli, cmd_receive, cmd_send, welcome
from ..errors import (ServerConnectionError, TransferError,
UnsendableFileError, WelcomeError, WrongPasswordError)
from .common import ServerBase, config
def build_offer(args): def build_offer(args):
@ -108,8 +118,8 @@ class OfferData(unittest.TestCase):
self.cfg.cwd = send_dir self.cfg.cwd = send_dir
e = self.assertRaises(TransferError, build_offer, self.cfg) e = self.assertRaises(TransferError, build_offer, self.cfg)
self.assertEqual(str(e), self.assertEqual(
"Cannot send: no file/directory named '%s'" % filename) str(e), "Cannot send: no file/directory named '%s'" % filename)
def _do_test_directory(self, addslash): def _do_test_directory(self, addslash):
parent_dir = self.mktemp() parent_dir = self.mktemp()
@ -178,8 +188,8 @@ class OfferData(unittest.TestCase):
self.assertFalse(os.path.isdir(abs_filename)) self.assertFalse(os.path.isdir(abs_filename))
e = self.assertRaises(TypeError, build_offer, self.cfg) e = self.assertRaises(TypeError, build_offer, self.cfg)
self.assertEqual(str(e), self.assertEqual(
"'%s' is neither file nor directory" % filename) str(e), "'%s' is neither file nor directory" % filename)
def test_symlink(self): def test_symlink(self):
if not hasattr(os, 'symlink'): if not hasattr(os, 'symlink'):
@ -213,8 +223,9 @@ class OfferData(unittest.TestCase):
os.mkdir(os.path.join(parent_dir, "B2", "C2")) os.mkdir(os.path.join(parent_dir, "B2", "C2"))
with open(os.path.join(parent_dir, "B2", "D.txt"), "wb") as f: with open(os.path.join(parent_dir, "B2", "D.txt"), "wb") as f:
f.write(b"success") f.write(b"success")
os.symlink(os.path.abspath(os.path.join(parent_dir, "B2", "C2")), os.symlink(
os.path.join(parent_dir, "B1", "C1")) os.path.abspath(os.path.join(parent_dir, "B2", "C2")),
os.path.join(parent_dir, "B1", "C1"))
# Now send "B1/C1/../D.txt" from A. The correct traversal will be: # Now send "B1/C1/../D.txt" from A. The correct traversal will be:
# * start: A # * start: A
# * B1: A/B1 # * B1: A/B1
@ -231,6 +242,7 @@ class OfferData(unittest.TestCase):
d, fd_to_send = build_offer(self.cfg) d, fd_to_send = build_offer(self.cfg)
self.assertEqual(d["file"]["filename"], "D.txt") self.assertEqual(d["file"]["filename"], "D.txt")
self.assertEqual(fd_to_send.read(), b"success") self.assertEqual(fd_to_send.read(), b"success")
if os.name == "nt": if os.name == "nt":
test_symlink_collapse.todo = "host OS has broken os.path.realpath()" test_symlink_collapse.todo = "host OS has broken os.path.realpath()"
# ntpath.py's realpath() is built out of normpath(), and does not # ntpath.py's realpath() is built out of normpath(), and does not
@ -241,6 +253,7 @@ class OfferData(unittest.TestCase):
# misbehavior (albeit in rare circumstances), 2: it probably used to # misbehavior (albeit in rare circumstances), 2: it probably used to
# work (sometimes, but not in #251). See cmd_send.py for more notes. # work (sometimes, but not in #251). See cmd_send.py for more notes.
class LocaleFinder: class LocaleFinder:
def __init__(self): def __init__(self):
self._run_once = False self._run_once = False
@ -266,10 +279,10 @@ class LocaleFinder:
# twisted.python.usage to avoid this problem in the future. # twisted.python.usage to avoid this problem in the future.
(out, err, rc) = yield getProcessOutputAndValue("locale", ["-a"]) (out, err, rc) = yield getProcessOutputAndValue("locale", ["-a"])
if rc != 0: if rc != 0:
log.msg("error running 'locale -a', rc=%s" % (rc,)) log.msg("error running 'locale -a', rc=%s" % (rc, ))
log.msg("stderr: %s" % (err,)) log.msg("stderr: %s" % (err, ))
returnValue(None) returnValue(None)
out = out.decode("utf-8") # make sure we get a string out = out.decode("utf-8") # make sure we get a string
utf8_locales = {} utf8_locales = {}
for locale in out.splitlines(): for locale in out.splitlines():
locale = locale.strip() locale = locale.strip()
@ -281,8 +294,11 @@ class LocaleFinder:
if utf8_locales: if utf8_locales:
returnValue(list(utf8_locales.values())[0]) returnValue(list(utf8_locales.values())[0])
returnValue(None) returnValue(None)
locale_finder = LocaleFinder() locale_finder = LocaleFinder()
class ScriptsBase: class ScriptsBase:
def find_executable(self): def find_executable(self):
# to make sure we're running the right executable (in a virtualenv), # to make sure we're running the right executable (in a virtualenv),
@ -292,12 +308,13 @@ class ScriptsBase:
if not locations: if not locations:
raise unittest.SkipTest("unable to find 'wormhole' in $PATH") raise unittest.SkipTest("unable to find 'wormhole' in $PATH")
wormhole = locations[0] wormhole = locations[0]
if (os.path.dirname(os.path.abspath(wormhole)) != if (os.path.dirname(os.path.abspath(wormhole)) != os.path.dirname(
os.path.dirname(sys.executable)): sys.executable)):
log.msg("locations: %s" % (locations,)) log.msg("locations: %s" % (locations, ))
log.msg("sys.executable: %s" % (sys.executable,)) log.msg("sys.executable: %s" % (sys.executable, ))
raise unittest.SkipTest("found the wrong 'wormhole' in $PATH: %s %s" raise unittest.SkipTest(
% (wormhole, sys.executable)) "found the wrong 'wormhole' in $PATH: %s %s" %
(wormhole, sys.executable))
return wormhole return wormhole
@inlineCallbacks @inlineCallbacks
@ -323,8 +340,8 @@ class ScriptsBase:
raise unittest.SkipTest("unable to find UTF-8 locale") raise unittest.SkipTest("unable to find UTF-8 locale")
locale_env = dict(LC_ALL=locale, LANG=locale) locale_env = dict(LC_ALL=locale, LANG=locale)
wormhole = self.find_executable() wormhole = self.find_executable()
res = yield getProcessOutputAndValue(wormhole, ["--version"], res = yield getProcessOutputAndValue(
env=locale_env) wormhole, ["--version"], env=locale_env)
out, err, rc = res out, err, rc = res
if rc != 0: if rc != 0:
log.msg("wormhole not runnable in this tree:") log.msg("wormhole not runnable in this tree:")
@ -334,6 +351,7 @@ class ScriptsBase:
raise unittest.SkipTest("wormhole is not runnable in this tree") raise unittest.SkipTest("wormhole is not runnable in this tree")
returnValue(locale_env) returnValue(locale_env)
class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase): class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase):
# we need Twisted to run the server, but we run the sender and receiver # we need Twisted to run the server, but we run the sender and receiver
# with deferToThread() # with deferToThread()
@ -347,26 +365,30 @@ class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase):
wormhole = self.find_executable() wormhole = self.find_executable()
# we must pass on the environment so that "something" doesn't # we must pass on the environment so that "something" doesn't
# get sad about UTF8 vs. ascii encodings # get sad about UTF8 vs. ascii encodings
out, err, rc = yield getProcessOutputAndValue(wormhole, ["--version"], out, err, rc = yield getProcessOutputAndValue(
env=os.environ) wormhole, ["--version"], env=os.environ)
err = err.decode("utf-8") err = err.decode("utf-8")
if "DistributionNotFound" in err: if "DistributionNotFound" in err:
log.msg("stderr was %s" % err) log.msg("stderr was %s" % err)
last = err.strip().split("\n")[-1] last = err.strip().split("\n")[-1]
self.fail("wormhole not runnable: %s" % last) self.fail("wormhole not runnable: %s" % last)
ver = out.decode("utf-8") or err ver = out.decode("utf-8") or err
self.failUnlessEqual(ver.strip(), "magic-wormhole {}".format(__version__)) self.failUnlessEqual(ver.strip(),
"magic-wormhole {}".format(__version__))
self.failUnlessEqual(rc, 0) self.failUnlessEqual(rc, 0)
@implementer(ITorManager) @implementer(ITorManager)
class FakeTor: class FakeTor:
# use normal endpoints, but record the fact that we were asked # use normal endpoints, but record the fact that we were asked
def __init__(self): def __init__(self):
self.endpoints = [] self.endpoints = []
def stream_via(self, host, port): def stream_via(self, host, port):
self.endpoints.append((host, port)) self.endpoints.append((host, port))
return endpoints.HostnameEndpoint(reactor, host, port) return endpoints.HostnameEndpoint(reactor, host, port)
class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
# we need Twisted to run the server, but we run the sender and receiver # we need Twisted to run the server, but we run the sender and receiver
# with deferToThread() # with deferToThread()
@ -377,11 +399,16 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
yield ServerBase.setUp(self) yield ServerBase.setUp(self)
@inlineCallbacks @inlineCallbacks
def _do_test(self, as_subprocess=False, def _do_test(self,
mode="text", addslash=False, override_filename=False, as_subprocess=False,
fake_tor=False, overwrite=False, mock_accept=False): mode="text",
assert mode in ("text", "file", "empty-file", "directory", addslash=False,
"slow-text", "slow-sender-text") override_filename=False,
fake_tor=False,
overwrite=False,
mock_accept=False):
assert mode in ("text", "file", "empty-file", "directory", "slow-text",
"slow-sender-text")
if fake_tor: if fake_tor:
assert not as_subprocess assert not as_subprocess
send_cfg = config("send") send_cfg = config("send")
@ -408,7 +435,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
elif mode in ("file", "empty-file"): elif mode in ("file", "empty-file"):
if mode == "empty-file": if mode == "empty-file":
message = "" message = ""
send_filename = u"testfil\u00EB" # e-with-diaeresis send_filename = u"testfil\u00EB" # e-with-diaeresis
with open(os.path.join(send_dir, send_filename), "w") as f: with open(os.path.join(send_dir, send_filename), "w") as f:
f.write(message) f.write(message)
send_cfg.what = send_filename send_cfg.what = send_filename
@ -433,8 +460,10 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
# expect: $receive_dir/$dirname/[12345] # expect: $receive_dir/$dirname/[12345]
send_dirname = u"testdir" send_dirname = u"testdir"
def message(i): def message(i):
return "test message %d\n" % i return "test message %d\n" % i
os.mkdir(os.path.join(send_dir, u"middle")) os.mkdir(os.path.join(send_dir, u"middle"))
source_dir = os.path.join(send_dir, u"middle", send_dirname) source_dir = os.path.join(send_dir, u"middle", send_dirname)
os.mkdir(source_dir) os.mkdir(source_dir)
@ -476,21 +505,27 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
env["_MAGIC_WORMHOLE_TEST_KEY_TIMER"] = "999999" env["_MAGIC_WORMHOLE_TEST_KEY_TIMER"] = "999999"
env["_MAGIC_WORMHOLE_TEST_VERIFY_TIMER"] = "999999" env["_MAGIC_WORMHOLE_TEST_VERIFY_TIMER"] = "999999"
send_args = [ send_args = [
'--relay-url', self.relayurl, '--relay-url',
'--transit-helper', '', self.relayurl,
'send', '--transit-helper',
'--hide-progress', '',
'--code', send_cfg.code, 'send',
] + content_args '--hide-progress',
'--code',
send_cfg.code,
] + content_args
send_d = getProcessOutputAndValue( send_d = getProcessOutputAndValue(
wormhole_bin, send_args, wormhole_bin,
send_args,
path=send_dir, path=send_dir,
env=env, env=env,
) )
recv_args = [ recv_args = [
'--relay-url', self.relayurl, '--relay-url',
'--transit-helper', '', self.relayurl,
'--transit-helper',
'',
'receive', 'receive',
'--hide-progress', '--hide-progress',
'--accept-file', '--accept-file',
@ -500,7 +535,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
recv_args.extend(['-o', receive_filename]) recv_args.extend(['-o', receive_filename])
receive_d = getProcessOutputAndValue( receive_d = getProcessOutputAndValue(
wormhole_bin, recv_args, wormhole_bin,
recv_args,
path=receive_dir, path=receive_dir,
env=env, env=env,
) )
@ -524,25 +560,27 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
send_cfg.tor = True send_cfg.tor = True
send_cfg.transit_helper = self.transit send_cfg.transit_helper = self.transit
tx_tm = FakeTor() tx_tm = FakeTor()
with mock.patch("wormhole.tor_manager.get_tor", with mock.patch(
return_value=tx_tm, "wormhole.tor_manager.get_tor",
) as mtx_tm: return_value=tx_tm,
) as mtx_tm:
send_d = cmd_send.send(send_cfg) send_d = cmd_send.send(send_cfg)
recv_cfg.tor = True recv_cfg.tor = True
recv_cfg.transit_helper = self.transit recv_cfg.transit_helper = self.transit
rx_tm = FakeTor() rx_tm = FakeTor()
with mock.patch("wormhole.tor_manager.get_tor", with mock.patch(
return_value=rx_tm, "wormhole.tor_manager.get_tor",
) as mrx_tm: return_value=rx_tm,
) as mrx_tm:
receive_d = cmd_receive.receive(recv_cfg) receive_d = cmd_receive.receive(recv_cfg)
else: else:
KEY_TIMER = 0 if mode == "slow-sender-text" else 99999 KEY_TIMER = 0 if mode == "slow-sender-text" else 99999
rxw = [] rxw = []
with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER): with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER):
send_d = cmd_send.send(send_cfg) send_d = cmd_send.send(send_cfg)
receive_d = cmd_receive.receive(recv_cfg, receive_d = cmd_receive.receive(
_debug_stash_wormhole=rxw) recv_cfg, _debug_stash_wormhole=rxw)
# we need to keep KEY_TIMER patched until the receiver # we need to keep KEY_TIMER patched until the receiver
# gets far enough to start the timer, which happens after # gets far enough to start the timer, which happens after
# the code is set # the code is set
@ -555,8 +593,9 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
with mock.patch.object(cmd_receive, "VERIFY_TIMER", VERIFY_TIMER): with mock.patch.object(cmd_receive, "VERIFY_TIMER", VERIFY_TIMER):
with mock.patch.object(cmd_send, "VERIFY_TIMER", VERIFY_TIMER): with mock.patch.object(cmd_send, "VERIFY_TIMER", VERIFY_TIMER):
if mock_accept: if mock_accept:
with mock.patch.object(cmd_receive.six.moves, with mock.patch.object(
'input', return_value='y'): cmd_receive.six.moves, 'input',
return_value='y'):
yield gatherResults([send_d, receive_d], True) yield gatherResults([send_d, receive_d], True)
else: else:
yield gatherResults([send_d, receive_d], True) yield gatherResults([send_d, receive_d], True)
@ -567,14 +606,14 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
expected_endpoints.append(("127.0.0.1", self.transitport)) expected_endpoints.append(("127.0.0.1", self.transitport))
tx_timing = mtx_tm.call_args[1]["timing"] tx_timing = mtx_tm.call_args[1]["timing"]
self.assertEqual(tx_tm.endpoints, expected_endpoints) self.assertEqual(tx_tm.endpoints, expected_endpoints)
self.assertEqual(mtx_tm.mock_calls, self.assertEqual(
[mock.call(reactor, False, None, mtx_tm.mock_calls,
timing=tx_timing)]) [mock.call(reactor, False, None, timing=tx_timing)])
rx_timing = mrx_tm.call_args[1]["timing"] rx_timing = mrx_tm.call_args[1]["timing"]
self.assertEqual(rx_tm.endpoints, expected_endpoints) self.assertEqual(rx_tm.endpoints, expected_endpoints)
self.assertEqual(mrx_tm.mock_calls, self.assertEqual(
[mock.call(reactor, False, None, mrx_tm.mock_calls,
timing=rx_timing)]) [mock.call(reactor, False, None, timing=rx_timing)])
send_stdout = send_cfg.stdout.getvalue() send_stdout = send_cfg.stdout.getvalue()
send_stderr = send_cfg.stderr.getvalue() send_stderr = send_cfg.stderr.getvalue()
@ -585,7 +624,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
# newlines, even if we're on windows # newlines, even if we're on windows
NL = "\n" NL = "\n"
self.maxDiff = None # show full output for assertion failures self.maxDiff = None # show full output for assertion failures
key_established = "" key_established = ""
if mode == "slow-text": if mode == "slow-text":
@ -600,38 +639,41 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
"On the other computer, please run:{NL}{NL}" "On the other computer, please run:{NL}{NL}"
"wormhole receive {code}{NL}{NL}" "wormhole receive {code}{NL}{NL}"
"{KE}" "{KE}"
"text message sent{NL}").format(bytes=len(message), "text message sent{NL}").format(
code=send_cfg.code, bytes=len(message),
NL=NL, code=send_cfg.code,
KE=key_established) NL=NL,
KE=key_established)
self.failUnlessEqual(send_stderr, expected) self.failUnlessEqual(send_stderr, expected)
elif mode == "file": elif mode == "file":
self.failUnlessIn(u"Sending {size:s} file named '{name}'{NL}" self.failUnlessIn(u"Sending {size:s} file named '{name}'{NL}"
.format(size=naturalsize(len(message)), .format(
name=send_filename, size=naturalsize(len(message)),
NL=NL), send_stderr) name=send_filename,
NL=NL), send_stderr)
self.failUnlessIn(u"Wormhole code is: {code}{NL}" self.failUnlessIn(u"Wormhole code is: {code}{NL}"
"On the other computer, please run:{NL}{NL}" "On the other computer, please run:{NL}{NL}"
"wormhole receive {code}{NL}{NL}" "wormhole receive {code}{NL}{NL}".format(
.format(code=send_cfg.code, NL=NL), code=send_cfg.code, NL=NL), send_stderr)
send_stderr) self.failUnlessIn(
self.failUnlessIn(u"File sent.. waiting for confirmation{NL}" u"File sent.. waiting for confirmation{NL}"
"Confirmation received. Transfer complete.{NL}" "Confirmation received. Transfer complete.{NL}".format(NL=NL),
.format(NL=NL), send_stderr) send_stderr)
elif mode == "directory": elif mode == "directory":
self.failUnlessIn(u"Sending directory", send_stderr) self.failUnlessIn(u"Sending directory", send_stderr)
self.failUnlessIn(u"named 'testdir'", send_stderr) self.failUnlessIn(u"named 'testdir'", send_stderr)
self.failUnlessIn(u"Wormhole code is: {code}{NL}" self.failUnlessIn(u"Wormhole code is: {code}{NL}"
"On the other computer, please run:{NL}{NL}" "On the other computer, please run:{NL}{NL}"
"wormhole receive {code}{NL}{NL}" "wormhole receive {code}{NL}{NL}".format(
.format(code=send_cfg.code, NL=NL), send_stderr) code=send_cfg.code, NL=NL), send_stderr)
self.failUnlessIn(u"File sent.. waiting for confirmation{NL}" self.failUnlessIn(
"Confirmation received. Transfer complete.{NL}" u"File sent.. waiting for confirmation{NL}"
.format(NL=NL), send_stderr) "Confirmation received. Transfer complete.{NL}".format(NL=NL),
send_stderr)
# check receiver # check receiver
if mode in ("text", "slow-text", "slow-sender-text"): if mode in ("text", "slow-text", "slow-sender-text"):
self.assertEqual(receive_stdout, message+NL) self.assertEqual(receive_stdout, message + NL)
if mode == "text": if mode == "text":
self.assertEqual(receive_stderr, "") self.assertEqual(receive_stderr, "")
elif mode == "slow-text": elif mode == "slow-text":
@ -640,9 +682,9 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
self.assertEqual(receive_stderr, "Waiting for sender...\n") self.assertEqual(receive_stderr, "Waiting for sender...\n")
elif mode == "file": elif mode == "file":
self.failUnlessEqual(receive_stdout, "") self.failUnlessEqual(receive_stdout, "")
self.failUnlessIn(u"Receiving file ({size:s}) into: {name}" self.failUnlessIn(u"Receiving file ({size:s}) into: {name}".format(
.format(size=naturalsize(len(message)), size=naturalsize(len(message)), name=receive_filename),
name=receive_filename), receive_stderr) receive_stderr)
self.failUnlessIn(u"Received file written to ", receive_stderr) self.failUnlessIn(u"Received file written to ", receive_stderr)
fn = os.path.join(receive_dir, receive_filename) fn = os.path.join(receive_dir, receive_filename)
self.failUnless(os.path.exists(fn)) self.failUnless(os.path.exists(fn))
@ -652,52 +694,67 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
self.failUnlessEqual(receive_stdout, "") self.failUnlessEqual(receive_stdout, "")
want = (r"Receiving directory \(\d+ \w+\) into: {name}/" want = (r"Receiving directory \(\d+ \w+\) into: {name}/"
.format(name=receive_dirname)) .format(name=receive_dirname))
self.failUnless(re.search(want, receive_stderr), self.failUnless(
(want, receive_stderr)) re.search(want, receive_stderr), (want, receive_stderr))
self.failUnlessIn(u"Received files written to {name}" self.failUnlessIn(
.format(name=receive_dirname), receive_stderr) u"Received files written to {name}"
.format(name=receive_dirname),
receive_stderr)
fn = os.path.join(receive_dir, receive_dirname) fn = os.path.join(receive_dir, receive_dirname)
self.failUnless(os.path.exists(fn), fn) self.failUnless(os.path.exists(fn), fn)
for i in range(5): for i in range(5):
fn = os.path.join(receive_dir, receive_dirname, str(i)) fn = os.path.join(receive_dir, receive_dirname, str(i))
with open(fn, "r") as f: with open(fn, "r") as f:
self.failUnlessEqual(f.read(), message(i)) self.failUnlessEqual(f.read(), message(i))
self.failUnlessEqual(modes[i], self.failUnlessEqual(modes[i], stat.S_IMODE(
stat.S_IMODE(os.stat(fn).st_mode)) os.stat(fn).st_mode))
def test_text(self): def test_text(self):
return self._do_test() return self._do_test()
def test_text_subprocess(self): def test_text_subprocess(self):
return self._do_test(as_subprocess=True) return self._do_test(as_subprocess=True)
def test_text_tor(self): def test_text_tor(self):
return self._do_test(fake_tor=True) return self._do_test(fake_tor=True)
def test_file(self): def test_file(self):
return self._do_test(mode="file") return self._do_test(mode="file")
def test_file_override(self): def test_file_override(self):
return self._do_test(mode="file", override_filename=True) return self._do_test(mode="file", override_filename=True)
def test_file_overwrite(self): def test_file_overwrite(self):
return self._do_test(mode="file", overwrite=True) return self._do_test(mode="file", overwrite=True)
def test_file_overwrite_mock_accept(self): def test_file_overwrite_mock_accept(self):
return self._do_test(mode="file", overwrite=True, mock_accept=True) return self._do_test(mode="file", overwrite=True, mock_accept=True)
def test_file_tor(self): def test_file_tor(self):
return self._do_test(mode="file", fake_tor=True) return self._do_test(mode="file", fake_tor=True)
def test_empty_file(self): def test_empty_file(self):
return self._do_test(mode="empty-file") return self._do_test(mode="empty-file")
def test_directory(self): def test_directory(self):
return self._do_test(mode="directory") return self._do_test(mode="directory")
def test_directory_addslash(self): def test_directory_addslash(self):
return self._do_test(mode="directory", addslash=True) return self._do_test(mode="directory", addslash=True)
def test_directory_override(self): def test_directory_override(self):
return self._do_test(mode="directory", override_filename=True) return self._do_test(mode="directory", override_filename=True)
def test_directory_overwrite(self): def test_directory_overwrite(self):
return self._do_test(mode="directory", overwrite=True) return self._do_test(mode="directory", overwrite=True)
def test_directory_overwrite_mock_accept(self): def test_directory_overwrite_mock_accept(self):
return self._do_test(mode="directory", overwrite=True, mock_accept=True) return self._do_test(
mode="directory", overwrite=True, mock_accept=True)
def test_slow_text(self): def test_slow_text(self):
return self._do_test(mode="slow-text") return self._do_test(mode="slow-text")
def test_slow_sender_text(self): def test_slow_sender_text(self):
return self._do_test(mode="slow-sender-text") return self._do_test(mode="slow-sender-text")
@ -721,7 +778,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
os.mkdir(send_dir) os.mkdir(send_dir)
receive_dir = self.mktemp() receive_dir = self.mktemp()
os.mkdir(receive_dir) os.mkdir(receive_dir)
recv_cfg.accept_file = True # don't ask for permission recv_cfg.accept_file = True # don't ask for permission
if mode == "file": if mode == "file":
message = "test message\n" message = "test message\n"
@ -765,10 +822,12 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
free_space = 10000000 free_space = 10000000
else: else:
free_space = 0 free_space = 0
with mock.patch("wormhole.cli.cmd_receive.estimate_free_space", with mock.patch(
return_value=free_space): "wormhole.cli.cmd_receive.estimate_free_space",
return_value=free_space):
f = yield self.assertFailure(send_d, TransferError) f = yield self.assertFailure(send_d, TransferError)
self.assertEqual(str(f), "remote error, transfer abandoned: transfer rejected") self.assertEqual(
str(f), "remote error, transfer abandoned: transfer rejected")
f = yield self.assertFailure(receive_d, TransferError) f = yield self.assertFailure(receive_d, TransferError)
self.assertEqual(str(f), "transfer rejected") self.assertEqual(str(f), "transfer rejected")
@ -781,7 +840,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
# newlines, even if we're on windows # newlines, even if we're on windows
NL = "\n" NL = "\n"
self.maxDiff = None # show full output for assertion failures self.maxDiff = None # show full output for assertion failures
self.assertEqual(send_stdout, "") self.assertEqual(send_stdout, "")
self.assertEqual(receive_stdout, "") self.assertEqual(receive_stdout, "")
@ -789,54 +848,63 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
# check sender # check sender
if mode == "file": if mode == "file":
self.failUnlessIn("Sending {size:s} file named '{name}'{NL}" self.failUnlessIn("Sending {size:s} file named '{name}'{NL}"
.format(size=naturalsize(size), .format(
name=send_filename, size=naturalsize(size),
NL=NL), send_stderr) name=send_filename,
NL=NL), send_stderr)
self.failUnlessIn("Wormhole code is: {code}{NL}" self.failUnlessIn("Wormhole code is: {code}{NL}"
"On the other computer, please run:{NL}{NL}" "On the other computer, please run:{NL}{NL}"
"wormhole receive {code}{NL}" "wormhole receive {code}{NL}".format(
.format(code=send_cfg.code, NL=NL), code=send_cfg.code, NL=NL), send_stderr)
send_stderr) self.failIfIn(
self.failIfIn("File sent.. waiting for confirmation{NL}" "File sent.. waiting for confirmation{NL}"
"Confirmation received. Transfer complete.{NL}" "Confirmation received. Transfer complete.{NL}".format(NL=NL),
.format(NL=NL), send_stderr) send_stderr)
elif mode == "directory": elif mode == "directory":
self.failUnlessIn("Sending directory", send_stderr) self.failUnlessIn("Sending directory", send_stderr)
self.failUnlessIn("named 'testdir'", send_stderr) self.failUnlessIn("named 'testdir'", send_stderr)
self.failUnlessIn("Wormhole code is: {code}{NL}" self.failUnlessIn("Wormhole code is: {code}{NL}"
"On the other computer, please run:{NL}{NL}" "On the other computer, please run:{NL}{NL}"
"wormhole receive {code}{NL}" "wormhole receive {code}{NL}".format(
.format(code=send_cfg.code, NL=NL), send_stderr) code=send_cfg.code, NL=NL), send_stderr)
self.failIfIn("File sent.. waiting for confirmation{NL}" self.failIfIn(
"Confirmation received. Transfer complete.{NL}" "File sent.. waiting for confirmation{NL}"
.format(NL=NL), send_stderr) "Confirmation received. Transfer complete.{NL}".format(NL=NL),
send_stderr)
# check receiver # check receiver
if mode == "file": if mode == "file":
self.failIfIn("Received file written to ", receive_stderr) self.failIfIn("Received file written to ", receive_stderr)
if failmode == "noclobber": if failmode == "noclobber":
self.failUnlessIn("Error: " self.failUnlessIn(
"refusing to overwrite existing 'testfile'{NL}" "Error: "
.format(NL=NL), receive_stderr) "refusing to overwrite existing 'testfile'{NL}"
.format(NL=NL),
receive_stderr)
else: else:
self.failUnlessIn("Error: " self.failUnlessIn(
"insufficient free space (0B) for file ({size:d}B){NL}" "Error: "
.format(NL=NL, size=size), receive_stderr) "insufficient free space (0B) for file ({size:d}B){NL}"
.format(NL=NL, size=size), receive_stderr)
elif mode == "directory": elif mode == "directory":
self.failIfIn("Received files written to {name}" self.failIfIn(
.format(name=receive_name), receive_stderr) "Received files written to {name}".format(name=receive_name),
#want = (r"Receiving directory \(\d+ \w+\) into: {name}/" receive_stderr)
# want = (r"Receiving directory \(\d+ \w+\) into: {name}/"
# .format(name=receive_name)) # .format(name=receive_name))
#self.failUnless(re.search(want, receive_stderr), # self.failUnless(re.search(want, receive_stderr),
# (want, receive_stderr)) # (want, receive_stderr))
if failmode == "noclobber": if failmode == "noclobber":
self.failUnlessIn("Error: " self.failUnlessIn(
"refusing to overwrite existing 'testdir'{NL}" "Error: "
.format(NL=NL), receive_stderr) "refusing to overwrite existing 'testdir'{NL}"
.format(NL=NL),
receive_stderr)
else: else:
self.failUnlessIn("Error: " self.failUnlessIn(("Error: "
"insufficient free space (0B) for directory ({size:d}B){NL}" "insufficient free space (0B) for directory"
.format(NL=NL, size=size), receive_stderr) " ({size:d}B){NL}").format(
NL=NL, size=size), receive_stderr)
if failmode == "noclobber": if failmode == "noclobber":
fn = os.path.join(receive_dir, receive_name) fn = os.path.join(receive_dir, receive_name)
@ -846,13 +914,17 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
def test_fail_file_noclobber(self): def test_fail_file_noclobber(self):
return self._do_test_fail("file", "noclobber") return self._do_test_fail("file", "noclobber")
def test_fail_directory_noclobber(self): def test_fail_directory_noclobber(self):
return self._do_test_fail("directory", "noclobber") return self._do_test_fail("directory", "noclobber")
def test_fail_file_toobig(self): def test_fail_file_toobig(self):
return self._do_test_fail("file", "toobig") return self._do_test_fail("file", "toobig")
def test_fail_directory_toobig(self): def test_fail_directory_toobig(self):
return self._do_test_fail("directory", "toobig") return self._do_test_fail("directory", "toobig")
class ZeroMode(ServerBase, unittest.TestCase): class ZeroMode(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_text(self): def test_text(self):
@ -871,8 +943,8 @@ class ZeroMode(ServerBase, unittest.TestCase):
send_cfg.text = message send_cfg.text = message
#send_cfg.cwd = send_dir # send_cfg.cwd = send_dir
#recv_cfg.cwd = receive_dir # recv_cfg.cwd = receive_dir
send_d = cmd_send.send(send_cfg) send_d = cmd_send.send(send_cfg)
receive_d = cmd_receive.receive(recv_cfg) receive_d = cmd_receive.receive(recv_cfg)
@ -888,7 +960,7 @@ class ZeroMode(ServerBase, unittest.TestCase):
# newlines, even if we're on windows # newlines, even if we're on windows
NL = "\n" NL = "\n"
self.maxDiff = None # show full output for assertion failures self.maxDiff = None # show full output for assertion failures
self.assertEqual(send_stdout, "") self.assertEqual(send_stdout, "")
@ -898,15 +970,15 @@ class ZeroMode(ServerBase, unittest.TestCase):
"{NL}" "{NL}"
"wormhole receive -0{NL}" "wormhole receive -0{NL}"
"{NL}" "{NL}"
"text message sent{NL}").format(bytes=len(message), "text message sent{NL}").format(
code=send_cfg.code, bytes=len(message), code=send_cfg.code, NL=NL)
NL=NL)
self.failUnlessEqual(send_stderr, expected) self.failUnlessEqual(send_stderr, expected)
# check receiver # check receiver
self.assertEqual(receive_stdout, message+NL) self.assertEqual(receive_stdout, message + NL)
self.assertEqual(receive_stderr, "") self.assertEqual(receive_stderr, "")
class NotWelcome(ServerBase, unittest.TestCase): class NotWelcome(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def setUp(self): def setUp(self):
@ -936,6 +1008,7 @@ class NotWelcome(ServerBase, unittest.TestCase):
f = yield self.assertFailure(receive_d, WelcomeError) f = yield self.assertFailure(receive_d, WelcomeError)
self.assertEqual(str(f), "please upgrade XYZ") self.assertEqual(str(f), "please upgrade XYZ")
class NoServer(ServerBase, unittest.TestCase): class NoServer(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def setUp(self): def setUp(self):
@ -991,8 +1064,8 @@ class NoServer(ServerBase, unittest.TestCase):
e = yield self.assertFailure(receive_d, ServerConnectionError) e = yield self.assertFailure(receive_d, ServerConnectionError)
self.assertIsInstance(e.reason, ConnectionRefusedError) self.assertIsInstance(e.reason, ConnectionRefusedError)
class Cleanup(ServerBase, unittest.TestCase):
class Cleanup(ServerBase, unittest.TestCase):
def make_config(self): def make_config(self):
cfg = config("send") cfg = config("send")
# common options for all tests in this suite # common options for all tests in this suite
@ -1040,6 +1113,7 @@ class Cleanup(ServerBase, unittest.TestCase):
cids = self._rendezvous.get_app(cmd_send.APPID).get_nameplate_ids() cids = self._rendezvous.get_app(cmd_send.APPID).get_nameplate_ids()
self.assertEqual(len(cids), 0) self.assertEqual(len(cids), 0)
class ExtractFile(unittest.TestCase): class ExtractFile(unittest.TestCase):
def test_filenames(self): def test_filenames(self):
args = mock.Mock() args = mock.Mock()
@ -1066,8 +1140,8 @@ class ExtractFile(unittest.TestCase):
zf = mock.Mock() zf = mock.Mock()
zi = mock.Mock() zi = mock.Mock()
zi.filename = "haha//root" # abspath squashes this, hopefully zipfile zi.filename = "haha//root" # abspath squashes this, hopefully zipfile
# does too # does too
zi.external_attr = 5 << 16 zi.external_attr = 5 << 16
expected = os.path.join(extract_dir, "haha", "root") expected = os.path.join(extract_dir, "haha", "root")
with mock.patch.object(cmd_receive.os, "chmod") as chmod: with mock.patch.object(cmd_receive.os, "chmod") as chmod:
@ -1082,6 +1156,7 @@ class ExtractFile(unittest.TestCase):
e = self.assertRaises(ValueError, ef, zf, zi, extract_dir) e = self.assertRaises(ValueError, ef, zf, zi, extract_dir)
self.assertIn("malicious zipfile", str(e)) self.assertIn("malicious zipfile", str(e))
class AppID(ServerBase, unittest.TestCase): class AppID(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def setUp(self): def setUp(self):
@ -1108,11 +1183,11 @@ class AppID(ServerBase, unittest.TestCase):
yield receive_d yield receive_d
used = self._usage_db.execute("SELECT DISTINCT `app_id`" used = self._usage_db.execute("SELECT DISTINCT `app_id`"
" FROM `nameplates`" " FROM `nameplates`").fetchall()
).fetchall()
self.assertEqual(len(used), 1, used) self.assertEqual(len(used), 1, used)
self.assertEqual(used[0]["app_id"], u"appid2") self.assertEqual(used[0]["app_id"], u"appid2")
class Welcome(unittest.TestCase): class Welcome(unittest.TestCase):
def do(self, welcome_message, my_version="2.0"): def do(self, welcome_message, my_version="2.0"):
stderr = io.StringIO() stderr = io.StringIO()
@ -1129,27 +1204,33 @@ class Welcome(unittest.TestCase):
def test_version_old(self): def test_version_old(self):
stderr = self.do({"current_cli_version": "3.0"}) stderr = self.do({"current_cli_version": "3.0"})
expected = ("Warning: errors may occur unless both sides are running the same version\n" + 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") "Server claims 3.0 is current, but ours is 2.0\n")
self.assertEqual(stderr, expected) self.assertEqual(stderr, expected)
def test_version_unreleased(self): def test_version_unreleased(self):
stderr = self.do({"current_cli_version": "3.0"}, stderr = self.do(
my_version="2.5+middle.something") {
"current_cli_version": "3.0"
}, my_version="2.5+middle.something")
self.assertEqual(stderr, "") self.assertEqual(stderr, "")
def test_motd(self): def test_motd(self):
stderr = self.do({"motd": "hello"}) stderr = self.do({"motd": "hello"})
self.assertEqual(stderr, "Server (at url) says:\n hello\n") self.assertEqual(stderr, "Server (at url) says:\n hello\n")
class Dispatch(unittest.TestCase): class Dispatch(unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_success(self): def test_success(self):
cfg = config("send") cfg = config("send")
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
called = [] called = []
def fake(): def fake():
called.append(1) called.append(1)
yield cli._dispatch_command(reactor, cfg, fake) yield cli._dispatch_command(reactor, cfg, fake)
self.assertEqual(called, [1]) self.assertEqual(called, [1])
self.assertEqual(cfg.stderr.getvalue(), "") self.assertEqual(cfg.stderr.getvalue(), "")
@ -1160,8 +1241,10 @@ class Dispatch(unittest.TestCase):
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
cfg.timing = mock.Mock() cfg.timing = mock.Mock()
cfg.dump_timing = "filename" cfg.dump_timing = "filename"
def fake(): def fake():
pass pass
yield cli._dispatch_command(reactor, cfg, fake) yield cli._dispatch_command(reactor, cfg, fake)
self.assertEqual(cfg.stderr.getvalue(), "") self.assertEqual(cfg.stderr.getvalue(), "")
self.assertEqual(cfg.timing.mock_calls[-1], self.assertEqual(cfg.timing.mock_calls[-1],
@ -1171,32 +1254,39 @@ class Dispatch(unittest.TestCase):
def test_wrong_password_error(self): def test_wrong_password_error(self):
cfg = config("send") cfg = config("send")
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
def fake(): def fake():
raise WrongPasswordError("abcd") raise WrongPasswordError("abcd")
yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake),
SystemExit) yield self.assertFailure(
expected = fill("ERROR: " + dedent(WrongPasswordError.__doc__))+"\n" cli._dispatch_command(reactor, cfg, fake), SystemExit)
expected = fill("ERROR: " + dedent(WrongPasswordError.__doc__)) + "\n"
self.assertEqual(cfg.stderr.getvalue(), expected) self.assertEqual(cfg.stderr.getvalue(), expected)
@inlineCallbacks @inlineCallbacks
def test_welcome_error(self): def test_welcome_error(self):
cfg = config("send") cfg = config("send")
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
def fake(): def fake():
raise WelcomeError("abcd") raise WelcomeError("abcd")
yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake),
SystemExit) yield self.assertFailure(
expected = fill("ERROR: " + dedent(WelcomeError.__doc__))+"\n\nabcd\n" cli._dispatch_command(reactor, cfg, fake), SystemExit)
expected = (
fill("ERROR: " + dedent(WelcomeError.__doc__)) + "\n\nabcd\n")
self.assertEqual(cfg.stderr.getvalue(), expected) self.assertEqual(cfg.stderr.getvalue(), expected)
@inlineCallbacks @inlineCallbacks
def test_transfer_error(self): def test_transfer_error(self):
cfg = config("send") cfg = config("send")
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
def fake(): def fake():
raise TransferError("abcd") raise TransferError("abcd")
yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake),
SystemExit) yield self.assertFailure(
cli._dispatch_command(reactor, cfg, fake), SystemExit)
expected = "TransferError: abcd\n" expected = "TransferError: abcd\n"
self.assertEqual(cfg.stderr.getvalue(), expected) self.assertEqual(cfg.stderr.getvalue(), expected)
@ -1204,11 +1294,14 @@ class Dispatch(unittest.TestCase):
def test_server_connection_error(self): def test_server_connection_error(self):
cfg = config("send") cfg = config("send")
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
def fake(): def fake():
raise ServerConnectionError("URL", ValueError("abcd")) raise ServerConnectionError("URL", ValueError("abcd"))
yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake),
SystemExit) yield self.assertFailure(
expected = fill("ERROR: " + dedent(ServerConnectionError.__doc__))+"\n" cli._dispatch_command(reactor, cfg, fake), SystemExit)
expected = fill(
"ERROR: " + dedent(ServerConnectionError.__doc__)) + "\n"
expected += "(relay URL was URL)\n" expected += "(relay URL was URL)\n"
expected += "abcd\n" expected += "abcd\n"
self.assertEqual(cfg.stderr.getvalue(), expected) self.assertEqual(cfg.stderr.getvalue(), expected)
@ -1217,21 +1310,26 @@ class Dispatch(unittest.TestCase):
def test_other_error(self): def test_other_error(self):
cfg = config("send") cfg = config("send")
cfg.stderr = io.StringIO() cfg.stderr = io.StringIO()
def fake(): def fake():
raise ValueError("abcd") raise ValueError("abcd")
# I'm seeing unicode problems with the Failure().printTraceback, and # I'm seeing unicode problems with the Failure().printTraceback, and
# the output would be kind of unpredictable anyways, so we'll mock it # the output would be kind of unpredictable anyways, so we'll mock it
# out here. # out here.
f = mock.Mock() f = mock.Mock()
def mock_print(file): def mock_print(file):
file.write(u"<TRACEBACK>\n") file.write(u"<TRACEBACK>\n")
f.printTraceback = mock_print f.printTraceback = mock_print
with mock.patch("wormhole.cli.cli.Failure", return_value=f): with mock.patch("wormhole.cli.cli.Failure", return_value=f):
yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake), yield self.assertFailure(
SystemExit) cli._dispatch_command(reactor, cfg, fake), SystemExit)
expected = "<TRACEBACK>\nERROR: abcd\n" expected = "<TRACEBACK>\nERROR: abcd\n"
self.assertEqual(cfg.stderr.getvalue(), expected) self.assertEqual(cfg.stderr.getvalue(), expected)
class Help(unittest.TestCase): class Help(unittest.TestCase):
def _check_top_level_help(self, got): def _check_top_level_help(self, got):
# the main wormhole.cli.cli.wormhole docstring should be in the # the main wormhole.cli.cli.wormhole docstring should be in the

View File

@ -1,14 +1,19 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import mock
from twisted.trial import unittest
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.task import Clock
from twisted.internet.defer import Deferred, inlineCallbacks from twisted.internet.defer import Deferred, inlineCallbacks
from twisted.internet.task import Clock
from twisted.trial import unittest
import mock
from ..eventual import EventualQueue from ..eventual import EventualQueue
class IntentionalError(Exception): class IntentionalError(Exception):
pass pass
class Eventual(unittest.TestCase, object): class Eventual(unittest.TestCase, object):
def test_eventually(self): def test_eventually(self):
c = Clock() c = Clock()
@ -23,9 +28,10 @@ class Eventual(unittest.TestCase, object):
self.assertNoResult(d3) self.assertNoResult(d3)
eq.flush_sync() eq.flush_sync()
self.assertEqual(c1.mock_calls, self.assertEqual(c1.mock_calls, [
[mock.call("arg1", "arg2", kwarg1="kw1"), mock.call("arg1", "arg2", kwarg1="kw1"),
mock.call("arg3", "arg4", kwarg5="kw5")]) mock.call("arg3", "arg4", kwarg5="kw5")
])
self.assertEqual(self.successResultOf(d2), None) self.assertEqual(self.successResultOf(d2), None)
self.assertEqual(self.successResultOf(d3), "value") self.assertEqual(self.successResultOf(d3), "value")
@ -47,11 +53,12 @@ class Eventual(unittest.TestCase, object):
eq = EventualQueue(reactor) eq = EventualQueue(reactor)
d1 = eq.fire_eventually() d1 = eq.fire_eventually()
d2 = Deferred() d2 = Deferred()
def _more(res): def _more(res):
eq.eventually(d2.callback, None) eq.eventually(d2.callback, None)
d1.addCallback(_more) d1.addCallback(_more)
yield eq.flush() yield eq.flush()
# d1 will fire, which will queue d2 to fire, and the flush() ought to # d1 will fire, which will queue d2 to fire, and the flush() ought to
# wait for d2 too # wait for d2 too
self.successResultOf(d2) self.successResultOf(d2)

View File

@ -1,44 +1,47 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import unittest import unittest
from binascii import unhexlify #, hexlify from binascii import unhexlify # , hexlify
from hkdf import Hkdf from hkdf import Hkdf
#def generate_KAT(): # def generate_KAT():
# print("KAT = [") # print("KAT = [")
# for salt in (b"", b"salt"): # for salt in (b"", b"salt"):
# for context in (b"", b"context"): # for context in (b"", b"context"):
# skm = b"secret" # skm = b"secret"
# out = HKDF(skm, 64, XTS=salt, CTXinfo=context) # out = HKDF(skm, 64, XTS=salt, CTXinfo=context)
# hexout = " '%s' +\n '%s'" % (hexlify(out[:32]), # hexout = " '%s' +\n '%s'" % (hexlify(out[:32]),
# hexlify(out[32:])) # hexlify(out[32:]))
# print(" (%r, %r, %r,\n%s)," % (salt, context, skm, hexout)) # print(" (%r, %r, %r,\n%s)," % (salt, context, skm, hexout))
# print("]") # print("]")
KAT = [ KAT = [
('', '', 'secret', ('', '', 'secret',
'2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf' + '2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf' +
'9cfd42fd37e1e5a214d15f03058d7fee63dc28f564b7b9fe3da514f80daad4bf'), '9cfd42fd37e1e5a214d15f03058d7fee63dc28f564b7b9fe3da514f80daad4bf'),
('', 'context', 'secret', ('', 'context', 'secret',
'c24c303a1adfb4c3e2b092e6254ed481c41d8955ba8ec3f6a1473493a60c957b' + 'c24c303a1adfb4c3e2b092e6254ed481c41d8955ba8ec3f6a1473493a60c957b' +
'31b723018ca75557214d3d5c61c0c7a5315b103b21ff00cb03ebe023dc347a47'), '31b723018ca75557214d3d5c61c0c7a5315b103b21ff00cb03ebe023dc347a47'),
('salt', '', 'secret', ('salt', '', 'secret',
'f1156507c39b0e326159e778696253122de430899a8df2484040a85a5f95ceb1' + 'f1156507c39b0e326159e778696253122de430899a8df2484040a85a5f95ceb1' +
'dfca555d4cc603bdf7153ed1560de8cbc3234b27a6d2be8e8ca202d90649679a'), 'dfca555d4cc603bdf7153ed1560de8cbc3234b27a6d2be8e8ca202d90649679a'),
('salt', 'context', 'secret', ('salt', 'context', 'secret',
'61a4f201a867bcc12381ddb180d27074408d03ee9d5750855e5a12d967fa060f' + '61a4f201a867bcc12381ddb180d27074408d03ee9d5750855e5a12d967fa060f' +
'10336ead9370927eaabb0d60b259346ee5f57eb7ceba8c72f1ed3f2932b1bf19'), '10336ead9370927eaabb0d60b259346ee5f57eb7ceba8c72f1ed3f2932b1bf19'),
] ]
class TestKAT(unittest.TestCase): class TestKAT(unittest.TestCase):
# note: this uses SHA256 # note: this uses SHA256
def test_kat(self): def test_kat(self):
for (salt, context, skm, expected_hexout) in KAT: for (salt, context, skm, expected_hexout) in KAT:
expected_out = unhexlify(expected_hexout) expected_out = unhexlify(expected_hexout)
for outlen in range(0, len(expected_out)): for outlen in range(0, len(expected_out)):
out = Hkdf(salt.encode("ascii"), out = Hkdf(salt.encode("ascii"), skm.encode("ascii")).expand(
skm.encode("ascii")).expand(context.encode("ascii"), context.encode("ascii"), outlen)
outlen)
self.assertEqual(out, expected_out[:outlen]) self.assertEqual(out, expected_out[:outlen])
#if __name__ == '__main__':
# generate_KAT() # if __name__ == '__main__':
# generate_KAT()

View File

@ -1,9 +1,13 @@
import errno
import os
import re
import subprocess
import re, errno, subprocess, os
from twisted.trial import unittest from twisted.trial import unittest
from .. import ipaddrs from .. import ipaddrs
DOTTED_QUAD_RE=re.compile("^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$") DOTTED_QUAD_RE = re.compile("^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$")
MOCK_IPADDR_OUTPUT = """\ MOCK_IPADDR_OUTPUT = """\
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN \n\ 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN \n\
@ -11,12 +15,14 @@ MOCK_IPADDR_OUTPUT = """\
inet 127.0.0.1/8 scope host lo inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host \n\ inet6 ::1/128 scope host \n\
valid_lft forever preferred_lft forever valid_lft forever preferred_lft forever
2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000 2: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP \
qlen 1000
link/ether d4:3d:7e:01:b4:3e brd ff:ff:ff:ff:ff:ff link/ether d4:3d:7e:01:b4:3e brd ff:ff:ff:ff:ff:ff
inet 192.168.0.6/24 brd 192.168.0.255 scope global eth1 inet 192.168.0.6/24 brd 192.168.0.255 scope global eth1
inet6 fe80::d63d:7eff:fe01:b43e/64 scope link \n\ inet6 fe80::d63d:7eff:fe01:b43e/64 scope link \n\
valid_lft forever preferred_lft forever valid_lft forever preferred_lft forever
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000 3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen\
1000
link/ether 90:f6:52:27:15:0a brd ff:ff:ff:ff:ff:ff link/ether 90:f6:52:27:15:0a brd ff:ff:ff:ff:ff:ff
inet 192.168.0.2/24 brd 192.168.0.255 scope global wlan0 inet 192.168.0.2/24 brd 192.168.0.255 scope global wlan0
inet6 fe80::92f6:52ff:fe27:150a/64 scope link \n\ inet6 fe80::92f6:52ff:fe27:150a/64 scope link \n\
@ -58,7 +64,8 @@ MOCK_ROUTE_OUTPUT = """\
=========================================================================== ===========================================================================
Interface List Interface List
0x1 ........................... MS TCP Loopback interface 0x1 ........................... MS TCP Loopback interface
0x2 ...08 00 27 c3 80 ad ...... AMD PCNET Family PCI Ethernet Adapter - Packet Scheduler Miniport 0x2 ...08 00 27 c3 80 ad ...... AMD PCNET Family PCI Ethernet Adapter - \
Packet Scheduler Miniport
=========================================================================== ===========================================================================
=========================================================================== ===========================================================================
Active Routes: Active Routes:
@ -85,6 +92,7 @@ class FakeProcess:
def __init__(self, output, err): def __init__(self, output, err):
self.output = output self.output = output
self.err = err self.err = err
def communicate(self): def communicate(self):
return (self.output, self.err) return (self.output, self.err)
@ -94,16 +102,28 @@ class ListAddresses(unittest.TestCase):
addresses = ipaddrs.find_addresses() addresses = ipaddrs.find_addresses()
self.failUnlessIn("127.0.0.1", addresses) self.failUnlessIn("127.0.0.1", addresses)
self.failIfIn("0.0.0.0", addresses) self.failIfIn("0.0.0.0", addresses)
# David A.'s OpenSolaris box timed out on this test one time when it was at # David A.'s OpenSolaris box timed out on this test one time when it was at
# 2s. # 2s.
test_list.timeout=4 test_list.timeout = 4
def _test_list_mock(self, command, output, expected): def _test_list_mock(self, command, output, expected):
self.first = True self.first = True
def call_Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, def call_Popen(args,
preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, bufsize=0,
universal_newlines=False, startupinfo=None, creationflags=0): executable=None,
stdin=None,
stdout=None,
stderr=None,
preexec_fn=None,
close_fds=False,
shell=False,
cwd=None,
env=None,
universal_newlines=False,
startupinfo=None,
creationflags=0):
if self.first: if self.first:
self.first = False self.first = False
e = OSError("EINTR") e = OSError("EINTR")
@ -115,11 +135,13 @@ class ListAddresses(unittest.TestCase):
e = OSError("[Errno 2] No such file or directory") e = OSError("[Errno 2] No such file or directory")
e.errno = errno.ENOENT e.errno = errno.ENOENT
raise e raise e
self.patch(subprocess, 'Popen', call_Popen) self.patch(subprocess, 'Popen', call_Popen)
self.patch(os.path, 'isfile', lambda x: True) self.patch(os.path, 'isfile', lambda x: True)
def call_which(name): def call_which(name):
return [name] return [name]
self.patch(ipaddrs, 'which', call_which) self.patch(ipaddrs, 'which', call_which)
addresses = ipaddrs.find_addresses() addresses = ipaddrs.find_addresses()
@ -131,11 +153,13 @@ class ListAddresses(unittest.TestCase):
def test_list_mock_ifconfig(self): def test_list_mock_ifconfig(self):
self.patch(ipaddrs, 'platform', "linux2") self.patch(ipaddrs, 'platform', "linux2")
self._test_list_mock("ifconfig", MOCK_IFCONFIG_OUTPUT, UNIX_TEST_ADDRESSES) self._test_list_mock("ifconfig", MOCK_IFCONFIG_OUTPUT,
UNIX_TEST_ADDRESSES)
def test_list_mock_route(self): def test_list_mock_route(self):
self.patch(ipaddrs, 'platform', "win32") self.patch(ipaddrs, 'platform', "win32")
self._test_list_mock("route.exe", MOCK_ROUTE_OUTPUT, WINDOWS_TEST_ADDRESSES) self._test_list_mock("route.exe", MOCK_ROUTE_OUTPUT,
WINDOWS_TEST_ADDRESSES)
def test_list_mock_cygwin(self): def test_list_mock_cygwin(self):
self.patch(ipaddrs, 'platform', "cygwin") self.patch(ipaddrs, 'platform', "cygwin")

View File

@ -1,8 +1,11 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
from twisted.trial import unittest from twisted.trial import unittest
from .. import journal from .. import journal
from .._interfaces import IJournal from .._interfaces import IJournal
class Journal(unittest.TestCase): class Journal(unittest.TestCase):
def test_journal(self): def test_journal(self):
events = [] events = []

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,11 @@
from twisted.trial import unittest
from twisted.internet.task import Clock from twisted.internet.task import Clock
from twisted.python.failure import Failure from twisted.python.failure import Failure
from twisted.trial import unittest
from ..eventual import EventualQueue from ..eventual import EventualQueue
from ..observer import OneShotObserver, SequenceObserver from ..observer import OneShotObserver, SequenceObserver
class OneShot(unittest.TestCase): class OneShot(unittest.TestCase):
def test_fire(self): def test_fire(self):
c = Clock() c = Clock()
@ -119,4 +121,3 @@ class Sequence(unittest.TestCase):
d2 = o.when_next_event() d2 = o.when_next_event()
eq.flush_sync() eq.flush_sync()
self.assertIdentical(self.failureResultOf(d2), f) self.assertIdentical(self.failureResultOf(d2), f)

View File

@ -1,39 +1,44 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import mock
from itertools import count from itertools import count
from twisted.trial import unittest
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks from twisted.internet.defer import inlineCallbacks
from twisted.internet.threads import deferToThread from twisted.internet.threads import deferToThread
from .._rlcompleter import (input_with_completion, from twisted.trial import unittest
_input_code_with_completion,
CodeInputter, warn_readline) import mock
from ..errors import KeyFormatError, AlreadyInputNameplateError
from .._rlcompleter import (CodeInputter, _input_code_with_completion,
input_with_completion, warn_readline)
from ..errors import AlreadyInputNameplateError, KeyFormatError
APPID = "appid" APPID = "appid"
class Input(unittest.TestCase): class Input(unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_wrapper(self): def test_wrapper(self):
helper = object() helper = object()
trueish = object() trueish = object()
with mock.patch("wormhole._rlcompleter._input_code_with_completion", with mock.patch(
return_value=trueish) as m: "wormhole._rlcompleter._input_code_with_completion",
used_completion = yield input_with_completion("prompt:", helper, return_value=trueish) as m:
reactor) used_completion = yield input_with_completion(
"prompt:", helper, reactor)
self.assertIs(used_completion, trueish) self.assertIs(used_completion, trueish)
self.assertEqual(m.mock_calls, self.assertEqual(m.mock_calls, [mock.call("prompt:", helper, reactor)])
[mock.call("prompt:", helper, reactor)])
# note: if this test fails, the warn_readline() message will probably # note: if this test fails, the warn_readline() message will probably
# get written to stderr # get written to stderr
class Sync(unittest.TestCase): class Sync(unittest.TestCase):
# exercise _input_code_with_completion, which uses the blocking builtin # exercise _input_code_with_completion, which uses the blocking builtin
# "input()" function, hence _input_code_with_completion is usually in a # "input()" function, hence _input_code_with_completion is usually in a
# thread with deferToThread # thread with deferToThread
@mock.patch("wormhole._rlcompleter.CodeInputter") @mock.patch("wormhole._rlcompleter.CodeInputter")
@mock.patch("wormhole._rlcompleter.readline", @mock.patch("wormhole._rlcompleter.readline", __doc__="I am GNU readline")
__doc__="I am GNU readline")
@mock.patch("wormhole._rlcompleter.input", return_value="code") @mock.patch("wormhole._rlcompleter.input", return_value="code")
def test_readline(self, input, readline, ci): def test_readline(self, input, readline, ci):
c = mock.Mock(name="inhibit parenting") c = mock.Mock(name="inhibit parenting")
@ -49,17 +54,17 @@ class Sync(unittest.TestCase):
self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)])
self.assertEqual(c.mock_calls, [mock.call.finish("code")]) self.assertEqual(c.mock_calls, [mock.call.finish("code")])
self.assertEqual(input.mock_calls, [mock.call(prompt)]) self.assertEqual(input.mock_calls, [mock.call(prompt)])
self.assertEqual(readline.mock_calls, self.assertEqual(readline.mock_calls, [
[mock.call.parse_and_bind("tab: complete"), mock.call.parse_and_bind("tab: complete"),
mock.call.set_completer(c.completer), mock.call.set_completer(c.completer),
mock.call.set_completer_delims(""), mock.call.set_completer_delims(""),
]) ])
@mock.patch("wormhole._rlcompleter.CodeInputter") @mock.patch("wormhole._rlcompleter.CodeInputter")
@mock.patch("wormhole._rlcompleter.readline") @mock.patch("wormhole._rlcompleter.readline")
@mock.patch("wormhole._rlcompleter.input", return_value="code") @mock.patch("wormhole._rlcompleter.input", return_value="code")
def test_readline_no_docstring(self, input, readline, ci): def test_readline_no_docstring(self, input, readline, ci):
del readline.__doc__ # when in doubt, it assumes GNU readline del readline.__doc__ # when in doubt, it assumes GNU readline
c = mock.Mock(name="inhibit parenting") c = mock.Mock(name="inhibit parenting")
c.completer = object() c.completer = object()
trueish = object() trueish = object()
@ -73,15 +78,14 @@ class Sync(unittest.TestCase):
self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)])
self.assertEqual(c.mock_calls, [mock.call.finish("code")]) self.assertEqual(c.mock_calls, [mock.call.finish("code")])
self.assertEqual(input.mock_calls, [mock.call(prompt)]) self.assertEqual(input.mock_calls, [mock.call(prompt)])
self.assertEqual(readline.mock_calls, self.assertEqual(readline.mock_calls, [
[mock.call.parse_and_bind("tab: complete"), mock.call.parse_and_bind("tab: complete"),
mock.call.set_completer(c.completer), mock.call.set_completer(c.completer),
mock.call.set_completer_delims(""), mock.call.set_completer_delims(""),
]) ])
@mock.patch("wormhole._rlcompleter.CodeInputter") @mock.patch("wormhole._rlcompleter.CodeInputter")
@mock.patch("wormhole._rlcompleter.readline", @mock.patch("wormhole._rlcompleter.readline", __doc__="I am libedit")
__doc__="I am libedit")
@mock.patch("wormhole._rlcompleter.input", return_value="code") @mock.patch("wormhole._rlcompleter.input", return_value="code")
def test_libedit(self, input, readline, ci): def test_libedit(self, input, readline, ci):
c = mock.Mock(name="inhibit parenting") c = mock.Mock(name="inhibit parenting")
@ -97,11 +101,11 @@ class Sync(unittest.TestCase):
self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)])
self.assertEqual(c.mock_calls, [mock.call.finish("code")]) self.assertEqual(c.mock_calls, [mock.call.finish("code")])
self.assertEqual(input.mock_calls, [mock.call(prompt)]) self.assertEqual(input.mock_calls, [mock.call(prompt)])
self.assertEqual(readline.mock_calls, self.assertEqual(readline.mock_calls, [
[mock.call.parse_and_bind("bind ^I rl_complete"), mock.call.parse_and_bind("bind ^I rl_complete"),
mock.call.set_completer(c.completer), mock.call.set_completer(c.completer),
mock.call.set_completer_delims(""), mock.call.set_completer_delims(""),
]) ])
@mock.patch("wormhole._rlcompleter.CodeInputter") @mock.patch("wormhole._rlcompleter.CodeInputter")
@mock.patch("wormhole._rlcompleter.readline", None) @mock.patch("wormhole._rlcompleter.readline", None)
@ -139,6 +143,7 @@ class Sync(unittest.TestCase):
self.assertEqual(c.mock_calls, [mock.call.finish(u"code")]) self.assertEqual(c.mock_calls, [mock.call.finish(u"code")])
self.assertEqual(input.mock_calls, [mock.call(prompt)]) self.assertEqual(input.mock_calls, [mock.call(prompt)])
def get_completions(c, prefix): def get_completions(c, prefix):
completions = [] completions = []
for state in count(0): for state in count(0):
@ -147,9 +152,11 @@ def get_completions(c, prefix):
return completions return completions
completions.append(text) completions.append(text)
def fake_blockingCallFromThread(f, *a, **kw): def fake_blockingCallFromThread(f, *a, **kw):
return f(*a, **kw) return f(*a, **kw)
class Completion(unittest.TestCase): class Completion(unittest.TestCase):
def test_simple(self): def test_simple(self):
# no actual completion # no actual completion
@ -158,12 +165,14 @@ class Completion(unittest.TestCase):
c.bcft = fake_blockingCallFromThread c.bcft = fake_blockingCallFromThread
c.finish("1-code-ghost") c.finish("1-code-ghost")
self.assertFalse(c.used_completion) self.assertFalse(c.used_completion)
self.assertEqual(helper.mock_calls, self.assertEqual(helper.mock_calls, [
[mock.call.choose_nameplate("1"), mock.call.choose_nameplate("1"),
mock.call.choose_words("code-ghost")]) mock.call.choose_words("code-ghost")
])
@mock.patch("wormhole._rlcompleter.readline", @mock.patch(
get_completion_type=mock.Mock(return_value=0)) "wormhole._rlcompleter.readline",
get_completion_type=mock.Mock(return_value=0))
def test_call(self, readline): def test_call(self, readline):
# check that it calls _commit_and_build_completions correctly # check that it calls _commit_and_build_completions correctly
helper = mock.Mock() helper = mock.Mock()
@ -188,14 +197,14 @@ class Completion(unittest.TestCase):
# now we have three "a" words: "and", "ark", "aaah!zombies!!" # now we have three "a" words: "and", "ark", "aaah!zombies!!"
cabc.reset_mock() cabc.reset_mock()
cabc.configure_mock(return_value=["aargh", "ark", "aaah!zombies!!"]) cabc.configure_mock(return_value=["aargh", "ark", "aaah!zombies!!"])
self.assertEqual(get_completions(c, "12-a"), self.assertEqual(
["aargh", "ark", "aaah!zombies!!"]) get_completions(c, "12-a"), ["aargh", "ark", "aaah!zombies!!"])
self.assertEqual(cabc.mock_calls, [mock.call("12-a")]) self.assertEqual(cabc.mock_calls, [mock.call("12-a")])
cabc.reset_mock() cabc.reset_mock()
cabc.configure_mock(return_value=["aargh", "aaah!zombies!!"]) cabc.configure_mock(return_value=["aargh", "aaah!zombies!!"])
self.assertEqual(get_completions(c, "12-aa"), self.assertEqual(
["aargh", "aaah!zombies!!"]) get_completions(c, "12-aa"), ["aargh", "aaah!zombies!!"])
self.assertEqual(cabc.mock_calls, [mock.call("12-aa")]) self.assertEqual(cabc.mock_calls, [mock.call("12-aa")])
cabc.reset_mock() cabc.reset_mock()
@ -223,16 +232,17 @@ class Completion(unittest.TestCase):
def test_build_completions(self): def test_build_completions(self):
rn = mock.Mock() rn = mock.Mock()
# InputHelper.get_nameplate_completions returns just the suffixes # InputHelper.get_nameplate_completions returns just the suffixes
gnc = mock.Mock() # get_nameplate_completions gnc = mock.Mock() # get_nameplate_completions
cn = mock.Mock() # choose_nameplate cn = mock.Mock() # choose_nameplate
gwc = mock.Mock() # get_word_completions gwc = mock.Mock() # get_word_completions
cw = mock.Mock() # choose_words cw = mock.Mock() # choose_words
helper = mock.Mock(refresh_nameplates=rn, helper = mock.Mock(
get_nameplate_completions=gnc, refresh_nameplates=rn,
choose_nameplate=cn, get_nameplate_completions=gnc,
get_word_completions=gwc, choose_nameplate=cn,
choose_words=cw, get_word_completions=gwc,
) choose_words=cw,
)
# this needs a real reactor, for blockingCallFromThread # this needs a real reactor, for blockingCallFromThread
c = CodeInputter(helper, reactor) c = CodeInputter(helper, reactor)
cabc = c._commit_and_build_completions cabc = c._commit_and_build_completions
@ -327,17 +337,18 @@ class Completion(unittest.TestCase):
gwc.configure_mock(return_value=["code", "court"]) gwc.configure_mock(return_value=["code", "court"])
c = CodeInputter(helper, reactor) c = CodeInputter(helper, reactor)
cabc = c._commit_and_build_completions cabc = c._commit_and_build_completions
matches = yield deferToThread(cabc, "1-co") # this commits us to 1- matches = yield deferToThread(cabc, "1-co") # this commits us to 1-
self.assertEqual(helper.mock_calls, self.assertEqual(helper.mock_calls, [
[mock.call.choose_nameplate("1"), mock.call.choose_nameplate("1"),
mock.call.when_wordlist_is_available(), mock.call.when_wordlist_is_available(),
mock.call.get_word_completions("co")]) mock.call.get_word_completions("co")
])
self.assertEqual(matches, ["1-code", "1-court"]) self.assertEqual(matches, ["1-code", "1-court"])
helper.reset_mock() helper.reset_mock()
with self.assertRaises(AlreadyInputNameplateError) as e: with self.assertRaises(AlreadyInputNameplateError) as e:
yield deferToThread(cabc, "2-co") yield deferToThread(cabc, "2-co")
self.assertEqual(str(e.exception), self.assertEqual(
"nameplate (1-) already entered, cannot go back") str(e.exception), "nameplate (1-) already entered, cannot go back")
self.assertEqual(helper.mock_calls, []) self.assertEqual(helper.mock_calls, [])
@inlineCallbacks @inlineCallbacks
@ -347,17 +358,18 @@ class Completion(unittest.TestCase):
gwc.configure_mock(return_value=["code", "court"]) gwc.configure_mock(return_value=["code", "court"])
c = CodeInputter(helper, reactor) c = CodeInputter(helper, reactor)
cabc = c._commit_and_build_completions cabc = c._commit_and_build_completions
matches = yield deferToThread(cabc, "1-co") # this commits us to 1- matches = yield deferToThread(cabc, "1-co") # this commits us to 1-
self.assertEqual(helper.mock_calls, self.assertEqual(helper.mock_calls, [
[mock.call.choose_nameplate("1"), mock.call.choose_nameplate("1"),
mock.call.when_wordlist_is_available(), mock.call.when_wordlist_is_available(),
mock.call.get_word_completions("co")]) mock.call.get_word_completions("co")
])
self.assertEqual(matches, ["1-code", "1-court"]) self.assertEqual(matches, ["1-code", "1-court"])
helper.reset_mock() helper.reset_mock()
with self.assertRaises(AlreadyInputNameplateError) as e: with self.assertRaises(AlreadyInputNameplateError) as e:
yield deferToThread(c.finish, "2-code") yield deferToThread(c.finish, "2-code")
self.assertEqual(str(e.exception), self.assertEqual(
"nameplate (1-) already entered, cannot go back") str(e.exception), "nameplate (1-) already entered, cannot go back")
self.assertEqual(helper.mock_calls, []) self.assertEqual(helper.mock_calls, [])
@mock.patch("wormhole._rlcompleter.stderr") @mock.patch("wormhole._rlcompleter.stderr")
@ -366,6 +378,7 @@ class Completion(unittest.TestCase):
# right time, since it involves a reactor and a "system event # right time, since it involves a reactor and a "system event
# trigger", but let's at least make sure it's invocable # trigger", but let's at least make sure it's invocable
warn_readline() warn_readline()
expected ="\nCommand interrupted: please press Return to quit" expected = "\nCommand interrupted: please press Return to quit"
self.assertEqual(stderr.mock_calls, [mock.call.write(expected), self.assertEqual(stderr.mock_calls,
mock.call.write("\n")]) [mock.call.write(expected),
mock.call.write("\n")])

View File

@ -1,10 +1,15 @@
import os, io import io
import mock import os
from twisted.trial import unittest from twisted.trial import unittest
import mock
from ..cli import cmd_ssh from ..cli import cmd_ssh
OTHERS = ["config", "config~", "known_hosts", "known_hosts~"] OTHERS = ["config", "config~", "known_hosts", "known_hosts~"]
class FindPubkey(unittest.TestCase): class FindPubkey(unittest.TestCase):
def test_find_one(self): def test_find_one(self):
files = OTHERS + ["id_rsa.pub", "id_rsa"] files = OTHERS + ["id_rsa.pub", "id_rsa"]
@ -12,8 +17,8 @@ class FindPubkey(unittest.TestCase):
pubkey_file = io.StringIO(pubkey_data) pubkey_file = io.StringIO(pubkey_data)
with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True): with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True):
with mock.patch("os.listdir", return_value=files) as ld: with mock.patch("os.listdir", return_value=files) as ld:
with mock.patch("wormhole.cli.cmd_ssh.open", with mock.patch(
return_value=pubkey_file): "wormhole.cli.cmd_ssh.open", return_value=pubkey_file):
res = cmd_ssh.find_public_key() res = cmd_ssh.find_public_key()
self.assertEqual(ld.mock_calls, self.assertEqual(ld.mock_calls,
[mock.call(os.path.expanduser("~/.ssh/"))]) [mock.call(os.path.expanduser("~/.ssh/"))])
@ -24,7 +29,7 @@ class FindPubkey(unittest.TestCase):
self.assertEqual(pubkey, pubkey_data) self.assertEqual(pubkey, pubkey_data)
def test_find_none(self): def test_find_none(self):
files = OTHERS # no pubkey files = OTHERS # no pubkey
with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True): with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True):
with mock.patch("os.listdir", return_value=files): with mock.patch("os.listdir", return_value=files):
e = self.assertRaises(cmd_ssh.PubkeyError, e = self.assertRaises(cmd_ssh.PubkeyError,
@ -34,12 +39,12 @@ class FindPubkey(unittest.TestCase):
def test_bad_hint(self): def test_bad_hint(self):
with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=False): with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=False):
e = self.assertRaises(cmd_ssh.PubkeyError, e = self.assertRaises(
cmd_ssh.find_public_key, cmd_ssh.PubkeyError,
hint="bogus/path") cmd_ssh.find_public_key,
hint="bogus/path")
self.assertEqual(str(e), "Can't find 'bogus/path'") self.assertEqual(str(e), "Can't find 'bogus/path'")
def test_find_multiple(self): def test_find_multiple(self):
files = OTHERS + ["id_rsa.pub", "id_rsa", "id_dsa.pub", "id_dsa"] files = OTHERS + ["id_rsa.pub", "id_rsa", "id_dsa.pub", "id_dsa"]
pubkey_data = u"ssh-rsa AAAAkeystuff email@host\n" pubkey_data = u"ssh-rsa AAAAkeystuff email@host\n"
@ -47,10 +52,11 @@ class FindPubkey(unittest.TestCase):
with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True): with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True):
with mock.patch("os.listdir", return_value=files): with mock.patch("os.listdir", return_value=files):
responses = iter(["frog", "NaN", "-1", "0"]) responses = iter(["frog", "NaN", "-1", "0"])
with mock.patch("click.prompt", with mock.patch(
side_effect=lambda p: next(responses)): "click.prompt", side_effect=lambda p: next(responses)):
with mock.patch("wormhole.cli.cmd_ssh.open", with mock.patch(
return_value=pubkey_file): "wormhole.cli.cmd_ssh.open",
return_value=pubkey_file):
res = cmd_ssh.find_public_key() res = cmd_ssh.find_public_key()
self.assertEqual(len(res), 3, res) self.assertEqual(len(res), 3, res)
kind, keyid, pubkey = res kind, keyid, pubkey = res

View File

@ -1,42 +1,51 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import mock, io
from twisted.trial import unittest import io
from twisted.internet import defer from twisted.internet import defer
from twisted.internet.error import ConnectError from twisted.internet.error import ConnectError
from twisted.trial import unittest
import mock
from ..tor_manager import get_tor, SocksOnlyTor
from ..errors import NoTorError
from .._interfaces import ITorManager from .._interfaces import ITorManager
from ..errors import NoTorError
from ..tor_manager import SocksOnlyTor, get_tor
class X(): class X():
pass pass
class Tor(unittest.TestCase): class Tor(unittest.TestCase):
def test_no_txtorcon(self): def test_no_txtorcon(self):
with mock.patch("wormhole.tor_manager.txtorcon", None): with mock.patch("wormhole.tor_manager.txtorcon", None):
self.failureResultOf(get_tor(None), NoTorError) self.failureResultOf(get_tor(None), NoTorError)
def test_bad_args(self): def test_bad_args(self):
f = self.failureResultOf(get_tor(None, launch_tor="not boolean"), f = self.failureResultOf(
TypeError) get_tor(None, launch_tor="not boolean"), TypeError)
self.assertEqual(str(f.value), "launch_tor= must be boolean") self.assertEqual(str(f.value), "launch_tor= must be boolean")
f = self.failureResultOf(get_tor(None, tor_control_port=1234), f = self.failureResultOf(
TypeError) get_tor(None, tor_control_port=1234), TypeError)
self.assertEqual(str(f.value), "tor_control_port= must be str or None") self.assertEqual(str(f.value), "tor_control_port= must be str or None")
f = self.failureResultOf(get_tor(None, launch_tor=True, f = self.failureResultOf(
tor_control_port="tcp:127.0.0.1:1234"), get_tor(
ValueError) None, launch_tor=True, tor_control_port="tcp:127.0.0.1:1234"),
self.assertEqual(str(f.value), ValueError)
"cannot combine --launch-tor and --tor-control-port=") self.assertEqual(
str(f.value),
"cannot combine --launch-tor and --tor-control-port=")
def test_launch(self): def test_launch(self):
reactor = object() reactor = object()
my_tor = X() # object() didn't like providedBy() my_tor = X() # object() didn't like providedBy()
launch_d = defer.Deferred() launch_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
with mock.patch("wormhole.tor_manager.txtorcon.launch", with mock.patch(
side_effect=launch_d) as launch: "wormhole.tor_manager.txtorcon.launch",
side_effect=launch_d) as launch:
d = get_tor(reactor, launch_tor=True, stderr=stderr) d = get_tor(reactor, launch_tor=True, stderr=stderr)
self.assertNoResult(d) self.assertNoResult(d)
self.assertEqual(launch.mock_calls, [mock.call(reactor)]) self.assertEqual(launch.mock_calls, [mock.call(reactor)])
@ -44,18 +53,21 @@ class Tor(unittest.TestCase):
tor = self.successResultOf(d) tor = self.successResultOf(d)
self.assertIs(tor, my_tor) self.assertIs(tor, my_tor)
self.assert_(ITorManager.providedBy(tor)) self.assert_(ITorManager.providedBy(tor))
self.assertEqual(stderr.getvalue(), self.assertEqual(
" launching a new Tor process, this may take a while..\n") stderr.getvalue(),
" launching a new Tor process, this may take a while..\n")
def test_connect(self): def test_connect(self):
reactor = object() reactor = object()
my_tor = X() # object() didn't like providedBy() my_tor = X() # object() didn't like providedBy()
connect_d = defer.Deferred() connect_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
with mock.patch("wormhole.tor_manager.txtorcon.connect", with mock.patch(
side_effect=connect_d) as connect: "wormhole.tor_manager.txtorcon.connect",
with mock.patch("wormhole.tor_manager.clientFromString", side_effect=connect_d) as connect:
side_effect=["foo"]) as sfs: with mock.patch(
"wormhole.tor_manager.clientFromString",
side_effect=["foo"]) as sfs:
d = get_tor(reactor, stderr=stderr) d = get_tor(reactor, stderr=stderr)
self.assertEqual(sfs.mock_calls, []) self.assertEqual(sfs.mock_calls, [])
self.assertNoResult(d) self.assertNoResult(d)
@ -71,10 +83,12 @@ class Tor(unittest.TestCase):
reactor = object() reactor = object()
connect_d = defer.Deferred() connect_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
with mock.patch("wormhole.tor_manager.txtorcon.connect", with mock.patch(
side_effect=connect_d) as connect: "wormhole.tor_manager.txtorcon.connect",
with mock.patch("wormhole.tor_manager.clientFromString", side_effect=connect_d) as connect:
side_effect=["foo"]) as sfs: with mock.patch(
"wormhole.tor_manager.clientFromString",
side_effect=["foo"]) as sfs:
d = get_tor(reactor, stderr=stderr) d = get_tor(reactor, stderr=stderr)
self.assertEqual(sfs.mock_calls, []) self.assertEqual(sfs.mock_calls, [])
self.assertNoResult(d) self.assertNoResult(d)
@ -85,20 +99,23 @@ class Tor(unittest.TestCase):
self.assertIsInstance(tor, SocksOnlyTor) self.assertIsInstance(tor, SocksOnlyTor)
self.assert_(ITorManager.providedBy(tor)) self.assert_(ITorManager.providedBy(tor))
self.assertEqual(tor._reactor, reactor) self.assertEqual(tor._reactor, reactor)
self.assertEqual(stderr.getvalue(), self.assertEqual(
" unable to find default Tor control port, using SOCKS\n") stderr.getvalue(),
" unable to find default Tor control port, using SOCKS\n")
def test_connect_custom_control_port(self): def test_connect_custom_control_port(self):
reactor = object() reactor = object()
my_tor = X() # object() didn't like providedBy() my_tor = X() # object() didn't like providedBy()
tcp = "PORT" tcp = "PORT"
ep = object() ep = object()
connect_d = defer.Deferred() connect_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
with mock.patch("wormhole.tor_manager.txtorcon.connect", with mock.patch(
side_effect=connect_d) as connect: "wormhole.tor_manager.txtorcon.connect",
with mock.patch("wormhole.tor_manager.clientFromString", side_effect=connect_d) as connect:
side_effect=[ep]) as sfs: with mock.patch(
"wormhole.tor_manager.clientFromString",
side_effect=[ep]) as sfs:
d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) d = get_tor(reactor, tor_control_port=tcp, stderr=stderr)
self.assertEqual(sfs.mock_calls, [mock.call(reactor, tcp)]) self.assertEqual(sfs.mock_calls, [mock.call(reactor, tcp)])
self.assertNoResult(d) self.assertNoResult(d)
@ -116,10 +133,12 @@ class Tor(unittest.TestCase):
ep = object() ep = object()
connect_d = defer.Deferred() connect_d = defer.Deferred()
stderr = io.StringIO() stderr = io.StringIO()
with mock.patch("wormhole.tor_manager.txtorcon.connect", with mock.patch(
side_effect=connect_d) as connect: "wormhole.tor_manager.txtorcon.connect",
with mock.patch("wormhole.tor_manager.clientFromString", side_effect=connect_d) as connect:
side_effect=[ep]) as sfs: with mock.patch(
"wormhole.tor_manager.clientFromString",
side_effect=[ep]) as sfs:
d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) d = get_tor(reactor, tor_control_port=tcp, stderr=stderr)
self.assertEqual(sfs.mock_calls, [mock.call(reactor, tcp)]) self.assertEqual(sfs.mock_calls, [mock.call(reactor, tcp)])
self.assertNoResult(d) self.assertNoResult(d)
@ -129,18 +148,22 @@ class Tor(unittest.TestCase):
self.failureResultOf(d, ConnectError) self.failureResultOf(d, ConnectError)
self.assertEqual(stderr.getvalue(), "") self.assertEqual(stderr.getvalue(), "")
class SocksOnly(unittest.TestCase): class SocksOnly(unittest.TestCase):
def test_tor(self): def test_tor(self):
reactor = object() reactor = object()
sot = SocksOnlyTor(reactor) sot = SocksOnlyTor(reactor)
fake_ep = object() fake_ep = object()
with mock.patch("wormhole.tor_manager.txtorcon.TorClientEndpoint", with mock.patch(
return_value=fake_ep) as tce: "wormhole.tor_manager.txtorcon.TorClientEndpoint",
return_value=fake_ep) as tce:
ep = sot.stream_via("host", "port") ep = sot.stream_via("host", "port")
self.assertIs(ep, fake_ep) self.assertIs(ep, fake_ep)
self.assertEqual(tce.mock_calls, [mock.call("host", "port", self.assertEqual(tce.mock_calls, [
socks_endpoint=None, mock.call(
tls=False, "host",
reactor=reactor)]) "port",
socks_endpoint=None,
tls=False,
reactor=reactor)
])

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import six
import mock
import unicodedata import unicodedata
import six
from twisted.trial import unittest from twisted.trial import unittest
import mock
from .. import util from .. import util
class Utils(unittest.TestCase): class Utils(unittest.TestCase):
def test_to_bytes(self): def test_to_bytes(self):
b = util.to_bytes("abc") b = util.to_bytes("abc")
@ -41,11 +46,12 @@ class Utils(unittest.TestCase):
self.assertIsInstance(d, dict) self.assertIsInstance(d, dict)
self.assertEqual(d, {"a": "b", "c": 2}) self.assertEqual(d, {"a": "b", "c": 2})
class Space(unittest.TestCase): class Space(unittest.TestCase):
def test_free_space(self): def test_free_space(self):
free = util.estimate_free_space(".") free = util.estimate_free_space(".")
self.assert_(isinstance(free, six.integer_types + (type(None),)), self.assert_(
repr(free)) isinstance(free, six.integer_types + (type(None), )), repr(free))
# some platforms (I think the VMs used by travis are in this # some platforms (I think the VMs used by travis are in this
# category) return 0, and windows will return None, so don't assert # category) return 0, and windows will return None, so don't assert
# anything more specific about the return value # anything more specific about the return value
@ -56,5 +62,5 @@ class Space(unittest.TestCase):
try: try:
with mock.patch("os.statvfs", side_effect=AttributeError()): with mock.patch("os.statvfs", side_effect=AttributeError()):
self.assertEqual(util.estimate_free_space("."), None) self.assertEqual(util.estimate_free_space("."), None)
except AttributeError: # raised by mock.get_original() except AttributeError: # raised by mock.get_original()
pass pass

View File

@ -1,8 +1,12 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import mock
from twisted.trial import unittest from twisted.trial import unittest
import mock
from .._wordlist import PGPWordList from .._wordlist import PGPWordList
class Completions(unittest.TestCase): class Completions(unittest.TestCase):
def test_completions(self): def test_completions(self):
wl = PGPWordList() wl = PGPWordList()
@ -14,16 +18,21 @@ class Completions(unittest.TestCase):
self.assertEqual(len(lots), 256, lots) self.assertEqual(len(lots), 256, lots)
first = list(lots)[0] first = list(lots)[0]
self.assert_(first.startswith("armistice-"), first) self.assert_(first.startswith("armistice-"), first)
self.assertEqual(gc("armistice-ba", 2), self.assertEqual(
{"armistice-baboon", "armistice-backfield", gc("armistice-ba", 2), {
"armistice-backward", "armistice-banjo"}) "armistice-baboon", "armistice-backfield",
self.assertEqual(gc("armistice-ba", 3), "armistice-backward", "armistice-banjo"
{"armistice-baboon-", "armistice-backfield-", })
"armistice-backward-", "armistice-banjo-"}) self.assertEqual(
gc("armistice-ba", 3), {
"armistice-baboon-", "armistice-backfield-",
"armistice-backward-", "armistice-banjo-"
})
self.assertEqual(gc("armistice-baboon", 2), {"armistice-baboon"}) self.assertEqual(gc("armistice-baboon", 2), {"armistice-baboon"})
self.assertEqual(gc("armistice-baboon", 3), {"armistice-baboon-"}) self.assertEqual(gc("armistice-baboon", 3), {"armistice-baboon-"})
self.assertEqual(gc("armistice-baboon", 4), {"armistice-baboon-"}) self.assertEqual(gc("armistice-baboon", 4), {"armistice-baboon-"})
class Choose(unittest.TestCase): class Choose(unittest.TestCase):
def test_choose_words(self): def test_choose_words(self):
wl = PGPWordList() wl = PGPWordList()

View File

@ -1,17 +1,22 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import io, re
import mock import io
from twisted.trial import unittest import re
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue
from twisted.internet.error import ConnectionRefusedError from twisted.internet.error import ConnectionRefusedError
from .common import ServerBase, poll_until from twisted.trial import unittest
from .. import wormhole, _rendezvous
from ..errors import (WrongPasswordError, ServerConnectionError, import mock
KeyFormatError, WormholeClosed, LonelyError,
NoKeyError, OnlyOneCodeError) from .. import _rendezvous, wormhole
from ..transit import allocate_tcp_port from ..errors import (KeyFormatError, LonelyError, NoKeyError,
OnlyOneCodeError, ServerConnectionError, WormholeClosed,
WrongPasswordError)
from ..eventual import EventualQueue from ..eventual import EventualQueue
from ..transit import allocate_tcp_port
from .common import ServerBase, poll_until
APPID = "appid" APPID = "appid"
@ -26,6 +31,7 @@ APPID = "appid"
# * set_code, then connected # * set_code, then connected
# * connected, receive_pake, send_phase, set_code # * connected, receive_pake, send_phase, set_code
class Delegate: class Delegate:
def __init__(self): def __init__(self):
self.welcome = None self.welcome = None
@ -35,28 +41,35 @@ class Delegate:
self.versions = None self.versions = None
self.messages = [] self.messages = []
self.closed = None self.closed = None
def wormhole_got_welcome(self, welcome): def wormhole_got_welcome(self, welcome):
self.welcome = welcome self.welcome = welcome
def wormhole_got_code(self, code): def wormhole_got_code(self, code):
self.code = code self.code = code
def wormhole_got_unverified_key(self, key): def wormhole_got_unverified_key(self, key):
self.key = key self.key = key
def wormhole_got_verifier(self, verifier): def wormhole_got_verifier(self, verifier):
self.verifier = verifier self.verifier = verifier
def wormhole_got_versions(self, versions): def wormhole_got_versions(self, versions):
self.versions = versions self.versions = versions
def wormhole_got_message(self, data): def wormhole_got_message(self, data):
self.messages.append(data) self.messages.append(data)
def wormhole_closed(self, result): def wormhole_closed(self, result):
self.closed = result self.closed = result
class Delegated(ServerBase, unittest.TestCase):
class Delegated(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_delegated(self): def test_delegated(self):
dg = Delegate() dg = Delegate()
w1 = wormhole.create(APPID, self.relayurl, reactor, delegate=dg) w1 = wormhole.create(APPID, self.relayurl, reactor, delegate=dg)
#w1.debug_set_trace("W1") # w1.debug_set_trace("W1")
with self.assertRaises(NoKeyError): with self.assertRaises(NoKeyError):
w1.derive_key("purpose", 12) w1.derive_key("purpose", 12)
w1.set_code("1-abc") w1.set_code("1-abc")
@ -103,6 +116,7 @@ class Delegated(ServerBase, unittest.TestCase):
yield poll_until(lambda: dg.code is not None) yield poll_until(lambda: dg.code is not None)
w1.close() w1.close()
class Wormholes(ServerBase, unittest.TestCase): class Wormholes(ServerBase, unittest.TestCase):
# integration test, with a real server # integration test, with a real server
@ -135,12 +149,12 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_basic(self): def test_basic(self):
w1 = wormhole.create(APPID, self.relayurl, reactor) w1 = wormhole.create(APPID, self.relayurl, reactor)
#w1.debug_set_trace("W1") # w1.debug_set_trace("W1")
with self.assertRaises(NoKeyError): with self.assertRaises(NoKeyError):
w1.derive_key("purpose", 12) w1.derive_key("purpose", 12)
w2 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor)
#w2.debug_set_trace(" W2") # w2.debug_set_trace(" W2")
w1.allocate_code() w1.allocate_code()
code = yield w1.get_code() code = yield w1.get_code()
w2.set_code(code) w2.set_code(code)
@ -302,7 +316,6 @@ class Wormholes(ServerBase, unittest.TestCase):
yield w1.close() yield w1.close()
yield w2.close() yield w2.close()
@inlineCallbacks @inlineCallbacks
def test_multiple_messages(self): def test_multiple_messages(self):
w1 = wormhole.create(APPID, self.relayurl, reactor) w1 = wormhole.create(APPID, self.relayurl, reactor)
@ -322,7 +335,6 @@ class Wormholes(ServerBase, unittest.TestCase):
yield w1.close() yield w1.close()
yield w2.close() yield w2.close()
@inlineCallbacks @inlineCallbacks
def test_closed(self): def test_closed(self):
eq = EventualQueue(reactor) eq = EventualQueue(reactor)
@ -377,7 +389,7 @@ class Wormholes(ServerBase, unittest.TestCase):
w2 = wormhole.create(APPID, self.relayurl, reactor, _eventual_queue=eq) w2 = wormhole.create(APPID, self.relayurl, reactor, _eventual_queue=eq)
w1.allocate_code() w1.allocate_code()
code = yield w1.get_code() code = yield w1.get_code()
w2.set_code(code+"not") w2.set_code(code + "not")
code2 = yield w2.get_code() code2 = yield w2.get_code()
self.assertNotEqual(code, code2) self.assertNotEqual(code, code2)
# That's enough to allow both sides to discover the mismatch, but # That's enough to allow both sides to discover the mismatch, but
@ -387,9 +399,9 @@ class Wormholes(ServerBase, unittest.TestCase):
w1.send_message(b"should still work") w1.send_message(b"should still work")
w2.send_message(b"should still work") w2.send_message(b"should still work")
key2 = yield w2.get_unverified_key() # should work key2 = yield w2.get_unverified_key() # should work
# w2 has just received w1.PAKE, and is about to send w2.VERSION # w2 has just received w1.PAKE, and is about to send w2.VERSION
key1 = yield w1.get_unverified_key() # should work key1 = yield w1.get_unverified_key() # should work
# w1 has just received w2.PAKE, and is about to send w1.VERSION, and # 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 # then will receive w2.VERSION. When it sees w2.VERSION, it will
# learn about the WrongPasswordError. # learn about the WrongPasswordError.
@ -451,7 +463,7 @@ class Wormholes(ServerBase, unittest.TestCase):
badcode = "4 oops spaces" badcode = "4 oops spaces"
with self.assertRaises(KeyFormatError) as ex: with self.assertRaises(KeyFormatError) as ex:
w.set_code(badcode) w.set_code(badcode)
expected_msg = "Code '%s' contains spaces." % (badcode,) expected_msg = "Code '%s' contains spaces." % (badcode, )
self.assertEqual(expected_msg, str(ex.exception)) self.assertEqual(expected_msg, str(ex.exception))
yield self.assertFailure(w.close(), LonelyError) yield self.assertFailure(w.close(), LonelyError)
@ -461,7 +473,7 @@ class Wormholes(ServerBase, unittest.TestCase):
badcode = " 4-oops-space" badcode = " 4-oops-space"
with self.assertRaises(KeyFormatError) as ex: with self.assertRaises(KeyFormatError) as ex:
w.set_code(badcode) w.set_code(badcode)
expected_msg = "Code '%s' contains spaces." % (badcode,) expected_msg = "Code '%s' contains spaces." % (badcode, )
self.assertEqual(expected_msg, str(ex.exception)) self.assertEqual(expected_msg, str(ex.exception))
yield self.assertFailure(w.close(), LonelyError) yield self.assertFailure(w.close(), LonelyError)
@ -478,8 +490,8 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_welcome(self): def test_welcome(self):
w1 = wormhole.create(APPID, self.relayurl, reactor) w1 = wormhole.create(APPID, self.relayurl, reactor)
wel1 = yield w1.get_welcome() # early: before connection established wel1 = yield w1.get_welcome() # early: before connection established
wel2 = yield w1.get_welcome() # late: already received welcome wel2 = yield w1.get_welcome() # late: already received welcome
self.assertEqual(wel1, wel2) self.assertEqual(wel1, wel2)
self.assertIn("current_cli_version", wel1) self.assertIn("current_cli_version", wel1)
@ -489,7 +501,7 @@ class Wormholes(ServerBase, unittest.TestCase):
w2.set_code("123-NOT") w2.set_code("123-NOT")
yield self.assertFailure(w1.get_verifier(), WrongPasswordError) yield self.assertFailure(w1.get_verifier(), WrongPasswordError)
yield self.assertFailure(w1.get_welcome(), WrongPasswordError) # late yield self.assertFailure(w1.get_welcome(), WrongPasswordError) # late
yield self.assertFailure(w1.close(), WrongPasswordError) yield self.assertFailure(w1.close(), WrongPasswordError)
yield self.assertFailure(w2.close(), WrongPasswordError) yield self.assertFailure(w2.close(), WrongPasswordError)
@ -502,7 +514,7 @@ class Wormholes(ServerBase, unittest.TestCase):
w1.allocate_code() w1.allocate_code()
code = yield w1.get_code() code = yield w1.get_code()
w2.set_code(code) w2.set_code(code)
v1 = yield w1.get_verifier() # early v1 = yield w1.get_verifier() # early
v2 = yield w2.get_verifier() v2 = yield w2.get_verifier()
self.failUnlessEqual(type(v1), type(b"")) self.failUnlessEqual(type(v1), type(b""))
self.failUnlessEqual(v1, v2) self.failUnlessEqual(v1, v2)
@ -525,10 +537,10 @@ class Wormholes(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_versions(self): def test_versions(self):
# there's no API for this yet, but make sure the internals work # there's no API for this yet, but make sure the internals work
w1 = wormhole.create(APPID, self.relayurl, reactor, w1 = wormhole.create(
versions={"w1": 123}) APPID, self.relayurl, reactor, versions={"w1": 123})
w2 = wormhole.create(APPID, self.relayurl, reactor, w2 = wormhole.create(
versions={"w2": 456}) APPID, self.relayurl, reactor, versions={"w2": 456})
w1.allocate_code() w1.allocate_code()
code = yield w1.get_code() code = yield w1.get_code()
w2.set_code(code) w2.set_code(code)
@ -564,17 +576,19 @@ class Wormholes(ServerBase, unittest.TestCase):
yield w1.close() yield w1.close()
yield w2.close() yield w2.close()
class MessageDoubler(_rendezvous.RendezvousConnector): class MessageDoubler(_rendezvous.RendezvousConnector):
# we could double messages on the sending side, but a future server will # we could double messages on the sending side, but a future server will
# strip those duplicates, so to really exercise the receiver, we must # strip those duplicates, so to really exercise the receiver, we must
# double them on the inbound side instead # double them on the inbound side instead
#def _msg_send(self, phase, body): # def _msg_send(self, phase, body):
# wormhole._Wormhole._msg_send(self, phase, body) # wormhole._Wormhole._msg_send(self, phase, body)
# self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) # self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body))
def _response_handle_message(self, msg): def _response_handle_message(self, msg):
_rendezvous.RendezvousConnector._response_handle_message(self, msg) _rendezvous.RendezvousConnector._response_handle_message(self, msg)
_rendezvous.RendezvousConnector._response_handle_message(self, msg) _rendezvous.RendezvousConnector._response_handle_message(self, msg)
class Errors(ServerBase, unittest.TestCase): class Errors(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_derive_key_early(self): def test_derive_key_early(self):
@ -602,16 +616,17 @@ class Errors(ServerBase, unittest.TestCase):
w.set_code("123-nope") w.set_code("123-nope")
yield self.assertFailure(w.close(), LonelyError) yield self.assertFailure(w.close(), LonelyError)
class Reconnection(ServerBase, unittest.TestCase): class Reconnection(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_basic(self): def test_basic(self):
w1 = wormhole.create(APPID, self.relayurl, reactor) w1 = wormhole.create(APPID, self.relayurl, reactor)
w1_in = [] w1_in = []
w1._boss._RC._debug_record_inbound_f = w1_in.append w1._boss._RC._debug_record_inbound_f = w1_in.append
#w1.debug_set_trace("W1") # w1.debug_set_trace("W1")
w1.allocate_code() w1.allocate_code()
code = yield w1.get_code() code = yield w1.get_code()
w1.send_message(b"data1") # queued until wormhole is established w1.send_message(b"data1") # queued until wormhole is established
# now wait until we've deposited all our messages on the server # now wait until we've deposited all our messages on the server
def seen_our_pake(): def seen_our_pake():
@ -619,6 +634,7 @@ class Reconnection(ServerBase, unittest.TestCase):
if m["type"] == "message" and m["phase"] == "pake": if m["type"] == "message" and m["phase"] == "pake":
return True return True
return False return False
yield poll_until(seen_our_pake) yield poll_until(seen_our_pake)
w1_in[:] = [] w1_in[:] = []
@ -634,7 +650,7 @@ class Reconnection(ServerBase, unittest.TestCase):
# receiver has started # receiver has started
w2 = wormhole.create(APPID, self.relayurl, reactor) w2 = wormhole.create(APPID, self.relayurl, reactor)
#w2.debug_set_trace(" W2") # w2.debug_set_trace(" W2")
w2.set_code(code) w2.set_code(code)
dataY = yield w2.get_message() dataY = yield w2.get_message()
@ -649,6 +665,7 @@ class Reconnection(ServerBase, unittest.TestCase):
c2 = yield w2.close() c2 = yield w2.close()
self.assertEqual(c2, "happy") self.assertEqual(c2, "happy")
class InitialFailure(unittest.TestCase): class InitialFailure(unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def assertSCEFailure(self, eq, d, innerType): def assertSCEFailure(self, eq, d, innerType):
@ -662,8 +679,8 @@ class InitialFailure(unittest.TestCase):
def test_bad_dns(self): def test_bad_dns(self):
eq = EventualQueue(reactor) eq = EventualQueue(reactor)
# point at a URL that will never connect # point at a URL that will never connect
w = wormhole.create(APPID, "ws://%%%.example.org:4000/v1", w = wormhole.create(
reactor, _eventual_queue=eq) APPID, "ws://%%%.example.org:4000/v1", reactor, _eventual_queue=eq)
# that should have already received an error, when it tried to # that should have already received an error, when it tried to
# resolve the bogus DNS name. All API calls will return an error. # resolve the bogus DNS name. All API calls will return an error.
@ -717,6 +734,7 @@ class InitialFailure(unittest.TestCase):
yield self.assertSCE(d4, ConnectionRefusedError) yield self.assertSCE(d4, ConnectionRefusedError)
yield self.assertSCE(d5, ConnectionRefusedError) yield self.assertSCE(d5, ConnectionRefusedError)
class Trace(unittest.TestCase): class Trace(unittest.TestCase):
def test_basic(self): def test_basic(self):
w1 = wormhole.create(APPID, "ws://localhost:1", reactor) w1 = wormhole.create(APPID, "ws://localhost:1", reactor)
@ -734,13 +752,11 @@ class Trace(unittest.TestCase):
["C1.M1[OLD].IN -> [NEW]"]) ["C1.M1[OLD].IN -> [NEW]"])
out("OUT1") out("OUT1")
self.assertEqual(stderr.getvalue().splitlines(), self.assertEqual(stderr.getvalue().splitlines(),
["C1.M1[OLD].IN -> [NEW]", ["C1.M1[OLD].IN -> [NEW]", " C1.M1.OUT1()"])
" C1.M1.OUT1()"])
w1._boss._print_trace("", "R.connected", "", "C1", "RC1", stderr) w1._boss._print_trace("", "R.connected", "", "C1", "RC1", stderr)
self.assertEqual(stderr.getvalue().splitlines(), self.assertEqual(
["C1.M1[OLD].IN -> [NEW]", stderr.getvalue().splitlines(),
" C1.M1.OUT1()", ["C1.M1[OLD].IN -> [NEW]", " C1.M1.OUT1()", "C1.RC1.R.connected"])
"C1.RC1.R.connected"])
def test_delegated(self): def test_delegated(self):
dg = Delegate() dg = Delegate()

View File

@ -1,11 +1,13 @@
from twisted.trial import unittest from twisted.internet import defer, reactor
from twisted.internet import reactor, defer
from twisted.internet.defer import inlineCallbacks from twisted.internet.defer import inlineCallbacks
from twisted.trial import unittest
from .. import xfer_util from .. import xfer_util
from .common import ServerBase from .common import ServerBase
APPID = u"appid" APPID = u"appid"
class Xfer(ServerBase, unittest.TestCase): class Xfer(ServerBase, unittest.TestCase):
@inlineCallbacks @inlineCallbacks
def test_xfer(self): def test_xfer(self):
@ -24,10 +26,15 @@ class Xfer(ServerBase, unittest.TestCase):
data = u"data" data = u"data"
send_code = [] send_code = []
receive_code = [] receive_code = []
d1 = xfer_util.send(reactor, APPID, self.relayurl, data, code, d1 = xfer_util.send(
on_code=send_code.append) reactor,
d2 = xfer_util.receive(reactor, APPID, self.relayurl, code, APPID,
on_code=receive_code.append) self.relayurl,
data,
code,
on_code=send_code.append)
d2 = xfer_util.receive(
reactor, APPID, self.relayurl, code, on_code=receive_code.append)
send_result = yield d1 send_result = yield d1
receive_result = yield d2 receive_result = yield d2
self.assertEqual(send_code, [code]) self.assertEqual(send_code, [code])
@ -39,8 +46,13 @@ class Xfer(ServerBase, unittest.TestCase):
def test_make_code(self): def test_make_code(self):
data = u"data" data = u"data"
got_code = defer.Deferred() got_code = defer.Deferred()
d1 = xfer_util.send(reactor, APPID, self.relayurl, data, code=None, d1 = xfer_util.send(
on_code=got_code.callback) reactor,
APPID,
self.relayurl,
data,
code=None,
on_code=got_code.callback)
code = yield got_code code = yield got_code
d2 = xfer_util.receive(reactor, APPID, self.relayurl, code) d2 = xfer_util.receive(reactor, APPID, self.relayurl, code)
send_result = yield d1 send_result = yield d1

View File

@ -1,8 +1,13 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import json, time
import json
import time
from zope.interface import implementer from zope.interface import implementer
from ._interfaces import ITiming from ._interfaces import ITiming
class Event: class Event:
def __init__(self, name, when, **details): def __init__(self, name, when, **details):
# data fields that will be dumped to JSON later # data fields that will be dumped to JSON later
@ -35,6 +40,7 @@ class Event:
else: else:
self.finish() self.finish()
@implementer(ITiming) @implementer(ITiming)
class DebugTiming: class DebugTiming:
def __init__(self): def __init__(self):
@ -47,11 +53,14 @@ class DebugTiming:
def write(self, fn, stderr): def write(self, fn, stderr):
with open(fn, "wt") as f: with open(fn, "wt") as f:
data = [ dict(name=e._name, data = [
start=e._start, stop=e._stop, dict(
details=e._details, name=e._name,
) start=e._start,
for e in self._events ] stop=e._stop,
details=e._details,
) for e in self._events
]
json.dump(data, f, indent=1) json.dump(data, f, indent=1)
f.write("\n") f.write("\n")
print("Timing data written to %s" % fn, file=stderr) print("Timing data written to %s" % fn, file=stderr)

View File

@ -1,15 +1,20 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import sys import sys
from attr import attrs, attrib
from zope.interface.declarations import directlyProvides from attr import attrib, attrs
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.endpoints import clientFromString from twisted.internet.endpoints import clientFromString
from zope.interface.declarations import directlyProvides
from . import _interfaces, errors
from .timing import DebugTiming
try: try:
import txtorcon import txtorcon
except ImportError: except ImportError:
txtorcon = None txtorcon = None
from . import _interfaces, errors
from .timing import DebugTiming
@attrs @attrs
class SocksOnlyTor(object): class SocksOnlyTor(object):
@ -17,15 +22,20 @@ class SocksOnlyTor(object):
def stream_via(self, host, port, tls=False): def stream_via(self, host, port, tls=False):
return txtorcon.TorClientEndpoint( return txtorcon.TorClientEndpoint(
host, port, host,
socks_endpoint=None, # tries localhost:9050 and 9150 port,
socks_endpoint=None, # tries localhost:9050 and 9150
tls=tls, tls=tls,
reactor=self._reactor, reactor=self._reactor,
) )
@inlineCallbacks @inlineCallbacks
def get_tor(reactor, launch_tor=False, tor_control_port=None, def get_tor(reactor,
timing=None, stderr=sys.stderr): launch_tor=False,
tor_control_port=None,
timing=None,
stderr=sys.stderr):
""" """
If launch_tor=True, I will try to launch a new Tor process, ask it If launch_tor=True, I will try to launch a new Tor process, ask it
for its SOCKS and control ports, and use those for outbound for its SOCKS and control ports, and use those for outbound
@ -59,7 +69,7 @@ def get_tor(reactor, launch_tor=False, tor_control_port=None,
if not txtorcon: if not txtorcon:
raise errors.NoTorError() raise errors.NoTorError()
if not isinstance(launch_tor, bool): # note: False is int if not isinstance(launch_tor, bool): # note: False is int
raise TypeError("launch_tor= must be boolean") raise TypeError("launch_tor= must be boolean")
if not isinstance(tor_control_port, (type(""), type(None))): if not isinstance(tor_control_port, (type(""), type(None))):
raise TypeError("tor_control_port= must be str or None") raise TypeError("tor_control_port= must be str or None")
@ -74,19 +84,21 @@ def get_tor(reactor, launch_tor=False, tor_control_port=None,
# need the control port. # need the control port.
if launch_tor: if launch_tor:
print(" launching a new Tor process, this may take a while..", print(
file=stderr) " launching a new Tor process, this may take a while..",
file=stderr)
with timing.add("launch tor"): with timing.add("launch tor"):
tor = yield txtorcon.launch(reactor, tor = yield txtorcon.launch(reactor,
#data_directory=, # data_directory=,
#tor_binary=, # tor_binary=,
) )
elif tor_control_port: elif tor_control_port:
with timing.add("find tor"): with timing.add("find tor"):
control_ep = clientFromString(reactor, tor_control_port) control_ep = clientFromString(reactor, tor_control_port)
tor = yield txtorcon.connect(reactor, control_ep) # might raise tor = yield txtorcon.connect(reactor, control_ep) # might raise
print(" using Tor via control port at %s" % tor_control_port, print(
file=stderr) " using Tor via control port at %s" % tor_control_port,
file=stderr)
else: else:
# Let txtorcon look through a list of usual places. If that fails, # Let txtorcon look through a list of usual places. If that fails,
# we'll arrange to attempt the default SOCKS port # we'll arrange to attempt the default SOCKS port
@ -98,8 +110,9 @@ def get_tor(reactor, launch_tor=False, tor_control_port=None,
# TODO: make this more specific. I think connect() is # TODO: make this more specific. I think connect() is
# likely to throw a reactor.connectTCP -type error, like # likely to throw a reactor.connectTCP -type error, like
# ConnectionFailed or ConnectionRefused or something # ConnectionFailed or ConnectionRefused or something
print(" unable to find default Tor control port, using SOCKS", print(
file=stderr) " unable to find default Tor control port, using SOCKS",
file=stderr)
tor = SocksOnlyTor(reactor) tor = SocksOnlyTor(reactor)
directlyProvides(tor, _interfaces.ITorManager) directlyProvides(tor, _interfaces.ITorManager)
returnValue(tor) returnValue(tor)

View File

@ -1,38 +1,51 @@
# no unicode_literals, revisit after twisted patch # no unicode_literals, revisit after twisted patch
from __future__ import print_function, absolute_import from __future__ import absolute_import, print_function
import os, re, sys, time, socket
from collections import namedtuple, deque import os
import re
import socket
import sys
import time
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from collections import deque, namedtuple
import six import six
from zope.interface import implementer from hkdf import Hkdf
from twisted.python import log from nacl.secret import SecretBox
from twisted.python.runtime import platformType from twisted.internet import (address, defer, endpoints, error, interfaces,
from twisted.internet import (reactor, interfaces, defer, protocol, protocol, reactor, task)
endpoints, task, address, error)
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.protocols import policies from twisted.protocols import policies
from nacl.secret import SecretBox from twisted.python import log
from hkdf import Hkdf from twisted.python.runtime import platformType
from zope.interface import implementer
from . import ipaddrs
from .errors import InternalError from .errors import InternalError
from .timing import DebugTiming from .timing import DebugTiming
from .util import bytes_to_hexstr from .util import bytes_to_hexstr
from . import ipaddrs
def HKDF(skm, outlen, salt=None, CTXinfo=b""): def HKDF(skm, outlen, salt=None, CTXinfo=b""):
return Hkdf(salt, skm).expand(CTXinfo, outlen) return Hkdf(salt, skm).expand(CTXinfo, outlen)
class TransitError(Exception): class TransitError(Exception):
pass pass
class BadHandshake(Exception): class BadHandshake(Exception):
pass pass
class TransitClosed(TransitError): class TransitClosed(TransitError):
pass pass
class BadNonce(TransitError): class BadNonce(TransitError):
pass pass
# The beginning of each TCP connection consists of the following handshake # The beginning of each TCP connection consists of the following handshake
# messages. The sender transmits the same text regardless of whether it is on # messages. The sender transmits the same text regardless of whether it is on
# the initiating/connecting end of the TCP connection, or on the # the initiating/connecting end of the TCP connection, or on the
@ -63,19 +76,23 @@ class BadNonce(TransitError):
# RXID_HEX ready\n\n" and then makes a first/not-first decision about sending # RXID_HEX ready\n\n" and then makes a first/not-first decision about sending
# "go\n" or "nevermind\n"+close(). # "go\n" or "nevermind\n"+close().
def build_receiver_handshake(key): def build_receiver_handshake(key):
hexid = HKDF(key, 32, CTXinfo=b"transit_receiver") hexid = HKDF(key, 32, CTXinfo=b"transit_receiver")
return b"transit receiver "+hexlify(hexid)+b" ready\n\n" return b"transit receiver " + hexlify(hexid) + b" ready\n\n"
def build_sender_handshake(key): def build_sender_handshake(key):
hexid = HKDF(key, 32, CTXinfo=b"transit_sender") hexid = HKDF(key, 32, CTXinfo=b"transit_sender")
return b"transit sender "+hexlify(hexid)+b" ready\n\n" return b"transit sender " + hexlify(hexid) + b" ready\n\n"
def build_sided_relay_handshake(key, side): def build_sided_relay_handshake(key, side):
assert isinstance(side, type(u"")) assert isinstance(side, type(u""))
assert len(side) == 8*2 assert len(side) == 8 * 2
token = HKDF(key, 32, CTXinfo=b"transit_relay_token") token = HKDF(key, 32, CTXinfo=b"transit_relay_token")
return b"please relay "+hexlify(token)+b" for side "+side.encode("ascii")+b"\n" return b"please relay " + hexlify(token) + b" for side " + side.encode(
"ascii") + b"\n"
# These namedtuples are "hint objects". The JSON-serializable dictionaries # These namedtuples are "hint objects". The JSON-serializable dictionaries
@ -87,7 +104,8 @@ def build_sided_relay_handshake(key, side):
# * expect to see the receiver/sender handshake bytes from the other side # * expect to see the receiver/sender handshake bytes from the other side
# * the sender writes "go\n", the receiver waits for "go\n" # * the sender writes "go\n", the receiver waits for "go\n"
# * the rest of the connection contains transit data # * the rest of the connection contains transit data
DirectTCPV1Hint = namedtuple("DirectTCPV1Hint", ["hostname", "port", "priority"]) DirectTCPV1Hint = namedtuple("DirectTCPV1Hint",
["hostname", "port", "priority"])
TorTCPV1Hint = namedtuple("TorTCPV1Hint", ["hostname", "port", "priority"]) TorTCPV1Hint = namedtuple("TorTCPV1Hint", ["hostname", "port", "priority"])
# RelayV1Hint contains a tuple of DirectTCPV1Hint and TorTCPV1Hint hints (we # RelayV1Hint contains a tuple of DirectTCPV1Hint and TorTCPV1Hint hints (we
# use a tuple rather than a list so they'll be hashable into a set). For each # use a tuple rather than a list so they'll be hashable into a set). For each
@ -95,6 +113,7 @@ TorTCPV1Hint = namedtuple("TorTCPV1Hint", ["hostname", "port", "priority"])
# rest of the V1 protocol. Only one hint per relay is useful. # rest of the V1 protocol. Only one hint per relay is useful.
RelayV1Hint = namedtuple("RelayV1Hint", ["hints"]) RelayV1Hint = namedtuple("RelayV1Hint", ["hints"])
def describe_hint_obj(hint): def describe_hint_obj(hint):
if isinstance(hint, DirectTCPV1Hint): if isinstance(hint, DirectTCPV1Hint):
return u"tcp:%s:%d" % (hint.hostname, hint.port) return u"tcp:%s:%d" % (hint.hostname, hint.port)
@ -103,27 +122,30 @@ def describe_hint_obj(hint):
else: else:
return str(hint) return str(hint)
def parse_hint_argv(hint, stderr=sys.stderr): def parse_hint_argv(hint, stderr=sys.stderr):
assert isinstance(hint, type(u"")) assert isinstance(hint, type(u""))
# return tuple or None for an unparseable hint # return tuple or None for an unparseable hint
priority = 0.0 priority = 0.0
mo = re.search(r'^([a-zA-Z0-9]+):(.*)$', hint) mo = re.search(r'^([a-zA-Z0-9]+):(.*)$', hint)
if not mo: if not mo:
print("unparseable hint '%s'" % (hint,), file=stderr) print("unparseable hint '%s'" % (hint, ), file=stderr)
return None return None
hint_type = mo.group(1) hint_type = mo.group(1)
if hint_type != "tcp": if hint_type != "tcp":
print("unknown hint type '%s' in '%s'" % (hint_type, hint), file=stderr) print(
"unknown hint type '%s' in '%s'" % (hint_type, hint), file=stderr)
return None return None
hint_value = mo.group(2) hint_value = mo.group(2)
pieces = hint_value.split(":") pieces = hint_value.split(":")
if len(pieces) < 2: if len(pieces) < 2:
print("unparseable TCP hint (need more colons) '%s'" % (hint,), print(
file=stderr) "unparseable TCP hint (need more colons) '%s'" % (hint, ),
file=stderr)
return None return None
mo = re.search(r'^(\d+)$', pieces[1]) mo = re.search(r'^(\d+)$', pieces[1])
if not mo: if not mo:
print("non-numeric port in TCP hint '%s'" % (hint,), file=stderr) print("non-numeric port in TCP hint '%s'" % (hint, ), file=stderr)
return None return None
hint_host = pieces[0] hint_host = pieces[0]
hint_port = int(pieces[1]) hint_port = int(pieces[1])
@ -133,12 +155,15 @@ def parse_hint_argv(hint, stderr=sys.stderr):
try: try:
priority = float(more_pieces[1]) priority = float(more_pieces[1])
except ValueError: except ValueError:
print("non-float priority= in TCP hint '%s'" % (hint,), print(
file=stderr) "non-float priority= in TCP hint '%s'" % (hint, ),
file=stderr)
return None return None
return DirectTCPV1Hint(hint_host, hint_port, priority) return DirectTCPV1Hint(hint_host, hint_port, priority)
TIMEOUT = 60 # seconds
TIMEOUT = 60 # seconds
@implementer(interfaces.IProducer, interfaces.IConsumer) @implementer(interfaces.IProducer, interfaces.IConsumer)
class Connection(protocol.Protocol, policies.TimeoutMixin): class Connection(protocol.Protocol, policies.TimeoutMixin):
@ -159,7 +184,7 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
self._waiting_reads = deque() self._waiting_reads = deque()
def connectionMade(self): def connectionMade(self):
self.setTimeout(TIMEOUT) # does timeoutConnection() when it expires self.setTimeout(TIMEOUT) # does timeoutConnection() when it expires
self.factory.connectionWasMade(self) self.factory.connectionWasMade(self)
def startNegotiation(self): def startNegotiation(self):
@ -168,11 +193,11 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
self.state = "relay" self.state = "relay"
else: else:
self.state = "start" self.state = "start"
self.dataReceived(b"") # cycle the state machine self.dataReceived(b"") # cycle the state machine
return self._negotiation_d return self._negotiation_d
def _cancel(self, d): def _cancel(self, d):
self.state = "hung up" # stop reacting to anything further self.state = "hung up" # stop reacting to anything further
self._error = defer.CancelledError() self._error = defer.CancelledError()
self.transport.loseConnection() self.transport.loseConnection()
# if connectionLost isn't called synchronously, then our # if connectionLost isn't called synchronously, then our
@ -181,7 +206,6 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
if self._negotiation_d: if self._negotiation_d:
self._negotiation_d = None self._negotiation_d = None
def dataReceived(self, data): def dataReceived(self, data):
try: try:
self._dataReceived(data) self._dataReceived(data)
@ -198,7 +222,7 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
if not self.buf.startswith(expected[:len(self.buf)]): if not self.buf.startswith(expected[:len(self.buf)]):
raise BadHandshake("got %r want %r" % (self.buf, expected)) raise BadHandshake("got %r want %r" % (self.buf, expected))
if len(self.buf) < len(expected): if len(self.buf) < len(expected):
return False # keep waiting return False # keep waiting
self.buf = self.buf[len(expected):] self.buf = self.buf[len(expected):]
return True return True
@ -245,9 +269,9 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
return self.dataReceivedRECORDS() return self.dataReceivedRECORDS()
if self.state == "hung up": if self.state == "hung up":
return return
if isinstance(self.state, Exception): # for tests if isinstance(self.state, Exception): # for tests
raise self.state raise self.state
raise ValueError("internal error: unknown state %s" % (self.state,)) raise ValueError("internal error: unknown state %s" % (self.state, ))
def _negotiationSuccessful(self): def _negotiationSuccessful(self):
self.state = "records" self.state = "records"
@ -266,19 +290,20 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
if len(self.buf) < 4: if len(self.buf) < 4:
return return
length = int(hexlify(self.buf[:4]), 16) length = int(hexlify(self.buf[:4]), 16)
if len(self.buf) < 4+length: if len(self.buf) < 4 + length:
return return
encrypted, self.buf = self.buf[4:4+length], self.buf[4+length:] encrypted, self.buf = self.buf[4:4 + length], self.buf[4 + length:]
record = self._decrypt_record(encrypted) record = self._decrypt_record(encrypted)
self.recordReceived(record) self.recordReceived(record)
def _decrypt_record(self, encrypted): def _decrypt_record(self, encrypted):
nonce_buf = encrypted[:SecretBox.NONCE_SIZE] # assume it's prepended nonce_buf = encrypted[:SecretBox.NONCE_SIZE] # assume it's prepended
nonce = int(hexlify(nonce_buf), 16) nonce = int(hexlify(nonce_buf), 16)
if nonce != self.next_receive_nonce: if nonce != self.next_receive_nonce:
raise BadNonce("received out-of-order record: got %d, expected %d" raise BadNonce(
% (nonce, self.next_receive_nonce)) "received out-of-order record: got %d, expected %d" %
(nonce, self.next_receive_nonce))
self.next_receive_nonce += 1 self.next_receive_nonce += 1
record = self.receive_box.decrypt(encrypted) record = self.receive_box.decrypt(encrypted)
return record return record
@ -287,14 +312,15 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
return self._description return self._description
def send_record(self, record): def send_record(self, record):
if not isinstance(record, type(b"")): raise InternalError if not isinstance(record, type(b"")):
raise InternalError
assert SecretBox.NONCE_SIZE == 24 assert SecretBox.NONCE_SIZE == 24
assert self.send_nonce < 2**(8*24) assert self.send_nonce < 2**(8 * 24)
assert len(record) < 2**(8*4) assert len(record) < 2**(8 * 4)
nonce = unhexlify("%048x" % self.send_nonce) # big-endian nonce = unhexlify("%048x" % self.send_nonce) # big-endian
self.send_nonce += 1 self.send_nonce += 1
encrypted = self.send_box.encrypt(record, nonce) encrypted = self.send_box.encrypt(record, nonce)
length = unhexlify("%08x" % len(encrypted)) # always 4 bytes long length = unhexlify("%08x" % len(encrypted)) # always 4 bytes long
self.transport.write(length) self.transport.write(length)
self.transport.write(encrypted) self.transport.write(encrypted)
@ -353,8 +379,10 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
def registerProducer(self, producer, streaming): def registerProducer(self, producer, streaming):
assert interfaces.IConsumer.providedBy(self.transport) assert interfaces.IConsumer.providedBy(self.transport)
self.transport.registerProducer(producer, streaming) self.transport.registerProducer(producer, streaming)
def unregisterProducer(self): def unregisterProducer(self):
self.transport.unregisterProducer() self.transport.unregisterProducer()
def write(self, data): def write(self, data):
self.send_record(data) self.send_record(data)
@ -362,8 +390,10 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
# the transport. # the transport.
def stopProducing(self): def stopProducing(self):
self.transport.stopProducing() self.transport.stopProducing()
def pauseProducing(self): def pauseProducing(self):
self.transport.pauseProducing() self.transport.pauseProducing()
def resumeProducing(self): def resumeProducing(self):
self.transport.resumeProducing() self.transport.resumeProducing()
@ -384,8 +414,8 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
Deferred, and you must call disconnectConsumer() when you are done.""" Deferred, and you must call disconnectConsumer() when you are done."""
if self._consumer: if self._consumer:
raise RuntimeError("A consumer is already attached: %r" % raise RuntimeError(
self._consumer) "A consumer is already attached: %r" % self._consumer)
# be aware of an ordering hazard: when we call the consumer's # be aware of an ordering hazard: when we call the consumer's
# .registerProducer method, they are likely to immediately call # .registerProducer method, they are likely to immediately call
@ -440,6 +470,7 @@ class Connection(protocol.Protocol, policies.TimeoutMixin):
fc = FileConsumer(f, progress, hasher) fc = FileConsumer(f, progress, hasher)
return self.connectConsumer(fc, expected) return self.connectConsumer(fc, expected)
class OutboundConnectionFactory(protocol.ClientFactory): class OutboundConnectionFactory(protocol.ClientFactory):
protocol = Connection protocol = Connection
@ -478,7 +509,7 @@ class InboundConnectionFactory(protocol.ClientFactory):
def _shutdown(self): def _shutdown(self):
for d in list(self._pending_connections): for d in list(self._pending_connections):
d.cancel() # that fires _remove and _proto_failed d.cancel() # that fires _remove and _proto_failed
def _describePeer(self, addr): def _describePeer(self, addr):
if isinstance(addr, address.HostnameAddress): if isinstance(addr, address.HostnameAddress):
@ -511,6 +542,7 @@ class InboundConnectionFactory(protocol.ClientFactory):
# ignore these two, let Twisted log everything else # ignore these two, let Twisted log everything else
f.trap(BadHandshake, defer.CancelledError) f.trap(BadHandshake, defer.CancelledError)
def allocate_tcp_port(): def allocate_tcp_port():
"""Return an (integer) available TCP port on localhost. This briefly """Return an (integer) available TCP port on localhost. This briefly
listens on the port in question, then closes it right away.""" listens on the port in question, then closes it right away."""
@ -527,6 +559,7 @@ def allocate_tcp_port():
s.close() s.close()
return port return port
class _ThereCanBeOnlyOne: class _ThereCanBeOnlyOne:
"""Accept a list of contender Deferreds, and return a summary Deferred. """Accept a list of contender Deferreds, and return a summary Deferred.
When the first contender fires successfully, cancel the rest and fire the When the first contender fires successfully, cancel the rest and fire the
@ -535,6 +568,7 @@ class _ThereCanBeOnlyOne:
status_cb=? status_cb=?
""" """
def __init__(self, contenders): def __init__(self, contenders):
self._remaining = set(contenders) self._remaining = set(contenders)
self._winner_d = defer.Deferred(self._cancel) self._winner_d = defer.Deferred(self._cancel)
@ -581,26 +615,32 @@ class _ThereCanBeOnlyOne:
else: else:
self._winner_d.errback(self._first_failure) self._winner_d.errback(self._first_failure)
def there_can_be_only_one(contenders): def there_can_be_only_one(contenders):
return _ThereCanBeOnlyOne(contenders).run() return _ThereCanBeOnlyOne(contenders).run()
class Common: class Common:
RELAY_DELAY = 2.0 RELAY_DELAY = 2.0
TRANSIT_KEY_LENGTH = SecretBox.KEY_SIZE TRANSIT_KEY_LENGTH = SecretBox.KEY_SIZE
def __init__(self, transit_relay, no_listen=False, tor=None, def __init__(self,
reactor=reactor, timing=None): transit_relay,
self._side = bytes_to_hexstr(os.urandom(8)) # unicode no_listen=False,
tor=None,
reactor=reactor,
timing=None):
self._side = bytes_to_hexstr(os.urandom(8)) # unicode
if transit_relay: if transit_relay:
if not isinstance(transit_relay, type(u"")): if not isinstance(transit_relay, type(u"")):
raise InternalError raise InternalError
# TODO: allow multiple hints for a single relay # TODO: allow multiple hints for a single relay
relay_hint = parse_hint_argv(transit_relay) relay_hint = parse_hint_argv(transit_relay)
relay = RelayV1Hint(hints=(relay_hint,)) relay = RelayV1Hint(hints=(relay_hint, ))
self._transit_relays = [relay] self._transit_relays = [relay]
else: else:
self._transit_relays = [] self._transit_relays = []
self._their_direct_hints = [] # hintobjs self._their_direct_hints = [] # hintobjs
self._our_relay_hints = set(self._transit_relays) self._our_relay_hints = set(self._transit_relays)
self._tor = tor self._tor = tor
self._transit_key = None self._transit_key = None
@ -622,33 +662,42 @@ class Common:
# some test hosts, including the appveyor VMs, *only* have # some test hosts, including the appveyor VMs, *only* have
# 127.0.0.1, and the tests will hang badly if we remove it. # 127.0.0.1, and the tests will hang badly if we remove it.
addresses = non_loopback_addresses addresses = non_loopback_addresses
direct_hints = [DirectTCPV1Hint(six.u(addr), portnum, 0.0) direct_hints = [
for addr in addresses] DirectTCPV1Hint(six.u(addr), portnum, 0.0) for addr in addresses
]
ep = endpoints.serverFromString(reactor, "tcp:%d" % portnum) ep = endpoints.serverFromString(reactor, "tcp:%d" % portnum)
return direct_hints, ep return direct_hints, ep
def get_connection_abilities(self): def get_connection_abilities(self):
return [{u"type": u"direct-tcp-v1"}, return [
{u"type": u"relay-v1"}, {
] u"type": u"direct-tcp-v1"
},
{
u"type": u"relay-v1"
},
]
@inlineCallbacks @inlineCallbacks
def get_connection_hints(self): def get_connection_hints(self):
hints = [] hints = []
direct_hints = yield self._get_direct_hints() direct_hints = yield self._get_direct_hints()
for dh in direct_hints: for dh in direct_hints:
hints.append({u"type": u"direct-tcp-v1", hints.append({
u"priority": dh.priority, u"type": u"direct-tcp-v1",
u"hostname": dh.hostname, u"priority": dh.priority,
u"port": dh.port, # integer u"hostname": dh.hostname,
}) u"port": dh.port, # integer
})
for relay in self._transit_relays: for relay in self._transit_relays:
rhint = {u"type": u"relay-v1", u"hints": []} rhint = {u"type": u"relay-v1", u"hints": []}
for rh in relay.hints: for rh in relay.hints:
rhint[u"hints"].append({u"type": u"direct-tcp-v1", rhint[u"hints"].append({
u"priority": rh.priority, u"type": u"direct-tcp-v1",
u"hostname": rh.hostname, u"priority": rh.priority,
u"port": rh.port}) u"hostname": rh.hostname,
u"port": rh.port
})
hints.append(rhint) hints.append(rhint)
returnValue(hints) returnValue(hints)
@ -665,24 +714,27 @@ class Common:
# listener will win. # listener will win.
self._my_direct_hints, self._listener = self._build_listener() self._my_direct_hints, self._listener = self._build_listener()
if self._listener is None: # don't listen if self._listener is None: # don't listen
self._listener_d = None self._listener_d = None
return defer.succeed(self._my_direct_hints) # empty return defer.succeed(self._my_direct_hints) # empty
# Start the server, so it will be running by the time anyone tries to # Start the server, so it will be running by the time anyone tries to
# connect to the direct hints we return. # connect to the direct hints we return.
f = InboundConnectionFactory(self) f = InboundConnectionFactory(self)
self._listener_f = f # for tests # XX move to __init__ ? self._listener_f = f # for tests # XX move to __init__ ?
self._listener_d = f.whenDone() self._listener_d = f.whenDone()
d = self._listener.listen(f) d = self._listener.listen(f)
def _listening(lp): def _listening(lp):
# lp is an IListeningPort # lp is an IListeningPort
#self._listener_port = lp # for tests # self._listener_port = lp # for tests
def _stop_listening(res): def _stop_listening(res):
lp.stopListening() lp.stopListening()
return res return res
self._listener_d.addBoth(_stop_listening) self._listener_d.addBoth(_stop_listening)
return self._my_direct_hints return self._my_direct_hints
d.addCallback(_listening) d.addCallback(_listening)
return d return d
@ -694,18 +746,18 @@ class Common:
self._listener_d.addErrback(lambda f: None) self._listener_d.addErrback(lambda f: None)
self._listener_d.cancel() self._listener_d.cancel()
def _parse_tcp_v1_hint(self, hint): # hint_struct -> hint_obj def _parse_tcp_v1_hint(self, hint): # hint_struct -> hint_obj
hint_type = hint.get(u"type", u"") hint_type = hint.get(u"type", u"")
if hint_type not in [u"direct-tcp-v1", u"tor-tcp-v1"]: if hint_type not in [u"direct-tcp-v1", u"tor-tcp-v1"]:
log.msg("unknown hint type: %r" % (hint,)) log.msg("unknown hint type: %r" % (hint, ))
return None return None
if not(u"hostname" in hint if not (u"hostname" in hint
and isinstance(hint[u"hostname"], type(u""))): and isinstance(hint[u"hostname"], type(u""))):
log.msg("invalid hostname in hint: %r" % (hint,)) log.msg("invalid hostname in hint: %r" % (hint, ))
return None return None
if not(u"port" in hint if not (u"port" in hint
and isinstance(hint[u"port"], six.integer_types)): and isinstance(hint[u"port"], six.integer_types)):
log.msg("invalid port in hint: %r" % (hint,)) log.msg("invalid port in hint: %r" % (hint, ))
return None return None
priority = hint.get(u"priority", 0.0) priority = hint.get(u"priority", 0.0)
if hint_type == u"direct-tcp-v1": if hint_type == u"direct-tcp-v1":
@ -714,12 +766,12 @@ class Common:
return TorTCPV1Hint(hint[u"hostname"], hint[u"port"], priority) return TorTCPV1Hint(hint[u"hostname"], hint[u"port"], priority)
def add_connection_hints(self, hints): def add_connection_hints(self, hints):
for h in hints: # hint structs for h in hints: # hint structs
hint_type = h.get(u"type", u"") hint_type = h.get(u"type", u"")
if hint_type in [u"direct-tcp-v1", u"tor-tcp-v1"]: if hint_type in [u"direct-tcp-v1", u"tor-tcp-v1"]:
dh = self._parse_tcp_v1_hint(h) dh = self._parse_tcp_v1_hint(h)
if dh: if dh:
self._their_direct_hints.append(dh) # hint_obj self._their_direct_hints.append(dh) # hint_obj
elif hint_type == u"relay-v1": elif hint_type == u"relay-v1":
# TODO: each relay-v1 clause describes a different relay, # TODO: each relay-v1 clause describes a different relay,
# with a set of equally-valid ways to connect to it. Treat # with a set of equally-valid ways to connect to it. Treat
@ -734,7 +786,7 @@ class Common:
rh = RelayV1Hint(hints=tuple(sorted(relay_hints))) rh = RelayV1Hint(hints=tuple(sorted(relay_hints)))
self._our_relay_hints.add(rh) self._our_relay_hints.add(rh)
else: else:
log.msg("unknown hint type: %r" % (h,)) log.msg("unknown hint type: %r" % (h, ))
def _send_this(self): def _send_this(self):
assert self._transit_key assert self._transit_key
@ -748,25 +800,33 @@ class Common:
if self.is_sender: if self.is_sender:
return build_receiver_handshake(self._transit_key) return build_receiver_handshake(self._transit_key)
else: else:
return build_sender_handshake(self._transit_key)# + b"go\n" return build_sender_handshake(self._transit_key) # + b"go\n"
def _sender_record_key(self): def _sender_record_key(self):
assert self._transit_key assert self._transit_key
if self.is_sender: if self.is_sender:
return HKDF(self._transit_key, SecretBox.KEY_SIZE, return HKDF(
CTXinfo=b"transit_record_sender_key") self._transit_key,
SecretBox.KEY_SIZE,
CTXinfo=b"transit_record_sender_key")
else: else:
return HKDF(self._transit_key, SecretBox.KEY_SIZE, return HKDF(
CTXinfo=b"transit_record_receiver_key") self._transit_key,
SecretBox.KEY_SIZE,
CTXinfo=b"transit_record_receiver_key")
def _receiver_record_key(self): def _receiver_record_key(self):
assert self._transit_key assert self._transit_key
if self.is_sender: if self.is_sender:
return HKDF(self._transit_key, SecretBox.KEY_SIZE, return HKDF(
CTXinfo=b"transit_record_receiver_key") self._transit_key,
SecretBox.KEY_SIZE,
CTXinfo=b"transit_record_receiver_key")
else: else:
return HKDF(self._transit_key, SecretBox.KEY_SIZE, return HKDF(
CTXinfo=b"transit_record_sender_key") self._transit_key,
SecretBox.KEY_SIZE,
CTXinfo=b"transit_record_sender_key")
def set_transit_key(self, key): def set_transit_key(self, key):
assert isinstance(key, type(b"")), type(key) assert isinstance(key, type(b"")), type(key)
@ -848,9 +908,13 @@ class Common:
description = "->relay:%s" % describe_hint_obj(hint_obj) description = "->relay:%s" % describe_hint_obj(hint_obj)
if self._tor: if self._tor:
description = "tor" + description description = "tor" + description
d = task.deferLater(self._reactor, relay_delay, d = task.deferLater(
self._start_connector, ep, description, self._reactor,
is_relay=True) relay_delay,
self._start_connector,
ep,
description,
is_relay=True)
contenders.append(d) contenders.append(d)
relay_delay += self.RELAY_DELAY relay_delay += self.RELAY_DELAY
@ -858,16 +922,18 @@ class Common:
raise TransitError("No contenders for connection") raise TransitError("No contenders for connection")
winner = there_can_be_only_one(contenders) winner = there_can_be_only_one(contenders)
return self._not_forever(2*TIMEOUT, winner) return self._not_forever(2 * TIMEOUT, winner)
def _not_forever(self, timeout, d): def _not_forever(self, timeout, d):
"""If the timer fires first, cancel the deferred. If the deferred fires """If the timer fires first, cancel the deferred. If the deferred fires
first, cancel the timer.""" first, cancel the timer."""
t = self._reactor.callLater(timeout, d.cancel) t = self._reactor.callLater(timeout, d.cancel)
def _done(res): def _done(res):
if t.active(): if t.active():
t.cancel() t.cancel()
return res return res
d.addBoth(_done) d.addBoth(_done)
return d return d
@ -896,8 +962,8 @@ class Common:
return None return None
return None return None
if isinstance(hint, DirectTCPV1Hint): if isinstance(hint, DirectTCPV1Hint):
return endpoints.HostnameEndpoint(self._reactor, return endpoints.HostnameEndpoint(self._reactor, hint.hostname,
hint.hostname, hint.port) hint.port)
return None return None
def connection_ready(self, p): def connection_ready(self, p):
@ -915,9 +981,11 @@ class Common:
self._winner = p self._winner = p
return "go" return "go"
class TransitSender(Common): class TransitSender(Common):
is_sender = True is_sender = True
class TransitReceiver(Common): class TransitReceiver(Common):
is_sender = False is_sender = False
@ -926,6 +994,7 @@ class TransitReceiver(Common):
# when done, and add a progress function that gets called with the length of # when done, and add a progress function that gets called with the length of
# each write, and a hasher function that gets called with the data. # each write, and a hasher function that gets called with the data.
@implementer(interfaces.IConsumer) @implementer(interfaces.IConsumer)
class FileConsumer: class FileConsumer:
def __init__(self, f, progress=None, hasher=None): def __init__(self, f, progress=None, hasher=None):
@ -950,6 +1019,7 @@ class FileConsumer:
assert self._producer assert self._producer
self._producer = None self._producer = None
# the TransitSender/Receiver.connect() yields a Connection, on which you can # the TransitSender/Receiver.connect() yields a Connection, on which you can
# do send_record(), but what should the receive API be? set a callback for # do send_record(), but what should the receive API be? set a callback for
# inbound records? get a Deferred for the next record? The producer/consumer # inbound records? get a Deferred for the next record? The producer/consumer

View File

@ -1,30 +1,42 @@
# No unicode_literals # No unicode_literals
import os, json, unicodedata import json
import os
import unicodedata
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
def to_bytes(u): def to_bytes(u):
return unicodedata.normalize("NFC", u).encode("utf-8") return unicodedata.normalize("NFC", u).encode("utf-8")
def bytes_to_hexstr(b): def bytes_to_hexstr(b):
assert isinstance(b, type(b"")) assert isinstance(b, type(b""))
hexstr = hexlify(b).decode("ascii") hexstr = hexlify(b).decode("ascii")
assert isinstance(hexstr, type(u"")) assert isinstance(hexstr, type(u""))
return hexstr return hexstr
def hexstr_to_bytes(hexstr): def hexstr_to_bytes(hexstr):
assert isinstance(hexstr, type(u"")) assert isinstance(hexstr, type(u""))
b = unhexlify(hexstr.encode("ascii")) b = unhexlify(hexstr.encode("ascii"))
assert isinstance(b, type(b"")) assert isinstance(b, type(b""))
return b return b
def dict_to_bytes(d): def dict_to_bytes(d):
assert isinstance(d, dict) assert isinstance(d, dict)
b = json.dumps(d).encode("utf-8") b = json.dumps(d).encode("utf-8")
assert isinstance(b, type(b"")) assert isinstance(b, type(b""))
return b return b
def bytes_to_dict(b): def bytes_to_dict(b):
assert isinstance(b, type(b"")) assert isinstance(b, type(b""))
d = json.loads(b.decode("utf-8")) d = json.loads(b.decode("utf-8"))
assert isinstance(d, dict) assert isinstance(d, dict)
return d return d
def estimate_free_space(target): def estimate_free_space(target):
# f_bfree is the blocks available to a root user. It might be more # f_bfree is the blocks available to a root user. It might be more
# accurate to use f_bavail (blocks available to non-root user), but we # accurate to use f_bavail (blocks available to non-root user), but we

View File

@ -1,19 +1,25 @@
from __future__ import print_function, absolute_import, unicode_literals from __future__ import absolute_import, print_function, unicode_literals
import os, sys
from attr import attrs, attrib import os
from zope.interface import implementer import sys
from attr import attrib, attrs
from twisted.python import failure from twisted.python import failure
from . import __version__ from zope.interface import implementer
from ._interfaces import IWormhole, IDeferredWormhole
from .util import bytes_to_hexstr
from .eventual import EventualQueue
from .observer import OneShotObserver, SequenceObserver
from .timing import DebugTiming
from .journal import ImmediateJournal
from ._boss import Boss from ._boss import Boss
from ._interfaces import IDeferredWormhole, IWormhole
from ._key import derive_key from ._key import derive_key
from .errors import NoKeyError, WormholeClosed from .errors import NoKeyError, WormholeClosed
from .util import to_bytes from .eventual import EventualQueue
from .journal import ImmediateJournal
from .observer import OneShotObserver, SequenceObserver
from .timing import DebugTiming
from .util import bytes_to_hexstr, to_bytes
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
# We can provide different APIs to different apps: # We can provide different APIs to different apps:
# * Deferreds # * Deferreds
@ -36,6 +42,7 @@ from .util import to_bytes
# wormhole(delegate=app, delegate_prefix="wormhole_", # wormhole(delegate=app, delegate_prefix="wormhole_",
# delegate_args=(args, kwargs)) # delegate_args=(args, kwargs))
@attrs @attrs
@implementer(IWormhole) @implementer(IWormhole)
class _DelegatedWormhole(object): class _DelegatedWormhole(object):
@ -51,16 +58,18 @@ class _DelegatedWormhole(object):
def allocate_code(self, code_length=2): def allocate_code(self, code_length=2):
self._boss.allocate_code(code_length) self._boss.allocate_code(code_length)
def input_code(self): def input_code(self):
return self._boss.input_code() return self._boss.input_code()
def set_code(self, code): def set_code(self, code):
self._boss.set_code(code) self._boss.set_code(code)
## def serialize(self): # def serialize(self):
## s = {"serialized_wormhole_version": 1, # s = {"serialized_wormhole_version": 1,
## "boss": self._boss.serialize(), # "boss": self._boss.serialize(),
## } # }
## return s # return s
def send_message(self, plaintext): def send_message(self, plaintext):
self._boss.send(plaintext) self._boss.send(plaintext)
@ -72,34 +81,45 @@ class _DelegatedWormhole(object):
cannot be called until when_verifier() has fired, nor after close() cannot be called until when_verifier() has fired, nor after close()
was called. was called.
""" """
if not isinstance(purpose, type("")): raise TypeError(type(purpose)) if not isinstance(purpose, type("")):
if not self._key: raise NoKeyError() raise TypeError(type(purpose))
if not self._key:
raise NoKeyError()
return derive_key(self._key, to_bytes(purpose), length) return derive_key(self._key, to_bytes(purpose), length)
def close(self): def close(self):
self._boss.close() self._boss.close()
def debug_set_trace(self, client_name, which="B N M S O K SK R RC L C T", def debug_set_trace(self,
client_name,
which="B N M S O K SK R RC L C T",
file=sys.stderr): file=sys.stderr):
self._boss._set_trace(client_name, which, file) self._boss._set_trace(client_name, which, file)
# from below # from below
def got_welcome(self, welcome): def got_welcome(self, welcome):
self._delegate.wormhole_got_welcome(welcome) self._delegate.wormhole_got_welcome(welcome)
def got_code(self, code): def got_code(self, code):
self._delegate.wormhole_got_code(code) self._delegate.wormhole_got_code(code)
def got_key(self, key): def got_key(self, key):
self._delegate.wormhole_got_unverified_key(key) self._delegate.wormhole_got_unverified_key(key)
self._key = key # for derive_key() self._key = key # for derive_key()
def got_verifier(self, verifier): def got_verifier(self, verifier):
self._delegate.wormhole_got_verifier(verifier) self._delegate.wormhole_got_verifier(verifier)
def got_versions(self, versions): def got_versions(self, versions):
self._delegate.wormhole_got_versions(versions) self._delegate.wormhole_got_versions(versions)
def received(self, plaintext): def received(self, plaintext):
self._delegate.wormhole_got_message(plaintext) self._delegate.wormhole_got_message(plaintext)
def closed(self, result): def closed(self, result):
self._delegate.wormhole_closed(result) self._delegate.wormhole_closed(result)
@implementer(IWormhole, IDeferredWormhole) @implementer(IWormhole, IDeferredWormhole)
class _DeferredWormhole(object): class _DeferredWormhole(object):
def __init__(self, eq): def __init__(self, eq):
@ -142,8 +162,10 @@ class _DeferredWormhole(object):
def allocate_code(self, code_length=2): def allocate_code(self, code_length=2):
self._boss.allocate_code(code_length) self._boss.allocate_code(code_length)
def input_code(self): def input_code(self):
return self._boss.input_code() return self._boss.input_code()
def set_code(self, code): def set_code(self, code):
self._boss.set_code(code) self._boss.set_code(code)
@ -159,20 +181,23 @@ class _DeferredWormhole(object):
cannot be called until when_verified() has fired, nor after close() cannot be called until when_verified() has fired, nor after close()
was called. was called.
""" """
if not isinstance(purpose, type("")): raise TypeError(type(purpose)) if not isinstance(purpose, type("")):
if not self._key: raise NoKeyError() raise TypeError(type(purpose))
if not self._key:
raise NoKeyError()
return derive_key(self._key, to_bytes(purpose), length) return derive_key(self._key, to_bytes(purpose), length)
def close(self): def close(self):
# fails with WormholeError unless we established a connection # fails with WormholeError unless we established a connection
# (state=="happy"). Fails with WrongPasswordError (a subclass of # (state=="happy"). Fails with WrongPasswordError (a subclass of
# WormholeError) if state=="scary". # WormholeError) if state=="scary".
d = self._closed_observer.when_fired() # maybe Failure d = self._closed_observer.when_fired() # maybe Failure
if not self._closed: if not self._closed:
self._boss.close() # only need to close if it wasn't already self._boss.close() # only need to close if it wasn't already
return d return d
def debug_set_trace(self, client_name, def debug_set_trace(self,
client_name,
which="B N M S O K SK R RC L A I C T", which="B N M S O K SK R RC L A I C T",
file=sys.stderr): file=sys.stderr):
self._boss._set_trace(client_name, which, file) self._boss._set_trace(client_name, which, file)
@ -180,14 +205,17 @@ class _DeferredWormhole(object):
# from below # from below
def got_welcome(self, welcome): def got_welcome(self, welcome):
self._welcome_observer.fire_if_not_fired(welcome) self._welcome_observer.fire_if_not_fired(welcome)
def got_code(self, code): def got_code(self, code):
self._code_observer.fire_if_not_fired(code) self._code_observer.fire_if_not_fired(code)
def got_key(self, key): def got_key(self, key):
self._key = key # for derive_key() self._key = key # for derive_key()
self._key_observer.fire_if_not_fired(key) self._key_observer.fire_if_not_fired(key)
def got_verifier(self, verifier): def got_verifier(self, verifier):
self._verifier_observer.fire_if_not_fired(verifier) self._verifier_observer.fire_if_not_fired(verifier)
def got_versions(self, versions): def got_versions(self, versions):
self._version_observer.fire_if_not_fired(versions) self._version_observer.fire_if_not_fired(versions)
@ -196,7 +224,7 @@ class _DeferredWormhole(object):
def closed(self, result): def closed(self, result):
self._closed = True self._closed = True
#print("closed", result, type(result), file=sys.stderr) # print("closed", result, type(result), file=sys.stderr)
if isinstance(result, Exception): if isinstance(result, Exception):
# everything pending gets an error, including close() # everything pending gets an error, including close()
f = failure.Failure(result) f = failure.Failure(result)
@ -215,12 +243,17 @@ class _DeferredWormhole(object):
self._received_observer.fire(f) self._received_observer.fire(f)
def create(appid, relay_url, reactor, # use keyword args for everything else def create(
versions={}, appid,
delegate=None, journal=None, tor=None, relay_url,
timing=None, reactor, # use keyword args for everything else
stderr=sys.stderr, versions={},
_eventual_queue=None): delegate=None,
journal=None,
tor=None,
timing=None,
stderr=sys.stderr,
_eventual_queue=None):
timing = timing or DebugTiming() timing = timing or DebugTiming()
side = bytes_to_hexstr(os.urandom(5)) side = bytes_to_hexstr(os.urandom(5))
journal = journal or ImmediateJournal() journal = journal or ImmediateJournal()
@ -229,27 +262,28 @@ def create(appid, relay_url, reactor, # use keyword args for everything else
w = _DelegatedWormhole(delegate) w = _DelegatedWormhole(delegate)
else: else:
w = _DeferredWormhole(eq) w = _DeferredWormhole(eq)
wormhole_versions = {} # will be used to indicate Wormhole capabilities wormhole_versions = {} # will be used to indicate Wormhole capabilities
wormhole_versions["app_versions"] = versions # app-specific capabilities wormhole_versions["app_versions"] = versions # app-specific capabilities
v = __version__ v = __version__
if isinstance(v, type(b"")): if isinstance(v, type(b"")):
v = v.decode("utf-8", errors="replace") v = v.decode("utf-8", errors="replace")
client_version = ("python", v) client_version = ("python", v)
b = Boss(w, side, relay_url, appid, wormhole_versions, client_version, b = Boss(w, side, relay_url, appid, wormhole_versions, client_version,
reactor, journal, tor, timing) reactor, journal, tor, timing)
w._set_boss(b) w._set_boss(b)
b.start() b.start()
return w return w
## def from_serialized(serialized, reactor, delegate,
## journal=None, tor=None, # def from_serialized(serialized, reactor, delegate,
## timing=None, stderr=sys.stderr): # journal=None, tor=None,
## assert serialized["serialized_wormhole_version"] == 1 # timing=None, stderr=sys.stderr):
## timing = timing or DebugTiming() # assert serialized["serialized_wormhole_version"] == 1
## w = _DelegatedWormhole(delegate) # timing = timing or DebugTiming()
## # now unpack state machines, including the SPAKE2 in Key # w = _DelegatedWormhole(delegate)
## b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing) # # now unpack state machines, including the SPAKE2 in Key
## w._set_boss(b) # b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing)
## b.start() # ?? # w._set_boss(b)
## raise NotImplemented # b.start() # ??
## # should the new Wormhole call got_code? only if it wasn't called before. # raise NotImplemented
# # should the new Wormhole call got_code? only if it wasn't called before.

View File

@ -1,12 +1,19 @@
import json import json
from twisted.internet.defer import inlineCallbacks, returnValue from twisted.internet.defer import inlineCallbacks, returnValue
from . import wormhole from . import wormhole
from .tor_manager import get_tor from .tor_manager import get_tor
@inlineCallbacks @inlineCallbacks
def receive(reactor, appid, relay_url, code, def receive(reactor,
use_tor=False, launch_tor=False, tor_control_port=None, appid,
relay_url,
code,
use_tor=False,
launch_tor=False,
tor_control_port=None,
on_code=None): on_code=None):
""" """
This is a convenience API which returns a Deferred that callbacks This is a convenience API which returns a Deferred that callbacks
@ -21,9 +28,11 @@ def receive(reactor, appid, relay_url, code,
:param unicode code: a pre-existing code to use, or None :param unicode code: a pre-existing code to use, or None
:param bool use_tor: True if we should use Tor, False to not use it (None for default) :param bool use_tor: True if we should use Tor, False to not use it (None
for default)
:param on_code: if not None, this is called when we have a code (even if you passed in one explicitly) :param on_code: if not None, this is called when we have a code (even if
you passed in one explicitly)
:type on_code: single-argument callable :type on_code: single-argument callable
""" """
tor = None tor = None
@ -48,27 +57,33 @@ def receive(reactor, appid, relay_url, code,
data = json.loads(data.decode("utf-8")) data = json.loads(data.decode("utf-8"))
offer = data.get('offer', None) offer = data.get('offer', None)
if not offer: if not offer:
raise Exception( raise Exception("Do not understand response: {}".format(data))
"Do not understand response: {}".format(data)
)
msg = None msg = None
if 'message' in offer: if 'message' in offer:
msg = offer['message'] msg = offer['message']
wh.send_message(json.dumps({"answer": wh.send_message(
{"message_ack": "ok"}}).encode("utf-8")) json.dumps({
"answer": {
"message_ack": "ok"
}
}).encode("utf-8"))
else: else:
raise Exception( raise Exception("Unknown offer type: {}".format(offer.keys()))
"Unknown offer type: {}".format(offer.keys())
)
yield wh.close() yield wh.close()
returnValue(msg) returnValue(msg)
@inlineCallbacks @inlineCallbacks
def send(reactor, appid, relay_url, data, code, def send(reactor,
use_tor=False, launch_tor=False, tor_control_port=None, appid,
relay_url,
data,
code,
use_tor=False,
launch_tor=False,
tor_control_port=None,
on_code=None): on_code=None):
""" """
This is a convenience API which returns a Deferred that callbacks This is a convenience API which returns a Deferred that callbacks
@ -83,9 +98,12 @@ def send(reactor, appid, relay_url, data, code,
:param unicode code: a pre-existing code to use, or None :param unicode code: a pre-existing code to use, or None
:param bool use_tor: True if we should use Tor, False to not use it (None for default) :param bool use_tor: True if we should use Tor, False to not use it (None
for default)
:param on_code: if not None, this is called when we have a code (even if
you passed in one explicitly)
:param on_code: if not None, this is called when we have a code (even if you passed in one explicitly)
:type on_code: single-argument callable :type on_code: single-argument callable
""" """
tor = None tor = None
@ -104,13 +122,7 @@ def send(reactor, appid, relay_url, data, code,
if on_code: if on_code:
on_code(code) on_code(code)
wh.send_message( wh.send_message(json.dumps({"offer": {"message": data}}).encode("utf-8"))
json.dumps({
"offer": {
"message": data
}
}).encode("utf-8")
)
data = yield wh.get_message() data = yield wh.get_message()
data = json.loads(data.decode("utf-8")) data = json.loads(data.decode("utf-8"))
answer = data.get('answer', None) answer = data.get('answer', None)
@ -118,6 +130,4 @@ def send(reactor, appid, relay_url, data, code,
if answer: if answer:
returnValue(None) returnValue(None)
else: else:
raise Exception( raise Exception("Unknown answer: {}".format(data))
"Unknown answer: {}".format(data)
)