Make code pep-8 compliant
This commit is contained in:
		
							parent
							
								
									355cc01aee
								
							
						
					
					
						commit
						12dcd6a184
					
				|  | @ -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 .wormhole import create, __version__ | ||||
| 
 | ||||
| __all__ = ["create", "input_with_completion", "__version__"] | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| from .cli import cli | ||||
| 
 | ||||
| if __name__ != "__main__": | ||||
|     raise ImportError('this module should not be imported') | ||||
| 
 | ||||
| 
 | ||||
| from .cli import cli | ||||
| 
 | ||||
| 
 | ||||
| cli.wormhole() | ||||
|  |  | |||
|  | @ -1,56 +1,78 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import provides | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IAllocator) | ||||
| class Allocator(object): | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) | ||||
|         self._C = _interfaces.ICode(code) | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0A_idle(self): pass # pragma: no cover | ||||
|     def S0A_idle(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S0B_idle_connected(self): pass # pragma: no cover | ||||
|     def S0B_idle_connected(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1A_allocating(self): pass # pragma: no cover | ||||
|     def S1A_allocating(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1B_allocating_connected(self): pass # pragma: no cover | ||||
|     def S1B_allocating_connected(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2_done(self): pass # pragma: no cover | ||||
|     def S2_done(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from Code | ||||
|     @m.input() | ||||
|     def allocate(self, length, wordlist): pass | ||||
|     def allocate(self, length, wordlist): | ||||
|         pass | ||||
| 
 | ||||
|     # from RendezvousConnector | ||||
|     @m.input() | ||||
|     def connected(self): pass | ||||
|     def connected(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def lost(self): pass | ||||
|     def lost(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_allocated(self, nameplate): pass | ||||
|     def rx_allocated(self, nameplate): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def stash(self, length, wordlist): | ||||
|         self._length = length | ||||
|         self._wordlist = _interfaces.IWordlist(wordlist) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def stash_and_RC_rx_allocate(self, length, wordlist): | ||||
|         self._length = length | ||||
|         self._wordlist = _interfaces.IWordlist(wordlist) | ||||
|         self._RC.tx_allocate() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_allocate(self): | ||||
|         self._RC.tx_allocate() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def build_and_notify(self, nameplate): | ||||
|         words = self._wordlist.choose_words(self._length) | ||||
|  | @ -61,15 +83,17 @@ class Allocator(object): | |||
|     S0B_idle_connected.upon(lost, enter=S0A_idle, outputs=[]) | ||||
| 
 | ||||
|     S0A_idle.upon(allocate, enter=S1A_allocating, outputs=[stash]) | ||||
|     S0B_idle_connected.upon(allocate, enter=S1B_allocating_connected, | ||||
|                             outputs=[stash_and_RC_rx_allocate]) | ||||
|     S0B_idle_connected.upon( | ||||
|         allocate, | ||||
|         enter=S1B_allocating_connected, | ||||
|         outputs=[stash_and_RC_rx_allocate]) | ||||
| 
 | ||||
|     S1A_allocating.upon(connected, enter=S1B_allocating_connected, | ||||
|                         outputs=[RC_tx_allocate]) | ||||
|     S1A_allocating.upon( | ||||
|         connected, enter=S1B_allocating_connected, outputs=[RC_tx_allocate]) | ||||
|     S1B_allocating_connected.upon(lost, enter=S1A_allocating, outputs=[]) | ||||
| 
 | ||||
|     S1B_allocating_connected.upon(rx_allocated, enter=S2_done, | ||||
|                                   outputs=[build_and_notify]) | ||||
|     S1B_allocating_connected.upon( | ||||
|         rx_allocated, enter=S2_done, outputs=[build_and_notify]) | ||||
| 
 | ||||
|     S2_done.upon(connected, enter=S2_done, outputs=[]) | ||||
|     S2_done.upon(lost, enter=S2_done, outputs=[]) | ||||
|  |  | |||
|  | @ -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 six | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| from attr.validators import provides, instance_of, optional | ||||
| from twisted.python import log | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import instance_of, optional, provides | ||||
| from automat import MethodicalMachine | ||||
| from twisted.python import log | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| from ._nameplate import Nameplate | ||||
| from ._mailbox import Mailbox | ||||
| from ._send import Send | ||||
| from ._order import Order | ||||
| from ._allocator import Allocator | ||||
| from ._code import Code, validate_code | ||||
| from ._input import Input | ||||
| 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 ._rendezvous import RendezvousConnector | ||||
| from ._lister import Lister | ||||
| from ._allocator import Allocator | ||||
| from ._input import Input | ||||
| from ._code import Code, validate_code | ||||
| from ._send import Send | ||||
| from ._terminator import Terminator | ||||
| from ._wordlist import PGPWordList | ||||
| from .errors import (ServerError, LonelyError, WrongPasswordError, | ||||
|                      OnlyOneCodeError, _UnknownPhaseError, WelcomeError) | ||||
| from .errors import (LonelyError, OnlyOneCodeError, ServerError, WelcomeError, | ||||
|                      WrongPasswordError, _UnknownPhaseError) | ||||
| from .util import bytes_to_dict | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IBoss) | ||||
| class Boss(object): | ||||
|  | @ -38,7 +42,8 @@ class Boss(object): | |||
|     _tor = attrib(validator=optional(provides(_interfaces.ITorManager))) | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._build_workers() | ||||
|  | @ -52,9 +57,8 @@ class Boss(object): | |||
|         self._K = Key(self._appid, self._versions, self._side, self._timing) | ||||
|         self._R = Receive(self._side, self._timing) | ||||
|         self._RC = RendezvousConnector(self._url, self._appid, self._side, | ||||
|                                        self._reactor, self._journal, | ||||
|                                        self._tor, self._timing, | ||||
|                                        self._client_version) | ||||
|                                        self._reactor, self._journal, self._tor, | ||||
|                                        self._timing, self._client_version) | ||||
|         self._L = Lister(self._timing) | ||||
|         self._A = Allocator(self._timing) | ||||
|         self._I = Input(self._timing) | ||||
|  | @ -78,7 +82,7 @@ class Boss(object): | |||
|         self._did_start_code = False | ||||
|         self._next_tx_phase = 0 | ||||
|         self._next_rx_phase = 0 | ||||
|         self._rx_phases = {} # phase -> plaintext | ||||
|         self._rx_phases = {}  # phase -> plaintext | ||||
| 
 | ||||
|         self._result = "empty" | ||||
| 
 | ||||
|  | @ -86,32 +90,45 @@ class Boss(object): | |||
|     def start(self): | ||||
|         self._RC.start() | ||||
| 
 | ||||
|     def _print_trace(self, old_state, input, new_state, | ||||
|                      client_name, machine, file): | ||||
|     def _print_trace(self, old_state, input, new_state, client_name, machine, | ||||
|                      file): | ||||
|         if new_state: | ||||
|             print("%s.%s[%s].%s -> [%s]" % | ||||
|                   (client_name, machine, old_state, input, | ||||
|                    new_state), file=file) | ||||
|             print( | ||||
|                 "%s.%s[%s].%s -> [%s]" % (client_name, machine, old_state, | ||||
|                                           input, new_state), | ||||
|                 file=file) | ||||
|         else: | ||||
|             # the RendezvousConnector emits message events as if | ||||
|             # they were state transitions, except that old_state | ||||
|             # and new_state are empty strings. "input" is one of | ||||
|             # R.connected, R.rx(type phase+side), R.tx(type | ||||
|             # phase), R.lost . | ||||
|             print("%s.%s.%s" % (client_name, machine, input), | ||||
|                   file=file) | ||||
|             print("%s.%s.%s" % (client_name, machine, input), file=file) | ||||
|         file.flush() | ||||
| 
 | ||||
|         def output_tracer(output): | ||||
|             print(" %s.%s.%s()" % (client_name, machine, output), | ||||
|                   file=file) | ||||
|             print(" %s.%s.%s()" % (client_name, machine, output), file=file) | ||||
|             file.flush() | ||||
| 
 | ||||
|         return output_tracer | ||||
| 
 | ||||
|     def _set_trace(self, client_name, which, file): | ||||
|         names = {"B": self, "N": self._N, "M": self._M, "S": self._S, | ||||
|                  "O": self._O, "K": self._K, "SK": self._K._SK, "R": self._R, | ||||
|                  "RC": self._RC, "L": self._L, "A": self._A, "I": self._I, | ||||
|                  "C": self._C, "T": self._T} | ||||
|         names = { | ||||
|             "B": self, | ||||
|             "N": self._N, | ||||
|             "M": self._M, | ||||
|             "S": self._S, | ||||
|             "O": self._O, | ||||
|             "K": self._K, | ||||
|             "SK": self._K._SK, | ||||
|             "R": self._R, | ||||
|             "RC": self._RC, | ||||
|             "L": self._L, | ||||
|             "A": self._A, | ||||
|             "I": self._I, | ||||
|             "C": self._C, | ||||
|             "T": self._T | ||||
|         } | ||||
|         for machine in which.split(): | ||||
|             t = (lambda old_state, input, new_state, machine=machine: | ||||
|                  self._print_trace(old_state, input, new_state, | ||||
|  | @ -121,21 +138,30 @@ class Boss(object): | |||
|             if machine == "I": | ||||
|                 self._I.set_debug(t) | ||||
| 
 | ||||
|     ## def serialize(self): | ||||
|     ##     raise NotImplemented | ||||
|     # def serialize(self): | ||||
|     #     raise NotImplemented | ||||
| 
 | ||||
|     # and these are the state-machine transition functions, which don't take | ||||
|     # args | ||||
|     @m.state(initial=True) | ||||
|     def S0_empty(self): pass # pragma: no cover | ||||
|     def S0_empty(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1_lonely(self): pass # pragma: no cover | ||||
|     def S1_lonely(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2_happy(self): pass # pragma: no cover | ||||
|     def S2_happy(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S3_closing(self): pass # pragma: no cover | ||||
|     def S3_closing(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state(terminal=True) | ||||
|     def S4_closed(self): pass # pragma: no cover | ||||
|     def S4_closed(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from the Wormhole | ||||
| 
 | ||||
|  | @ -155,23 +181,28 @@ class Boss(object): | |||
|             raise OnlyOneCodeError() | ||||
|         self._did_start_code = True | ||||
|         return self._C.input_code() | ||||
| 
 | ||||
|     def allocate_code(self, code_length): | ||||
|         if self._did_start_code: | ||||
|             raise OnlyOneCodeError() | ||||
|         self._did_start_code = True | ||||
|         wl = PGPWordList() | ||||
|         self._C.allocate_code(code_length, wl) | ||||
| 
 | ||||
|     def set_code(self, code): | ||||
|         validate_code(code) # can raise KeyFormatError | ||||
|         validate_code(code)  # can raise KeyFormatError | ||||
|         if self._did_start_code: | ||||
|             raise OnlyOneCodeError() | ||||
|         self._did_start_code = True | ||||
|         self._C.set_code(code) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def send(self, plaintext): pass | ||||
|     def send(self, plaintext): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def close(self): pass | ||||
|     def close(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from RendezvousConnector: | ||||
|     # * "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 | ||||
|             # middle of processing the rx_welcome input, and I wasn't sure | ||||
|             # 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: | ||||
|             self.rx_unwelcome(welcome_error) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_unwelcome(self, welcome_error): pass | ||||
|     def rx_unwelcome(self, welcome_error): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_error(self, errmsg, orig): pass | ||||
|     def rx_error(self, errmsg, orig): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def error(self, err): pass | ||||
|     def error(self, err): | ||||
|         pass | ||||
| 
 | ||||
|     # from Code (provoked by input/allocate/set_code) | ||||
|     @m.input() | ||||
|     def got_code(self, code): pass | ||||
|     def got_code(self, code): | ||||
|         pass | ||||
| 
 | ||||
|     # Key sends (got_key, scared) | ||||
|     # Receive sends (got_message, happy, got_verifier, scared) | ||||
|     @m.input() | ||||
|     def happy(self): pass | ||||
|     def happy(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def scared(self): pass | ||||
|     def scared(self): | ||||
|         pass | ||||
| 
 | ||||
|     def got_message(self, phase, plaintext): | ||||
|         assert isinstance(phase, type("")), type(phase) | ||||
|  | @ -222,22 +263,32 @@ class Boss(object): | |||
|             # Ignore unrecognized phases, for forwards-compatibility. Use | ||||
|             # log.err so tests will catch surprises. | ||||
|             log.err(_UnknownPhaseError("received unknown phase '%s'" % phase)) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def _got_version(self, plaintext): pass | ||||
|     def _got_version(self, plaintext): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def _got_phase(self, phase, plaintext): pass | ||||
|     def _got_phase(self, phase, plaintext): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_key(self, key): pass | ||||
|     def got_key(self, key): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_verifier(self, verifier): pass | ||||
|     def got_verifier(self, verifier): | ||||
|         pass | ||||
| 
 | ||||
|     # Terminator sends closed | ||||
|     @m.input() | ||||
|     def closed(self): pass | ||||
|     def closed(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def do_got_code(self, code): | ||||
|         self._W.got_code(code) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def process_version(self, plaintext): | ||||
|         # most of this is wormhole-to-wormhole, ignored for now | ||||
|  | @ -256,21 +307,25 @@ class Boss(object): | |||
| 
 | ||||
|     @m.output() | ||||
|     def close_unwelcome(self, welcome_error): | ||||
|         #assert isinstance(err, WelcomeError) | ||||
|         # assert isinstance(err, WelcomeError) | ||||
|         self._result = welcome_error | ||||
|         self._T.close("unwelcome") | ||||
| 
 | ||||
|     @m.output() | ||||
|     def close_error(self, errmsg, orig): | ||||
|         self._result = ServerError(errmsg) | ||||
|         self._T.close("errory") | ||||
| 
 | ||||
|     @m.output() | ||||
|     def close_scared(self): | ||||
|         self._result = WrongPasswordError() | ||||
|         self._T.close("scary") | ||||
| 
 | ||||
|     @m.output() | ||||
|     def close_lonely(self): | ||||
|         self._result = LonelyError() | ||||
|         self._T.close("lonely") | ||||
| 
 | ||||
|     @m.output() | ||||
|     def close_happy(self): | ||||
|         self._result = "happy" | ||||
|  | @ -279,9 +334,11 @@ class Boss(object): | |||
|     @m.output() | ||||
|     def W_got_key(self, key): | ||||
|         self._W.got_key(key) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def W_got_verifier(self, verifier): | ||||
|         self._W.got_verifier(verifier) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def W_received(self, phase, plaintext): | ||||
|         assert isinstance(phase, six.integer_types), type(phase) | ||||
|  | @ -293,7 +350,7 @@ class Boss(object): | |||
| 
 | ||||
|     @m.output() | ||||
|     def W_close_with_error(self, err): | ||||
|         self._result = err # exception | ||||
|         self._result = err  # exception | ||||
|         self._W.closed(self._result) | ||||
| 
 | ||||
|     @m.output() | ||||
|  |  | |||
|  | @ -7,21 +7,25 @@ from . import _interfaces | |||
| from ._nameplate import validate_nameplate | ||||
| from .errors import KeyFormatError | ||||
| 
 | ||||
| 
 | ||||
| def validate_code(code): | ||||
|     if ' ' in code: | ||||
|         raise KeyFormatError("Code '%s' contains spaces." % code) | ||||
|     nameplate = code.split("-", 2)[0] | ||||
|     validate_nameplate(nameplate) # can raise KeyFormatError | ||||
|     validate_nameplate(nameplate)  # can raise KeyFormatError | ||||
| 
 | ||||
| 
 | ||||
| def first(outputs): | ||||
|     return list(outputs)[0] | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.ICode) | ||||
| class Code(object): | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._B = _interfaces.IBoss(boss) | ||||
|  | @ -31,36 +35,55 @@ class Code(object): | |||
|         self._I = _interfaces.IInput(input) | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0_idle(self): pass # pragma: no cover | ||||
|     def S0_idle(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1_inputting_nameplate(self): pass # pragma: no cover | ||||
|     def S1_inputting_nameplate(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2_inputting_words(self): pass # pragma: no cover | ||||
|     def S2_inputting_words(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S3_allocating(self): pass # pragma: no cover | ||||
|     def S3_allocating(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S4_known(self): pass # pragma: no cover | ||||
|     def S4_known(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from App | ||||
|     @m.input() | ||||
|     def allocate_code(self, length, wordlist): pass | ||||
|     def allocate_code(self, length, wordlist): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def input_code(self): pass | ||||
|     def input_code(self): | ||||
|         pass | ||||
| 
 | ||||
|     def set_code(self, code): | ||||
|         validate_code(code) # can raise KeyFormatError | ||||
|         validate_code(code)  # can raise KeyFormatError | ||||
|         self._set_code(code) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def _set_code(self, code): pass | ||||
|     def _set_code(self, code): | ||||
|         pass | ||||
| 
 | ||||
|     # from Allocator | ||||
|     @m.input() | ||||
|     def allocated(self, nameplate, code): pass | ||||
|     def allocated(self, nameplate, code): | ||||
|         pass | ||||
| 
 | ||||
|     # from Input | ||||
|     @m.input() | ||||
|     def got_nameplate(self, nameplate): pass | ||||
|     def got_nameplate(self, nameplate): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def finished_input(self, code): pass | ||||
|     def finished_input(self, code): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def do_set_code(self, code): | ||||
|  | @ -72,9 +95,11 @@ class Code(object): | |||
|     @m.output() | ||||
|     def do_start_input(self): | ||||
|         return self._I.start() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def do_middle_input(self, nameplate): | ||||
|         self._N.set_nameplate(nameplate) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def do_finish_input(self, code): | ||||
|         self._B.got_code(code) | ||||
|  | @ -83,19 +108,24 @@ class Code(object): | |||
|     @m.output() | ||||
|     def do_start_allocate(self, length, wordlist): | ||||
|         self._A.allocate(length, wordlist) | ||||
| 
 | ||||
|     @m.output() | ||||
|     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._B.got_code(code) | ||||
|         self._K.got_code(code) | ||||
| 
 | ||||
|     S0_idle.upon(_set_code, enter=S4_known, outputs=[do_set_code]) | ||||
|     S0_idle.upon(input_code, enter=S1_inputting_nameplate, | ||||
|                  outputs=[do_start_input], collector=first) | ||||
|     S1_inputting_nameplate.upon(got_nameplate, enter=S2_inputting_words, | ||||
|                                 outputs=[do_middle_input]) | ||||
|     S2_inputting_words.upon(finished_input, enter=S4_known, | ||||
|                             outputs=[do_finish_input]) | ||||
|     S0_idle.upon(allocate_code, enter=S3_allocating, outputs=[do_start_allocate]) | ||||
|     S0_idle.upon( | ||||
|         input_code, | ||||
|         enter=S1_inputting_nameplate, | ||||
|         outputs=[do_start_input], | ||||
|         collector=first) | ||||
|     S1_inputting_nameplate.upon( | ||||
|         got_nameplate, enter=S2_inputting_words, outputs=[do_middle_input]) | ||||
|     S2_inputting_words.upon( | ||||
|         finished_input, enter=S4_known, 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]) | ||||
|  |  | |||
|  | @ -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 | ||||
| # non-main thread. _rlcompleter.py is the only internal Wormhole code that | ||||
| # deliberately creates a new thread. | ||||
| import threading | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import provides | ||||
| from twisted.internet import defer | ||||
| from automat import MethodicalMachine | ||||
| from twisted.internet import defer | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces, errors | ||||
| from ._nameplate import validate_nameplate | ||||
| 
 | ||||
| 
 | ||||
| def first(outputs): | ||||
|     return list(outputs)[0] | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IInput) | ||||
| class Input(object): | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._all_nameplates = set() | ||||
|  | @ -30,7 +36,8 @@ class Input(object): | |||
| 
 | ||||
|     def set_debug(self, f): | ||||
|         self._trace = f | ||||
|     def _debug(self, what): # pragma: no cover | ||||
| 
 | ||||
|     def _debug(self, what):  # pragma: no cover | ||||
|         if self._trace: | ||||
|             self._trace(old_state="", input=what, new_state="") | ||||
| 
 | ||||
|  | @ -46,55 +53,80 @@ class Input(object): | |||
|         return d | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0_idle(self): pass # pragma: no cover | ||||
|     def S0_idle(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1_typing_nameplate(self): pass # pragma: no cover | ||||
|     def S1_typing_nameplate(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @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() | ||||
|     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) | ||||
|     def S4_done(self): pass # pragma: no cover | ||||
|     def S4_done(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from Code | ||||
|     @m.input() | ||||
|     def start(self): pass | ||||
|     def start(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from Lister | ||||
|     @m.input() | ||||
|     def got_nameplates(self, all_nameplates): pass | ||||
|     def got_nameplates(self, all_nameplates): | ||||
|         pass | ||||
| 
 | ||||
|     # from Nameplate | ||||
|     @m.input() | ||||
|     def got_wordlist(self, wordlist): pass | ||||
|     def got_wordlist(self, wordlist): | ||||
|         pass | ||||
| 
 | ||||
|     # API provided to app as ICodeInputHelper | ||||
|     @m.input() | ||||
|     def refresh_nameplates(self): pass | ||||
|     def refresh_nameplates(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def get_nameplate_completions(self, prefix): pass | ||||
|     def get_nameplate_completions(self, prefix): | ||||
|         pass | ||||
| 
 | ||||
|     def choose_nameplate(self, nameplate): | ||||
|         validate_nameplate(nameplate) # can raise KeyFormatError | ||||
|         validate_nameplate(nameplate)  # can raise KeyFormatError | ||||
|         self._choose_nameplate(nameplate) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def _choose_nameplate(self, nameplate): pass | ||||
|     def _choose_nameplate(self, nameplate): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def get_word_completions(self, prefix): pass | ||||
|     def get_word_completions(self, prefix): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def choose_words(self, words): pass | ||||
|     def choose_words(self, words): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def do_start(self): | ||||
|         self._start_timing = self._timing.add("input code", waiting="user") | ||||
|         self._L.refresh() | ||||
|         return Helper(self) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def do_refresh(self): | ||||
|         self._L.refresh() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_nameplates(self, all_nameplates): | ||||
|         # we get a set of nameplate id strings | ||||
|         self._all_nameplates = all_nameplates | ||||
| 
 | ||||
|     @m.output() | ||||
|     def _get_nameplate_completions(self, prefix): | ||||
|         completions = set() | ||||
|  | @ -102,17 +134,20 @@ class Input(object): | |||
|             if nameplate.startswith(prefix): | ||||
|                 # TODO: it's a little weird that Input is responsible for the | ||||
|                 # hyphen on nameplates, but WordList owns it for words | ||||
|                 completions.add(nameplate+"-") | ||||
|                 completions.add(nameplate + "-") | ||||
|         return completions | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_all_nameplates(self, nameplate): | ||||
|         self._nameplate = nameplate | ||||
|         self._C.got_nameplate(nameplate) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_wordlist(self, wordlist): | ||||
|         from ._rlcompleter import debug | ||||
|         debug("  -record_wordlist") | ||||
|         self._wordlist = wordlist | ||||
| 
 | ||||
|     @m.output() | ||||
|     def notify_wordlist_waiters(self, wordlist): | ||||
|         while self._wordlist_waiters: | ||||
|  | @ -122,6 +157,7 @@ class Input(object): | |||
|     @m.output() | ||||
|     def no_word_completions(self, prefix): | ||||
|         return set() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def _get_word_completions(self, prefix): | ||||
|         assert self._wordlist | ||||
|  | @ -130,21 +166,27 @@ class Input(object): | |||
|     @m.output() | ||||
|     def raise_must_choose_nameplate1(self, prefix): | ||||
|         raise errors.MustChooseNameplateFirstError() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def raise_must_choose_nameplate2(self, words): | ||||
|         raise errors.MustChooseNameplateFirstError() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def raise_already_chose_nameplate1(self): | ||||
|         raise errors.AlreadyChoseNameplateError() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def raise_already_chose_nameplate2(self, prefix): | ||||
|         raise errors.AlreadyChoseNameplateError() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def raise_already_chose_nameplate3(self, nameplate): | ||||
|         raise errors.AlreadyChoseNameplateError() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def raise_already_chose_words1(self, prefix): | ||||
|         raise errors.AlreadyChoseWordsError() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def raise_already_chose_words2(self, words): | ||||
|         raise errors.AlreadyChoseWordsError() | ||||
|  | @ -155,88 +197,110 @@ class Input(object): | |||
|         self._start_timing.finish() | ||||
|         self._C.finished_input(code) | ||||
| 
 | ||||
|     S0_idle.upon(start, enter=S1_typing_nameplate, | ||||
|                  outputs=[do_start], collector=first) | ||||
|     S0_idle.upon( | ||||
|         start, enter=S1_typing_nameplate, outputs=[do_start], collector=first) | ||||
|     # wormholes that don't use input_code (i.e. they use allocate_code or | ||||
|     # generate_code) will never start() us, but Nameplate will give us a | ||||
|     # wordlist anyways (as soon as the nameplate is claimed), so handle it. | ||||
|     S0_idle.upon(got_wordlist, enter=S0_idle, outputs=[record_wordlist, | ||||
|                                                        notify_wordlist_waiters]) | ||||
|     S1_typing_nameplate.upon(got_nameplates, enter=S1_typing_nameplate, | ||||
|                              outputs=[record_nameplates]) | ||||
|     S0_idle.upon( | ||||
|         got_wordlist, | ||||
|         enter=S0_idle, | ||||
|         outputs=[record_wordlist, notify_wordlist_waiters]) | ||||
|     S1_typing_nameplate.upon( | ||||
|         got_nameplates, enter=S1_typing_nameplate, outputs=[record_nameplates]) | ||||
|     # but wormholes that *do* use input_code should not get got_wordlist | ||||
|     # until after we tell Code that we got_nameplate, which is the earliest | ||||
|     # it can be claimed | ||||
|     S1_typing_nameplate.upon(refresh_nameplates, enter=S1_typing_nameplate, | ||||
|                              outputs=[do_refresh]) | ||||
|     S1_typing_nameplate.upon(get_nameplate_completions, | ||||
|                              enter=S1_typing_nameplate, | ||||
|                              outputs=[_get_nameplate_completions], | ||||
|                              collector=first) | ||||
|     S1_typing_nameplate.upon(_choose_nameplate, enter=S2_typing_code_no_wordlist, | ||||
|                              outputs=[record_all_nameplates]) | ||||
|     S1_typing_nameplate.upon(get_word_completions, | ||||
|                              enter=S1_typing_nameplate, | ||||
|                              outputs=[raise_must_choose_nameplate1]) | ||||
|     S1_typing_nameplate.upon(choose_words, enter=S1_typing_nameplate, | ||||
|                              outputs=[raise_must_choose_nameplate2]) | ||||
|     S1_typing_nameplate.upon( | ||||
|         refresh_nameplates, enter=S1_typing_nameplate, outputs=[do_refresh]) | ||||
|     S1_typing_nameplate.upon( | ||||
|         get_nameplate_completions, | ||||
|         enter=S1_typing_nameplate, | ||||
|         outputs=[_get_nameplate_completions], | ||||
|         collector=first) | ||||
|     S1_typing_nameplate.upon( | ||||
|         _choose_nameplate, | ||||
|         enter=S2_typing_code_no_wordlist, | ||||
|         outputs=[record_all_nameplates]) | ||||
|     S1_typing_nameplate.upon( | ||||
|         get_word_completions, | ||||
|         enter=S1_typing_nameplate, | ||||
|         outputs=[raise_must_choose_nameplate1]) | ||||
|     S1_typing_nameplate.upon( | ||||
|         choose_words, | ||||
|         enter=S1_typing_nameplate, | ||||
|         outputs=[raise_must_choose_nameplate2]) | ||||
| 
 | ||||
|     S2_typing_code_no_wordlist.upon(got_nameplates, | ||||
|                                     enter=S2_typing_code_no_wordlist, outputs=[]) | ||||
|     S2_typing_code_no_wordlist.upon(got_wordlist, | ||||
|                                     enter=S3_typing_code_yes_wordlist, | ||||
|                                     outputs=[record_wordlist, | ||||
|                                              notify_wordlist_waiters]) | ||||
|     S2_typing_code_no_wordlist.upon(refresh_nameplates, | ||||
|                                     enter=S2_typing_code_no_wordlist, | ||||
|                                     outputs=[raise_already_chose_nameplate1]) | ||||
|     S2_typing_code_no_wordlist.upon(get_nameplate_completions, | ||||
|                                     enter=S2_typing_code_no_wordlist, | ||||
|                                     outputs=[raise_already_chose_nameplate2]) | ||||
|     S2_typing_code_no_wordlist.upon(_choose_nameplate, | ||||
|                                     enter=S2_typing_code_no_wordlist, | ||||
|                                     outputs=[raise_already_chose_nameplate3]) | ||||
|     S2_typing_code_no_wordlist.upon(get_word_completions, | ||||
|                                     enter=S2_typing_code_no_wordlist, | ||||
|                                     outputs=[no_word_completions], | ||||
|                                     collector=first) | ||||
|     S2_typing_code_no_wordlist.upon(choose_words, enter=S4_done, | ||||
|                                     outputs=[do_words]) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         got_nameplates, enter=S2_typing_code_no_wordlist, outputs=[]) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         got_wordlist, | ||||
|         enter=S3_typing_code_yes_wordlist, | ||||
|         outputs=[record_wordlist, notify_wordlist_waiters]) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         refresh_nameplates, | ||||
|         enter=S2_typing_code_no_wordlist, | ||||
|         outputs=[raise_already_chose_nameplate1]) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         get_nameplate_completions, | ||||
|         enter=S2_typing_code_no_wordlist, | ||||
|         outputs=[raise_already_chose_nameplate2]) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         _choose_nameplate, | ||||
|         enter=S2_typing_code_no_wordlist, | ||||
|         outputs=[raise_already_chose_nameplate3]) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         get_word_completions, | ||||
|         enter=S2_typing_code_no_wordlist, | ||||
|         outputs=[no_word_completions], | ||||
|         collector=first) | ||||
|     S2_typing_code_no_wordlist.upon( | ||||
|         choose_words, enter=S4_done, outputs=[do_words]) | ||||
| 
 | ||||
|     S3_typing_code_yes_wordlist.upon(got_nameplates, | ||||
|                                      enter=S3_typing_code_yes_wordlist, | ||||
|                                      outputs=[]) | ||||
|     S3_typing_code_yes_wordlist.upon( | ||||
|         got_nameplates, enter=S3_typing_code_yes_wordlist, outputs=[]) | ||||
|     # got_wordlist: should never happen | ||||
|     S3_typing_code_yes_wordlist.upon(refresh_nameplates, | ||||
|                                      enter=S3_typing_code_yes_wordlist, | ||||
|                                      outputs=[raise_already_chose_nameplate1]) | ||||
|     S3_typing_code_yes_wordlist.upon(get_nameplate_completions, | ||||
|                                      enter=S3_typing_code_yes_wordlist, | ||||
|                                      outputs=[raise_already_chose_nameplate2]) | ||||
|     S3_typing_code_yes_wordlist.upon(_choose_nameplate, | ||||
|                                      enter=S3_typing_code_yes_wordlist, | ||||
|                                      outputs=[raise_already_chose_nameplate3]) | ||||
|     S3_typing_code_yes_wordlist.upon(get_word_completions, | ||||
|                                      enter=S3_typing_code_yes_wordlist, | ||||
|                                      outputs=[_get_word_completions], | ||||
|                                      collector=first) | ||||
|     S3_typing_code_yes_wordlist.upon(choose_words, enter=S4_done, | ||||
|                                      outputs=[do_words]) | ||||
|     S3_typing_code_yes_wordlist.upon( | ||||
|         refresh_nameplates, | ||||
|         enter=S3_typing_code_yes_wordlist, | ||||
|         outputs=[raise_already_chose_nameplate1]) | ||||
|     S3_typing_code_yes_wordlist.upon( | ||||
|         get_nameplate_completions, | ||||
|         enter=S3_typing_code_yes_wordlist, | ||||
|         outputs=[raise_already_chose_nameplate2]) | ||||
|     S3_typing_code_yes_wordlist.upon( | ||||
|         _choose_nameplate, | ||||
|         enter=S3_typing_code_yes_wordlist, | ||||
|         outputs=[raise_already_chose_nameplate3]) | ||||
|     S3_typing_code_yes_wordlist.upon( | ||||
|         get_word_completions, | ||||
|         enter=S3_typing_code_yes_wordlist, | ||||
|         outputs=[_get_word_completions], | ||||
|         collector=first) | ||||
|     S3_typing_code_yes_wordlist.upon( | ||||
|         choose_words, enter=S4_done, outputs=[do_words]) | ||||
| 
 | ||||
|     S4_done.upon(got_nameplates, enter=S4_done, outputs=[]) | ||||
|     S4_done.upon(got_wordlist, enter=S4_done, outputs=[]) | ||||
|     S4_done.upon(refresh_nameplates, | ||||
|                  enter=S4_done, | ||||
|                  outputs=[raise_already_chose_nameplate1]) | ||||
|     S4_done.upon(get_nameplate_completions, | ||||
|                  enter=S4_done, | ||||
|                  outputs=[raise_already_chose_nameplate2]) | ||||
|     S4_done.upon(_choose_nameplate, enter=S4_done, | ||||
|                  outputs=[raise_already_chose_nameplate3]) | ||||
|     S4_done.upon(get_word_completions, enter=S4_done, | ||||
|                  outputs=[raise_already_chose_words1]) | ||||
|     S4_done.upon(choose_words, enter=S4_done, | ||||
|                  outputs=[raise_already_chose_words2]) | ||||
|     S4_done.upon( | ||||
|         refresh_nameplates, | ||||
|         enter=S4_done, | ||||
|         outputs=[raise_already_chose_nameplate1]) | ||||
|     S4_done.upon( | ||||
|         get_nameplate_completions, | ||||
|         enter=S4_done, | ||||
|         outputs=[raise_already_chose_nameplate2]) | ||||
|     S4_done.upon( | ||||
|         _choose_nameplate, | ||||
|         enter=S4_done, | ||||
|         outputs=[raise_already_chose_nameplate3]) | ||||
|     S4_done.upon( | ||||
|         get_word_completions, | ||||
|         enter=S4_done, | ||||
|         outputs=[raise_already_chose_words1]) | ||||
|     S4_done.upon( | ||||
|         choose_words, enter=S4_done, outputs=[raise_already_chose_words2]) | ||||
| 
 | ||||
| 
 | ||||
| # we only expose the Helper to application code, not _Input | ||||
| @attrs | ||||
|  | @ -250,20 +314,25 @@ class Helper(object): | |||
|     def refresh_nameplates(self): | ||||
|         assert threading.current_thread().ident == self._main_thread | ||||
|         self._input.refresh_nameplates() | ||||
| 
 | ||||
|     def get_nameplate_completions(self, prefix): | ||||
|         assert threading.current_thread().ident == self._main_thread | ||||
|         return self._input.get_nameplate_completions(prefix) | ||||
| 
 | ||||
|     def choose_nameplate(self, nameplate): | ||||
|         assert threading.current_thread().ident == self._main_thread | ||||
|         self._input._debug("I.choose_nameplate") | ||||
|         self._input.choose_nameplate(nameplate) | ||||
|         self._input._debug("I.choose_nameplate finished") | ||||
| 
 | ||||
|     def when_wordlist_is_available(self): | ||||
|         assert threading.current_thread().ident == self._main_thread | ||||
|         return self._input.when_wordlist_is_available() | ||||
| 
 | ||||
|     def get_word_completions(self, prefix): | ||||
|         assert threading.current_thread().ident == self._main_thread | ||||
|         return self._input.get_word_completions(prefix) | ||||
| 
 | ||||
|     def choose_words(self, words): | ||||
|         assert threading.current_thread().ident == self._main_thread | ||||
|         self._input._debug("I.choose_words") | ||||
|  |  | |||
|  | @ -3,64 +3,105 @@ from zope.interface import Interface | |||
| # These interfaces are private: we use them as markers to detect | ||||
| # swapped argument bugs in the various .wire() calls | ||||
| 
 | ||||
| 
 | ||||
| class IWormhole(Interface): | ||||
|     """Internal: this contains the methods invoked 'from below'.""" | ||||
| 
 | ||||
|     def got_welcome(welcome): | ||||
|         pass | ||||
| 
 | ||||
|     def got_code(code): | ||||
|         pass | ||||
| 
 | ||||
|     def got_key(key): | ||||
|         pass | ||||
| 
 | ||||
|     def got_verifier(verifier): | ||||
|         pass | ||||
| 
 | ||||
|     def got_versions(versions): | ||||
|         pass | ||||
| 
 | ||||
|     def received(plaintext): | ||||
|         pass | ||||
| 
 | ||||
|     def closed(result): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
| class IBoss(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class INameplate(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IMailbox(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ISend(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IOrder(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IKey(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IReceive(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IRendezvousConnector(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ILister(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ICode(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IInput(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IAllocator(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ITerminator(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ITiming(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class ITorManager(Interface): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class IWordlist(Interface): | ||||
|     def choose_words(length): | ||||
|         """Randomly select LENGTH words, join them with hyphens, return the | ||||
|         result.""" | ||||
| 
 | ||||
|     def get_completions(prefix): | ||||
|         """Return a list of all suffixes that could complete the given | ||||
|         prefix.""" | ||||
| 
 | ||||
| 
 | ||||
| # These interfaces are public, and are re-exported by __init__.py | ||||
| 
 | ||||
| 
 | ||||
| class IDeferredWormhole(Interface): | ||||
|     def get_welcome(): | ||||
|         """ | ||||
|  | @ -277,6 +318,7 @@ class IDeferredWormhole(Interface): | |||
|         :rtype: ``Deferred`` | ||||
|         """ | ||||
| 
 | ||||
| 
 | ||||
| class IInputHelper(Interface): | ||||
|     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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
| import six | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| 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 attr import attrib, attrs | ||||
| from attr.validators import instance_of, provides | ||||
| from automat import MethodicalMachine | ||||
| from .util import (to_bytes, bytes_to_hexstr, hexstr_to_bytes, | ||||
|                    bytes_to_dict, dict_to_bytes) | ||||
| from hkdf import Hkdf | ||||
| 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 .util import (bytes_to_dict, bytes_to_hexstr, dict_to_bytes, | ||||
|                    hexstr_to_bytes, to_bytes) | ||||
| 
 | ||||
| CryptoError | ||||
| __all__ = ["derive_key", "derive_phase_key", "CryptoError", | ||||
|            "Key"] | ||||
| __all__ = ["derive_key", "derive_phase_key", "CryptoError", "Key"] | ||||
| 
 | ||||
| 
 | ||||
| def HKDF(skm, outlen, salt=None, CTXinfo=b""): | ||||
|     return Hkdf(salt, skm).expand(CTXinfo, outlen) | ||||
| 
 | ||||
| 
 | ||||
| def derive_key(key, purpose, length=SecretBox.KEY_SIZE): | ||||
|     if not isinstance(key, type(b"")): raise TypeError(type(key)) | ||||
|     if not isinstance(purpose, type(b"")): raise TypeError(type(purpose)) | ||||
|     if not isinstance(length, six.integer_types): raise TypeError(type(length)) | ||||
|     if not isinstance(key, type(b"")): | ||||
|         raise TypeError(type(key)) | ||||
|     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) | ||||
| 
 | ||||
| 
 | ||||
| def derive_phase_key(key, side, phase): | ||||
|     assert isinstance(side, type("")), type(side) | ||||
|     assert isinstance(phase, type("")), type(phase) | ||||
|     side_bytes = side.encode("ascii") | ||||
|     phase_bytes = phase.encode("ascii") | ||||
|     purpose = (b"wormhole:phase:" | ||||
|                + sha256(side_bytes).digest() | ||||
|                + sha256(phase_bytes).digest()) | ||||
|     purpose = (b"wormhole:phase:" + sha256(side_bytes).digest() + | ||||
|                sha256(phase_bytes).digest()) | ||||
|     return derive_key(key, purpose) | ||||
| 
 | ||||
| 
 | ||||
| def decrypt_data(key, encrypted): | ||||
|     assert isinstance(key, type(b"")), type(key) | ||||
|     assert isinstance(encrypted, type(b"")), type(encrypted) | ||||
|  | @ -44,6 +53,7 @@ def decrypt_data(key, encrypted): | |||
|     data = box.decrypt(encrypted) | ||||
|     return data | ||||
| 
 | ||||
| 
 | ||||
| def encrypt_data(key, plaintext): | ||||
|     assert isinstance(key, type(b"")), type(key) | ||||
|     assert isinstance(plaintext, type(b"")), type(plaintext) | ||||
|  | @ -52,10 +62,12 @@ def encrypt_data(key, plaintext): | |||
|     nonce = utils.random(SecretBox.NONCE_SIZE) | ||||
|     return box.encrypt(plaintext, nonce) | ||||
| 
 | ||||
| 
 | ||||
| # the Key we expose to callers (Boss, Ordering) is responsible for sorting | ||||
| # the two messages (got_code and got_pake), then delivering them to | ||||
| # _SortedKey in the right order. | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IKey) | ||||
| class Key(object): | ||||
|  | @ -64,40 +76,54 @@ class Key(object): | |||
|     _side = attrib(validator=instance_of(type(u""))) | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._SK = _SortedKey(self._appid, self._versions, self._side, | ||||
|                               self._timing) | ||||
|         self._debug_pake_stashed = False # for tests | ||||
|         self._debug_pake_stashed = False  # for tests | ||||
| 
 | ||||
|     def wire(self, boss, mailbox, receive): | ||||
|         self._SK.wire(boss, mailbox, receive) | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S00(self): pass # pragma: no cover | ||||
|     def S00(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S01(self): pass # pragma: no cover | ||||
|     def S01(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S10(self): pass # pragma: no cover | ||||
|     def S10(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S11(self): pass # pragma: no cover | ||||
|     def S11(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_code(self, code): pass | ||||
|     def got_code(self, code): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_pake(self, body): pass | ||||
|     def got_pake(self, body): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def stash_pake(self, body): | ||||
|         self._pake = body | ||||
|         self._debug_pake_stashed = True | ||||
| 
 | ||||
|     @m.output() | ||||
|     def deliver_code(self, code): | ||||
|         self._SK.got_code(code) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def deliver_pake(self, body): | ||||
|         self._SK.got_pake(body) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def deliver_code_and_stashed_pake(self, code): | ||||
|         self._SK.got_code(code) | ||||
|  | @ -108,6 +134,7 @@ class Key(object): | |||
|     S00.upon(got_pake, enter=S01, outputs=[stash_pake]) | ||||
|     S01.upon(got_code, enter=S11, outputs=[deliver_code_and_stashed_pake]) | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| class _SortedKey(object): | ||||
|     _appid = attrib(validator=instance_of(type(u""))) | ||||
|  | @ -115,7 +142,8 @@ class _SortedKey(object): | |||
|     _side = attrib(validator=instance_of(type(u""))) | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._B = _interfaces.IBoss(boss) | ||||
|  | @ -123,17 +151,25 @@ class _SortedKey(object): | |||
|         self._R = _interfaces.IReceive(receive) | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0_know_nothing(self): pass # pragma: no cover | ||||
|     def S0_know_nothing(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1_know_code(self): pass # pragma: no cover | ||||
|     def S1_know_code(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2_know_key(self): pass # pragma: no cover | ||||
|     def S2_know_key(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state(terminal=True) | ||||
|     def S3_scared(self): pass # pragma: no cover | ||||
|     def S3_scared(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from Boss | ||||
|     @m.input() | ||||
|     def got_code(self, code): pass | ||||
|     def got_code(self, code): | ||||
|         pass | ||||
| 
 | ||||
|     # from Ordering | ||||
|     def got_pake(self, body): | ||||
|  | @ -143,16 +179,20 @@ class _SortedKey(object): | |||
|             self.got_pake_good(hexstr_to_bytes(payload["pake_v1"])) | ||||
|         else: | ||||
|             self.got_pake_bad() | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_pake_good(self, msg2): pass | ||||
|     def got_pake_good(self, msg2): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_pake_bad(self): pass | ||||
|     def got_pake_bad(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def build_pake(self, code): | ||||
|         with self._timing.add("pake1", waiting="crypto"): | ||||
|             self._sp = SPAKE2_Symmetric(to_bytes(code), | ||||
|                                         idSymmetric=to_bytes(self._appid)) | ||||
|             self._sp = SPAKE2_Symmetric( | ||||
|                 to_bytes(code), idSymmetric=to_bytes(self._appid)) | ||||
|             msg1 = self._sp.start() | ||||
|         body = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg1)}) | ||||
|         self._M.add_message("pake", body) | ||||
|  | @ -160,6 +200,7 @@ class _SortedKey(object): | |||
|     @m.output() | ||||
|     def scared(self): | ||||
|         self._B.scared() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def compute_key(self, msg2): | ||||
|         assert isinstance(msg2, type(b"")) | ||||
|  |  | |||
|  | @ -1,16 +1,20 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import provides | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.ILister) | ||||
| class Lister(object): | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._RC = _interfaces.IRendezvousConnector(rendezvous_connector) | ||||
|  | @ -26,26 +30,41 @@ class Lister(object): | |||
|     # request arrives, both requests will be satisfied by the same response. | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0A_idle_disconnected(self): pass # pragma: no cover | ||||
|     def S0A_idle_disconnected(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1A_wanting_disconnected(self): pass # pragma: no cover | ||||
|     def S1A_wanting_disconnected(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S0B_idle_connected(self): pass # pragma: no cover | ||||
|     def S0B_idle_connected(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1B_wanting_connected(self): pass # pragma: no cover | ||||
|     def S1B_wanting_connected(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.input() | ||||
|     def connected(self): pass | ||||
|     def connected(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def lost(self): pass | ||||
|     def lost(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def refresh(self): pass | ||||
|     def refresh(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_nameplates(self, all_nameplates): pass | ||||
|     def rx_nameplates(self, all_nameplates): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_list(self): | ||||
|         self._RC.tx_list() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def I_got_nameplates(self, all_nameplates): | ||||
|         # 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=[]) | ||||
|     S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[]) | ||||
| 
 | ||||
|     S0A_idle_disconnected.upon(refresh, | ||||
|                                enter=S1A_wanting_disconnected, outputs=[]) | ||||
|     S1A_wanting_disconnected.upon(refresh, | ||||
|                                   enter=S1A_wanting_disconnected, outputs=[]) | ||||
|     S1A_wanting_disconnected.upon(connected, enter=S1B_wanting_connected, | ||||
|                                   outputs=[RC_tx_list]) | ||||
|     S0B_idle_connected.upon(refresh, enter=S1B_wanting_connected, | ||||
|                             outputs=[RC_tx_list]) | ||||
|     S0B_idle_connected.upon(rx_nameplates, enter=S0B_idle_connected, | ||||
|                             outputs=[I_got_nameplates]) | ||||
|     S1B_wanting_connected.upon(lost, enter=S1A_wanting_disconnected, outputs=[]) | ||||
|     S1B_wanting_connected.upon(refresh, enter=S1B_wanting_connected, | ||||
|                                outputs=[RC_tx_list]) | ||||
|     S1B_wanting_connected.upon(rx_nameplates, enter=S0B_idle_connected, | ||||
|                                outputs=[I_got_nameplates]) | ||||
|     S0A_idle_disconnected.upon( | ||||
|         refresh, enter=S1A_wanting_disconnected, outputs=[]) | ||||
|     S1A_wanting_disconnected.upon( | ||||
|         refresh, enter=S1A_wanting_disconnected, outputs=[]) | ||||
|     S1A_wanting_disconnected.upon( | ||||
|         connected, enter=S1B_wanting_connected, outputs=[RC_tx_list]) | ||||
|     S0B_idle_connected.upon( | ||||
|         refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list]) | ||||
|     S0B_idle_connected.upon( | ||||
|         rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates]) | ||||
|     S1B_wanting_connected.upon( | ||||
|         lost, enter=S1A_wanting_disconnected, outputs=[]) | ||||
|     S1B_wanting_connected.upon( | ||||
|         refresh, enter=S1B_wanting_connected, outputs=[RC_tx_list]) | ||||
|     S1B_wanting_connected.upon( | ||||
|         rx_nameplates, enter=S0B_idle_connected, outputs=[I_got_nameplates]) | ||||
|  |  | |||
|  | @ -1,16 +1,20 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import instance_of | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IMailbox) | ||||
| class Mailbox(object): | ||||
|     _side = attrib(validator=instance_of(type(u""))) | ||||
|     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): | ||||
|         self._mailbox = None | ||||
|  | @ -29,52 +33,68 @@ class Mailbox(object): | |||
| 
 | ||||
|     # S0: know nothing | ||||
|     @m.state(initial=True) | ||||
|     def S0A(self): pass # pragma: no cover | ||||
|     def S0A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S0B(self): pass # pragma: no cover | ||||
|     def S0B(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S1: mailbox known, not opened | ||||
|     @m.state() | ||||
|     def S1A(self): pass # pragma: no cover | ||||
|     def S1A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S2: mailbox known, opened | ||||
|     # We've definitely tried to open the mailbox at least once, but it must | ||||
|     # be re-opened with each connection, because open() is also subscribe() | ||||
|     @m.state() | ||||
|     def S2A(self): pass # pragma: no cover | ||||
|     def S2A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2B(self): pass # pragma: no cover | ||||
|     def S2B(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S3: closing | ||||
|     @m.state() | ||||
|     def S3A(self): pass # pragma: no cover | ||||
|     def S3A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @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 | ||||
|     #@m.state() | ||||
|     #def S4A(self): pass | ||||
|     #@m.state() | ||||
|     #def S4B(self): pass | ||||
|     # @m.state() | ||||
|     # def S4A(self): pass | ||||
|     # @m.state() | ||||
|     # def S4B(self): pass | ||||
|     @m.state(terminal=True) | ||||
|     def S4(self): pass # pragma: no cover | ||||
|     def S4(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     S4A = S4 | ||||
|     S4B = S4 | ||||
| 
 | ||||
| 
 | ||||
|     # from Terminator | ||||
|     @m.input() | ||||
|     def close(self, mood): pass | ||||
|     def close(self, mood): | ||||
|         pass | ||||
| 
 | ||||
|     # from Nameplate | ||||
|     @m.input() | ||||
|     def got_mailbox(self, mailbox): pass | ||||
|     def got_mailbox(self, mailbox): | ||||
|         pass | ||||
| 
 | ||||
|     # from RendezvousConnector | ||||
|     @m.input() | ||||
|     def connected(self): pass | ||||
|     def connected(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def lost(self): pass | ||||
|     def lost(self): | ||||
|         pass | ||||
| 
 | ||||
|     def rx_message(self, side, phase, body): | ||||
|         assert isinstance(side, type("")), type(side) | ||||
|  | @ -84,73 +104,91 @@ class Mailbox(object): | |||
|             self.rx_message_ours(phase, body) | ||||
|         else: | ||||
|             self.rx_message_theirs(side, phase, body) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_message_ours(self, phase, body): pass | ||||
|     def rx_message_ours(self, phase, body): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_message_theirs(self, side, phase, body): pass | ||||
|     def rx_message_theirs(self, side, phase, body): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_closed(self): pass | ||||
|     def rx_closed(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from Send or Key | ||||
|     @m.input() | ||||
|     def add_message(self, phase, body): | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_mailbox(self, mailbox): | ||||
|         self._mailbox = mailbox | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_open(self): | ||||
|         assert self._mailbox | ||||
|         self._RC.tx_open(self._mailbox) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def queue(self, phase, body): | ||||
|         assert isinstance(phase, type("")), type(phase) | ||||
|         assert isinstance(body, type(b"")), (type(body), phase, body) | ||||
|         self._pending_outbound[phase] = body | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_mailbox_and_RC_tx_open_and_drain(self, mailbox): | ||||
|         self._mailbox = mailbox | ||||
|         self._RC.tx_open(mailbox) | ||||
|         self._drain() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def drain(self): | ||||
|         self._drain() | ||||
| 
 | ||||
|     def _drain(self): | ||||
|         for phase, body in self._pending_outbound.items(): | ||||
|             self._RC.tx_add(phase, body) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_add(self, phase, body): | ||||
|         assert isinstance(phase, type("")), type(phase) | ||||
|         assert isinstance(body, type(b"")), type(body) | ||||
|         self._RC.tx_add(phase, body) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def N_release_and_accept(self, side, phase, body): | ||||
|         self._N.release() | ||||
|         if phase not in self._processed: | ||||
|             self._processed.add(phase) | ||||
|             self._O.got_message(side, phase, body) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_close(self): | ||||
|         assert self._mood | ||||
|         self._RC_tx_close() | ||||
| 
 | ||||
|     def _RC_tx_close(self): | ||||
|         self._RC.tx_close(self._mailbox, self._mood) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def dequeue(self, phase, body): | ||||
|         self._pending_outbound.pop(phase, None) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_mood(self, mood): | ||||
|         self._mood = mood | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_mood_and_RC_tx_close(self, mood): | ||||
|         self._mood = mood | ||||
|         self._RC_tx_close() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def ignore_mood_and_T_mailbox_done(self, mood): | ||||
|         self._T.mailbox_done() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def T_mailbox_done(self): | ||||
|         self._T.mailbox_done() | ||||
|  | @ -162,8 +200,10 @@ class Mailbox(object): | |||
|     S0B.upon(lost, enter=S0A, outputs=[]) | ||||
|     S0B.upon(add_message, enter=S0B, outputs=[queue]) | ||||
|     S0B.upon(close, enter=S4B, outputs=[ignore_mood_and_T_mailbox_done]) | ||||
|     S0B.upon(got_mailbox, enter=S2B, | ||||
|              outputs=[record_mailbox_and_RC_tx_open_and_drain]) | ||||
|     S0B.upon( | ||||
|         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(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_ours, enter=S4, outputs=[]) | ||||
|     S4.upon(close, enter=S4, outputs=[]) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,10 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| import re | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| from ._wordlist import PGPWordList | ||||
| from .errors import KeyFormatError | ||||
|  | @ -9,13 +12,15 @@ from .errors import KeyFormatError | |||
| 
 | ||||
| def validate_nameplate(nameplate): | ||||
|     if not re.search(r'^\d+$', nameplate): | ||||
|         raise KeyFormatError("Nameplate '%s' must be numeric, with no spaces." | ||||
|                              % nameplate) | ||||
|         raise KeyFormatError( | ||||
|             "Nameplate '%s' must be numeric, with no spaces." % nameplate) | ||||
| 
 | ||||
| 
 | ||||
| @implementer(_interfaces.INameplate) | ||||
| class Nameplate(object): | ||||
|     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): | ||||
|         self._nameplate = None | ||||
|  | @ -32,94 +37,125 @@ class Nameplate(object): | |||
| 
 | ||||
|     # S0: know nothing | ||||
|     @m.state(initial=True) | ||||
|     def S0A(self): pass # pragma: no cover | ||||
|     def S0A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S0B(self): pass # pragma: no cover | ||||
|     def S0B(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S1: nameplate known, never claimed | ||||
|     @m.state() | ||||
|     def S1A(self): pass # pragma: no cover | ||||
|     def S1A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S2: nameplate known, maybe claimed | ||||
|     @m.state() | ||||
|     def S2A(self): pass # pragma: no cover | ||||
|     def S2A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2B(self): pass # pragma: no cover | ||||
|     def S2B(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S3: nameplate claimed | ||||
|     @m.state() | ||||
|     def S3A(self): pass # pragma: no cover | ||||
|     def S3A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S3B(self): pass # pragma: no cover | ||||
|     def S3B(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S4: maybe released | ||||
|     @m.state() | ||||
|     def S4A(self): pass # pragma: no cover | ||||
|     def S4A(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S4B(self): pass # pragma: no cover | ||||
|     def S4B(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # S5: released | ||||
|     # we no longer care whether we're connected or not | ||||
|     #@m.state() | ||||
|     #def S5A(self): pass | ||||
|     #@m.state() | ||||
|     #def S5B(self): pass | ||||
|     # @m.state() | ||||
|     # def S5A(self): pass | ||||
|     # @m.state() | ||||
|     # def S5B(self): pass | ||||
|     @m.state() | ||||
|     def S5(self): pass # pragma: no cover | ||||
|     def S5(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     S5A = S5 | ||||
|     S5B = S5 | ||||
| 
 | ||||
|     # from Boss | ||||
|     def set_nameplate(self, nameplate): | ||||
|         validate_nameplate(nameplate) # can raise KeyFormatError | ||||
|         validate_nameplate(nameplate)  # can raise KeyFormatError | ||||
|         self._set_nameplate(nameplate) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def _set_nameplate(self, nameplate): pass | ||||
|     def _set_nameplate(self, nameplate): | ||||
|         pass | ||||
| 
 | ||||
|     # from Mailbox | ||||
|     @m.input() | ||||
|     def release(self): pass | ||||
|     def release(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from Terminator | ||||
|     @m.input() | ||||
|     def close(self): pass | ||||
|     def close(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from RendezvousConnector | ||||
|     @m.input() | ||||
|     def connected(self): pass | ||||
|     @m.input() | ||||
|     def lost(self): pass | ||||
|     def connected(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_claimed(self, mailbox): pass | ||||
|     def lost(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_released(self): pass | ||||
|     def rx_claimed(self, mailbox): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def rx_released(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_nameplate(self, nameplate): | ||||
|         validate_nameplate(nameplate) | ||||
|         self._nameplate = nameplate | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_nameplate_and_RC_tx_claim(self, nameplate): | ||||
|         validate_nameplate(nameplate) | ||||
|         self._nameplate = nameplate | ||||
|         self._RC.tx_claim(self._nameplate) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_claim(self): | ||||
|         # when invoked via M.connected(), we must use the stored nameplate | ||||
|         self._RC.tx_claim(self._nameplate) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def I_got_wordlist(self, mailbox): | ||||
|         # TODO select wordlist based on nameplate properties, in rx_claimed | ||||
|         wordlist = PGPWordList() | ||||
|         self._I.got_wordlist(wordlist) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def M_got_mailbox(self, mailbox): | ||||
|         self._M.got_mailbox(mailbox) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_tx_release(self): | ||||
|         assert self._nameplate | ||||
|         self._RC.tx_release(self._nameplate) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def T_nameplate_done(self): | ||||
|         self._T.nameplate_done() | ||||
|  | @ -127,8 +163,8 @@ class Nameplate(object): | |||
|     S0A.upon(_set_nameplate, enter=S1A, outputs=[record_nameplate]) | ||||
|     S0A.upon(connected, enter=S0B, outputs=[]) | ||||
|     S0A.upon(close, enter=S5A, outputs=[T_nameplate_done]) | ||||
|     S0B.upon(_set_nameplate, enter=S2B, | ||||
|              outputs=[record_nameplate_and_RC_tx_claim]) | ||||
|     S0B.upon( | ||||
|         _set_nameplate, enter=S2B, outputs=[record_nameplate_and_RC_tx_claim]) | ||||
|     S0B.upon(lost, enter=S0A, outputs=[]) | ||||
|     S0B.upon(close, enter=S5A, outputs=[T_nameplate_done]) | ||||
| 
 | ||||
|  | @ -144,7 +180,7 @@ class Nameplate(object): | |||
|     S3A.upon(connected, enter=S3B, outputs=[]) | ||||
|     S3A.upon(close, enter=S4A, outputs=[]) | ||||
|     S3B.upon(lost, enter=S3A, outputs=[]) | ||||
|     #S3B.upon(rx_claimed, enter=S3B, outputs=[]) # shouldn't happen | ||||
|     # S3B.upon(rx_claimed, enter=S3B, outputs=[]) # shouldn't happen | ||||
|     S3B.upon(release, enter=S4B, outputs=[RC_tx_release]) | ||||
|     S3B.upon(close, enter=S4B, outputs=[RC_tx_release]) | ||||
| 
 | ||||
|  | @ -153,7 +189,7 @@ class Nameplate(object): | |||
|     S4B.upon(lost, enter=S4A, outputs=[]) | ||||
|     S4B.upon(rx_claimed, enter=S4B, outputs=[]) | ||||
|     S4B.upon(rx_released, enter=S5B, outputs=[T_nameplate_done]) | ||||
|     S4B.upon(release, enter=S4B, outputs=[]) # mailbox is lazy | ||||
|     S4B.upon(release, enter=S4B, outputs=[])  # mailbox is lazy | ||||
|     # Mailbox doesn't remember how many times it's sent a release, and will | ||||
|     # re-send a new one for each peer message it receives. Ignoring it here | ||||
|     # is easier than adding a new pair of states to Mailbox. | ||||
|  | @ -161,5 +197,5 @@ class Nameplate(object): | |||
| 
 | ||||
|     S5A.upon(connected, enter=S5B, outputs=[]) | ||||
|     S5B.upon(lost, enter=S5A, outputs=[]) | ||||
|     S5.upon(release, enter=S5, outputs=[]) # mailbox is lazy | ||||
|     S5.upon(release, enter=S5, outputs=[])  # mailbox is lazy | ||||
|     S5.upon(close, enter=S5, outputs=[]) | ||||
|  |  | |||
|  | @ -1,32 +1,40 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| from attr.validators import provides, instance_of | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import instance_of, provides | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IOrder) | ||||
| class Order(object): | ||||
|     _side = attrib(validator=instance_of(type(u""))) | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     m = MethodicalMachine() | ||||
|     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): | ||||
|         self._key = None | ||||
|         self._queue = [] | ||||
| 
 | ||||
|     def wire(self, key, receive): | ||||
|         self._K = _interfaces.IKey(key) | ||||
|         self._R = _interfaces.IReceive(receive) | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0_no_pake(self): pass # pragma: no cover | ||||
|     def S0_no_pake(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @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): | ||||
|         #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(phase, type("")), type(phase) | ||||
|         assert isinstance(body, type(b"")), type(body) | ||||
|  | @ -36,9 +44,12 @@ class Order(object): | |||
|             self.got_non_pake(side, phase, body) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_pake(self, side, phase, body): pass | ||||
|     def got_pake(self, side, phase, body): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_non_pake(self, side, phase, body): pass | ||||
|     def got_non_pake(self, side, phase, body): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def queue(self, side, phase, body): | ||||
|  | @ -46,9 +57,11 @@ class Order(object): | |||
|         assert isinstance(phase, type("")), type(phase) | ||||
|         assert isinstance(body, type(b"")), type(body) | ||||
|         self._queue.append((side, phase, body)) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def notify_key(self, side, phase, body): | ||||
|         self._K.got_pake(body) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def drain(self, side, phase, body): | ||||
|         del phase | ||||
|  | @ -56,6 +69,7 @@ class Order(object): | |||
|         for (side, phase, body) in self._queue: | ||||
|             self._deliver(side, phase, body) | ||||
|         self._queue[:] = [] | ||||
| 
 | ||||
|     @m.output() | ||||
|     def deliver(self, side, phase, body): | ||||
|         self._deliver(side, phase, body) | ||||
|  |  | |||
|  | @ -1,10 +1,13 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from attr import attrs, attrib | ||||
| from attr.validators import provides, instance_of | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import instance_of, provides | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| 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 | ||||
| @implementer(_interfaces.IReceive) | ||||
|  | @ -12,7 +15,8 @@ class Receive(object): | |||
|     _side = attrib(validator=instance_of(type(u""))) | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     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): | ||||
|         self._key = None | ||||
|  | @ -22,13 +26,20 @@ class Receive(object): | |||
|         self._S = _interfaces.ISend(send) | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def S0_unknown_key(self): pass # pragma: no cover | ||||
|     def S0_unknown_key(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S1_unverified_key(self): pass # pragma: no cover | ||||
|     def S1_unverified_key(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S2_verified_key(self): pass # pragma: no cover | ||||
|     def S2_verified_key(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state(terminal=True) | ||||
|     def S3_scared(self): pass # pragma: no cover | ||||
|     def S3_scared(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from Ordering | ||||
|     def got_message(self, side, phase, body): | ||||
|  | @ -43,47 +54,56 @@ class Receive(object): | |||
|             self.got_message_bad() | ||||
|             return | ||||
|         self.got_message_good(phase, plaintext) | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_message_good(self, phase, plaintext): pass | ||||
|     def got_message_good(self, phase, plaintext): | ||||
|         pass | ||||
| 
 | ||||
|     @m.input() | ||||
|     def got_message_bad(self): pass | ||||
|     def got_message_bad(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from Key | ||||
|     @m.input() | ||||
|     def got_key(self, key): pass | ||||
|     def got_key(self, key): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_key(self, key): | ||||
|         self._key = key | ||||
| 
 | ||||
|     @m.output() | ||||
|     def S_got_verified_key(self, phase, plaintext): | ||||
|         assert self._key | ||||
|         self._S.got_verified_key(self._key) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def W_happy(self, phase, plaintext): | ||||
|         self._B.happy() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def W_got_verifier(self, phase, plaintext): | ||||
|         self._B.got_verifier(derive_key(self._key, b"wormhole:verifier")) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def W_got_message(self, phase, plaintext): | ||||
|         assert isinstance(phase, type("")), type(phase) | ||||
|         assert isinstance(plaintext, type(b"")), type(plaintext) | ||||
|         self._B.got_message(phase, plaintext) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def W_scared(self): | ||||
|         self._B.scared() | ||||
| 
 | ||||
|     S0_unknown_key.upon(got_key, enter=S1_unverified_key, outputs=[record_key]) | ||||
|     S1_unverified_key.upon(got_message_good, enter=S2_verified_key, | ||||
|                            outputs=[S_got_verified_key, | ||||
|                                     W_happy, W_got_verifier, W_got_message]) | ||||
|     S1_unverified_key.upon(got_message_bad, enter=S3_scared, | ||||
|                            outputs=[W_scared]) | ||||
|     S2_verified_key.upon(got_message_bad, enter=S3_scared, | ||||
|                          outputs=[W_scared]) | ||||
|     S2_verified_key.upon(got_message_good, enter=S2_verified_key, | ||||
|                          outputs=[W_got_message]) | ||||
|     S1_unverified_key.upon( | ||||
|         got_message_good, | ||||
|         enter=S2_verified_key, | ||||
|         outputs=[S_got_verified_key, W_happy, W_got_verifier, W_got_message]) | ||||
|     S1_unverified_key.upon( | ||||
|         got_message_bad, enter=S3_scared, outputs=[W_scared]) | ||||
|     S2_verified_key.upon(got_message_bad, enter=S3_scared, outputs=[W_scared]) | ||||
|     S2_verified_key.upon( | ||||
|         got_message_good, enter=S2_verified_key, outputs=[W_got_message]) | ||||
|     S3_scared.upon(got_message_good, enter=S3_scared, outputs=[]) | ||||
|     S3_scared.upon(got_message_bad, enter=S3_scared, outputs=[]) | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,44 +9,47 @@ from twisted.internet import defer, endpoints, task | |||
| from twisted.application import internet | ||||
| from autobahn.twisted import websocket | ||||
| from . import _interfaces, errors | ||||
| from .util import (bytes_to_hexstr, hexstr_to_bytes, | ||||
|                    bytes_to_dict, dict_to_bytes) | ||||
| from .util import (bytes_to_hexstr, hexstr_to_bytes, bytes_to_dict, | ||||
|                    dict_to_bytes) | ||||
| 
 | ||||
| 
 | ||||
| class WSClient(websocket.WebSocketClientProtocol): | ||||
|     def onConnect(self, response): | ||||
|         # this fires during WebSocket negotiation, and isn't very useful | ||||
|         # unless you want to modify the protocol settings | ||||
|         #print("onConnect", response) | ||||
|         # print("onConnect", response) | ||||
|         pass | ||||
| 
 | ||||
|     def onOpen(self, *args): | ||||
|         # this fires when the WebSocket is ready to go. No arguments | ||||
|         #print("onOpen", args) | ||||
|         #self.wormhole_open = True | ||||
|         # print("onOpen", args) | ||||
|         # self.wormhole_open = True | ||||
|         self._RC.ws_open(self) | ||||
| 
 | ||||
|     def onMessage(self, payload, isBinary): | ||||
|         assert not isBinary | ||||
|         try: | ||||
|             self._RC.ws_message(payload) | ||||
|         except: | ||||
|         except Exception: | ||||
|             from twisted.python.failure import Failure | ||||
|             print("LOGGING", Failure()) | ||||
|             log.err() | ||||
|             raise | ||||
| 
 | ||||
|     def onClose(self, wasClean, code, reason): | ||||
|         #print("onClose") | ||||
|         # print("onClose") | ||||
|         self._RC.ws_close(wasClean, code, reason) | ||||
|         #if self.wormhole_open: | ||||
|         #    self.wormhole._ws_closed(wasClean, code, reason) | ||||
|         #else: | ||||
|         #    # we closed before establishing a connection (onConnect) or | ||||
|         #    # finishing WebSocket negotiation (onOpen): errback | ||||
|         #    self.factory.d.errback(error.ConnectError(reason)) | ||||
|         # if self.wormhole_open: | ||||
|         #     self.wormhole._ws_closed(wasClean, code, reason) | ||||
|         # else: | ||||
|         #     # we closed before establishing a connection (onConnect) or | ||||
|         #     # finishing WebSocket negotiation (onOpen): errback | ||||
|         #     self.factory.d.errback(error.ConnectError(reason)) | ||||
| 
 | ||||
| 
 | ||||
| class WSFactory(websocket.WebSocketClientFactory): | ||||
|     protocol = WSClient | ||||
| 
 | ||||
|     def __init__(self, RC, *args, **kwargs): | ||||
|         websocket.WebSocketClientFactory.__init__(self, *args, **kwargs) | ||||
|         self._RC = RC | ||||
|  | @ -54,9 +57,10 @@ class WSFactory(websocket.WebSocketClientFactory): | |||
|     def buildProtocol(self, addr): | ||||
|         proto = websocket.WebSocketClientFactory.buildProtocol(self, addr) | ||||
|         proto._RC = self._RC | ||||
|         #proto.wormhole_open = False | ||||
|         # proto.wormhole_open = False | ||||
|         return proto | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.IRendezvousConnector) | ||||
| class RendezvousConnector(object): | ||||
|  | @ -90,6 +94,7 @@ class RendezvousConnector(object): | |||
| 
 | ||||
|     def set_trace(self, f): | ||||
|         self._trace = f | ||||
| 
 | ||||
|     def _debug(self, what): | ||||
|         if self._trace: | ||||
|             self._trace(old_state="", input=what, new_state="") | ||||
|  | @ -133,14 +138,13 @@ class RendezvousConnector(object): | |||
|     def stop(self): | ||||
|         # ClientService.stopService is defined to "Stop attempting to | ||||
|         # 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) | ||||
|         # ClientService.stopService always fires with None, even if the | ||||
|         # initial connection failed, so log.err just in case | ||||
|         d.addErrback(log.err) | ||||
|         d.addBoth(self._stopped) | ||||
| 
 | ||||
| 
 | ||||
|     # from Lister | ||||
|     def tx_list(self): | ||||
|         self._tx("list") | ||||
|  | @ -157,7 +161,7 @@ class RendezvousConnector(object): | |||
|             # this should happen right away: the ClientService ought to be in | ||||
|             # the "_waiting" state, and everything in the _waiting.stop | ||||
|             # 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)) | ||||
| 
 | ||||
|     # from our WSClient (the WebSocket protocol) | ||||
|  | @ -166,8 +170,11 @@ class RendezvousConnector(object): | |||
|         self._have_made_a_successful_connection = True | ||||
|         self._ws = proto | ||||
|         try: | ||||
|             self._tx("bind", appid=self._appid, side=self._side, | ||||
|                      client_version=self._client_version) | ||||
|             self._tx( | ||||
|                 "bind", | ||||
|                 appid=self._appid, | ||||
|                 side=self._side, | ||||
|                 client_version=self._client_version) | ||||
|             self._N.connected() | ||||
|             self._M.connected() | ||||
|             self._L.connected() | ||||
|  | @ -180,19 +187,22 @@ class RendezvousConnector(object): | |||
|     def ws_message(self, payload): | ||||
|         msg = bytes_to_dict(payload) | ||||
|         if msg["type"] != "ack": | ||||
|                 self._debug("R.rx(%s %s%s)" % | ||||
|                             (msg["type"], msg.get("phase",""), | ||||
|                         "[mine]" if msg.get("side","") == self._side else "", | ||||
|                              )) | ||||
|             self._debug("R.rx(%s %s%s)" % ( | ||||
|                 msg["type"], | ||||
|                 msg.get("phase", ""), | ||||
|                 "[mine]" if msg.get("side", "") == self._side else "", | ||||
|             )) | ||||
| 
 | ||||
|         self._timing.add("ws_receive", _side=self._side, message=msg) | ||||
|         if self._debug_record_inbound_f: | ||||
|             self._debug_record_inbound_f(msg) | ||||
|         mtype = msg["type"] | ||||
|         meth = getattr(self, "_response_handle_"+mtype, None) | ||||
|         meth = getattr(self, "_response_handle_" + mtype, None) | ||||
|         if not meth: | ||||
|             # 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 | ||||
|         try: | ||||
|             return meth(msg) | ||||
|  | @ -229,7 +239,7 @@ class RendezvousConnector(object): | |||
|             # valid connection | ||||
|             sce = errors.ServerConnectionError(self._url, reason) | ||||
|             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 | ||||
|             d.addCallback(lambda _: self._B.error(sce)) | ||||
| 
 | ||||
|  | @ -292,7 +302,7 @@ class RendezvousConnector(object): | |||
|         side = msg["side"] | ||||
|         phase = msg["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) | ||||
| 
 | ||||
|     def _response_handle_released(self, msg): | ||||
|  | @ -301,5 +311,4 @@ class RendezvousConnector(object): | |||
|     def _response_handle_closed(self, msg): | ||||
|         self._M.rx_closed() | ||||
| 
 | ||||
| 
 | ||||
|     # record, message, payload, packet, bundle, ciphertext, plaintext | ||||
|  |  | |||
|  | @ -1,33 +1,41 @@ | |||
| from __future__ import print_function, unicode_literals | ||||
| 
 | ||||
| import traceback | ||||
| 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: | ||||
|     import readline | ||||
| except ImportError: | ||||
|     readline = None | ||||
| from six.moves import input | ||||
| from attr import attrs, attrib | ||||
| from twisted.internet.defer import inlineCallbacks, returnValue | ||||
| from twisted.internet.threads import deferToThread, blockingCallFromThread | ||||
| from .errors import KeyFormatError, AlreadyInputNameplateError | ||||
| 
 | ||||
| errf = None | ||||
| 
 | ||||
| 
 | ||||
| # uncomment this to enable tab-completion debugging | ||||
| #import os ; errf = open("err", "w") if os.path.exists("err") else None | ||||
| def debug(*args, **kwargs): # pragma: no cover | ||||
| # import os ; errf = open("err", "w") if os.path.exists("err") else None | ||||
| def debug(*args, **kwargs):  # pragma: no cover | ||||
|     if errf: | ||||
|         print(*args, file=errf, **kwargs) | ||||
|         errf.flush() | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| class CodeInputter(object): | ||||
|     _input_helper = attrib() | ||||
|     _reactor = attrib() | ||||
| 
 | ||||
|     def __attrs_post_init__(self): | ||||
|         self.used_completion = False | ||||
|         self._matches = None | ||||
|         # 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): | ||||
|         return blockingCallFromThread(self._reactor, f, *a, **kw) | ||||
|  | @ -66,7 +74,7 @@ class CodeInputter(object): | |||
|             nameplate, words = text.split("-", 1) | ||||
|         else: | ||||
|             got_nameplate = False | ||||
|             nameplate = text # partial | ||||
|             nameplate = text  # partial | ||||
| 
 | ||||
|         # 'text' is one of these categories: | ||||
|         #  "" 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 | ||||
|                 # this. For now, bail, but in the future let's find a | ||||
|                 # 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: | ||||
|             # 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") | ||||
|             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 | ||||
|             if not self._committed_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 | ||||
|                 # for the rest of the code, a simple wait-for-wordlist will | ||||
|                 # improve the user experience. | ||||
|                 self.bcft(ih.when_wordlist_is_available) # blocks on CLAIM | ||||
|                 self.bcft(ih.when_wordlist_is_available)  # blocks on CLAIM | ||||
|             # and we're completing on words now | ||||
|             debug("  getting words (%s)" % (words,)) | ||||
|             completions = [nameplate+"-"+c | ||||
|                            for c in self.bcft(ih.get_word_completions, words)] | ||||
|             debug("  getting words (%s)" % (words, )) | ||||
|             completions = [ | ||||
|                 nameplate + "-" + c | ||||
|                 for c in self.bcft(ih.get_word_completions, words) | ||||
|             ] | ||||
| 
 | ||||
|         # rlcompleter wants full strings | ||||
|         return sorted(completions) | ||||
|  | @ -131,13 +143,16 @@ class CodeInputter(object): | |||
|                 # they deleted past the committment point: we can't use | ||||
|                 # this. For now, bail, but in the future let's find a | ||||
|                 # gentler way to encourage them to not do that. | ||||
|                 raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate) | ||||
|                 raise AlreadyInputNameplateError( | ||||
|                     "nameplate (%s-) already entered, cannot go back" % | ||||
|                     self._committed_nameplate) | ||||
|         else: | ||||
|             debug("  choose_nameplate(%s)" % nameplate) | ||||
|             self.bcft(self._input_helper.choose_nameplate, nameplate) | ||||
|         debug("  choose_words(%s)" % words) | ||||
|         self.bcft(self._input_helper.choose_words, words) | ||||
| 
 | ||||
| 
 | ||||
| def _input_code_with_completion(prompt, input_helper, reactor): | ||||
|     # reminder: this all occurs in a separate thread. All calls to input_helper | ||||
|     # must go through blockingCallFromThread() | ||||
|  | @ -159,6 +174,7 @@ def _input_code_with_completion(prompt, input_helper, reactor): | |||
|     c.finish(code) | ||||
|     return c.used_completion | ||||
| 
 | ||||
| 
 | ||||
| def warn_readline(): | ||||
|     # When our process receives a SIGINT, Twisted's SIGINT handler will | ||||
|     # stop the reactor and wait for all threads to terminate before the | ||||
|  | @ -192,11 +208,12 @@ def warn_readline(): | |||
|     # doesn't see the signal, and we must still wait for stdin to make | ||||
|     # readline finish. | ||||
| 
 | ||||
| 
 | ||||
| @inlineCallbacks | ||||
| def input_with_completion(prompt, input_helper, reactor): | ||||
|     t = reactor.addSystemEventTrigger("before", "shutdown", warn_readline) | ||||
|     #input_helper.refresh_nameplates() | ||||
|     used_completion = yield deferToThread(_input_code_with_completion, | ||||
|                                           prompt, input_helper, reactor) | ||||
|     # input_helper.refresh_nameplates() | ||||
|     used_completion = yield deferToThread(_input_code_with_completion, prompt, | ||||
|                                           input_helper, reactor) | ||||
|     reactor.removeSystemEventTrigger(t) | ||||
|     returnValue(used_completion) | ||||
|  |  | |||
|  | @ -1,18 +1,22 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from attr import attrs, attrib | ||||
| from attr.validators import provides, instance_of | ||||
| from zope.interface import implementer | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from attr.validators import instance_of, provides | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| from ._key import derive_phase_key, encrypt_data | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(_interfaces.ISend) | ||||
| class Send(object): | ||||
|     _side = attrib(validator=instance_of(type(u""))) | ||||
|     _timing = attrib(validator=provides(_interfaces.ITiming)) | ||||
|     m = MethodicalMachine() | ||||
|     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): | ||||
|         self._queue = [] | ||||
|  | @ -21,31 +25,40 @@ class Send(object): | |||
|         self._M = _interfaces.IMailbox(mailbox) | ||||
| 
 | ||||
|     @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) | ||||
|     def S1_verified_key(self): pass # pragma: no cover | ||||
|     def S1_verified_key(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     # from Receive | ||||
|     @m.input() | ||||
|     def got_verified_key(self, key): pass | ||||
|     def got_verified_key(self, key): | ||||
|         pass | ||||
| 
 | ||||
|     # from Boss | ||||
|     @m.input() | ||||
|     def send(self, phase, plaintext): pass | ||||
|     def send(self, phase, plaintext): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def queue(self, phase, plaintext): | ||||
|         assert isinstance(phase, type("")), type(phase) | ||||
|         assert isinstance(plaintext, type(b"")), type(plaintext) | ||||
|         self._queue.append((phase, plaintext)) | ||||
| 
 | ||||
|     @m.output() | ||||
|     def record_key(self, key): | ||||
|         self._key = key | ||||
| 
 | ||||
|     @m.output() | ||||
|     def drain(self, key): | ||||
|         del key | ||||
|         for (phase, plaintext) in self._queue: | ||||
|             self._encrypt_and_send(phase, plaintext) | ||||
|         self._queue[:] = [] | ||||
| 
 | ||||
|     @m.output() | ||||
|     def deliver(self, phase, plaintext): | ||||
|         assert isinstance(phase, type("")), type(phase) | ||||
|  | @ -59,6 +72,6 @@ class Send(object): | |||
|         self._M.add_message(phase, encrypted) | ||||
| 
 | ||||
|     S0_no_key.upon(send, enter=S0_no_key, outputs=[queue]) | ||||
|     S0_no_key.upon(got_verified_key, enter=S1_verified_key, | ||||
|                    outputs=[record_key, drain]) | ||||
|     S0_no_key.upon( | ||||
|         got_verified_key, enter=S1_verified_key, outputs=[record_key, drain]) | ||||
|     S1_verified_key.upon(send, enter=S1_verified_key, outputs=[deliver]) | ||||
|  |  | |||
|  | @ -1,12 +1,16 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from automat import MethodicalMachine | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import _interfaces | ||||
| 
 | ||||
| 
 | ||||
| @implementer(_interfaces.ITerminator) | ||||
| class Terminator(object): | ||||
|     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): | ||||
|         self._mood = None | ||||
|  | @ -29,48 +33,68 @@ class Terminator(object): | |||
|     # done, and we're closing, then we stop the RendezvousConnector | ||||
| 
 | ||||
|     @m.state(initial=True) | ||||
|     def Snmo(self): pass # pragma: no cover | ||||
|     @m.state() | ||||
|     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 | ||||
|     def Snmo(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 | ||||
|     def Smo(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @m.state() | ||||
|     def S_stopping(self): pass # pragma: no cover | ||||
|     def Sno(self): | ||||
|         pass  # pragma: no cover | ||||
| 
 | ||||
|     @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 | ||||
|     @m.input() | ||||
|     def close(self, mood): pass | ||||
|     def close(self, mood): | ||||
|         pass | ||||
| 
 | ||||
|     # from Nameplate | ||||
|     @m.input() | ||||
|     def nameplate_done(self): pass | ||||
|     def nameplate_done(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from Mailbox | ||||
|     @m.input() | ||||
|     def mailbox_done(self): pass | ||||
|     def mailbox_done(self): | ||||
|         pass | ||||
| 
 | ||||
|     # from RendezvousConnector | ||||
|     @m.input() | ||||
|     def stopped(self): pass | ||||
| 
 | ||||
|     def stopped(self): | ||||
|         pass | ||||
| 
 | ||||
|     @m.output() | ||||
|     def close_nameplate(self, mood): | ||||
|         self._N.close() # ignores mood | ||||
|         self._N.close()  # ignores mood | ||||
| 
 | ||||
|     @m.output() | ||||
|     def close_mailbox(self, mood): | ||||
|         self._M.close(mood) | ||||
|  | @ -78,9 +102,11 @@ class Terminator(object): | |||
|     @m.output() | ||||
|     def ignore_mood_and_RC_stop(self, mood): | ||||
|         self._RC.stop() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def RC_stop(self): | ||||
|         self._RC.stop() | ||||
| 
 | ||||
|     @m.output() | ||||
|     def B_closed(self): | ||||
|         self._B.closed() | ||||
|  | @ -99,8 +125,10 @@ class Terminator(object): | |||
|     Snm.upon(nameplate_done, enter=Sm, outputs=[]) | ||||
| 
 | ||||
|     Sn.upon(nameplate_done, enter=S_stopping, outputs=[RC_stop]) | ||||
|     S0o.upon(close, enter=S_stopping, | ||||
|              outputs=[close_nameplate, close_mailbox, ignore_mood_and_RC_stop]) | ||||
|     S0o.upon( | ||||
|         close, | ||||
|         enter=S_stopping, | ||||
|         outputs=[close_nameplate, close_mailbox, ignore_mood_and_RC_stop]) | ||||
|     Sm.upon(mailbox_done, enter=S_stopping, outputs=[RC_stop]) | ||||
| 
 | ||||
|     S_stopping.upon(stopped, enter=S_stopped, outputs=[B_closed]) | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| from __future__ import unicode_literals, print_function | ||||
| from __future__ import print_function, unicode_literals | ||||
| 
 | ||||
| import os | ||||
| from binascii import unhexlify | ||||
| 
 | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from ._interfaces import IWordlist | ||||
| 
 | ||||
| # 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: | ||||
| # https://github.com/warrenguy/javascript-pgp-word-list | ||||
| 
 | ||||
| from binascii import unhexlify | ||||
| 
 | ||||
| raw_words = { | ||||
| '00': ['aardvark', 'adroitness'], '01': ['absurd', 'adviser'], | ||||
| '02': ['accrue', 'aftermath'], '03': ['acme', 'aggregate'], | ||||
| '04': ['adrift', 'alkali'], '05': ['adult', 'almighty'], | ||||
| '06': ['afflict', 'amulet'], '07': ['ahead', 'amusement'], | ||||
| '08': ['aimless', 'antenna'], '09': ['Algol', 'applicant'], | ||||
| '0A': ['allow', 'Apollo'], '0B': ['alone', 'armistice'], | ||||
| '0C': ['ammo', 'article'], '0D': ['ancient', 'asteroid'], | ||||
| '0E': ['apple', 'Atlantic'], '0F': ['artist', 'atmosphere'], | ||||
| '10': ['assume', 'autopsy'], '11': ['Athens', 'Babylon'], | ||||
| '12': ['atlas', 'backwater'], '13': ['Aztec', 'barbecue'], | ||||
| '14': ['baboon', 'belowground'], '15': ['backfield', 'bifocals'], | ||||
| '16': ['backward', 'bodyguard'], '17': ['banjo', 'bookseller'], | ||||
| '18': ['beaming', 'borderline'], '19': ['bedlamp', 'bottomless'], | ||||
| '1A': ['beehive', 'Bradbury'], '1B': ['beeswax', 'bravado'], | ||||
| '1C': ['befriend', 'Brazilian'], '1D': ['Belfast', 'breakaway'], | ||||
| '1E': ['berserk', 'Burlington'], '1F': ['billiard', 'businessman'], | ||||
| '20': ['bison', 'butterfat'], '21': ['blackjack', 'Camelot'], | ||||
| '22': ['blockade', 'candidate'], '23': ['blowtorch', 'cannonball'], | ||||
| '24': ['bluebird', 'Capricorn'], '25': ['bombast', 'caravan'], | ||||
| '26': ['bookshelf', 'caretaker'], '27': ['brackish', 'celebrate'], | ||||
| '28': ['breadline', 'cellulose'], '29': ['breakup', 'certify'], | ||||
| '2A': ['brickyard', 'chambermaid'], '2B': ['briefcase', 'Cherokee'], | ||||
| '2C': ['Burbank', 'Chicago'], '2D': ['button', 'clergyman'], | ||||
| '2E': ['buzzard', 'coherence'], '2F': ['cement', 'combustion'], | ||||
| '30': ['chairlift', 'commando'], '31': ['chatter', 'company'], | ||||
| '32': ['checkup', 'component'], '33': ['chisel', 'concurrent'], | ||||
| '34': ['choking', 'confidence'], '35': ['chopper', 'conformist'], | ||||
| '36': ['Christmas', 'congregate'], '37': ['clamshell', 'consensus'], | ||||
| '38': ['classic', 'consulting'], '39': ['classroom', 'corporate'], | ||||
| '3A': ['cleanup', 'corrosion'], '3B': ['clockwork', 'councilman'], | ||||
| '3C': ['cobra', 'crossover'], '3D': ['commence', 'crucifix'], | ||||
| '3E': ['concert', 'cumbersome'], '3F': ['cowbell', 'customer'], | ||||
| '40': ['crackdown', 'Dakota'], '41': ['cranky', 'decadence'], | ||||
| '42': ['crowfoot', 'December'], '43': ['crucial', 'decimal'], | ||||
| '44': ['crumpled', 'designing'], '45': ['crusade', 'detector'], | ||||
| '46': ['cubic', 'detergent'], '47': ['dashboard', 'determine'], | ||||
| '48': ['deadbolt', 'dictator'], '49': ['deckhand', 'dinosaur'], | ||||
| '4A': ['dogsled', 'direction'], '4B': ['dragnet', 'disable'], | ||||
| '4C': ['drainage', 'disbelief'], '4D': ['dreadful', 'disruptive'], | ||||
| '4E': ['drifter', 'distortion'], '4F': ['dropper', 'document'], | ||||
| '50': ['drumbeat', 'embezzle'], '51': ['drunken', 'enchanting'], | ||||
| '52': ['Dupont', 'enrollment'], '53': ['dwelling', 'enterprise'], | ||||
| '54': ['eating', 'equation'], '55': ['edict', 'equipment'], | ||||
| '56': ['egghead', 'escapade'], '57': ['eightball', 'Eskimo'], | ||||
| '58': ['endorse', 'everyday'], '59': ['endow', 'examine'], | ||||
| '5A': ['enlist', 'existence'], '5B': ['erase', 'exodus'], | ||||
| '5C': ['escape', 'fascinate'], '5D': ['exceed', 'filament'], | ||||
| '5E': ['eyeglass', 'finicky'], '5F': ['eyetooth', 'forever'], | ||||
| '60': ['facial', 'fortitude'], '61': ['fallout', 'frequency'], | ||||
| '62': ['flagpole', 'gadgetry'], '63': ['flatfoot', 'Galveston'], | ||||
| '64': ['flytrap', 'getaway'], '65': ['fracture', 'glossary'], | ||||
| '66': ['framework', 'gossamer'], '67': ['freedom', 'graduate'], | ||||
| '68': ['frighten', 'gravity'], '69': ['gazelle', 'guitarist'], | ||||
| '6A': ['Geiger', 'hamburger'], '6B': ['glitter', 'Hamilton'], | ||||
| '6C': ['glucose', 'handiwork'], '6D': ['goggles', 'hazardous'], | ||||
| '6E': ['goldfish', 'headwaters'], '6F': ['gremlin', 'hemisphere'], | ||||
| '70': ['guidance', 'hesitate'], '71': ['hamlet', 'hideaway'], | ||||
| '72': ['highchair', 'holiness'], '73': ['hockey', 'hurricane'], | ||||
| '74': ['indoors', 'hydraulic'], '75': ['indulge', 'impartial'], | ||||
| '76': ['inverse', 'impetus'], '77': ['involve', 'inception'], | ||||
| '78': ['island', 'indigo'], '79': ['jawbone', 'inertia'], | ||||
| '7A': ['keyboard', 'infancy'], '7B': ['kickoff', 'inferno'], | ||||
| '7C': ['kiwi', 'informant'], '7D': ['klaxon', 'insincere'], | ||||
| '7E': ['locale', 'insurgent'], '7F': ['lockup', 'integrate'], | ||||
| '80': ['merit', 'intention'], '81': ['minnow', 'inventive'], | ||||
| '82': ['miser', 'Istanbul'], '83': ['Mohawk', 'Jamaica'], | ||||
| '84': ['mural', 'Jupiter'], '85': ['music', 'leprosy'], | ||||
| '86': ['necklace', 'letterhead'], '87': ['Neptune', 'liberty'], | ||||
| '88': ['newborn', 'maritime'], '89': ['nightbird', 'matchmaker'], | ||||
| '8A': ['Oakland', 'maverick'], '8B': ['obtuse', 'Medusa'], | ||||
| '8C': ['offload', 'megaton'], '8D': ['optic', 'microscope'], | ||||
| '8E': ['orca', 'microwave'], '8F': ['payday', 'midsummer'], | ||||
| '90': ['peachy', 'millionaire'], '91': ['pheasant', 'miracle'], | ||||
| '92': ['physique', 'misnomer'], '93': ['playhouse', 'molasses'], | ||||
| '94': ['Pluto', 'molecule'], '95': ['preclude', 'Montana'], | ||||
| '96': ['prefer', 'monument'], '97': ['preshrunk', 'mosquito'], | ||||
| '98': ['printer', 'narrative'], '99': ['prowler', 'nebula'], | ||||
| '9A': ['pupil', 'newsletter'], '9B': ['puppy', 'Norwegian'], | ||||
| '9C': ['python', 'October'], '9D': ['quadrant', 'Ohio'], | ||||
| '9E': ['quiver', 'onlooker'], '9F': ['quota', 'opulent'], | ||||
| 'A0': ['ragtime', 'Orlando'], 'A1': ['ratchet', 'outfielder'], | ||||
| 'A2': ['rebirth', 'Pacific'], 'A3': ['reform', 'pandemic'], | ||||
| 'A4': ['regain', 'Pandora'], 'A5': ['reindeer', 'paperweight'], | ||||
| 'A6': ['rematch', 'paragon'], 'A7': ['repay', 'paragraph'], | ||||
| 'A8': ['retouch', 'paramount'], 'A9': ['revenge', 'passenger'], | ||||
| 'AA': ['reward', 'pedigree'], 'AB': ['rhythm', 'Pegasus'], | ||||
| 'AC': ['ribcage', 'penetrate'], 'AD': ['ringbolt', 'perceptive'], | ||||
| 'AE': ['robust', 'performance'], 'AF': ['rocker', 'pharmacy'], | ||||
| 'B0': ['ruffled', 'phonetic'], 'B1': ['sailboat', 'photograph'], | ||||
| 'B2': ['sawdust', 'pioneer'], 'B3': ['scallion', 'pocketful'], | ||||
| 'B4': ['scenic', 'politeness'], 'B5': ['scorecard', 'positive'], | ||||
| 'B6': ['Scotland', 'potato'], 'B7': ['seabird', 'processor'], | ||||
| 'B8': ['select', 'provincial'], 'B9': ['sentence', 'proximate'], | ||||
| 'BA': ['shadow', 'puberty'], 'BB': ['shamrock', 'publisher'], | ||||
| 'BC': ['showgirl', 'pyramid'], 'BD': ['skullcap', 'quantity'], | ||||
| 'BE': ['skydive', 'racketeer'], 'BF': ['slingshot', 'rebellion'], | ||||
| 'C0': ['slowdown', 'recipe'], 'C1': ['snapline', 'recover'], | ||||
| 'C2': ['snapshot', 'repellent'], 'C3': ['snowcap', 'replica'], | ||||
| 'C4': ['snowslide', 'reproduce'], 'C5': ['solo', 'resistor'], | ||||
| 'C6': ['southward', 'responsive'], 'C7': ['soybean', 'retraction'], | ||||
| 'C8': ['spaniel', 'retrieval'], 'C9': ['spearhead', 'retrospect'], | ||||
| 'CA': ['spellbind', 'revenue'], 'CB': ['spheroid', 'revival'], | ||||
| 'CC': ['spigot', 'revolver'], 'CD': ['spindle', 'sandalwood'], | ||||
| 'CE': ['spyglass', 'sardonic'], 'CF': ['stagehand', 'Saturday'], | ||||
| 'D0': ['stagnate', 'savagery'], 'D1': ['stairway', 'scavenger'], | ||||
| 'D2': ['standard', 'sensation'], 'D3': ['stapler', 'sociable'], | ||||
| 'D4': ['steamship', 'souvenir'], 'D5': ['sterling', 'specialist'], | ||||
| 'D6': ['stockman', 'speculate'], 'D7': ['stopwatch', 'stethoscope'], | ||||
| 'D8': ['stormy', 'stupendous'], 'D9': ['sugar', 'supportive'], | ||||
| 'DA': ['surmount', 'surrender'], 'DB': ['suspense', 'suspicious'], | ||||
| 'DC': ['sweatband', 'sympathy'], 'DD': ['swelter', 'tambourine'], | ||||
| 'DE': ['tactics', 'telephone'], 'DF': ['talon', 'therapist'], | ||||
| 'E0': ['tapeworm', 'tobacco'], 'E1': ['tempest', 'tolerance'], | ||||
| 'E2': ['tiger', 'tomorrow'], 'E3': ['tissue', 'torpedo'], | ||||
| 'E4': ['tonic', 'tradition'], 'E5': ['topmost', 'travesty'], | ||||
| 'E6': ['tracker', 'trombonist'], 'E7': ['transit', 'truncated'], | ||||
| 'E8': ['trauma', 'typewriter'], 'E9': ['treadmill', 'ultimate'], | ||||
| 'EA': ['Trojan', 'undaunted'], 'EB': ['trouble', 'underfoot'], | ||||
| 'EC': ['tumor', 'unicorn'], 'ED': ['tunnel', 'unify'], | ||||
| 'EE': ['tycoon', 'universe'], 'EF': ['uncut', 'unravel'], | ||||
| 'F0': ['unearth', 'upcoming'], 'F1': ['unwind', 'vacancy'], | ||||
| 'F2': ['uproot', 'vagabond'], 'F3': ['upset', 'vertigo'], | ||||
| 'F4': ['upshot', 'Virginia'], 'F5': ['vapor', 'visitor'], | ||||
| 'F6': ['village', 'vocalist'], 'F7': ['virus', 'voyager'], | ||||
| 'F8': ['Vulcan', 'warranty'], 'F9': ['waffle', 'Waterloo'], | ||||
| 'FA': ['wallet', 'whimsical'], 'FB': ['watchword', 'Wichita'], | ||||
| 'FC': ['wayside', 'Wilmington'], 'FD': ['willow', 'Wyoming'], | ||||
| 'FE': ['woodlark', 'yesteryear'], 'FF': ['Zulu', 'Yucatan'] | ||||
| }; | ||||
|     '00': ['aardvark', 'adroitness'], | ||||
|     '01': ['absurd', 'adviser'], | ||||
|     '02': ['accrue', 'aftermath'], | ||||
|     '03': ['acme', 'aggregate'], | ||||
|     '04': ['adrift', 'alkali'], | ||||
|     '05': ['adult', 'almighty'], | ||||
|     '06': ['afflict', 'amulet'], | ||||
|     '07': ['ahead', 'amusement'], | ||||
|     '08': ['aimless', 'antenna'], | ||||
|     '09': ['Algol', 'applicant'], | ||||
|     '0A': ['allow', 'Apollo'], | ||||
|     '0B': ['alone', 'armistice'], | ||||
|     '0C': ['ammo', 'article'], | ||||
|     '0D': ['ancient', 'asteroid'], | ||||
|     '0E': ['apple', 'Atlantic'], | ||||
|     '0F': ['artist', 'atmosphere'], | ||||
|     '10': ['assume', 'autopsy'], | ||||
|     '11': ['Athens', 'Babylon'], | ||||
|     '12': ['atlas', 'backwater'], | ||||
|     '13': ['Aztec', 'barbecue'], | ||||
|     '14': ['baboon', 'belowground'], | ||||
|     '15': ['backfield', 'bifocals'], | ||||
|     '16': ['backward', 'bodyguard'], | ||||
|     '17': ['banjo', 'bookseller'], | ||||
|     '18': ['beaming', 'borderline'], | ||||
|     '19': ['bedlamp', 'bottomless'], | ||||
|     '1A': ['beehive', 'Bradbury'], | ||||
|     '1B': ['beeswax', 'bravado'], | ||||
|     '1C': ['befriend', 'Brazilian'], | ||||
|     '1D': ['Belfast', 'breakaway'], | ||||
|     '1E': ['berserk', 'Burlington'], | ||||
|     '1F': ['billiard', 'businessman'], | ||||
|     '20': ['bison', 'butterfat'], | ||||
|     '21': ['blackjack', 'Camelot'], | ||||
|     '22': ['blockade', 'candidate'], | ||||
|     '23': ['blowtorch', 'cannonball'], | ||||
|     '24': ['bluebird', 'Capricorn'], | ||||
|     '25': ['bombast', 'caravan'], | ||||
|     '26': ['bookshelf', 'caretaker'], | ||||
|     '27': ['brackish', 'celebrate'], | ||||
|     '28': ['breadline', 'cellulose'], | ||||
|     '29': ['breakup', 'certify'], | ||||
|     '2A': ['brickyard', 'chambermaid'], | ||||
|     '2B': ['briefcase', 'Cherokee'], | ||||
|     '2C': ['Burbank', 'Chicago'], | ||||
|     '2D': ['button', 'clergyman'], | ||||
|     '2E': ['buzzard', 'coherence'], | ||||
|     '2F': ['cement', 'combustion'], | ||||
|     '30': ['chairlift', 'commando'], | ||||
|     '31': ['chatter', 'company'], | ||||
|     '32': ['checkup', 'component'], | ||||
|     '33': ['chisel', 'concurrent'], | ||||
|     '34': ['choking', 'confidence'], | ||||
|     '35': ['chopper', 'conformist'], | ||||
|     '36': ['Christmas', 'congregate'], | ||||
|     '37': ['clamshell', 'consensus'], | ||||
|     '38': ['classic', 'consulting'], | ||||
|     '39': ['classroom', 'corporate'], | ||||
|     '3A': ['cleanup', 'corrosion'], | ||||
|     '3B': ['clockwork', 'councilman'], | ||||
|     '3C': ['cobra', 'crossover'], | ||||
|     '3D': ['commence', 'crucifix'], | ||||
|     '3E': ['concert', 'cumbersome'], | ||||
|     '3F': ['cowbell', 'customer'], | ||||
|     '40': ['crackdown', 'Dakota'], | ||||
|     '41': ['cranky', 'decadence'], | ||||
|     '42': ['crowfoot', 'December'], | ||||
|     '43': ['crucial', 'decimal'], | ||||
|     '44': ['crumpled', 'designing'], | ||||
|     '45': ['crusade', 'detector'], | ||||
|     '46': ['cubic', 'detergent'], | ||||
|     '47': ['dashboard', 'determine'], | ||||
|     '48': ['deadbolt', 'dictator'], | ||||
|     '49': ['deckhand', 'dinosaur'], | ||||
|     '4A': ['dogsled', 'direction'], | ||||
|     '4B': ['dragnet', 'disable'], | ||||
|     '4C': ['drainage', 'disbelief'], | ||||
|     '4D': ['dreadful', 'disruptive'], | ||||
|     '4E': ['drifter', 'distortion'], | ||||
|     '4F': ['dropper', 'document'], | ||||
|     '50': ['drumbeat', 'embezzle'], | ||||
|     '51': ['drunken', 'enchanting'], | ||||
|     '52': ['Dupont', 'enrollment'], | ||||
|     '53': ['dwelling', 'enterprise'], | ||||
|     '54': ['eating', 'equation'], | ||||
|     '55': ['edict', 'equipment'], | ||||
|     '56': ['egghead', 'escapade'], | ||||
|     '57': ['eightball', 'Eskimo'], | ||||
|     '58': ['endorse', 'everyday'], | ||||
|     '59': ['endow', 'examine'], | ||||
|     '5A': ['enlist', 'existence'], | ||||
|     '5B': ['erase', 'exodus'], | ||||
|     '5C': ['escape', 'fascinate'], | ||||
|     '5D': ['exceed', 'filament'], | ||||
|     '5E': ['eyeglass', 'finicky'], | ||||
|     '5F': ['eyetooth', 'forever'], | ||||
|     '60': ['facial', 'fortitude'], | ||||
|     '61': ['fallout', 'frequency'], | ||||
|     '62': ['flagpole', 'gadgetry'], | ||||
|     '63': ['flatfoot', 'Galveston'], | ||||
|     '64': ['flytrap', 'getaway'], | ||||
|     '65': ['fracture', 'glossary'], | ||||
|     '66': ['framework', 'gossamer'], | ||||
|     '67': ['freedom', 'graduate'], | ||||
|     '68': ['frighten', 'gravity'], | ||||
|     '69': ['gazelle', 'guitarist'], | ||||
|     '6A': ['Geiger', 'hamburger'], | ||||
|     '6B': ['glitter', 'Hamilton'], | ||||
|     '6C': ['glucose', 'handiwork'], | ||||
|     '6D': ['goggles', 'hazardous'], | ||||
|     '6E': ['goldfish', 'headwaters'], | ||||
|     '6F': ['gremlin', 'hemisphere'], | ||||
|     '70': ['guidance', 'hesitate'], | ||||
|     '71': ['hamlet', 'hideaway'], | ||||
|     '72': ['highchair', 'holiness'], | ||||
|     '73': ['hockey', 'hurricane'], | ||||
|     '74': ['indoors', 'hydraulic'], | ||||
|     '75': ['indulge', 'impartial'], | ||||
|     '76': ['inverse', 'impetus'], | ||||
|     '77': ['involve', 'inception'], | ||||
|     '78': ['island', 'indigo'], | ||||
|     '79': ['jawbone', 'inertia'], | ||||
|     '7A': ['keyboard', 'infancy'], | ||||
|     '7B': ['kickoff', 'inferno'], | ||||
|     '7C': ['kiwi', 'informant'], | ||||
|     '7D': ['klaxon', 'insincere'], | ||||
|     '7E': ['locale', 'insurgent'], | ||||
|     '7F': ['lockup', 'integrate'], | ||||
|     '80': ['merit', 'intention'], | ||||
|     '81': ['minnow', 'inventive'], | ||||
|     '82': ['miser', 'Istanbul'], | ||||
|     '83': ['Mohawk', 'Jamaica'], | ||||
|     '84': ['mural', 'Jupiter'], | ||||
|     '85': ['music', 'leprosy'], | ||||
|     '86': ['necklace', 'letterhead'], | ||||
|     '87': ['Neptune', 'liberty'], | ||||
|     '88': ['newborn', 'maritime'], | ||||
|     '89': ['nightbird', 'matchmaker'], | ||||
|     '8A': ['Oakland', 'maverick'], | ||||
|     '8B': ['obtuse', 'Medusa'], | ||||
|     '8C': ['offload', 'megaton'], | ||||
|     '8D': ['optic', 'microscope'], | ||||
|     '8E': ['orca', 'microwave'], | ||||
|     '8F': ['payday', 'midsummer'], | ||||
|     '90': ['peachy', 'millionaire'], | ||||
|     '91': ['pheasant', 'miracle'], | ||||
|     '92': ['physique', 'misnomer'], | ||||
|     '93': ['playhouse', 'molasses'], | ||||
|     '94': ['Pluto', 'molecule'], | ||||
|     '95': ['preclude', 'Montana'], | ||||
|     '96': ['prefer', 'monument'], | ||||
|     '97': ['preshrunk', 'mosquito'], | ||||
|     '98': ['printer', 'narrative'], | ||||
|     '99': ['prowler', 'nebula'], | ||||
|     '9A': ['pupil', 'newsletter'], | ||||
|     '9B': ['puppy', 'Norwegian'], | ||||
|     '9C': ['python', 'October'], | ||||
|     '9D': ['quadrant', 'Ohio'], | ||||
|     '9E': ['quiver', 'onlooker'], | ||||
|     '9F': ['quota', 'opulent'], | ||||
|     'A0': ['ragtime', 'Orlando'], | ||||
|     'A1': ['ratchet', 'outfielder'], | ||||
|     'A2': ['rebirth', 'Pacific'], | ||||
|     'A3': ['reform', 'pandemic'], | ||||
|     'A4': ['regain', 'Pandora'], | ||||
|     'A5': ['reindeer', 'paperweight'], | ||||
|     'A6': ['rematch', 'paragon'], | ||||
|     'A7': ['repay', 'paragraph'], | ||||
|     'A8': ['retouch', 'paramount'], | ||||
|     'A9': ['revenge', 'passenger'], | ||||
|     'AA': ['reward', 'pedigree'], | ||||
|     'AB': ['rhythm', 'Pegasus'], | ||||
|     'AC': ['ribcage', 'penetrate'], | ||||
|     'AD': ['ringbolt', 'perceptive'], | ||||
|     'AE': ['robust', 'performance'], | ||||
|     'AF': ['rocker', 'pharmacy'], | ||||
|     'B0': ['ruffled', 'phonetic'], | ||||
|     'B1': ['sailboat', 'photograph'], | ||||
|     'B2': ['sawdust', 'pioneer'], | ||||
|     'B3': ['scallion', 'pocketful'], | ||||
|     'B4': ['scenic', 'politeness'], | ||||
|     'B5': ['scorecard', 'positive'], | ||||
|     'B6': ['Scotland', 'potato'], | ||||
|     'B7': ['seabird', 'processor'], | ||||
|     'B8': ['select', 'provincial'], | ||||
|     'B9': ['sentence', 'proximate'], | ||||
|     'BA': ['shadow', 'puberty'], | ||||
|     'BB': ['shamrock', 'publisher'], | ||||
|     'BC': ['showgirl', 'pyramid'], | ||||
|     'BD': ['skullcap', 'quantity'], | ||||
|     'BE': ['skydive', 'racketeer'], | ||||
|     'BF': ['slingshot', 'rebellion'], | ||||
|     'C0': ['slowdown', 'recipe'], | ||||
|     'C1': ['snapline', 'recover'], | ||||
|     'C2': ['snapshot', 'repellent'], | ||||
|     'C3': ['snowcap', 'replica'], | ||||
|     'C4': ['snowslide', 'reproduce'], | ||||
|     'C5': ['solo', 'resistor'], | ||||
|     'C6': ['southward', 'responsive'], | ||||
|     'C7': ['soybean', 'retraction'], | ||||
|     'C8': ['spaniel', 'retrieval'], | ||||
|     'C9': ['spearhead', 'retrospect'], | ||||
|     'CA': ['spellbind', 'revenue'], | ||||
|     'CB': ['spheroid', 'revival'], | ||||
|     'CC': ['spigot', 'revolver'], | ||||
|     'CD': ['spindle', 'sandalwood'], | ||||
|     'CE': ['spyglass', 'sardonic'], | ||||
|     'CF': ['stagehand', 'Saturday'], | ||||
|     'D0': ['stagnate', 'savagery'], | ||||
|     'D1': ['stairway', 'scavenger'], | ||||
|     'D2': ['standard', 'sensation'], | ||||
|     'D3': ['stapler', 'sociable'], | ||||
|     'D4': ['steamship', 'souvenir'], | ||||
|     'D5': ['sterling', 'specialist'], | ||||
|     'D6': ['stockman', 'speculate'], | ||||
|     'D7': ['stopwatch', 'stethoscope'], | ||||
|     'D8': ['stormy', 'stupendous'], | ||||
|     'D9': ['sugar', 'supportive'], | ||||
|     'DA': ['surmount', 'surrender'], | ||||
|     'DB': ['suspense', 'suspicious'], | ||||
|     'DC': ['sweatband', 'sympathy'], | ||||
|     'DD': ['swelter', 'tambourine'], | ||||
|     'DE': ['tactics', 'telephone'], | ||||
|     'DF': ['talon', 'therapist'], | ||||
|     'E0': ['tapeworm', 'tobacco'], | ||||
|     'E1': ['tempest', 'tolerance'], | ||||
|     'E2': ['tiger', 'tomorrow'], | ||||
|     'E3': ['tissue', 'torpedo'], | ||||
|     'E4': ['tonic', 'tradition'], | ||||
|     'E5': ['topmost', 'travesty'], | ||||
|     'E6': ['tracker', 'trombonist'], | ||||
|     'E7': ['transit', 'truncated'], | ||||
|     'E8': ['trauma', 'typewriter'], | ||||
|     'E9': ['treadmill', 'ultimate'], | ||||
|     'EA': ['Trojan', 'undaunted'], | ||||
|     'EB': ['trouble', 'underfoot'], | ||||
|     'EC': ['tumor', 'unicorn'], | ||||
|     'ED': ['tunnel', 'unify'], | ||||
|     'EE': ['tycoon', 'universe'], | ||||
|     'EF': ['uncut', 'unravel'], | ||||
|     'F0': ['unearth', 'upcoming'], | ||||
|     'F1': ['unwind', 'vacancy'], | ||||
|     'F2': ['uproot', 'vagabond'], | ||||
|     'F3': ['upset', 'vertigo'], | ||||
|     'F4': ['upshot', 'Virginia'], | ||||
|     'F5': ['vapor', 'visitor'], | ||||
|     'F6': ['village', 'vocalist'], | ||||
|     'F7': ['virus', 'voyager'], | ||||
|     'F8': ['Vulcan', 'warranty'], | ||||
|     'F9': ['waffle', 'Waterloo'], | ||||
|     'FA': ['wallet', 'whimsical'], | ||||
|     'FB': ['watchword', 'Wichita'], | ||||
|     'FC': ['wayside', 'Wilmington'], | ||||
|     'FD': ['willow', 'Wyoming'], | ||||
|     'FE': ['woodlark', 'yesteryear'], | ||||
|     'FF': ['Zulu', 'Yucatan'] | ||||
| } | ||||
| 
 | ||||
| byte_to_even_word = dict([(unhexlify(k.encode("ascii")), both_words[0]) | ||||
|                           for k,both_words | ||||
|                           in raw_words.items()]) | ||||
|                           for k, both_words in raw_words.items()]) | ||||
| 
 | ||||
| byte_to_odd_word = dict([(unhexlify(k.encode("ascii")), both_words[1]) | ||||
|                          for k,both_words | ||||
|                          in raw_words.items()]) | ||||
|                          for k, both_words in raw_words.items()]) | ||||
| 
 | ||||
| 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_words_lowercase.add(even_word.lower()) | ||||
|     odd_words_lowercase.add(odd_word.lower()) | ||||
| 
 | ||||
| 
 | ||||
| @implementer(IWordlist) | ||||
| class PGPWordList(object): | ||||
|     def get_completions(self, prefix, num_words=2): | ||||
|  | @ -177,7 +307,7 @@ class PGPWordList(object): | |||
|                 else: | ||||
|                     suffix = prefix[:-lp] + word | ||||
|                 # append a hyphen if we expect more words | ||||
|                 if count+1 < num_words: | ||||
|                 if count + 1 < num_words: | ||||
|                     suffix += "-" | ||||
|                 completions.add(suffix) | ||||
|         return completions | ||||
|  |  | |||
|  | @ -2,21 +2,24 @@ from __future__ import print_function | |||
| 
 | ||||
| import os | ||||
| import time | ||||
| start = time.time() | ||||
| import six | ||||
| 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 | ||||
| from sys import stderr, stdout | ||||
| from textwrap import dedent, fill | ||||
| 
 | ||||
| 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() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -24,6 +27,7 @@ class Config(object): | |||
|     """ | ||||
|     Union of config options that we pass down to (sub) commands. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         # This only holds attributes which are *not* set by CLI arguments. | ||||
|         # Everything else comes from Click decorators, so we can be sure | ||||
|  | @ -34,11 +38,13 @@ class Config(object): | |||
|         self.stderr = stderr | ||||
|         self.tor = False  # XXX? | ||||
| 
 | ||||
| 
 | ||||
| def _compose(*decorators): | ||||
|     def decorate(f): | ||||
|         for d in reversed(decorators): | ||||
|             f = d(f) | ||||
|         return f | ||||
| 
 | ||||
|     return decorate | ||||
| 
 | ||||
| 
 | ||||
|  | @ -48,6 +54,8 @@ ALIASES = { | |||
|     "recieve": "receive", | ||||
|     "recv": "receive", | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class AliasedGroup(click.Group): | ||||
|     def get_command(self, ctx, cmd_name): | ||||
|         cmd_name = ALIASES.get(cmd_name, cmd_name) | ||||
|  | @ -56,22 +64,24 @@ class AliasedGroup(click.Group): | |||
| 
 | ||||
| # top-level command ("wormhole ...") | ||||
| @click.group(cls=AliasedGroup) | ||||
| @click.option("--appid", default=None, metavar="APPID", help="appid to use") | ||||
| @click.option( | ||||
|     "--appid", default=None, metavar="APPID", help="appid to use") | ||||
| @click.option( | ||||
|     "--relay-url", default=public_relay.RENDEZVOUS_RELAY, | ||||
|     "--relay-url", | ||||
|     default=public_relay.RENDEZVOUS_RELAY, | ||||
|     envvar='WORMHOLE_RELAY_URL', | ||||
|     metavar="URL", | ||||
|     help="rendezvous relay to use", | ||||
| ) | ||||
| @click.option( | ||||
|     "--transit-helper", default=public_relay.TRANSIT_RELAY, | ||||
|     "--transit-helper", | ||||
|     default=public_relay.TRANSIT_RELAY, | ||||
|     envvar='WORMHOLE_TRANSIT_HELPER', | ||||
|     metavar="tcp:HOST:PORT", | ||||
|     help="transit relay to use", | ||||
| ) | ||||
| @click.option( | ||||
|     "--dump-timing", type=type(u""), # TODO: hide from --help output | ||||
|     "--dump-timing", | ||||
|     type=type(u""),  # TODO: hide from --help output | ||||
|     default=None, | ||||
|     metavar="FILE.json", | ||||
|     help="(debug) write timing data to file", | ||||
|  | @ -104,7 +114,8 @@ def _dispatch_command(reactor, cfg, command): | |||
|     errors for the user. | ||||
|     """ | ||||
|     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: | ||||
|         yield maybeDeferred(command) | ||||
|  | @ -141,56 +152,89 @@ def _dispatch_command(reactor, cfg, command): | |||
| 
 | ||||
| 
 | ||||
| CommonArgs = _compose( | ||||
|     click.option("-0", "zeromode", default=False, is_flag=True, | ||||
|                  help="enable no-code anything-goes mode", | ||||
|                  ), | ||||
|     click.option("-c", "--code-length", default=2, metavar="NUMWORDS", | ||||
|                  help="length of code (in bytes/words)", | ||||
|                  ), | ||||
|     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", | ||||
|                  ), | ||||
|     click.option( | ||||
|         "-0", | ||||
|         "zeromode", | ||||
|         default=False, | ||||
|         is_flag=True, | ||||
|         help="enable no-code anything-goes mode", | ||||
|     ), | ||||
|     click.option( | ||||
|         "-c", | ||||
|         "--code-length", | ||||
|         default=2, | ||||
|         metavar="NUMWORDS", | ||||
|         help="length of code (in bytes/words)", | ||||
|     ), | ||||
|     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( | ||||
|     click.option("--tor", is_flag=True, default=False, | ||||
|                  help="use Tor when connecting", | ||||
|                  ), | ||||
|     click.option("--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", | ||||
|                  ), | ||||
|     click.option( | ||||
|         "--tor", | ||||
|         is_flag=True, | ||||
|         default=False, | ||||
|         help="use Tor when connecting", | ||||
|     ), | ||||
|     click.option( | ||||
|         "--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() | ||||
| @click.pass_context | ||||
| def help(context, **kwargs): | ||||
|     print(context.find_root().get_help()) | ||||
| 
 | ||||
| 
 | ||||
| # wormhole send (or "wormhole tx") | ||||
| @wormhole.command() | ||||
| @CommonArgs | ||||
| @TorArgs | ||||
| @click.option( | ||||
|     "--code", metavar="CODE", | ||||
|     "--code", | ||||
|     metavar="CODE", | ||||
|     help="human-generated code phrase", | ||||
| ) | ||||
| @click.option( | ||||
|     "--text", default=None, metavar="MESSAGE", | ||||
|     help="text message to send, instead of a file. Use '-' to read from stdin.", | ||||
|     "--text", | ||||
|     default=None, | ||||
|     metavar="MESSAGE", | ||||
|     help=("text message to send, instead of a file." | ||||
|           " Use '-' to read from stdin."), | ||||
| ) | ||||
| @click.option( | ||||
|     "--ignore-unsendable-files", default=False, is_flag=True, | ||||
|     help="Don't raise an error if a file can't be read." | ||||
| ) | ||||
|     "--ignore-unsendable-files", | ||||
|     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.pass_obj | ||||
| def send(cfg, **kwargs): | ||||
|  | @ -202,6 +246,7 @@ def send(cfg, **kwargs): | |||
| 
 | ||||
|     return go(cmd_send.send, cfg) | ||||
| 
 | ||||
| 
 | ||||
| # this intermediate function can be mocked by tests that need to build a | ||||
| # Config object | ||||
| def go(f, cfg): | ||||
|  | @ -214,23 +259,29 @@ def go(f, cfg): | |||
| @CommonArgs | ||||
| @TorArgs | ||||
| @click.option( | ||||
|     "--only-text", "-t", is_flag=True, | ||||
|     "--only-text", | ||||
|     "-t", | ||||
|     is_flag=True, | ||||
|     help="refuse file transfers, only accept text transfers", | ||||
| ) | ||||
| @click.option( | ||||
|     "--accept-file", is_flag=True, | ||||
|     "--accept-file", | ||||
|     is_flag=True, | ||||
|     help="accept file transfer without asking for confirmation", | ||||
| ) | ||||
| @click.option( | ||||
|     "--output-file", "-o", | ||||
|     "--output-file", | ||||
|     "-o", | ||||
|     metavar="FILENAME|DIRNAME", | ||||
|     help=("The file or directory to create, overriding the name suggested" | ||||
|           " by the sender."), | ||||
| ) | ||||
| @click.argument( | ||||
|     "code", nargs=-1, default=None, | ||||
| #    help=("The magic-wormhole code, from the sender. If omitted, the" | ||||
| #          " program will ask for it, using tab-completion."), | ||||
|     "code", | ||||
|     nargs=-1, | ||||
|     default=None, | ||||
|     #    help=("The magic-wormhole code, from the sender. If omitted, the" | ||||
|     #          " program will ask for it, using tab-completion."), | ||||
| ) | ||||
| @click.pass_obj | ||||
| def receive(cfg, code, **kwargs): | ||||
|  | @ -244,10 +295,8 @@ def receive(cfg, code, **kwargs): | |||
|     if len(code) == 1: | ||||
|         cfg.code = code[0] | ||||
|     elif len(code) > 1: | ||||
|         print( | ||||
|             "Pass either no code or just one code; you passed" | ||||
|             " {}: {}".format(len(code), ', '.join(code)) | ||||
|         ) | ||||
|         print("Pass either no code or just one code; you passed" | ||||
|               " {}: {}".format(len(code), ', '.join(code))) | ||||
|         raise SystemExit(1) | ||||
|     else: | ||||
|         cfg.code = None | ||||
|  | @ -260,17 +309,19 @@ def ssh(): | |||
|     """ | ||||
|     Facilitate sending/receiving SSH public keys | ||||
|     """ | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| @ssh.command(name="invite") | ||||
| @click.option( | ||||
|     "-c", "--code-length", default=2, | ||||
|     "-c", | ||||
|     "--code-length", | ||||
|     default=2, | ||||
|     metavar="NUMWORDS", | ||||
|     help="length of code (in bytes/words)", | ||||
| ) | ||||
| @click.option( | ||||
|     "--user", "-u", | ||||
|     "--user", | ||||
|     "-u", | ||||
|     default=None, | ||||
|     metavar="USER", | ||||
|     help="Add to USER's ~/.ssh/authorized_keys", | ||||
|  | @ -291,15 +342,20 @@ def ssh_invite(ctx, code_length, user, **kwargs): | |||
| 
 | ||||
| @ssh.command(name="accept") | ||||
| @click.argument( | ||||
|     "code", nargs=1, required=True, | ||||
|     "code", | ||||
|     nargs=1, | ||||
|     required=True, | ||||
| ) | ||||
| @click.option( | ||||
|     "--key-file", "-F", | ||||
|     "--key-file", | ||||
|     "-F", | ||||
|     default=None, | ||||
|     type=click.Path(exists=True), | ||||
| ) | ||||
| @click.option( | ||||
|     "--yes", "-y", is_flag=True, | ||||
|     "--yes", | ||||
|     "-y", | ||||
|     is_flag=True, | ||||
|     help="Skip confirmation prompt to send key", | ||||
| ) | ||||
| @TorArgs | ||||
|  | @ -318,7 +374,8 @@ def ssh_accept(cfg, code, key_file, yes, **kwargs): | |||
|     kind, keyid, pubkey = cmd_ssh.find_public_key(key_file) | ||||
|     print("Sending public key type='{}' keyid='{}'".format(kind, keyid)) | ||||
|     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.code = code | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,14 +1,23 @@ | |||
| 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 tqdm import tqdm | ||||
| from twisted.internet import reactor | ||||
| from twisted.internet.defer import inlineCallbacks, returnValue | ||||
| from twisted.python import log | ||||
| from wormhole import create, input_with_completion, __version__ | ||||
| from ..transit import TransitReceiver | ||||
| from wormhole import __version__, create, input_with_completion | ||||
| 
 | ||||
| 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) | ||||
| 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)) | ||||
| VERIFY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_VERIFY_TIMER", 1.0)) | ||||
| 
 | ||||
| 
 | ||||
| class RespondError(Exception): | ||||
|     def __init__(self, response): | ||||
|         self.response = response | ||||
| 
 | ||||
| 
 | ||||
| class TransferRejectedError(RespondError): | ||||
|     def __init__(self): | ||||
|         RespondError.__init__(self, "transfer rejected") | ||||
| 
 | ||||
| 
 | ||||
| def receive(args, reactor=reactor, _debug_stash_wormhole=None): | ||||
|     """I implement 'wormhole receive'. I return a Deferred that fires with | ||||
|     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 | ||||
|             # can lazy-provide an endpoint, and overlap the startup process | ||||
|             # with the user handing off the wormhole code | ||||
|             self._tor = yield get_tor(self._reactor, | ||||
|                                       self.args.launch_tor, | ||||
|                                       self.args.tor_control_port, | ||||
|                                       timing=self.args.timing) | ||||
|             self._tor = yield get_tor( | ||||
|                 self._reactor, | ||||
|                 self.args.launch_tor, | ||||
|                 self.args.tor_control_port, | ||||
|                 timing=self.args.timing) | ||||
| 
 | ||||
|         w = create(self.args.appid or APPID, self.args.relay_url, | ||||
|                    self._reactor, | ||||
|                    tor=self._tor, | ||||
|                    timing=self.args.timing) | ||||
|         self._w = w # so tests can wait on events too | ||||
|         w = create( | ||||
|             self.args.appid or APPID, | ||||
|             self.args.relay_url, | ||||
|             self._reactor, | ||||
|             tor=self._tor, | ||||
|             timing=self.args.timing) | ||||
|         self._w = w  # so tests can wait on events too | ||||
| 
 | ||||
|         # I wanted to do this instead: | ||||
|         # | ||||
|  | @ -87,7 +102,7 @@ class Receiver: | |||
|         # (which might be an error) | ||||
|         @inlineCallbacks | ||||
|         def _good(res): | ||||
|             yield w.close() # wait for ack | ||||
|             yield w.close()  # wait for ack | ||||
|             returnValue(res) | ||||
| 
 | ||||
|         # if we raise an error, we should close and then return the original | ||||
|  | @ -96,8 +111,8 @@ class Receiver: | |||
|         @inlineCallbacks | ||||
|         def _bad(f): | ||||
|             try: | ||||
|                 yield w.close() # might be an error too | ||||
|             except: | ||||
|                 yield w.close()  # might be an error too | ||||
|             except Exception: | ||||
|                 pass | ||||
|             returnValue(f) | ||||
| 
 | ||||
|  | @ -114,6 +129,7 @@ class Receiver: | |||
| 
 | ||||
|         def on_slow_key(): | ||||
|             print(u"Waiting for sender...", file=self.args.stderr) | ||||
| 
 | ||||
|         notify = self._reactor.callLater(KEY_TIMER, on_slow_key) | ||||
|         try: | ||||
|             # We wait here until we connect to the server and see the senders | ||||
|  | @ -129,8 +145,10 @@ class Receiver: | |||
|                 notify.cancel() | ||||
| 
 | ||||
|         def on_slow_verification(): | ||||
|             print(u"Key established, waiting for confirmation...", | ||||
|                   file=self.args.stderr) | ||||
|             print( | ||||
|                 u"Key established, waiting for confirmation...", | ||||
|                 file=self.args.stderr) | ||||
| 
 | ||||
|         notify = self._reactor.callLater(VERIFY_TIMER, on_slow_verification) | ||||
|         try: | ||||
|             # We wait here until we've seen their VERSION message (which they | ||||
|  | @ -155,7 +173,7 @@ class Receiver: | |||
| 
 | ||||
|         while True: | ||||
|             them_d = yield self._get_data(w) | ||||
|             #print("GOT", them_d) | ||||
|             # print("GOT", them_d) | ||||
|             recognized = False | ||||
|             if u"transit" in them_d: | ||||
|                 recognized = True | ||||
|  | @ -172,7 +190,7 @@ class Receiver: | |||
|                     raise TransferError(r.response) | ||||
|                 returnValue(None) | ||||
|             if not recognized: | ||||
|                 log.msg("unrecognized message %r" % (them_d,)) | ||||
|                 log.msg("unrecognized message %r" % (them_d, )) | ||||
| 
 | ||||
|     def _send_data(self, data, w): | ||||
|         data_bytes = dict_to_bytes(data) | ||||
|  | @ -197,12 +215,12 @@ class Receiver: | |||
|             w.set_code(code) | ||||
|         else: | ||||
|             prompt = "Enter receive wormhole code: " | ||||
|             used_completion = yield input_with_completion(prompt, | ||||
|                                                           w.input_code(), | ||||
|                                                           self._reactor) | ||||
|             used_completion = yield input_with_completion( | ||||
|                 prompt, w.input_code(), self._reactor) | ||||
|             if not used_completion: | ||||
|                 print(" (note: you can use <Tab> to complete words)", | ||||
|                       file=self.args.stderr) | ||||
|                 print( | ||||
|                     " (note: you can use <Tab> to complete words)", | ||||
|                     file=self.args.stderr) | ||||
|         yield w.get_code() | ||||
| 
 | ||||
|     def _show_verifier(self, verifier_bytes): | ||||
|  | @ -220,21 +238,24 @@ class Receiver: | |||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def _build_transit(self, w, sender_transit): | ||||
|         tr = TransitReceiver(self.args.transit_helper, | ||||
|                              no_listen=(not self.args.listen), | ||||
|                              tor=self._tor, | ||||
|                              reactor=self._reactor, | ||||
|                              timing=self.args.timing) | ||||
|         tr = TransitReceiver( | ||||
|             self.args.transit_helper, | ||||
|             no_listen=(not self.args.listen), | ||||
|             tor=self._tor, | ||||
|             reactor=self._reactor, | ||||
|             timing=self.args.timing) | ||||
|         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.add_connection_hints(sender_transit.get("hints-v1", [])) | ||||
|         receiver_abilities = tr.get_connection_abilities() | ||||
|         receiver_hints = yield tr.get_connection_hints() | ||||
|         receiver_transit = {"abilities-v1": receiver_abilities, | ||||
|                             "hints-v1": receiver_hints, | ||||
|                             } | ||||
|         receiver_transit = { | ||||
|             "abilities-v1": receiver_abilities, | ||||
|             "hints-v1": receiver_hints, | ||||
|         } | ||||
|         self._send_data({u"transit": receiver_transit}, w) | ||||
|         # TODO: send more hints as the TransitReceiver produces them | ||||
| 
 | ||||
|  | @ -260,7 +281,7 @@ class Receiver: | |||
|             yield self._close_transit(rp, datahash) | ||||
|         else: | ||||
|             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") | ||||
| 
 | ||||
|     def _handle_text(self, them_d, w): | ||||
|  | @ -276,12 +297,13 @@ class Receiver: | |||
|         self.xfersize = file_data["filesize"] | ||||
|         free = estimate_free_space(self.abs_destname) | ||||
|         if free is not None and free < self.xfersize: | ||||
|             self._msg(u"Error: insufficient free space (%sB) for file (%sB)" | ||||
|                       % (free, self.xfersize)) | ||||
|             self._msg(u"Error: insufficient free space (%sB) for file (%sB)" % | ||||
|                       (free, self.xfersize)) | ||||
|             raise TransferRejectedError() | ||||
| 
 | ||||
|         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() | ||||
|         tmp_destname = self.abs_destname + ".tmp" | ||||
|         return open(tmp_destname, "wb") | ||||
|  | @ -290,19 +312,22 @@ class Receiver: | |||
|         file_data = them_d["directory"] | ||||
|         zipmode = file_data["mode"] | ||||
|         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") | ||||
|         self.abs_destname = self._decide_destname("directory", | ||||
|                                                   file_data["dirname"]) | ||||
|         self.xfersize = file_data["zipsize"] | ||||
|         free = estimate_free_space(self.abs_destname) | ||||
|         if free is not None and free < file_data["numbytes"]: | ||||
|             self._msg(u"Error: insufficient free space (%sB) for directory (%sB)" | ||||
|                       % (free, file_data["numbytes"])) | ||||
|             self._msg( | ||||
|                 u"Error: insufficient free space (%sB) for directory (%sB)" % | ||||
|                 (free, file_data["numbytes"])) | ||||
|             raise TransferRejectedError() | ||||
| 
 | ||||
|         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)" % | ||||
|                   (file_data["numfiles"], naturalsize(file_data["numbytes"]))) | ||||
|         self._ask_permission() | ||||
|  | @ -313,23 +338,26 @@ class Receiver: | |||
|         # "~/.ssh/authorized_keys" and other attacks | ||||
|         destname = os.path.basename(destname) | ||||
|         if self.args.output_file: | ||||
|             destname = self.args.output_file # override | ||||
|         abs_destname = os.path.abspath( os.path.join(self.args.cwd, destname) ) | ||||
|             destname = self.args.output_file  # override | ||||
|         abs_destname = os.path.abspath(os.path.join(self.args.cwd, destname)) | ||||
| 
 | ||||
|         # get confirmation from the user before writing to the local directory | ||||
|         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) | ||||
|                 if self.args.accept_file: | ||||
|                     self._remove_existing(abs_destname) | ||||
|             else: | ||||
|                 self._msg(u"Error: refusing to overwrite existing '%s'" % destname) | ||||
|                 self._msg( | ||||
|                     u"Error: refusing to overwrite existing '%s'" % destname) | ||||
|                 raise TransferRejectedError() | ||||
|         return abs_destname | ||||
| 
 | ||||
|     def _remove_existing(self, path): | ||||
|         if os.path.isfile(path): os.remove(path) | ||||
|         if os.path.isdir(path): shutil.rmtree(path) | ||||
|         if os.path.isfile(path): | ||||
|             os.remove(path) | ||||
|         if os.path.isdir(path): | ||||
|             shutil.rmtree(path) | ||||
| 
 | ||||
|     def _ask_permission(self): | ||||
|         with self.args.timing.add("permission", waiting="user") as t: | ||||
|  | @ -345,7 +373,7 @@ class Receiver: | |||
|             t.detail(answer="yes") | ||||
| 
 | ||||
|     def _send_permission(self, w): | ||||
|         self._send_data({"answer": { "file_ack": "ok" }}, w) | ||||
|         self._send_data({"answer": {"file_ack": "ok"}}, w) | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def _establish_transit(self): | ||||
|  | @ -359,14 +387,16 @@ class Receiver: | |||
|         self._msg(u"Receiving (%s).." % record_pipe.describe()) | ||||
| 
 | ||||
|         with self.args.timing.add("rx file"): | ||||
|             progress = tqdm(file=self.args.stderr, | ||||
|                             disable=self.args.hide_progress, | ||||
|                             unit="B", unit_scale=True, total=self.xfersize) | ||||
|             progress = tqdm( | ||||
|                 file=self.args.stderr, | ||||
|                 disable=self.args.hide_progress, | ||||
|                 unit="B", | ||||
|                 unit_scale=True, | ||||
|                 total=self.xfersize) | ||||
|             hasher = hashlib.sha256() | ||||
|             with progress: | ||||
|                 received = yield record_pipe.writeToFile(f, self.xfersize, | ||||
|                                                          progress.update, | ||||
|                                                          hasher.update) | ||||
|                 received = yield record_pipe.writeToFile( | ||||
|                     f, self.xfersize, progress.update, hasher.update) | ||||
|             datahash = hasher.digest() | ||||
| 
 | ||||
|         # except TransitError | ||||
|  | @ -382,25 +412,26 @@ class Receiver: | |||
|         tmp_name = f.name | ||||
|         f.close() | ||||
|         os.rename(tmp_name, self.abs_destname) | ||||
|         self._msg(u"Received file written to %s" % | ||||
|                   os.path.basename(self.abs_destname)) | ||||
|         self._msg(u"Received file written to %s" % os.path.basename( | ||||
|             self.abs_destname)) | ||||
| 
 | ||||
|     def _extract_file(self, zf, info, extract_dir): | ||||
|         """ | ||||
|         the zipfile module does not restore file permissions | ||||
|         so we'll do it manually | ||||
|         """ | ||||
|         out_path = os.path.join( extract_dir, info.filename ) | ||||
|         out_path = os.path.abspath( out_path ) | ||||
|         if not out_path.startswith( extract_dir ): | ||||
|             raise ValueError( "malicious zipfile, %s outside of extract_dir %s" | ||||
|                     % (info.filename, extract_dir) ) | ||||
|         out_path = os.path.join(extract_dir, info.filename) | ||||
|         out_path = os.path.abspath(out_path) | ||||
|         if not out_path.startswith(extract_dir): | ||||
|             raise ValueError( | ||||
|                 "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 | ||||
|         perm = info.external_attr >> 16 | ||||
|         os.chmod( out_path, perm ) | ||||
|         os.chmod(out_path, perm) | ||||
| 
 | ||||
|     def _write_directory(self, f): | ||||
| 
 | ||||
|  | @ -408,10 +439,10 @@ class Receiver: | |||
|         with self.args.timing.add("unpack zip"): | ||||
|             with zipfile.ZipFile(f, "r", zipfile.ZIP_DEFLATED) as zf: | ||||
|                 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/" % | ||||
|                       os.path.basename(self.abs_destname)) | ||||
|             self._msg(u"Received files written to %s/" % os.path.basename( | ||||
|                 self.abs_destname)) | ||||
|             f.close() | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|  |  | |||
|  | @ -1,20 +1,29 @@ | |||
| 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 twisted.python import log | ||||
| from twisted.protocols import basic | ||||
| from tqdm import tqdm | ||||
| from twisted.internet import reactor | ||||
| 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 wormhole import create, __version__ | ||||
| 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 | ||||
| 
 | ||||
| APPID = u"lothar.com/wormhole/text-or-file-xfer" | ||||
| VERIFY_TIMER = float(os.environ.get("_MAGIC_WORMHOLE_TEST_VERIFY_TIMER", 1.0)) | ||||
| 
 | ||||
| 
 | ||||
| def send(args, reactor=reactor): | ||||
|     """I implement 'wormhole send'. I return a Deferred that fires with None | ||||
|     (for success), or signals one of the following errors: | ||||
|  | @ -26,6 +35,7 @@ def send(args, reactor=reactor): | |||
|     """ | ||||
|     return Sender(args, reactor).go() | ||||
| 
 | ||||
| 
 | ||||
| class Sender: | ||||
|     def __init__(self, args, reactor): | ||||
|         self._args = args | ||||
|  | @ -45,22 +55,25 @@ class Sender: | |||
|             # tor in parallel with everything else, make sure the Tor object | ||||
|             # can lazy-provide an endpoint, and overlap the startup process | ||||
|             # with the user handing off the wormhole code | ||||
|             self._tor = yield get_tor(reactor, | ||||
|                                       self._args.launch_tor, | ||||
|                                       self._args.tor_control_port, | ||||
|                                       timing=self._timing) | ||||
|             self._tor = yield get_tor( | ||||
|                 reactor, | ||||
|                 self._args.launch_tor, | ||||
|                 self._args.tor_control_port, | ||||
|                 timing=self._timing) | ||||
| 
 | ||||
|         w = create(self._args.appid or APPID, self._args.relay_url, | ||||
|                    self._reactor, | ||||
|                    tor=self._tor, | ||||
|                    timing=self._timing) | ||||
|         w = create( | ||||
|             self._args.appid or APPID, | ||||
|             self._args.relay_url, | ||||
|             self._reactor, | ||||
|             tor=self._tor, | ||||
|             timing=self._timing) | ||||
|         d = self._go(w) | ||||
| 
 | ||||
|         # if we succeed, we should close and return the w.close results | ||||
|         # (which might be an error) | ||||
|         @inlineCallbacks | ||||
|         def _good(res): | ||||
|             yield w.close() # wait for ack | ||||
|             yield w.close()  # wait for ack | ||||
|             returnValue(res) | ||||
| 
 | ||||
|         # if we raise an error, we should close and then return the original | ||||
|  | @ -69,8 +82,8 @@ class Sender: | |||
|         @inlineCallbacks | ||||
|         def _bad(f): | ||||
|             try: | ||||
|                 yield w.close() # might be an error too | ||||
|             except: | ||||
|                 yield w.close()  # might be an error too | ||||
|             except Exception: | ||||
|                 pass | ||||
|             returnValue(f) | ||||
| 
 | ||||
|  | @ -125,8 +138,10 @@ class Sender: | |||
| 
 | ||||
|         # TODO: don't stall on w.get_verifier() unless they want it | ||||
|         def on_slow_connection(): | ||||
|             print(u"Key established, waiting for confirmation...", | ||||
|                   file=args.stderr) | ||||
|             print( | ||||
|                 u"Key established, waiting for confirmation...", | ||||
|                 file=args.stderr) | ||||
| 
 | ||||
|         notify = self._reactor.callLater(VERIFY_TIMER, on_slow_connection) | ||||
|         try: | ||||
|             # 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 | ||||
|             # seems like a good idea. It might even be appropriate to give up | ||||
|             # after a while. | ||||
|             verifier_bytes = yield w.get_verifier() # might WrongPasswordError | ||||
|             verifier_bytes = yield w.get_verifier()  # might WrongPasswordError | ||||
|         finally: | ||||
|             if not notify.called: | ||||
|                 notify.cancel() | ||||
| 
 | ||||
|         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: | ||||
|             ts = TransitSender(args.transit_helper, | ||||
|                                no_listen=(not args.listen), | ||||
|                                tor=self._tor, | ||||
|                                reactor=self._reactor, | ||||
|                                timing=self._timing) | ||||
|             ts = TransitSender( | ||||
|                 args.transit_helper, | ||||
|                 no_listen=(not args.listen), | ||||
|                 tor=self._tor, | ||||
|                 reactor=self._reactor, | ||||
|                 timing=self._timing) | ||||
|             self._transit_sender = ts | ||||
| 
 | ||||
|             # for now, send this before the main offer | ||||
|             sender_abilities = ts.get_connection_abilities() | ||||
|             sender_hints = yield ts.get_connection_hints() | ||||
|             sender_transit = {"abilities-v1": sender_abilities, | ||||
|                               "hints-v1": sender_hints, | ||||
|                               } | ||||
|             sender_transit = { | ||||
|                 "abilities-v1": sender_abilities, | ||||
|                 "hints-v1": sender_hints, | ||||
|             } | ||||
|             self._send_data({u"transit": sender_transit}, w) | ||||
| 
 | ||||
|             # 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.set_transit_key(transit_key) | ||||
| 
 | ||||
|  | @ -175,11 +193,11 @@ class Sender: | |||
|             # TODO: get_message() fired, so get_verifier must have fired, so | ||||
|             # now it's safe to use w.derive_key() | ||||
|             them_d = bytes_to_dict(them_d_bytes) | ||||
|             #print("GOT", them_d) | ||||
|             # print("GOT", them_d) | ||||
|             recognized = False | ||||
|             if u"error" in them_d: | ||||
|                 raise TransferError("remote error, transfer abandoned: %s" | ||||
|                                     % them_d["error"]) | ||||
|                 raise TransferError( | ||||
|                     "remote error, transfer abandoned: %s" % them_d["error"]) | ||||
|             if u"transit" in them_d: | ||||
|                 recognized = True | ||||
|                 yield self._handle_transit(them_d[u"transit"]) | ||||
|  | @ -191,7 +209,7 @@ class Sender: | |||
|                 yield self._handle_answer(them_d[u"answer"]) | ||||
|                 returnValue(None) | ||||
|             if not recognized: | ||||
|                 log.msg("unrecognized message %r" % (them_d,)) | ||||
|                 log.msg("unrecognized message %r" % (them_d, )) | ||||
| 
 | ||||
|     def _check_verifier(self, w, verifier_bytes): | ||||
|         verifier = bytes_to_hexstr(verifier_bytes) | ||||
|  | @ -221,9 +239,10 @@ class Sender: | |||
|             text = six.moves.input("Text to send: ") | ||||
| 
 | ||||
|         if text is not None: | ||||
|             print(u"Sending text message (%s)" % naturalsize(len(text)), | ||||
|                   file=args.stderr) | ||||
|             offer = { "message": text } | ||||
|             print( | ||||
|                 u"Sending text message (%s)" % naturalsize(len(text)), | ||||
|                 file=args.stderr) | ||||
|             offer = {"message": text} | ||||
|             fd_to_send = None | ||||
|             return offer, fd_to_send | ||||
| 
 | ||||
|  | @ -244,7 +263,7 @@ class Sender: | |||
|         # is a symlink to something with a different name. The normpath() is | ||||
|         # there to remove trailing slashes. | ||||
|         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 | ||||
|         # file/directory, because the path might contain symlinks, and | ||||
|  | @ -273,8 +292,8 @@ class Sender: | |||
| 
 | ||||
|         what = os.path.realpath(what) | ||||
|         if not os.path.exists(what): | ||||
|             raise TransferError("Cannot send: no file/directory named '%s'" % | ||||
|                                 args.what) | ||||
|             raise TransferError( | ||||
|                 "Cannot send: no file/directory named '%s'" % args.what) | ||||
| 
 | ||||
|         if os.path.isfile(what): | ||||
|             # we're sending a file | ||||
|  | @ -282,10 +301,11 @@ class Sender: | |||
|             offer["file"] = { | ||||
|                 "filename": basename, | ||||
|                 "filesize": filesize, | ||||
|                 } | ||||
|             print(u"Sending %s file named '%s'" | ||||
|                   % (naturalsize(filesize), basename), | ||||
|                   file=args.stderr) | ||||
|             } | ||||
|             print( | ||||
|                 u"Sending %s file named '%s'" % (naturalsize(filesize), | ||||
|                                                  basename), | ||||
|                 file=args.stderr) | ||||
|             fd_to_send = open(what, "rb") | ||||
|             return offer, fd_to_send | ||||
| 
 | ||||
|  | @ -297,16 +317,18 @@ class Sender: | |||
|             num_files = 0 | ||||
|             num_bytes = 0 | ||||
|             tostrip = len(what.split(os.sep)) | ||||
|             with zipfile.ZipFile(fd_to_send, "w", | ||||
|                                  compression=zipfile.ZIP_DEFLATED, | ||||
|                                  allowZip64=True) as zf: | ||||
|                 for path,dirs,files in os.walk(what): | ||||
|             with zipfile.ZipFile( | ||||
|                     fd_to_send, | ||||
|                     "w", | ||||
|                     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 | ||||
|                     # have "/subdir" appended. We want the zipfile to contain | ||||
|                     # "" or "subdir" | ||||
|                     localpath = list(path.split(os.sep)[tostrip:]) | ||||
|                     for fn in files: | ||||
|                         archivename = os.path.join(*tuple(localpath+[fn])) | ||||
|                         archivename = os.path.join(*tuple(localpath + [fn])) | ||||
|                         localfilename = os.path.join(path, fn) | ||||
|                         try: | ||||
|                             zf.write(localfilename, archivename) | ||||
|  | @ -315,22 +337,25 @@ class Sender: | |||
|                         except OSError as e: | ||||
|                             errmsg = u"{}: {}".format(fn, e.strerror) | ||||
|                             if self._args.ignore_unsendable_files: | ||||
|                                 print(u"{} (ignoring error)".format(errmsg), | ||||
|                                       file=args.stderr) | ||||
|                                 print( | ||||
|                                     u"{} (ignoring error)".format(errmsg), | ||||
|                                     file=args.stderr) | ||||
|                             else: | ||||
|                                 raise UnsendableFileError(errmsg) | ||||
|             fd_to_send.seek(0,2) | ||||
|             fd_to_send.seek(0, 2) | ||||
|             filesize = fd_to_send.tell() | ||||
|             fd_to_send.seek(0,0) | ||||
|             fd_to_send.seek(0, 0) | ||||
|             offer["directory"] = { | ||||
|                 "mode": "zipfile/deflated", | ||||
|                 "dirname": basename, | ||||
|                 "zipsize": filesize, | ||||
|                 "numbytes": num_bytes, | ||||
|                 "numfiles": num_files, | ||||
|                 } | ||||
|             print(u"Sending directory (%s compressed) named '%s'" | ||||
|                   % (naturalsize(filesize), basename), file=args.stderr) | ||||
|             } | ||||
|             print( | ||||
|                 u"Sending directory (%s compressed) named '%s'" % | ||||
|                 (naturalsize(filesize), basename), | ||||
|                 file=args.stderr) | ||||
|             return offer, fd_to_send | ||||
| 
 | ||||
|         raise TypeError("'%s' is neither file nor directory" % args.what) | ||||
|  | @ -340,23 +365,22 @@ class Sender: | |||
|         if self._fd_to_send is None: | ||||
|             if them_answer["message_ack"] == "ok": | ||||
|                 print(u"text message sent", file=self._args.stderr) | ||||
|                 returnValue(None) # terminates this function | ||||
|             raise TransferError("error sending text: %r" % (them_answer,)) | ||||
|                 returnValue(None)  # terminates this function | ||||
|             raise TransferError("error sending text: %r" % (them_answer, )) | ||||
| 
 | ||||
|         if them_answer.get("file_ack") != "ok": | ||||
|             raise TransferError("ambiguous response from remote, " | ||||
|                                 "transfer abandoned: %s" % (them_answer,)) | ||||
|                                 "transfer abandoned: %s" % (them_answer, )) | ||||
| 
 | ||||
|         yield self._send_file() | ||||
| 
 | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def _send_file(self): | ||||
|         ts = self._transit_sender | ||||
| 
 | ||||
|         self._fd_to_send.seek(0,2) | ||||
|         self._fd_to_send.seek(0, 2) | ||||
|         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() | ||||
|         self._timing.add("transit connected") | ||||
|  | @ -365,21 +389,28 @@ class Sender: | |||
|         print(u"Sending (%s).." % record_pipe.describe(), file=stderr) | ||||
| 
 | ||||
|         hasher = hashlib.sha256() | ||||
|         progress = tqdm(file=stderr, disable=self._args.hide_progress, | ||||
|                         unit="B", unit_scale=True, | ||||
|                         total=filesize) | ||||
|         progress = tqdm( | ||||
|             file=stderr, | ||||
|             disable=self._args.hide_progress, | ||||
|             unit="B", | ||||
|             unit_scale=True, | ||||
|             total=filesize) | ||||
| 
 | ||||
|         def _count_and_hash(data): | ||||
|             hasher.update(data) | ||||
|             progress.update(len(data)) | ||||
|             return data | ||||
| 
 | ||||
|         fs = basic.FileSender() | ||||
| 
 | ||||
|         with self._timing.add("tx file"): | ||||
|             with progress: | ||||
|                 if filesize: | ||||
|                     # don't send zero-length files | ||||
|                     yield fs.beginFileTransfer(self._fd_to_send, record_pipe, | ||||
|                                                transform=_count_and_hash) | ||||
|                     yield fs.beginFileTransfer( | ||||
|                         self._fd_to_send, | ||||
|                         record_pipe, | ||||
|                         transform=_count_and_hash) | ||||
| 
 | ||||
|         expected_hash = hasher.digest() | ||||
|         expected_hex = bytes_to_hexstr(expected_hash) | ||||
|  |  | |||
|  | @ -1,16 +1,19 @@ | |||
| from __future__ import print_function | ||||
| 
 | ||||
| import os | ||||
| from os.path import expanduser, exists, join | ||||
| from twisted.internet.defer import inlineCallbacks | ||||
| from twisted.internet import reactor | ||||
| from os.path import exists, expanduser, join | ||||
| 
 | ||||
| import click | ||||
| from twisted.internet import reactor | ||||
| from twisted.internet.defer import inlineCallbacks | ||||
| 
 | ||||
| from .. import xfer_util | ||||
| 
 | ||||
| 
 | ||||
| class PubkeyError(Exception): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def find_public_key(hint=None): | ||||
|     """ | ||||
|     This looks for an appropriate SSH key to send, possibly querying | ||||
|  | @ -34,8 +37,9 @@ def find_public_key(hint=None): | |||
|         got_key = False | ||||
|         while not got_key: | ||||
|             ans = click.prompt( | ||||
|                 "Multiple public-keys found:\n" + \ | ||||
|                 "\n".join(["  {}: {}".format(a, b) for a, b in enumerate(pubkeys)]) + \ | ||||
|                 "Multiple public-keys found:\n" + | ||||
|                 "\n".join(["  {}: {}".format(a, b) | ||||
|                            for a, b in enumerate(pubkeys)]) + | ||||
|                 "\nSend which one?" | ||||
|             ) | ||||
|             try: | ||||
|  | @ -76,7 +80,6 @@ def accept(cfg, reactor=reactor): | |||
| 
 | ||||
| @inlineCallbacks | ||||
| def invite(cfg, reactor=reactor): | ||||
| 
 | ||||
|     def on_code_created(code): | ||||
|         print("Now tell the other user to run:") | ||||
|         print() | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| 
 | ||||
| # This is a relay I run on a personal server. If it gets too expensive to | ||||
| # run, I'll shut it down. | ||||
| RENDEZVOUS_RELAY = u"ws://relay.magic-wormhole.io:4000/v1" | ||||
|  |  | |||
|  | @ -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): | ||||
|     if "motd" in welcome: | ||||
|         motd_lines = welcome["motd"].splitlines() | ||||
|         motd_formatted = "\n ".join(motd_lines) | ||||
|         print("Server (at %s) says:\n %s" % (relay_url, motd_formatted), | ||||
|               file=stderr) | ||||
|         print( | ||||
|             "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 | ||||
|     # 0.0.6+DISTANCE.gHASH). Only warn once. | ||||
|     if ("current_cli_version" in welcome | ||||
|         and "+" not in my_version | ||||
|         and welcome["current_cli_version"] != my_version): | ||||
|         print("Warning: errors may occur unless both sides are running the same version", file=stderr) | ||||
|         print("Server claims %s is current, but ours is %s" | ||||
|               % (welcome["current_cli_version"], my_version), | ||||
|               file=stderr) | ||||
|     if ("current_cli_version" in welcome and "+" not in my_version | ||||
|             and welcome["current_cli_version"] != my_version): | ||||
|         print( | ||||
|             ("Warning: errors may occur unless both sides are running the" | ||||
|              " same version"), | ||||
|             file=stderr) | ||||
|         print( | ||||
|             "Server claims %s is current, but ours is %s" % | ||||
|             (welcome["current_cli_version"], my_version), | ||||
|             file=stderr) | ||||
|  |  | |||
|  | @ -1,8 +1,10 @@ | |||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| 
 | ||||
| class WormholeError(Exception): | ||||
|     """Parent class for all wormhole-related errors""" | ||||
| 
 | ||||
| 
 | ||||
| class UnsendableFileError(Exception): | ||||
|     """ | ||||
|     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. | ||||
|     """ | ||||
| 
 | ||||
| 
 | ||||
| class ServerError(WormholeError): | ||||
|     """The relay server complained about something we did.""" | ||||
| 
 | ||||
| 
 | ||||
| class ServerConnectionError(WormholeError): | ||||
|     """We had a problem connecting to the relay server:""" | ||||
| 
 | ||||
|     def __init__(self, url, reason): | ||||
|         self.url = url | ||||
|         self.reason = reason | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         return str(self.reason) | ||||
| 
 | ||||
| 
 | ||||
| class Timeout(WormholeError): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class WelcomeError(WormholeError): | ||||
|     """ | ||||
|     The relay server told us to signal an error, probably because our version | ||||
|     is too old to possibly work. The server said:""" | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class LonelyError(WormholeError): | ||||
|     """wormhole.close() was called before the peer connection could be | ||||
|     established""" | ||||
| 
 | ||||
| 
 | ||||
| class WrongPasswordError(WormholeError): | ||||
|     """ | ||||
|     Key confirmation failed. Either you or your correspondent typed the code | ||||
|  | @ -47,6 +57,7 @@ class WrongPasswordError(WormholeError): | |||
|     # or the data blob was corrupted, and that's why decrypt failed | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class KeyFormatError(WormholeError): | ||||
|     """ | ||||
|     The key you entered contains spaces or was missing a dash. Magic-wormhole | ||||
|  | @ -55,43 +66,61 @@ class KeyFormatError(WormholeError): | |||
|     dashes. | ||||
|     """ | ||||
| 
 | ||||
| 
 | ||||
| class ReflectionAttack(WormholeError): | ||||
|     """An attacker (or bug) reflected our outgoing message back to us.""" | ||||
| 
 | ||||
| 
 | ||||
| class InternalError(WormholeError): | ||||
|     """The programmer did something wrong.""" | ||||
| 
 | ||||
| 
 | ||||
| class TransferError(WormholeError): | ||||
|     """Something bad happened and the transfer failed.""" | ||||
| 
 | ||||
| 
 | ||||
| class NoTorError(WormholeError): | ||||
|     """--tor was requested, but 'txtorcon' is not installed.""" | ||||
| 
 | ||||
| 
 | ||||
| class NoKeyError(WormholeError): | ||||
|     """w.derive_key() was called before got_verifier() fired""" | ||||
| 
 | ||||
| 
 | ||||
| class OnlyOneCodeError(WormholeError): | ||||
|     """Only one w.generate_code/w.set_code/w.input_code may be called""" | ||||
| 
 | ||||
| 
 | ||||
| class MustChooseNameplateFirstError(WormholeError): | ||||
|     """The InputHelper was asked to do get_word_completions() or | ||||
|     choose_words() before the nameplate was chosen.""" | ||||
| 
 | ||||
| 
 | ||||
| class AlreadyChoseNameplateError(WormholeError): | ||||
|     """The InputHelper was asked to do get_nameplate_completions() after | ||||
|     choose_nameplate() was called, or choose_nameplate() was called a second | ||||
|     time.""" | ||||
| 
 | ||||
| 
 | ||||
| class AlreadyChoseWordsError(WormholeError): | ||||
|     """The InputHelper was asked to do get_word_completions() after | ||||
|     choose_words() was called, or choose_words() was called a second time.""" | ||||
| 
 | ||||
| 
 | ||||
| class AlreadyInputNameplateError(WormholeError): | ||||
|     """The CodeInputter was asked to do completion on a nameplate, when we | ||||
|     had already committed to a different one.""" | ||||
| 
 | ||||
| 
 | ||||
| class WormholeClosed(Exception): | ||||
|     """Deferred-returning API calls errback with WormholeClosed if the | ||||
|     wormhole was already closed, or if it closes before a real result can be | ||||
|     obtained.""" | ||||
| 
 | ||||
| 
 | ||||
| class _UnknownPhaseError(Exception): | ||||
|     """internal exception type, for tests.""" | ||||
| 
 | ||||
| 
 | ||||
| class _UnknownMessageTypeError(Exception): | ||||
|     """internal exception type, for tests.""" | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ from twisted.internet.defer import Deferred | |||
| from twisted.internet.interfaces import IReactorTime | ||||
| from twisted.python import log | ||||
| 
 | ||||
| 
 | ||||
| class EventualQueue(object): | ||||
|     def __init__(self, clock): | ||||
|         # pass clock=reactor unless you're testing | ||||
|  | @ -14,7 +15,7 @@ class EventualQueue(object): | |||
|         self._timer = None | ||||
| 
 | ||||
|     def eventually(self, f, *args, **kwargs): | ||||
|         self._calls.append( (f, args, kwargs) ) | ||||
|         self._calls.append((f, args, kwargs)) | ||||
|         if not self._timer: | ||||
|             self._timer = self._clock.callLater(0, self._turn) | ||||
| 
 | ||||
|  | @ -28,7 +29,7 @@ class EventualQueue(object): | |||
|             (f, args, kwargs) = self._calls.pop(0) | ||||
|             try: | ||||
|                 f(*args, **kwargs) | ||||
|             except: | ||||
|             except Exception: | ||||
|                 log.err() | ||||
|         self._timer = None | ||||
|         d, self._flush_d = self._flush_d, None | ||||
|  |  | |||
|  | @ -1,27 +1,37 @@ | |||
| # no unicode_literals | ||||
| # 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 twisted.python.procutils import which | ||||
| 
 | ||||
| # 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 | ||||
| # versions so far.  Still, the real system calls would much be preferred... | ||||
| # ... 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_commands = (('route.exe', ('print',), _win32_re),) | ||||
| _win32_re = re.compile( | ||||
|     (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. | ||||
| _addr_re = re.compile(r'^\s*inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)[\s/].+$', flags=re.M|re.I|re.S) | ||||
| _unix_commands = (('/bin/ip', ('addr',), _addr_re), | ||||
|                   ('/sbin/ip', ('addr',), _addr_re), | ||||
|                   ('/sbin/ifconfig', ('-a',), _addr_re), | ||||
|                   ('/usr/sbin/ifconfig', ('-a',), _addr_re), | ||||
|                   ('/usr/etc/ifconfig', ('-a',), _addr_re), | ||||
|                   ('ifconfig', ('-a',), _addr_re), | ||||
|                   ('/sbin/ifconfig', (), _addr_re), | ||||
|                  ) | ||||
| _addr_re = re.compile( | ||||
|     r'^\s*inet [a-zA-Z]*:?(?P<address>\d+\.\d+\.\d+\.\d+)[\s/].+$', | ||||
|     flags=re.M | re.I | re.S) | ||||
| _unix_commands = ( | ||||
|     ('/bin/ip', ('addr', ), _addr_re), | ||||
|     ('/sbin/ip', ('addr', ), _addr_re), | ||||
|     ('/sbin/ifconfig', ('-a', ), _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(): | ||||
|  | @ -54,17 +64,19 @@ def find_addresses(): | |||
| 
 | ||||
|     return ["127.0.0.1"] | ||||
| 
 | ||||
| 
 | ||||
| def _query(path, args, regex): | ||||
|     env = {'LANG': 'en_US.UTF-8'} | ||||
|     trial = 0 | ||||
|     while True: | ||||
|         trial += 1 | ||||
|         try: | ||||
|             p = subprocess.Popen([path] + list(args), | ||||
|                                  stdout=subprocess.PIPE, | ||||
|                                  stderr=subprocess.PIPE, | ||||
|                                  env=env, | ||||
|                                  universal_newlines=True) | ||||
|             p = subprocess.Popen( | ||||
|                 [path] + list(args), | ||||
|                 stdout=subprocess.PIPE, | ||||
|                 stderr=subprocess.PIPE, | ||||
|                 env=env, | ||||
|                 universal_newlines=True) | ||||
|             (output, err) = p.communicate() | ||||
|             break | ||||
|         except OSError as e: | ||||
|  |  | |||
|  | @ -1,8 +1,12 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| from zope.interface import implementer | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| import contextlib | ||||
| 
 | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from ._interfaces import IJournal | ||||
| 
 | ||||
| 
 | ||||
| @implementer(IJournal) | ||||
| class Journal(object): | ||||
|     def __init__(self, save_checkpoint): | ||||
|  | @ -19,7 +23,7 @@ class Journal(object): | |||
|         assert not self._processing | ||||
|         assert not self._outbound_queue | ||||
|         self._processing = True | ||||
|         yield # process inbound messages, change state, queue outbound | ||||
|         yield  # process inbound messages, change state, queue outbound | ||||
|         self._save_checkpoint() | ||||
|         for (fn, args, kwargs) in self._outbound_queue: | ||||
|             fn(*args, **kwargs) | ||||
|  | @ -31,8 +35,10 @@ class Journal(object): | |||
| class ImmediateJournal(object): | ||||
|     def __init__(self): | ||||
|         pass | ||||
| 
 | ||||
|     def queue_outbound(self, fn, *args, **kwargs): | ||||
|         fn(*args, **kwargs) | ||||
| 
 | ||||
|     @contextlib.contextmanager | ||||
|     def process(self): | ||||
|         yield | ||||
|  |  | |||
|  | @ -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.python.failure import Failure | ||||
| 
 | ||||
| NoResult = object() | ||||
| 
 | ||||
| 
 | ||||
| class OneShotObserver(object): | ||||
|     def __init__(self, eventual_queue): | ||||
|         self._eq = eventual_queue | ||||
|         self._result = NoResult | ||||
|         self._observers = [] # list of Deferreds | ||||
|         self._observers = []  # list of Deferreds | ||||
| 
 | ||||
|     def when_fired(self): | ||||
|         d = Deferred() | ||||
|  | @ -38,6 +40,7 @@ class OneShotObserver(object): | |||
|         if self._result is NoResult: | ||||
|             self.fire(result) | ||||
| 
 | ||||
| 
 | ||||
| class SequenceObserver(object): | ||||
|     def __init__(self, eventual_queue): | ||||
|         self._eq = eventual_queue | ||||
|  |  | |||
|  | @ -1,16 +1,19 @@ | |||
| # 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 twisted.application import internet, service | ||||
| from twisted.internet import defer, endpoints, reactor, task | ||||
| from twisted.python import log | ||||
| 
 | ||||
| import mock | ||||
| from ..cli import cli | ||||
| from ..transit import allocate_tcp_port | ||||
| from wormhole_mailbox_server.database import create_channel_db, create_usage_db | ||||
| from wormhole_mailbox_server.server import make_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 ..cli import cli | ||||
| from ..transit import allocate_tcp_port | ||||
| 
 | ||||
| 
 | ||||
| class MyInternetService(service.Service, object): | ||||
|     # like StreamServerEndpointService, but you can retrieve the port | ||||
|     def __init__(self, endpoint, factory): | ||||
|  | @ -22,12 +25,15 @@ class MyInternetService(service.Service, object): | |||
|     def startService(self): | ||||
|         super(MyInternetService, self).startService() | ||||
|         d = self.endpoint.listen(self.factory) | ||||
| 
 | ||||
|         def good(lp): | ||||
|             self._lp = lp | ||||
|             self._port_d.callback(lp.getHost().port) | ||||
| 
 | ||||
|         def bad(f): | ||||
|             log.err(f) | ||||
|             self._port_d.errback(f) | ||||
| 
 | ||||
|         d.addCallbacks(good, bad) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|  | @ -35,9 +41,10 @@ class MyInternetService(service.Service, object): | |||
|         if self._lp: | ||||
|             yield self._lp.stopListening() | ||||
| 
 | ||||
|     def getPort(self): # only call once! | ||||
|     def getPort(self):  # only call once! | ||||
|         return self._port_d | ||||
| 
 | ||||
| 
 | ||||
| class ServerBase: | ||||
|     @defer.inlineCallbacks | ||||
|     def setUp(self): | ||||
|  | @ -51,27 +58,27 @@ class ServerBase: | |||
|         # endpoints.serverFromString | ||||
|         db = create_channel_db(":memory:") | ||||
|         self._usage_db = create_usage_db(":memory:") | ||||
|         self._rendezvous = make_server(db, | ||||
|                                        advertise_version=advertise_version, | ||||
|                                        signal_error=error, | ||||
|                                        usage_db=self._usage_db) | ||||
|         self._rendezvous = make_server( | ||||
|             db, | ||||
|             advertise_version=advertise_version, | ||||
|             signal_error=error, | ||||
|             usage_db=self._usage_db) | ||||
|         ep = endpoints.TCP4ServerEndpoint(reactor, 0, interface="127.0.0.1") | ||||
|         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.setServiceParent(self.sp) | ||||
|         self.rdv_ws_port = yield s.getPort() | ||||
|         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 | ||||
|         # ws://127.0.0.1:%d/wormhole-relay/ws | ||||
| 
 | ||||
|         self.transitport = allocate_tcp_port() | ||||
|         ep = endpoints.serverFromString(reactor, | ||||
|                                         "tcp:%d:interface=127.0.0.1" % | ||||
|                                         self.transitport) | ||||
|         self._transit_server = f = Transit(blur_usage=None, log_file=None, | ||||
|                                            usage_db=None) | ||||
|         ep = endpoints.serverFromString( | ||||
|             reactor, "tcp:%d:interface=127.0.0.1" % self.transitport) | ||||
|         self._transit_server = f = Transit( | ||||
|             blur_usage=None, log_file=None, usage_db=None) | ||||
|         internet.StreamServerEndpointService(ep, f).setServiceParent(self.sp) | ||||
|         self.transit = u"tcp:127.0.0.1:%d" % self.transitport | ||||
| 
 | ||||
|  | @ -109,6 +116,7 @@ class ServerBase: | |||
|                     " I convinced all threads to exit.") | ||||
|         yield d | ||||
| 
 | ||||
| 
 | ||||
| def config(*argv): | ||||
|     r = CliRunner() | ||||
|     with mock.patch("wormhole.cli.cli.go") as go: | ||||
|  | @ -121,6 +129,7 @@ def config(*argv): | |||
|         cfg = go.call_args[0][1] | ||||
|     return cfg | ||||
| 
 | ||||
| 
 | ||||
| @defer.inlineCallbacks | ||||
| def poll_until(predicate): | ||||
|     # return a Deferred that won't fire until the predicate is True | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| # This is a tiny helper module, to let "python -m wormhole.test.run_trial | ||||
| # ARGS" does the same thing as running "trial ARGS" (unfortunately | ||||
| # twisted/scripts/trial.py does not have a '__name__=="__main__"' clause). | ||||
|  |  | |||
|  | @ -1,15 +1,17 @@ | |||
| import os | ||||
| import sys | ||||
| import mock | ||||
| 
 | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from ..cli.public_relay import RENDEZVOUS_RELAY, TRANSIT_RELAY | ||||
| from .common import config | ||||
| #from pprint import pprint | ||||
| 
 | ||||
| 
 | ||||
| class Send(unittest.TestCase): | ||||
|     def test_baseline(self): | ||||
|         cfg = config("send", "--text", "hi") | ||||
|         #pprint(cfg.__dict__) | ||||
|         self.assertEqual(cfg.what, None) | ||||
|         self.assertEqual(cfg.code, None) | ||||
|         self.assertEqual(cfg.code_length, 2) | ||||
|  | @ -32,7 +34,6 @@ class Send(unittest.TestCase): | |||
| 
 | ||||
|     def test_file(self): | ||||
|         cfg = config("send", "fn") | ||||
|         #pprint(cfg.__dict__) | ||||
|         self.assertEqual(cfg.what, u"fn") | ||||
|         self.assertEqual(cfg.text, None) | ||||
| 
 | ||||
|  | @ -101,7 +102,6 @@ class Send(unittest.TestCase): | |||
| class Receive(unittest.TestCase): | ||||
|     def test_baseline(self): | ||||
|         cfg = config("receive") | ||||
|         #pprint(cfg.__dict__) | ||||
|         self.assertEqual(cfg.accept_file, False) | ||||
|         self.assertEqual(cfg.code, None) | ||||
|         self.assertEqual(cfg.code_length, 2) | ||||
|  | @ -191,10 +191,12 @@ class Receive(unittest.TestCase): | |||
|             cfg = config("--transit-helper", transit_url_2, "receive") | ||||
|         self.assertEqual(cfg.transit_helper, transit_url_2) | ||||
| 
 | ||||
| 
 | ||||
| class Config(unittest.TestCase): | ||||
|     def test_send(self): | ||||
|         cfg = config("send") | ||||
|         self.assertEqual(cfg.stdout, sys.stdout) | ||||
| 
 | ||||
|     def test_receive(self): | ||||
|         cfg = config("receive") | ||||
|         self.assertEqual(cfg.stdout, sys.stdout) | ||||
|  |  | |||
|  | @ -1,22 +1,32 @@ | |||
| from __future__ import print_function | ||||
| import os, sys, re, io, zipfile, six, stat | ||||
| from textwrap import fill, dedent | ||||
| from humanize import naturalsize | ||||
| import mock | ||||
| 
 | ||||
| import io | ||||
| import os | ||||
| import re | ||||
| import stat | ||||
| import sys | ||||
| import zipfile | ||||
| from textwrap import dedent, fill | ||||
| 
 | ||||
| import six | ||||
| from click.testing import CliRunner | ||||
| from zope.interface import implementer | ||||
| from twisted.trial import unittest | ||||
| from twisted.python import procutils, log | ||||
| from humanize import naturalsize | ||||
| from twisted.internet import endpoints, reactor | ||||
| from twisted.internet.utils import getProcessOutputAndValue | ||||
| from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue | ||||
| 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 .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 ..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): | ||||
|  | @ -108,8 +118,8 @@ class OfferData(unittest.TestCase): | |||
|         self.cfg.cwd = send_dir | ||||
| 
 | ||||
|         e = self.assertRaises(TransferError, build_offer, self.cfg) | ||||
|         self.assertEqual(str(e), | ||||
|                          "Cannot send: no file/directory named '%s'" % filename) | ||||
|         self.assertEqual( | ||||
|             str(e), "Cannot send: no file/directory named '%s'" % filename) | ||||
| 
 | ||||
|     def _do_test_directory(self, addslash): | ||||
|         parent_dir = self.mktemp() | ||||
|  | @ -178,8 +188,8 @@ class OfferData(unittest.TestCase): | |||
|         self.assertFalse(os.path.isdir(abs_filename)) | ||||
| 
 | ||||
|         e = self.assertRaises(TypeError, build_offer, self.cfg) | ||||
|         self.assertEqual(str(e), | ||||
|                          "'%s' is neither file nor directory" % filename) | ||||
|         self.assertEqual( | ||||
|             str(e), "'%s' is neither file nor directory" % filename) | ||||
| 
 | ||||
|     def test_symlink(self): | ||||
|         if not hasattr(os, 'symlink'): | ||||
|  | @ -213,8 +223,9 @@ class OfferData(unittest.TestCase): | |||
|         os.mkdir(os.path.join(parent_dir, "B2", "C2")) | ||||
|         with open(os.path.join(parent_dir, "B2", "D.txt"), "wb") as f: | ||||
|             f.write(b"success") | ||||
|         os.symlink(os.path.abspath(os.path.join(parent_dir, "B2", "C2")), | ||||
|                    os.path.join(parent_dir, "B1", "C1")) | ||||
|         os.symlink( | ||||
|             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: | ||||
|         # * start: A | ||||
|         # * B1: A/B1 | ||||
|  | @ -231,6 +242,7 @@ class OfferData(unittest.TestCase): | |||
|         d, fd_to_send = build_offer(self.cfg) | ||||
|         self.assertEqual(d["file"]["filename"], "D.txt") | ||||
|         self.assertEqual(fd_to_send.read(), b"success") | ||||
| 
 | ||||
|     if os.name == "nt": | ||||
|         test_symlink_collapse.todo = "host OS has broken os.path.realpath()" | ||||
|         # 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 | ||||
|         # work (sometimes, but not in #251). See cmd_send.py for more notes. | ||||
| 
 | ||||
| 
 | ||||
| class LocaleFinder: | ||||
|     def __init__(self): | ||||
|         self._run_once = False | ||||
|  | @ -266,10 +279,10 @@ class LocaleFinder: | |||
|         # twisted.python.usage to avoid this problem in the future. | ||||
|         (out, err, rc) = yield getProcessOutputAndValue("locale", ["-a"]) | ||||
|         if rc != 0: | ||||
|             log.msg("error running 'locale -a', rc=%s" % (rc,)) | ||||
|             log.msg("stderr: %s" % (err,)) | ||||
|             log.msg("error running 'locale -a', rc=%s" % (rc, )) | ||||
|             log.msg("stderr: %s" % (err, )) | ||||
|             returnValue(None) | ||||
|         out = out.decode("utf-8") # make sure we get a string | ||||
|         out = out.decode("utf-8")  # make sure we get a string | ||||
|         utf8_locales = {} | ||||
|         for locale in out.splitlines(): | ||||
|             locale = locale.strip() | ||||
|  | @ -281,8 +294,11 @@ class LocaleFinder: | |||
|         if utf8_locales: | ||||
|             returnValue(list(utf8_locales.values())[0]) | ||||
|         returnValue(None) | ||||
| 
 | ||||
| 
 | ||||
| locale_finder = LocaleFinder() | ||||
| 
 | ||||
| 
 | ||||
| class ScriptsBase: | ||||
|     def find_executable(self): | ||||
|         # to make sure we're running the right executable (in a virtualenv), | ||||
|  | @ -292,12 +308,13 @@ class ScriptsBase: | |||
|         if not locations: | ||||
|             raise unittest.SkipTest("unable to find 'wormhole' in $PATH") | ||||
|         wormhole = locations[0] | ||||
|         if (os.path.dirname(os.path.abspath(wormhole)) != | ||||
|             os.path.dirname(sys.executable)): | ||||
|             log.msg("locations: %s" % (locations,)) | ||||
|             log.msg("sys.executable: %s" % (sys.executable,)) | ||||
|             raise unittest.SkipTest("found the wrong 'wormhole' in $PATH: %s %s" | ||||
|                                     % (wormhole, sys.executable)) | ||||
|         if (os.path.dirname(os.path.abspath(wormhole)) != os.path.dirname( | ||||
|                 sys.executable)): | ||||
|             log.msg("locations: %s" % (locations, )) | ||||
|             log.msg("sys.executable: %s" % (sys.executable, )) | ||||
|             raise unittest.SkipTest( | ||||
|                 "found the wrong 'wormhole' in $PATH: %s %s" % | ||||
|                 (wormhole, sys.executable)) | ||||
|         return wormhole | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|  | @ -323,8 +340,8 @@ class ScriptsBase: | |||
|             raise unittest.SkipTest("unable to find UTF-8 locale") | ||||
|         locale_env = dict(LC_ALL=locale, LANG=locale) | ||||
|         wormhole = self.find_executable() | ||||
|         res = yield getProcessOutputAndValue(wormhole, ["--version"], | ||||
|                                              env=locale_env) | ||||
|         res = yield getProcessOutputAndValue( | ||||
|             wormhole, ["--version"], env=locale_env) | ||||
|         out, err, rc = res | ||||
|         if rc != 0: | ||||
|             log.msg("wormhole not runnable in this tree:") | ||||
|  | @ -334,6 +351,7 @@ class ScriptsBase: | |||
|             raise unittest.SkipTest("wormhole is not runnable in this tree") | ||||
|         returnValue(locale_env) | ||||
| 
 | ||||
| 
 | ||||
| class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase): | ||||
|     # we need Twisted to run the server, but we run the sender and receiver | ||||
|     # with deferToThread() | ||||
|  | @ -347,26 +365,30 @@ class ScriptVersion(ServerBase, ScriptsBase, unittest.TestCase): | |||
|         wormhole = self.find_executable() | ||||
|         # we must pass on the environment so that "something" doesn't | ||||
|         # get sad about UTF8 vs. ascii encodings | ||||
|         out, err, rc = yield getProcessOutputAndValue(wormhole, ["--version"], | ||||
|                                                       env=os.environ) | ||||
|         out, err, rc = yield getProcessOutputAndValue( | ||||
|             wormhole, ["--version"], env=os.environ) | ||||
|         err = err.decode("utf-8") | ||||
|         if "DistributionNotFound" in err: | ||||
|             log.msg("stderr was %s" % err) | ||||
|             last = err.strip().split("\n")[-1] | ||||
|             self.fail("wormhole not runnable: %s" % last) | ||||
|         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) | ||||
| 
 | ||||
| 
 | ||||
| @implementer(ITorManager) | ||||
| class FakeTor: | ||||
|     # use normal endpoints, but record the fact that we were asked | ||||
|     def __init__(self): | ||||
|         self.endpoints = [] | ||||
| 
 | ||||
|     def stream_via(self, host, port): | ||||
|         self.endpoints.append((host, port)) | ||||
|         return endpoints.HostnameEndpoint(reactor, host, port) | ||||
| 
 | ||||
| 
 | ||||
| class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | ||||
|     # we need Twisted to run the server, but we run the sender and receiver | ||||
|     # with deferToThread() | ||||
|  | @ -377,11 +399,16 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|         yield ServerBase.setUp(self) | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def _do_test(self, as_subprocess=False, | ||||
|                  mode="text", addslash=False, override_filename=False, | ||||
|                  fake_tor=False, overwrite=False, mock_accept=False): | ||||
|         assert mode in ("text", "file", "empty-file", "directory", | ||||
|                         "slow-text", "slow-sender-text") | ||||
|     def _do_test(self, | ||||
|                  as_subprocess=False, | ||||
|                  mode="text", | ||||
|                  addslash=False, | ||||
|                  override_filename=False, | ||||
|                  fake_tor=False, | ||||
|                  overwrite=False, | ||||
|                  mock_accept=False): | ||||
|         assert mode in ("text", "file", "empty-file", "directory", "slow-text", | ||||
|                         "slow-sender-text") | ||||
|         if fake_tor: | ||||
|             assert not as_subprocess | ||||
|         send_cfg = config("send") | ||||
|  | @ -408,7 +435,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|         elif mode in ("file", "empty-file"): | ||||
|             if mode == "empty-file": | ||||
|                 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: | ||||
|                 f.write(message) | ||||
|             send_cfg.what = send_filename | ||||
|  | @ -433,8 +460,10 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|             # expect: $receive_dir/$dirname/[12345] | ||||
| 
 | ||||
|             send_dirname = u"testdir" | ||||
| 
 | ||||
|             def message(i): | ||||
|                 return "test message %d\n" % i | ||||
| 
 | ||||
|             os.mkdir(os.path.join(send_dir, u"middle")) | ||||
|             source_dir = os.path.join(send_dir, u"middle", send_dirname) | ||||
|             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_VERIFY_TIMER"] = "999999" | ||||
|             send_args = [ | ||||
|                     '--relay-url', self.relayurl, | ||||
|                     '--transit-helper', '', | ||||
|                     'send', | ||||
|                     '--hide-progress', | ||||
|                     '--code', send_cfg.code, | ||||
|                 ] + content_args | ||||
|                 '--relay-url', | ||||
|                 self.relayurl, | ||||
|                 '--transit-helper', | ||||
|                 '', | ||||
|                 'send', | ||||
|                 '--hide-progress', | ||||
|                 '--code', | ||||
|                 send_cfg.code, | ||||
|             ] + content_args | ||||
| 
 | ||||
|             send_d = getProcessOutputAndValue( | ||||
|                 wormhole_bin, send_args, | ||||
|                 wormhole_bin, | ||||
|                 send_args, | ||||
|                 path=send_dir, | ||||
|                 env=env, | ||||
|             ) | ||||
|             recv_args = [ | ||||
|                 '--relay-url', self.relayurl, | ||||
|                 '--transit-helper', '', | ||||
|                 '--relay-url', | ||||
|                 self.relayurl, | ||||
|                 '--transit-helper', | ||||
|                 '', | ||||
|                 'receive', | ||||
|                 '--hide-progress', | ||||
|                 '--accept-file', | ||||
|  | @ -500,7 +535,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|                 recv_args.extend(['-o', receive_filename]) | ||||
| 
 | ||||
|             receive_d = getProcessOutputAndValue( | ||||
|                 wormhole_bin, recv_args, | ||||
|                 wormhole_bin, | ||||
|                 recv_args, | ||||
|                 path=receive_dir, | ||||
|                 env=env, | ||||
|             ) | ||||
|  | @ -524,25 +560,27 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|                 send_cfg.tor = True | ||||
|                 send_cfg.transit_helper = self.transit | ||||
|                 tx_tm = FakeTor() | ||||
|                 with mock.patch("wormhole.tor_manager.get_tor", | ||||
|                                 return_value=tx_tm, | ||||
|                                 ) as mtx_tm: | ||||
|                 with mock.patch( | ||||
|                         "wormhole.tor_manager.get_tor", | ||||
|                         return_value=tx_tm, | ||||
|                 ) as mtx_tm: | ||||
|                     send_d = cmd_send.send(send_cfg) | ||||
| 
 | ||||
|                 recv_cfg.tor = True | ||||
|                 recv_cfg.transit_helper = self.transit | ||||
|                 rx_tm = FakeTor() | ||||
|                 with mock.patch("wormhole.tor_manager.get_tor", | ||||
|                                 return_value=rx_tm, | ||||
|                                 ) as mrx_tm: | ||||
|                 with mock.patch( | ||||
|                         "wormhole.tor_manager.get_tor", | ||||
|                         return_value=rx_tm, | ||||
|                 ) as mrx_tm: | ||||
|                     receive_d = cmd_receive.receive(recv_cfg) | ||||
|             else: | ||||
|                 KEY_TIMER = 0 if mode == "slow-sender-text" else 99999 | ||||
|                 rxw = [] | ||||
|                 with mock.patch.object(cmd_receive, "KEY_TIMER", KEY_TIMER): | ||||
|                     send_d = cmd_send.send(send_cfg) | ||||
|                     receive_d = cmd_receive.receive(recv_cfg, | ||||
|                                                     _debug_stash_wormhole=rxw) | ||||
|                     receive_d = cmd_receive.receive( | ||||
|                         recv_cfg, _debug_stash_wormhole=rxw) | ||||
|                     # we need to keep KEY_TIMER patched until the receiver | ||||
|                     # gets far enough to start the timer, which happens after | ||||
|                     # 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_send, "VERIFY_TIMER", VERIFY_TIMER): | ||||
|                     if mock_accept: | ||||
|                         with mock.patch.object(cmd_receive.six.moves, | ||||
|                                                'input', return_value='y'): | ||||
|                         with mock.patch.object( | ||||
|                                 cmd_receive.six.moves, 'input', | ||||
|                                 return_value='y'): | ||||
|                             yield gatherResults([send_d, receive_d], True) | ||||
|                     else: | ||||
|                         yield gatherResults([send_d, receive_d], True) | ||||
|  | @ -567,14 +606,14 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|                     expected_endpoints.append(("127.0.0.1", self.transitport)) | ||||
|                 tx_timing = mtx_tm.call_args[1]["timing"] | ||||
|                 self.assertEqual(tx_tm.endpoints, expected_endpoints) | ||||
|                 self.assertEqual(mtx_tm.mock_calls, | ||||
|                                  [mock.call(reactor, False, None, | ||||
|                                             timing=tx_timing)]) | ||||
|                 self.assertEqual( | ||||
|                     mtx_tm.mock_calls, | ||||
|                     [mock.call(reactor, False, None, timing=tx_timing)]) | ||||
|                 rx_timing = mrx_tm.call_args[1]["timing"] | ||||
|                 self.assertEqual(rx_tm.endpoints, expected_endpoints) | ||||
|                 self.assertEqual(mrx_tm.mock_calls, | ||||
|                                  [mock.call(reactor, False, None, | ||||
|                                             timing=rx_timing)]) | ||||
|                 self.assertEqual( | ||||
|                     mrx_tm.mock_calls, | ||||
|                     [mock.call(reactor, False, None, timing=rx_timing)]) | ||||
| 
 | ||||
|             send_stdout = send_cfg.stdout.getvalue() | ||||
|             send_stderr = send_cfg.stderr.getvalue() | ||||
|  | @ -585,7 +624,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|             # newlines, even if we're on windows | ||||
|             NL = "\n" | ||||
| 
 | ||||
|         self.maxDiff = None # show full output for assertion failures | ||||
|         self.maxDiff = None  # show full output for assertion failures | ||||
| 
 | ||||
|         key_established = "" | ||||
|         if mode == "slow-text": | ||||
|  | @ -600,38 +639,41 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|                         "On the other computer, please run:{NL}{NL}" | ||||
|                         "wormhole receive {code}{NL}{NL}" | ||||
|                         "{KE}" | ||||
|                         "text message sent{NL}").format(bytes=len(message), | ||||
|                                                         code=send_cfg.code, | ||||
|                                                         NL=NL, | ||||
|                                                         KE=key_established) | ||||
|                         "text message sent{NL}").format( | ||||
|                             bytes=len(message), | ||||
|                             code=send_cfg.code, | ||||
|                             NL=NL, | ||||
|                             KE=key_established) | ||||
|             self.failUnlessEqual(send_stderr, expected) | ||||
|         elif mode == "file": | ||||
|             self.failUnlessIn(u"Sending {size:s} file named '{name}'{NL}" | ||||
|                               .format(size=naturalsize(len(message)), | ||||
|                                       name=send_filename, | ||||
|                                       NL=NL), send_stderr) | ||||
|                               .format( | ||||
|                                   size=naturalsize(len(message)), | ||||
|                                   name=send_filename, | ||||
|                                   NL=NL), send_stderr) | ||||
|             self.failUnlessIn(u"Wormhole code is: {code}{NL}" | ||||
|                               "On the other computer, please run:{NL}{NL}" | ||||
|                               "wormhole receive {code}{NL}{NL}" | ||||
|                               .format(code=send_cfg.code, NL=NL), | ||||
|                               send_stderr) | ||||
|             self.failUnlessIn(u"File sent.. waiting for confirmation{NL}" | ||||
|                               "Confirmation received. Transfer complete.{NL}" | ||||
|                               .format(NL=NL), send_stderr) | ||||
|                               "wormhole receive {code}{NL}{NL}".format( | ||||
|                                   code=send_cfg.code, NL=NL), send_stderr) | ||||
|             self.failUnlessIn( | ||||
|                 u"File sent.. waiting for confirmation{NL}" | ||||
|                 "Confirmation received. Transfer complete.{NL}".format(NL=NL), | ||||
|                 send_stderr) | ||||
|         elif mode == "directory": | ||||
|             self.failUnlessIn(u"Sending directory", send_stderr) | ||||
|             self.failUnlessIn(u"named 'testdir'", send_stderr) | ||||
|             self.failUnlessIn(u"Wormhole code is: {code}{NL}" | ||||
|                               "On the other computer, please run:{NL}{NL}" | ||||
|                               "wormhole receive {code}{NL}{NL}" | ||||
|                               .format(code=send_cfg.code, NL=NL), send_stderr) | ||||
|             self.failUnlessIn(u"File sent.. waiting for confirmation{NL}" | ||||
|                               "Confirmation received. Transfer complete.{NL}" | ||||
|                               .format(NL=NL), send_stderr) | ||||
|                               "wormhole receive {code}{NL}{NL}".format( | ||||
|                                   code=send_cfg.code, NL=NL), send_stderr) | ||||
|             self.failUnlessIn( | ||||
|                 u"File sent.. waiting for confirmation{NL}" | ||||
|                 "Confirmation received. Transfer complete.{NL}".format(NL=NL), | ||||
|                 send_stderr) | ||||
| 
 | ||||
|         # check receiver | ||||
|         if mode in ("text", "slow-text", "slow-sender-text"): | ||||
|             self.assertEqual(receive_stdout, message+NL) | ||||
|             self.assertEqual(receive_stdout, message + NL) | ||||
|             if mode == "text": | ||||
|                 self.assertEqual(receive_stderr, "") | ||||
|             elif mode == "slow-text": | ||||
|  | @ -640,9 +682,9 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|                 self.assertEqual(receive_stderr, "Waiting for sender...\n") | ||||
|         elif mode == "file": | ||||
|             self.failUnlessEqual(receive_stdout, "") | ||||
|             self.failUnlessIn(u"Receiving file ({size:s}) into: {name}" | ||||
|                               .format(size=naturalsize(len(message)), | ||||
|                                       name=receive_filename), receive_stderr) | ||||
|             self.failUnlessIn(u"Receiving file ({size:s}) into: {name}".format( | ||||
|                 size=naturalsize(len(message)), name=receive_filename), | ||||
|                               receive_stderr) | ||||
|             self.failUnlessIn(u"Received file written to ", receive_stderr) | ||||
|             fn = os.path.join(receive_dir, receive_filename) | ||||
|             self.failUnless(os.path.exists(fn)) | ||||
|  | @ -652,52 +694,67 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|             self.failUnlessEqual(receive_stdout, "") | ||||
|             want = (r"Receiving directory \(\d+ \w+\) into: {name}/" | ||||
|                     .format(name=receive_dirname)) | ||||
|             self.failUnless(re.search(want, receive_stderr), | ||||
|                             (want, receive_stderr)) | ||||
|             self.failUnlessIn(u"Received files written to {name}" | ||||
|                               .format(name=receive_dirname), receive_stderr) | ||||
|             self.failUnless( | ||||
|                 re.search(want, receive_stderr), (want, receive_stderr)) | ||||
|             self.failUnlessIn( | ||||
|                 u"Received files written to {name}" | ||||
|                 .format(name=receive_dirname), | ||||
|                 receive_stderr) | ||||
|             fn = os.path.join(receive_dir, receive_dirname) | ||||
|             self.failUnless(os.path.exists(fn), fn) | ||||
|             for i in range(5): | ||||
|                 fn = os.path.join(receive_dir, receive_dirname, str(i)) | ||||
|                 with open(fn, "r") as f: | ||||
|                     self.failUnlessEqual(f.read(), message(i)) | ||||
|                 self.failUnlessEqual(modes[i], | ||||
|                                      stat.S_IMODE(os.stat(fn).st_mode)) | ||||
|                 self.failUnlessEqual(modes[i], stat.S_IMODE( | ||||
|                     os.stat(fn).st_mode)) | ||||
| 
 | ||||
|     def test_text(self): | ||||
|         return self._do_test() | ||||
| 
 | ||||
|     def test_text_subprocess(self): | ||||
|         return self._do_test(as_subprocess=True) | ||||
| 
 | ||||
|     def test_text_tor(self): | ||||
|         return self._do_test(fake_tor=True) | ||||
| 
 | ||||
|     def test_file(self): | ||||
|         return self._do_test(mode="file") | ||||
| 
 | ||||
|     def test_file_override(self): | ||||
|         return self._do_test(mode="file", override_filename=True) | ||||
| 
 | ||||
|     def test_file_overwrite(self): | ||||
|         return self._do_test(mode="file", overwrite=True) | ||||
| 
 | ||||
|     def test_file_overwrite_mock_accept(self): | ||||
|         return self._do_test(mode="file", overwrite=True, mock_accept=True) | ||||
| 
 | ||||
|     def test_file_tor(self): | ||||
|         return self._do_test(mode="file", fake_tor=True) | ||||
| 
 | ||||
|     def test_empty_file(self): | ||||
|         return self._do_test(mode="empty-file") | ||||
| 
 | ||||
|     def test_directory(self): | ||||
|         return self._do_test(mode="directory") | ||||
| 
 | ||||
|     def test_directory_addslash(self): | ||||
|         return self._do_test(mode="directory", addslash=True) | ||||
| 
 | ||||
|     def test_directory_override(self): | ||||
|         return self._do_test(mode="directory", override_filename=True) | ||||
| 
 | ||||
|     def test_directory_overwrite(self): | ||||
|         return self._do_test(mode="directory", overwrite=True) | ||||
| 
 | ||||
|     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): | ||||
|         return self._do_test(mode="slow-text") | ||||
| 
 | ||||
|     def test_slow_sender_text(self): | ||||
|         return self._do_test(mode="slow-sender-text") | ||||
| 
 | ||||
|  | @ -721,7 +778,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|         os.mkdir(send_dir) | ||||
|         receive_dir = self.mktemp() | ||||
|         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": | ||||
|             message = "test message\n" | ||||
|  | @ -765,10 +822,12 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|             free_space = 10000000 | ||||
|         else: | ||||
|             free_space = 0 | ||||
|         with mock.patch("wormhole.cli.cmd_receive.estimate_free_space", | ||||
|                         return_value=free_space): | ||||
|         with mock.patch( | ||||
|                 "wormhole.cli.cmd_receive.estimate_free_space", | ||||
|                 return_value=free_space): | ||||
|             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) | ||||
|             self.assertEqual(str(f), "transfer rejected") | ||||
| 
 | ||||
|  | @ -781,7 +840,7 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|         # newlines, even if we're on windows | ||||
|         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(receive_stdout, "") | ||||
|  | @ -789,54 +848,63 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
|         # check sender | ||||
|         if mode == "file": | ||||
|             self.failUnlessIn("Sending {size:s} file named '{name}'{NL}" | ||||
|                               .format(size=naturalsize(size), | ||||
|                                       name=send_filename, | ||||
|                                       NL=NL), send_stderr) | ||||
|                               .format( | ||||
|                                   size=naturalsize(size), | ||||
|                                   name=send_filename, | ||||
|                                   NL=NL), send_stderr) | ||||
|             self.failUnlessIn("Wormhole code is: {code}{NL}" | ||||
|                               "On the other computer, please run:{NL}{NL}" | ||||
|                               "wormhole receive {code}{NL}" | ||||
|                               .format(code=send_cfg.code, NL=NL), | ||||
|                               send_stderr) | ||||
|             self.failIfIn("File sent.. waiting for confirmation{NL}" | ||||
|                           "Confirmation received. Transfer complete.{NL}" | ||||
|                           .format(NL=NL), send_stderr) | ||||
|                               "wormhole receive {code}{NL}".format( | ||||
|                                   code=send_cfg.code, NL=NL), send_stderr) | ||||
|             self.failIfIn( | ||||
|                 "File sent.. waiting for confirmation{NL}" | ||||
|                 "Confirmation received. Transfer complete.{NL}".format(NL=NL), | ||||
|                 send_stderr) | ||||
|         elif mode == "directory": | ||||
|             self.failUnlessIn("Sending directory", send_stderr) | ||||
|             self.failUnlessIn("named 'testdir'", send_stderr) | ||||
|             self.failUnlessIn("Wormhole code is: {code}{NL}" | ||||
|                               "On the other computer, please run:{NL}{NL}" | ||||
|                               "wormhole receive {code}{NL}" | ||||
|                               .format(code=send_cfg.code, NL=NL), send_stderr) | ||||
|             self.failIfIn("File sent.. waiting for confirmation{NL}" | ||||
|                           "Confirmation received. Transfer complete.{NL}" | ||||
|                           .format(NL=NL), send_stderr) | ||||
|                               "wormhole receive {code}{NL}".format( | ||||
|                                   code=send_cfg.code, NL=NL), send_stderr) | ||||
|             self.failIfIn( | ||||
|                 "File sent.. waiting for confirmation{NL}" | ||||
|                 "Confirmation received. Transfer complete.{NL}".format(NL=NL), | ||||
|                 send_stderr) | ||||
| 
 | ||||
|         # check receiver | ||||
|         if mode == "file": | ||||
|             self.failIfIn("Received file written to ", receive_stderr) | ||||
|             if failmode == "noclobber": | ||||
|                 self.failUnlessIn("Error: " | ||||
|                                   "refusing to overwrite existing 'testfile'{NL}" | ||||
|                                   .format(NL=NL), receive_stderr) | ||||
|                 self.failUnlessIn( | ||||
|                     "Error: " | ||||
|                     "refusing to overwrite existing 'testfile'{NL}" | ||||
|                     .format(NL=NL), | ||||
|                     receive_stderr) | ||||
|             else: | ||||
|                 self.failUnlessIn("Error: " | ||||
|                                   "insufficient free space (0B) for file ({size:d}B){NL}" | ||||
|                                   .format(NL=NL, size=size), receive_stderr) | ||||
|                 self.failUnlessIn( | ||||
|                     "Error: " | ||||
|                     "insufficient free space (0B) for file ({size:d}B){NL}" | ||||
|                     .format(NL=NL, size=size), receive_stderr) | ||||
|         elif mode == "directory": | ||||
|             self.failIfIn("Received files written to {name}" | ||||
|                           .format(name=receive_name), receive_stderr) | ||||
|             #want = (r"Receiving directory \(\d+ \w+\) into: {name}/" | ||||
|             self.failIfIn( | ||||
|                 "Received files written to {name}".format(name=receive_name), | ||||
|                 receive_stderr) | ||||
|             # want = (r"Receiving directory \(\d+ \w+\) into: {name}/" | ||||
|             #        .format(name=receive_name)) | ||||
|             #self.failUnless(re.search(want, receive_stderr), | ||||
|             # self.failUnless(re.search(want, receive_stderr), | ||||
|             #                (want, receive_stderr)) | ||||
|             if failmode == "noclobber": | ||||
|                 self.failUnlessIn("Error: " | ||||
|                                   "refusing to overwrite existing 'testdir'{NL}" | ||||
|                                   .format(NL=NL), receive_stderr) | ||||
|                 self.failUnlessIn( | ||||
|                     "Error: " | ||||
|                     "refusing to overwrite existing 'testdir'{NL}" | ||||
|                     .format(NL=NL), | ||||
|                     receive_stderr) | ||||
|             else: | ||||
|                 self.failUnlessIn("Error: " | ||||
|                                   "insufficient free space (0B) for directory ({size:d}B){NL}" | ||||
|                                   .format(NL=NL, size=size), receive_stderr) | ||||
|                 self.failUnlessIn(("Error: " | ||||
|                                    "insufficient free space (0B) for directory" | ||||
|                                    " ({size:d}B){NL}").format( | ||||
|                                        NL=NL, size=size), receive_stderr) | ||||
| 
 | ||||
|         if failmode == "noclobber": | ||||
|             fn = os.path.join(receive_dir, receive_name) | ||||
|  | @ -846,13 +914,17 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase): | |||
| 
 | ||||
|     def test_fail_file_noclobber(self): | ||||
|         return self._do_test_fail("file", "noclobber") | ||||
| 
 | ||||
|     def test_fail_directory_noclobber(self): | ||||
|         return self._do_test_fail("directory", "noclobber") | ||||
| 
 | ||||
|     def test_fail_file_toobig(self): | ||||
|         return self._do_test_fail("file", "toobig") | ||||
| 
 | ||||
|     def test_fail_directory_toobig(self): | ||||
|         return self._do_test_fail("directory", "toobig") | ||||
| 
 | ||||
| 
 | ||||
| class ZeroMode(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_text(self): | ||||
|  | @ -871,8 +943,8 @@ class ZeroMode(ServerBase, unittest.TestCase): | |||
| 
 | ||||
|         send_cfg.text = message | ||||
| 
 | ||||
|         #send_cfg.cwd = send_dir | ||||
|         #recv_cfg.cwd = receive_dir | ||||
|         # send_cfg.cwd = send_dir | ||||
|         # recv_cfg.cwd = receive_dir | ||||
| 
 | ||||
|         send_d = cmd_send.send(send_cfg) | ||||
|         receive_d = cmd_receive.receive(recv_cfg) | ||||
|  | @ -888,7 +960,7 @@ class ZeroMode(ServerBase, unittest.TestCase): | |||
|         # newlines, even if we're on windows | ||||
|         NL = "\n" | ||||
| 
 | ||||
|         self.maxDiff = None # show full output for assertion failures | ||||
|         self.maxDiff = None  # show full output for assertion failures | ||||
| 
 | ||||
|         self.assertEqual(send_stdout, "") | ||||
| 
 | ||||
|  | @ -898,15 +970,15 @@ class ZeroMode(ServerBase, unittest.TestCase): | |||
|                     "{NL}" | ||||
|                     "wormhole receive -0{NL}" | ||||
|                     "{NL}" | ||||
|                     "text message sent{NL}").format(bytes=len(message), | ||||
|                                                     code=send_cfg.code, | ||||
|                                                     NL=NL) | ||||
|                     "text message sent{NL}").format( | ||||
|                         bytes=len(message), code=send_cfg.code, NL=NL) | ||||
|         self.failUnlessEqual(send_stderr, expected) | ||||
| 
 | ||||
|         # check receiver | ||||
|         self.assertEqual(receive_stdout, message+NL) | ||||
|         self.assertEqual(receive_stdout, message + NL) | ||||
|         self.assertEqual(receive_stderr, "") | ||||
| 
 | ||||
| 
 | ||||
| class NotWelcome(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def setUp(self): | ||||
|  | @ -936,6 +1008,7 @@ class NotWelcome(ServerBase, unittest.TestCase): | |||
|         f = yield self.assertFailure(receive_d, WelcomeError) | ||||
|         self.assertEqual(str(f), "please upgrade XYZ") | ||||
| 
 | ||||
| 
 | ||||
| class NoServer(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def setUp(self): | ||||
|  | @ -991,8 +1064,8 @@ class NoServer(ServerBase, unittest.TestCase): | |||
|         e = yield self.assertFailure(receive_d, ServerConnectionError) | ||||
|         self.assertIsInstance(e.reason, ConnectionRefusedError) | ||||
| 
 | ||||
| class Cleanup(ServerBase, unittest.TestCase): | ||||
| 
 | ||||
| class Cleanup(ServerBase, unittest.TestCase): | ||||
|     def make_config(self): | ||||
|         cfg = config("send") | ||||
|         # 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() | ||||
|         self.assertEqual(len(cids), 0) | ||||
| 
 | ||||
| 
 | ||||
| class ExtractFile(unittest.TestCase): | ||||
|     def test_filenames(self): | ||||
|         args = mock.Mock() | ||||
|  | @ -1066,8 +1140,8 @@ class ExtractFile(unittest.TestCase): | |||
| 
 | ||||
|         zf = mock.Mock() | ||||
|         zi = mock.Mock() | ||||
|         zi.filename = "haha//root" # abspath squashes this, hopefully zipfile | ||||
|                                    # does too | ||||
|         zi.filename = "haha//root"  # abspath squashes this, hopefully zipfile | ||||
|         # does too | ||||
|         zi.external_attr = 5 << 16 | ||||
|         expected = os.path.join(extract_dir, "haha", "root") | ||||
|         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) | ||||
|         self.assertIn("malicious zipfile", str(e)) | ||||
| 
 | ||||
| 
 | ||||
| class AppID(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def setUp(self): | ||||
|  | @ -1108,11 +1183,11 @@ class AppID(ServerBase, unittest.TestCase): | |||
|         yield receive_d | ||||
| 
 | ||||
|         used = self._usage_db.execute("SELECT DISTINCT `app_id`" | ||||
|                                       " FROM `nameplates`" | ||||
|                                       ).fetchall() | ||||
|                                       " FROM `nameplates`").fetchall() | ||||
|         self.assertEqual(len(used), 1, used) | ||||
|         self.assertEqual(used[0]["app_id"], u"appid2") | ||||
| 
 | ||||
| 
 | ||||
| class Welcome(unittest.TestCase): | ||||
|     def do(self, welcome_message, my_version="2.0"): | ||||
|         stderr = io.StringIO() | ||||
|  | @ -1129,27 +1204,33 @@ class Welcome(unittest.TestCase): | |||
| 
 | ||||
|     def test_version_old(self): | ||||
|         stderr = self.do({"current_cli_version": "3.0"}) | ||||
|         expected = ("Warning: errors may occur unless both sides are running the same version\n" + | ||||
|         expected = ("Warning: errors may occur unless both sides are" | ||||
|                     " running the same version\n" | ||||
|                     "Server claims 3.0 is current, but ours is 2.0\n") | ||||
|         self.assertEqual(stderr, expected) | ||||
| 
 | ||||
|     def test_version_unreleased(self): | ||||
|         stderr = self.do({"current_cli_version": "3.0"}, | ||||
|                          my_version="2.5+middle.something") | ||||
|         stderr = self.do( | ||||
|             { | ||||
|                 "current_cli_version": "3.0" | ||||
|             }, my_version="2.5+middle.something") | ||||
|         self.assertEqual(stderr, "") | ||||
| 
 | ||||
|     def test_motd(self): | ||||
|         stderr = self.do({"motd": "hello"}) | ||||
|         self.assertEqual(stderr, "Server (at url) says:\n hello\n") | ||||
| 
 | ||||
| 
 | ||||
| class Dispatch(unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_success(self): | ||||
|         cfg = config("send") | ||||
|         cfg.stderr = io.StringIO() | ||||
|         called = [] | ||||
| 
 | ||||
|         def fake(): | ||||
|             called.append(1) | ||||
| 
 | ||||
|         yield cli._dispatch_command(reactor, cfg, fake) | ||||
|         self.assertEqual(called, [1]) | ||||
|         self.assertEqual(cfg.stderr.getvalue(), "") | ||||
|  | @ -1160,8 +1241,10 @@ class Dispatch(unittest.TestCase): | |||
|         cfg.stderr = io.StringIO() | ||||
|         cfg.timing = mock.Mock() | ||||
|         cfg.dump_timing = "filename" | ||||
| 
 | ||||
|         def fake(): | ||||
|             pass | ||||
| 
 | ||||
|         yield cli._dispatch_command(reactor, cfg, fake) | ||||
|         self.assertEqual(cfg.stderr.getvalue(), "") | ||||
|         self.assertEqual(cfg.timing.mock_calls[-1], | ||||
|  | @ -1171,32 +1254,39 @@ class Dispatch(unittest.TestCase): | |||
|     def test_wrong_password_error(self): | ||||
|         cfg = config("send") | ||||
|         cfg.stderr = io.StringIO() | ||||
| 
 | ||||
|         def fake(): | ||||
|             raise WrongPasswordError("abcd") | ||||
|         yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake), | ||||
|                                  SystemExit) | ||||
|         expected = fill("ERROR: " + dedent(WrongPasswordError.__doc__))+"\n" | ||||
| 
 | ||||
|         yield self.assertFailure( | ||||
|             cli._dispatch_command(reactor, cfg, fake), SystemExit) | ||||
|         expected = fill("ERROR: " + dedent(WrongPasswordError.__doc__)) + "\n" | ||||
|         self.assertEqual(cfg.stderr.getvalue(), expected) | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def test_welcome_error(self): | ||||
|         cfg = config("send") | ||||
|         cfg.stderr = io.StringIO() | ||||
| 
 | ||||
|         def fake(): | ||||
|             raise WelcomeError("abcd") | ||||
|         yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake), | ||||
|                                  SystemExit) | ||||
|         expected = fill("ERROR: " + dedent(WelcomeError.__doc__))+"\n\nabcd\n" | ||||
| 
 | ||||
|         yield self.assertFailure( | ||||
|             cli._dispatch_command(reactor, cfg, fake), SystemExit) | ||||
|         expected = ( | ||||
|             fill("ERROR: " + dedent(WelcomeError.__doc__)) + "\n\nabcd\n") | ||||
|         self.assertEqual(cfg.stderr.getvalue(), expected) | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def test_transfer_error(self): | ||||
|         cfg = config("send") | ||||
|         cfg.stderr = io.StringIO() | ||||
| 
 | ||||
|         def fake(): | ||||
|             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" | ||||
|         self.assertEqual(cfg.stderr.getvalue(), expected) | ||||
| 
 | ||||
|  | @ -1204,11 +1294,14 @@ class Dispatch(unittest.TestCase): | |||
|     def test_server_connection_error(self): | ||||
|         cfg = config("send") | ||||
|         cfg.stderr = io.StringIO() | ||||
| 
 | ||||
|         def fake(): | ||||
|             raise ServerConnectionError("URL", ValueError("abcd")) | ||||
|         yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake), | ||||
|                                  SystemExit) | ||||
|         expected = fill("ERROR: " + dedent(ServerConnectionError.__doc__))+"\n" | ||||
| 
 | ||||
|         yield self.assertFailure( | ||||
|             cli._dispatch_command(reactor, cfg, fake), SystemExit) | ||||
|         expected = fill( | ||||
|             "ERROR: " + dedent(ServerConnectionError.__doc__)) + "\n" | ||||
|         expected += "(relay URL was URL)\n" | ||||
|         expected += "abcd\n" | ||||
|         self.assertEqual(cfg.stderr.getvalue(), expected) | ||||
|  | @ -1217,21 +1310,26 @@ class Dispatch(unittest.TestCase): | |||
|     def test_other_error(self): | ||||
|         cfg = config("send") | ||||
|         cfg.stderr = io.StringIO() | ||||
| 
 | ||||
|         def fake(): | ||||
|             raise ValueError("abcd") | ||||
| 
 | ||||
|         # I'm seeing unicode problems with the Failure().printTraceback, and | ||||
|         # the output would be kind of unpredictable anyways, so we'll mock it | ||||
|         # out here. | ||||
|         f = mock.Mock() | ||||
| 
 | ||||
|         def mock_print(file): | ||||
|             file.write(u"<TRACEBACK>\n") | ||||
| 
 | ||||
|         f.printTraceback = mock_print | ||||
|         with mock.patch("wormhole.cli.cli.Failure", return_value=f): | ||||
|             yield self.assertFailure(cli._dispatch_command(reactor, cfg, fake), | ||||
|                                      SystemExit) | ||||
|             yield self.assertFailure( | ||||
|                 cli._dispatch_command(reactor, cfg, fake), SystemExit) | ||||
|         expected = "<TRACEBACK>\nERROR: abcd\n" | ||||
|         self.assertEqual(cfg.stderr.getvalue(), expected) | ||||
| 
 | ||||
| 
 | ||||
| class Help(unittest.TestCase): | ||||
|     def _check_top_level_help(self, got): | ||||
|         # the main wormhole.cli.cli.wormhole docstring should be in the | ||||
|  |  | |||
|  | @ -1,14 +1,19 @@ | |||
| from __future__ import print_function, unicode_literals | ||||
| import mock | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| from twisted.internet import reactor | ||||
| from twisted.internet.task import Clock | ||||
| from twisted.internet.defer import Deferred, inlineCallbacks | ||||
| from twisted.internet.task import Clock | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from ..eventual import EventualQueue | ||||
| 
 | ||||
| 
 | ||||
| class IntentionalError(Exception): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class Eventual(unittest.TestCase, object): | ||||
|     def test_eventually(self): | ||||
|         c = Clock() | ||||
|  | @ -23,9 +28,10 @@ class Eventual(unittest.TestCase, object): | |||
|         self.assertNoResult(d3) | ||||
| 
 | ||||
|         eq.flush_sync() | ||||
|         self.assertEqual(c1.mock_calls, | ||||
|                          [mock.call("arg1", "arg2", kwarg1="kw1"), | ||||
|                           mock.call("arg3", "arg4", kwarg5="kw5")]) | ||||
|         self.assertEqual(c1.mock_calls, [ | ||||
|             mock.call("arg1", "arg2", kwarg1="kw1"), | ||||
|             mock.call("arg3", "arg4", kwarg5="kw5") | ||||
|         ]) | ||||
|         self.assertEqual(self.successResultOf(d2), None) | ||||
|         self.assertEqual(self.successResultOf(d3), "value") | ||||
| 
 | ||||
|  | @ -47,11 +53,12 @@ class Eventual(unittest.TestCase, object): | |||
|         eq = EventualQueue(reactor) | ||||
|         d1 = eq.fire_eventually() | ||||
|         d2 = Deferred() | ||||
| 
 | ||||
|         def _more(res): | ||||
|             eq.eventually(d2.callback, None) | ||||
| 
 | ||||
|         d1.addCallback(_more) | ||||
|         yield eq.flush() | ||||
|         # d1 will fire, which will queue d2 to fire, and the flush() ought to | ||||
|         # wait for d2 too | ||||
|         self.successResultOf(d2) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,44 +1,47 @@ | |||
| from __future__ import print_function, unicode_literals | ||||
| 
 | ||||
| import unittest | ||||
| from binascii import unhexlify #, hexlify | ||||
| from binascii import unhexlify  # , hexlify | ||||
| 
 | ||||
| from hkdf import Hkdf | ||||
| 
 | ||||
| #def generate_KAT(): | ||||
| #    print("KAT = [") | ||||
| #    for salt in (b"", b"salt"): | ||||
| #        for context in (b"", b"context"): | ||||
| #            skm = b"secret" | ||||
| #            out = HKDF(skm, 64, XTS=salt, CTXinfo=context) | ||||
| #            hexout = "  '%s' +\n  '%s'" % (hexlify(out[:32]), | ||||
| #                                           hexlify(out[32:])) | ||||
| #            print(" (%r, %r, %r,\n%s)," % (salt, context, skm, hexout)) | ||||
| #    print("]") | ||||
| # def generate_KAT(): | ||||
| #     print("KAT = [") | ||||
| #     for salt in (b"", b"salt"): | ||||
| #         for context in (b"", b"context"): | ||||
| #             skm = b"secret" | ||||
| #             out = HKDF(skm, 64, XTS=salt, CTXinfo=context) | ||||
| #             hexout = "  '%s' +\n  '%s'" % (hexlify(out[:32]), | ||||
| #                                            hexlify(out[32:])) | ||||
| #             print(" (%r, %r, %r,\n%s)," % (salt, context, skm, hexout)) | ||||
| #     print("]") | ||||
| 
 | ||||
| KAT = [ | ||||
|  ('', '', 'secret', | ||||
|   '2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf' + | ||||
|   '9cfd42fd37e1e5a214d15f03058d7fee63dc28f564b7b9fe3da514f80daad4bf'), | ||||
|  ('', 'context', 'secret', | ||||
|   'c24c303a1adfb4c3e2b092e6254ed481c41d8955ba8ec3f6a1473493a60c957b' + | ||||
|   '31b723018ca75557214d3d5c61c0c7a5315b103b21ff00cb03ebe023dc347a47'), | ||||
|  ('salt', '', 'secret', | ||||
|   'f1156507c39b0e326159e778696253122de430899a8df2484040a85a5f95ceb1' + | ||||
|   'dfca555d4cc603bdf7153ed1560de8cbc3234b27a6d2be8e8ca202d90649679a'), | ||||
|  ('salt', 'context', 'secret', | ||||
|   '61a4f201a867bcc12381ddb180d27074408d03ee9d5750855e5a12d967fa060f' + | ||||
|   '10336ead9370927eaabb0d60b259346ee5f57eb7ceba8c72f1ed3f2932b1bf19'), | ||||
|     ('', '', 'secret', | ||||
|      '2f34e5ff91ec85d53ca9b543683174d0cf550b60d5f52b24c97b386cfcf6cbbf' + | ||||
|      '9cfd42fd37e1e5a214d15f03058d7fee63dc28f564b7b9fe3da514f80daad4bf'), | ||||
|     ('', 'context', 'secret', | ||||
|      'c24c303a1adfb4c3e2b092e6254ed481c41d8955ba8ec3f6a1473493a60c957b' + | ||||
|      '31b723018ca75557214d3d5c61c0c7a5315b103b21ff00cb03ebe023dc347a47'), | ||||
|     ('salt', '', 'secret', | ||||
|      'f1156507c39b0e326159e778696253122de430899a8df2484040a85a5f95ceb1' + | ||||
|      'dfca555d4cc603bdf7153ed1560de8cbc3234b27a6d2be8e8ca202d90649679a'), | ||||
|     ('salt', 'context', 'secret', | ||||
|      '61a4f201a867bcc12381ddb180d27074408d03ee9d5750855e5a12d967fa060f' + | ||||
|      '10336ead9370927eaabb0d60b259346ee5f57eb7ceba8c72f1ed3f2932b1bf19'), | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| class TestKAT(unittest.TestCase): | ||||
|     # note: this uses SHA256 | ||||
|     def test_kat(self): | ||||
|         for (salt, context, skm, expected_hexout) in KAT: | ||||
|             expected_out = unhexlify(expected_hexout) | ||||
|             for outlen in range(0, len(expected_out)): | ||||
|                 out = Hkdf(salt.encode("ascii"), | ||||
|                            skm.encode("ascii")).expand(context.encode("ascii"), | ||||
|                                                        outlen) | ||||
|                 out = Hkdf(salt.encode("ascii"), skm.encode("ascii")).expand( | ||||
|                     context.encode("ascii"), outlen) | ||||
|                 self.assertEqual(out, expected_out[:outlen]) | ||||
| 
 | ||||
| #if __name__ == '__main__': | ||||
| #    generate_KAT() | ||||
| 
 | ||||
| # if __name__ == '__main__': | ||||
| #     generate_KAT() | ||||
|  |  | |||
|  | @ -1,9 +1,13 @@ | |||
| import errno | ||||
| import os | ||||
| import re | ||||
| import subprocess | ||||
| 
 | ||||
| import re, errno, subprocess, os | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| 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 = """\ | ||||
| 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 | ||||
|     inet6 ::1/128 scope host \n\ | ||||
|        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 | ||||
|     inet 192.168.0.6/24 brd 192.168.0.255 scope global eth1 | ||||
|     inet6 fe80::d63d:7eff:fe01:b43e/64 scope link \n\ | ||||
|        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 | ||||
|     inet 192.168.0.2/24 brd 192.168.0.255 scope global wlan0 | ||||
|     inet6 fe80::92f6:52ff:fe27:150a/64 scope link \n\ | ||||
|  | @ -58,7 +64,8 @@ MOCK_ROUTE_OUTPUT = """\ | |||
| =========================================================================== | ||||
| Interface List | ||||
| 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: | ||||
|  | @ -85,6 +92,7 @@ class FakeProcess: | |||
|     def __init__(self, output, err): | ||||
|         self.output = output | ||||
|         self.err = err | ||||
| 
 | ||||
|     def communicate(self): | ||||
|         return (self.output, self.err) | ||||
| 
 | ||||
|  | @ -94,16 +102,28 @@ class ListAddresses(unittest.TestCase): | |||
|         addresses = ipaddrs.find_addresses() | ||||
|         self.failUnlessIn("127.0.0.1", addresses) | ||||
|         self.failIfIn("0.0.0.0", addresses) | ||||
| 
 | ||||
|     # David A.'s OpenSolaris box timed out on this test one time when it was at | ||||
|     # 2s. | ||||
|     test_list.timeout=4 | ||||
|     test_list.timeout = 4 | ||||
| 
 | ||||
|     def _test_list_mock(self, command, output, expected): | ||||
|         self.first = True | ||||
| 
 | ||||
|         def call_Popen(args, bufsize=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): | ||||
|         def call_Popen(args, | ||||
|                        bufsize=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: | ||||
|                 self.first = False | ||||
|                 e = OSError("EINTR") | ||||
|  | @ -115,11 +135,13 @@ class ListAddresses(unittest.TestCase): | |||
|                 e = OSError("[Errno 2] No such file or directory") | ||||
|                 e.errno = errno.ENOENT | ||||
|                 raise e | ||||
| 
 | ||||
|         self.patch(subprocess, 'Popen', call_Popen) | ||||
|         self.patch(os.path, 'isfile', lambda x: True) | ||||
| 
 | ||||
|         def call_which(name): | ||||
|             return [name] | ||||
| 
 | ||||
|         self.patch(ipaddrs, 'which', call_which) | ||||
| 
 | ||||
|         addresses = ipaddrs.find_addresses() | ||||
|  | @ -131,11 +153,13 @@ class ListAddresses(unittest.TestCase): | |||
| 
 | ||||
|     def test_list_mock_ifconfig(self): | ||||
|         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): | ||||
|         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): | ||||
|         self.patch(ipaddrs, 'platform', "cygwin") | ||||
|  |  | |||
|  | @ -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 .. import journal | ||||
| from .._interfaces import IJournal | ||||
| 
 | ||||
| 
 | ||||
| class Journal(unittest.TestCase): | ||||
|     def test_journal(self): | ||||
|         events = [] | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,9 +1,11 @@ | |||
| from twisted.trial import unittest | ||||
| from twisted.internet.task import Clock | ||||
| from twisted.python.failure import Failure | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| from ..eventual import EventualQueue | ||||
| from ..observer import OneShotObserver, SequenceObserver | ||||
| 
 | ||||
| 
 | ||||
| class OneShot(unittest.TestCase): | ||||
|     def test_fire(self): | ||||
|         c = Clock() | ||||
|  | @ -119,4 +121,3 @@ class Sequence(unittest.TestCase): | |||
|         d2 = o.when_next_event() | ||||
|         eq.flush_sync() | ||||
|         self.assertIdentical(self.failureResultOf(d2), f) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,39 +1,44 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| import mock | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| from itertools import count | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| from twisted.internet import reactor | ||||
| from twisted.internet.defer import inlineCallbacks | ||||
| from twisted.internet.threads import deferToThread | ||||
| from .._rlcompleter import (input_with_completion, | ||||
|                             _input_code_with_completion, | ||||
|                             CodeInputter, warn_readline) | ||||
| from ..errors import KeyFormatError, AlreadyInputNameplateError | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from .._rlcompleter import (CodeInputter, _input_code_with_completion, | ||||
|                             input_with_completion, warn_readline) | ||||
| from ..errors import AlreadyInputNameplateError, KeyFormatError | ||||
| 
 | ||||
| APPID = "appid" | ||||
| 
 | ||||
| 
 | ||||
| class Input(unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_wrapper(self): | ||||
|         helper = object() | ||||
|         trueish = object() | ||||
|         with mock.patch("wormhole._rlcompleter._input_code_with_completion", | ||||
|                         return_value=trueish) as m: | ||||
|             used_completion = yield input_with_completion("prompt:", helper, | ||||
|                                                           reactor) | ||||
|         with mock.patch( | ||||
|                 "wormhole._rlcompleter._input_code_with_completion", | ||||
|                 return_value=trueish) as m: | ||||
|             used_completion = yield input_with_completion( | ||||
|                 "prompt:", helper, reactor) | ||||
|         self.assertIs(used_completion, trueish) | ||||
|         self.assertEqual(m.mock_calls, | ||||
|                          [mock.call("prompt:", helper, reactor)]) | ||||
|         self.assertEqual(m.mock_calls, [mock.call("prompt:", helper, reactor)]) | ||||
|         # note: if this test fails, the warn_readline() message will probably | ||||
|         # get written to stderr | ||||
| 
 | ||||
| 
 | ||||
| class Sync(unittest.TestCase): | ||||
|     # exercise _input_code_with_completion, which uses the blocking builtin | ||||
|     # "input()" function, hence _input_code_with_completion is usually in a | ||||
|     # thread with deferToThread | ||||
| 
 | ||||
|     @mock.patch("wormhole._rlcompleter.CodeInputter") | ||||
|     @mock.patch("wormhole._rlcompleter.readline", | ||||
|                 __doc__="I am GNU readline") | ||||
|     @mock.patch("wormhole._rlcompleter.readline", __doc__="I am GNU readline") | ||||
|     @mock.patch("wormhole._rlcompleter.input", return_value="code") | ||||
|     def test_readline(self, input, readline, ci): | ||||
|         c = mock.Mock(name="inhibit parenting") | ||||
|  | @ -49,17 +54,17 @@ class Sync(unittest.TestCase): | |||
|         self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) | ||||
|         self.assertEqual(c.mock_calls, [mock.call.finish("code")]) | ||||
|         self.assertEqual(input.mock_calls, [mock.call(prompt)]) | ||||
|         self.assertEqual(readline.mock_calls, | ||||
|                          [mock.call.parse_and_bind("tab: complete"), | ||||
|                           mock.call.set_completer(c.completer), | ||||
|                           mock.call.set_completer_delims(""), | ||||
|                           ]) | ||||
|         self.assertEqual(readline.mock_calls, [ | ||||
|             mock.call.parse_and_bind("tab: complete"), | ||||
|             mock.call.set_completer(c.completer), | ||||
|             mock.call.set_completer_delims(""), | ||||
|         ]) | ||||
| 
 | ||||
|     @mock.patch("wormhole._rlcompleter.CodeInputter") | ||||
|     @mock.patch("wormhole._rlcompleter.readline") | ||||
|     @mock.patch("wormhole._rlcompleter.input", return_value="code") | ||||
|     def test_readline_no_docstring(self, input, readline, ci): | ||||
|         del readline.__doc__ # when in doubt, it assumes GNU readline | ||||
|         del readline.__doc__  # when in doubt, it assumes GNU readline | ||||
|         c = mock.Mock(name="inhibit parenting") | ||||
|         c.completer = object() | ||||
|         trueish = object() | ||||
|  | @ -73,15 +78,14 @@ class Sync(unittest.TestCase): | |||
|         self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) | ||||
|         self.assertEqual(c.mock_calls, [mock.call.finish("code")]) | ||||
|         self.assertEqual(input.mock_calls, [mock.call(prompt)]) | ||||
|         self.assertEqual(readline.mock_calls, | ||||
|                          [mock.call.parse_and_bind("tab: complete"), | ||||
|                           mock.call.set_completer(c.completer), | ||||
|                           mock.call.set_completer_delims(""), | ||||
|                           ]) | ||||
|         self.assertEqual(readline.mock_calls, [ | ||||
|             mock.call.parse_and_bind("tab: complete"), | ||||
|             mock.call.set_completer(c.completer), | ||||
|             mock.call.set_completer_delims(""), | ||||
|         ]) | ||||
| 
 | ||||
|     @mock.patch("wormhole._rlcompleter.CodeInputter") | ||||
|     @mock.patch("wormhole._rlcompleter.readline", | ||||
|                 __doc__="I am libedit") | ||||
|     @mock.patch("wormhole._rlcompleter.readline", __doc__="I am libedit") | ||||
|     @mock.patch("wormhole._rlcompleter.input", return_value="code") | ||||
|     def test_libedit(self, input, readline, ci): | ||||
|         c = mock.Mock(name="inhibit parenting") | ||||
|  | @ -97,11 +101,11 @@ class Sync(unittest.TestCase): | |||
|         self.assertEqual(ci.mock_calls, [mock.call(input_helper, reactor)]) | ||||
|         self.assertEqual(c.mock_calls, [mock.call.finish("code")]) | ||||
|         self.assertEqual(input.mock_calls, [mock.call(prompt)]) | ||||
|         self.assertEqual(readline.mock_calls, | ||||
|                          [mock.call.parse_and_bind("bind ^I rl_complete"), | ||||
|                           mock.call.set_completer(c.completer), | ||||
|                           mock.call.set_completer_delims(""), | ||||
|                           ]) | ||||
|         self.assertEqual(readline.mock_calls, [ | ||||
|             mock.call.parse_and_bind("bind ^I rl_complete"), | ||||
|             mock.call.set_completer(c.completer), | ||||
|             mock.call.set_completer_delims(""), | ||||
|         ]) | ||||
| 
 | ||||
|     @mock.patch("wormhole._rlcompleter.CodeInputter") | ||||
|     @mock.patch("wormhole._rlcompleter.readline", None) | ||||
|  | @ -139,6 +143,7 @@ class Sync(unittest.TestCase): | |||
|         self.assertEqual(c.mock_calls, [mock.call.finish(u"code")]) | ||||
|         self.assertEqual(input.mock_calls, [mock.call(prompt)]) | ||||
| 
 | ||||
| 
 | ||||
| def get_completions(c, prefix): | ||||
|     completions = [] | ||||
|     for state in count(0): | ||||
|  | @ -147,9 +152,11 @@ def get_completions(c, prefix): | |||
|             return completions | ||||
|         completions.append(text) | ||||
| 
 | ||||
| 
 | ||||
| def fake_blockingCallFromThread(f, *a, **kw): | ||||
|     return f(*a, **kw) | ||||
| 
 | ||||
| 
 | ||||
| class Completion(unittest.TestCase): | ||||
|     def test_simple(self): | ||||
|         # no actual completion | ||||
|  | @ -158,12 +165,14 @@ class Completion(unittest.TestCase): | |||
|         c.bcft = fake_blockingCallFromThread | ||||
|         c.finish("1-code-ghost") | ||||
|         self.assertFalse(c.used_completion) | ||||
|         self.assertEqual(helper.mock_calls, | ||||
|                          [mock.call.choose_nameplate("1"), | ||||
|                           mock.call.choose_words("code-ghost")]) | ||||
|         self.assertEqual(helper.mock_calls, [ | ||||
|             mock.call.choose_nameplate("1"), | ||||
|             mock.call.choose_words("code-ghost") | ||||
|         ]) | ||||
| 
 | ||||
|     @mock.patch("wormhole._rlcompleter.readline", | ||||
|                 get_completion_type=mock.Mock(return_value=0)) | ||||
|     @mock.patch( | ||||
|         "wormhole._rlcompleter.readline", | ||||
|         get_completion_type=mock.Mock(return_value=0)) | ||||
|     def test_call(self, readline): | ||||
|         # check that it calls _commit_and_build_completions correctly | ||||
|         helper = mock.Mock() | ||||
|  | @ -188,14 +197,14 @@ class Completion(unittest.TestCase): | |||
|         # now we have three "a" words: "and", "ark", "aaah!zombies!!" | ||||
|         cabc.reset_mock() | ||||
|         cabc.configure_mock(return_value=["aargh", "ark", "aaah!zombies!!"]) | ||||
|         self.assertEqual(get_completions(c, "12-a"), | ||||
|                          ["aargh", "ark", "aaah!zombies!!"]) | ||||
|         self.assertEqual( | ||||
|             get_completions(c, "12-a"), ["aargh", "ark", "aaah!zombies!!"]) | ||||
|         self.assertEqual(cabc.mock_calls, [mock.call("12-a")]) | ||||
| 
 | ||||
|         cabc.reset_mock() | ||||
|         cabc.configure_mock(return_value=["aargh", "aaah!zombies!!"]) | ||||
|         self.assertEqual(get_completions(c, "12-aa"), | ||||
|                          ["aargh", "aaah!zombies!!"]) | ||||
|         self.assertEqual( | ||||
|             get_completions(c, "12-aa"), ["aargh", "aaah!zombies!!"]) | ||||
|         self.assertEqual(cabc.mock_calls, [mock.call("12-aa")]) | ||||
| 
 | ||||
|         cabc.reset_mock() | ||||
|  | @ -223,16 +232,17 @@ class Completion(unittest.TestCase): | |||
|     def test_build_completions(self): | ||||
|         rn = mock.Mock() | ||||
|         # InputHelper.get_nameplate_completions returns just the suffixes | ||||
|         gnc = mock.Mock() # get_nameplate_completions | ||||
|         cn = mock.Mock() # choose_nameplate | ||||
|         gwc = mock.Mock() # get_word_completions | ||||
|         cw = mock.Mock() # choose_words | ||||
|         helper = mock.Mock(refresh_nameplates=rn, | ||||
|                            get_nameplate_completions=gnc, | ||||
|                            choose_nameplate=cn, | ||||
|                            get_word_completions=gwc, | ||||
|                            choose_words=cw, | ||||
|                            ) | ||||
|         gnc = mock.Mock()  # get_nameplate_completions | ||||
|         cn = mock.Mock()  # choose_nameplate | ||||
|         gwc = mock.Mock()  # get_word_completions | ||||
|         cw = mock.Mock()  # choose_words | ||||
|         helper = mock.Mock( | ||||
|             refresh_nameplates=rn, | ||||
|             get_nameplate_completions=gnc, | ||||
|             choose_nameplate=cn, | ||||
|             get_word_completions=gwc, | ||||
|             choose_words=cw, | ||||
|         ) | ||||
|         # this needs a real reactor, for blockingCallFromThread | ||||
|         c = CodeInputter(helper, reactor) | ||||
|         cabc = c._commit_and_build_completions | ||||
|  | @ -327,17 +337,18 @@ class Completion(unittest.TestCase): | |||
|         gwc.configure_mock(return_value=["code", "court"]) | ||||
|         c = CodeInputter(helper, reactor) | ||||
|         cabc = c._commit_and_build_completions | ||||
|         matches = yield deferToThread(cabc, "1-co") # this commits us to 1- | ||||
|         self.assertEqual(helper.mock_calls, | ||||
|                          [mock.call.choose_nameplate("1"), | ||||
|                           mock.call.when_wordlist_is_available(), | ||||
|                           mock.call.get_word_completions("co")]) | ||||
|         matches = yield deferToThread(cabc, "1-co")  # this commits us to 1- | ||||
|         self.assertEqual(helper.mock_calls, [ | ||||
|             mock.call.choose_nameplate("1"), | ||||
|             mock.call.when_wordlist_is_available(), | ||||
|             mock.call.get_word_completions("co") | ||||
|         ]) | ||||
|         self.assertEqual(matches, ["1-code", "1-court"]) | ||||
|         helper.reset_mock() | ||||
|         with self.assertRaises(AlreadyInputNameplateError) as e: | ||||
|             yield deferToThread(cabc, "2-co") | ||||
|         self.assertEqual(str(e.exception), | ||||
|                          "nameplate (1-) already entered, cannot go back") | ||||
|         self.assertEqual( | ||||
|             str(e.exception), "nameplate (1-) already entered, cannot go back") | ||||
|         self.assertEqual(helper.mock_calls, []) | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|  | @ -347,17 +358,18 @@ class Completion(unittest.TestCase): | |||
|         gwc.configure_mock(return_value=["code", "court"]) | ||||
|         c = CodeInputter(helper, reactor) | ||||
|         cabc = c._commit_and_build_completions | ||||
|         matches = yield deferToThread(cabc, "1-co") # this commits us to 1- | ||||
|         self.assertEqual(helper.mock_calls, | ||||
|                          [mock.call.choose_nameplate("1"), | ||||
|                           mock.call.when_wordlist_is_available(), | ||||
|                           mock.call.get_word_completions("co")]) | ||||
|         matches = yield deferToThread(cabc, "1-co")  # this commits us to 1- | ||||
|         self.assertEqual(helper.mock_calls, [ | ||||
|             mock.call.choose_nameplate("1"), | ||||
|             mock.call.when_wordlist_is_available(), | ||||
|             mock.call.get_word_completions("co") | ||||
|         ]) | ||||
|         self.assertEqual(matches, ["1-code", "1-court"]) | ||||
|         helper.reset_mock() | ||||
|         with self.assertRaises(AlreadyInputNameplateError) as e: | ||||
|             yield deferToThread(c.finish, "2-code") | ||||
|         self.assertEqual(str(e.exception), | ||||
|                          "nameplate (1-) already entered, cannot go back") | ||||
|         self.assertEqual( | ||||
|             str(e.exception), "nameplate (1-) already entered, cannot go back") | ||||
|         self.assertEqual(helper.mock_calls, []) | ||||
| 
 | ||||
|     @mock.patch("wormhole._rlcompleter.stderr") | ||||
|  | @ -366,6 +378,7 @@ class Completion(unittest.TestCase): | |||
|         # right time, since it involves a reactor and a "system event | ||||
|         # trigger", but let's at least make sure it's invocable | ||||
|         warn_readline() | ||||
|         expected ="\nCommand interrupted: please press Return to quit" | ||||
|         self.assertEqual(stderr.mock_calls, [mock.call.write(expected), | ||||
|                                              mock.call.write("\n")]) | ||||
|         expected = "\nCommand interrupted: please press Return to quit" | ||||
|         self.assertEqual(stderr.mock_calls, | ||||
|                          [mock.call.write(expected), | ||||
|                           mock.call.write("\n")]) | ||||
|  |  | |||
|  | @ -1,10 +1,15 @@ | |||
| import os, io | ||||
| import mock | ||||
| import io | ||||
| import os | ||||
| 
 | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from ..cli import cmd_ssh | ||||
| 
 | ||||
| OTHERS = ["config", "config~", "known_hosts", "known_hosts~"] | ||||
| 
 | ||||
| 
 | ||||
| class FindPubkey(unittest.TestCase): | ||||
|     def test_find_one(self): | ||||
|         files = OTHERS + ["id_rsa.pub", "id_rsa"] | ||||
|  | @ -12,8 +17,8 @@ class FindPubkey(unittest.TestCase): | |||
|         pubkey_file = io.StringIO(pubkey_data) | ||||
|         with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=True): | ||||
|             with mock.patch("os.listdir", return_value=files) as ld: | ||||
|                 with mock.patch("wormhole.cli.cmd_ssh.open", | ||||
|                                 return_value=pubkey_file): | ||||
|                 with mock.patch( | ||||
|                         "wormhole.cli.cmd_ssh.open", return_value=pubkey_file): | ||||
|                     res = cmd_ssh.find_public_key() | ||||
|         self.assertEqual(ld.mock_calls, | ||||
|                          [mock.call(os.path.expanduser("~/.ssh/"))]) | ||||
|  | @ -24,7 +29,7 @@ class FindPubkey(unittest.TestCase): | |||
|         self.assertEqual(pubkey, pubkey_data) | ||||
| 
 | ||||
|     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("os.listdir", return_value=files): | ||||
|                 e = self.assertRaises(cmd_ssh.PubkeyError, | ||||
|  | @ -34,12 +39,12 @@ class FindPubkey(unittest.TestCase): | |||
| 
 | ||||
|     def test_bad_hint(self): | ||||
|         with mock.patch("wormhole.cli.cmd_ssh.exists", return_value=False): | ||||
|             e = self.assertRaises(cmd_ssh.PubkeyError, | ||||
|                                   cmd_ssh.find_public_key, | ||||
|                                   hint="bogus/path") | ||||
|             e = self.assertRaises( | ||||
|                 cmd_ssh.PubkeyError, | ||||
|                 cmd_ssh.find_public_key, | ||||
|                 hint="bogus/path") | ||||
|         self.assertEqual(str(e), "Can't find 'bogus/path'") | ||||
| 
 | ||||
| 
 | ||||
|     def test_find_multiple(self): | ||||
|         files = OTHERS + ["id_rsa.pub", "id_rsa", "id_dsa.pub", "id_dsa"] | ||||
|         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("os.listdir", return_value=files): | ||||
|                 responses = iter(["frog", "NaN", "-1", "0"]) | ||||
|                 with mock.patch("click.prompt", | ||||
|                                 side_effect=lambda p: next(responses)): | ||||
|                     with mock.patch("wormhole.cli.cmd_ssh.open", | ||||
|                                     return_value=pubkey_file): | ||||
|                 with mock.patch( | ||||
|                         "click.prompt", side_effect=lambda p: next(responses)): | ||||
|                     with mock.patch( | ||||
|                             "wormhole.cli.cmd_ssh.open", | ||||
|                             return_value=pubkey_file): | ||||
|                         res = cmd_ssh.find_public_key() | ||||
|         self.assertEqual(len(res), 3, res) | ||||
|         kind, keyid, pubkey = res | ||||
|  |  | |||
|  | @ -1,42 +1,51 @@ | |||
| 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.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 ..errors import NoTorError | ||||
| from ..tor_manager import SocksOnlyTor, get_tor | ||||
| 
 | ||||
| 
 | ||||
| class X(): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class Tor(unittest.TestCase): | ||||
|     def test_no_txtorcon(self): | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon", None): | ||||
|             self.failureResultOf(get_tor(None), NoTorError) | ||||
| 
 | ||||
|     def test_bad_args(self): | ||||
|         f = self.failureResultOf(get_tor(None, launch_tor="not boolean"), | ||||
|                                  TypeError) | ||||
|         f = self.failureResultOf( | ||||
|             get_tor(None, launch_tor="not boolean"), TypeError) | ||||
|         self.assertEqual(str(f.value), "launch_tor= must be boolean") | ||||
| 
 | ||||
|         f = self.failureResultOf(get_tor(None, tor_control_port=1234), | ||||
|                                  TypeError) | ||||
|         f = self.failureResultOf( | ||||
|             get_tor(None, tor_control_port=1234), TypeError) | ||||
|         self.assertEqual(str(f.value), "tor_control_port= must be str or None") | ||||
|         f = self.failureResultOf(get_tor(None, launch_tor=True, | ||||
|                                          tor_control_port="tcp:127.0.0.1:1234"), | ||||
|                                  ValueError) | ||||
|         self.assertEqual(str(f.value), | ||||
|                          "cannot combine --launch-tor and --tor-control-port=") | ||||
|         f = self.failureResultOf( | ||||
|             get_tor( | ||||
|                 None, launch_tor=True, tor_control_port="tcp:127.0.0.1:1234"), | ||||
|             ValueError) | ||||
|         self.assertEqual( | ||||
|             str(f.value), | ||||
|             "cannot combine --launch-tor and --tor-control-port=") | ||||
| 
 | ||||
|     def test_launch(self): | ||||
|         reactor = object() | ||||
|         my_tor = X() # object() didn't like providedBy() | ||||
|         my_tor = X()  # object() didn't like providedBy() | ||||
|         launch_d = defer.Deferred() | ||||
|         stderr = io.StringIO() | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon.launch", | ||||
|                         side_effect=launch_d) as launch: | ||||
|         with mock.patch( | ||||
|                 "wormhole.tor_manager.txtorcon.launch", | ||||
|                 side_effect=launch_d) as launch: | ||||
|             d = get_tor(reactor, launch_tor=True, stderr=stderr) | ||||
|             self.assertNoResult(d) | ||||
|             self.assertEqual(launch.mock_calls, [mock.call(reactor)]) | ||||
|  | @ -44,18 +53,21 @@ class Tor(unittest.TestCase): | |||
|             tor = self.successResultOf(d) | ||||
|             self.assertIs(tor, my_tor) | ||||
|             self.assert_(ITorManager.providedBy(tor)) | ||||
|             self.assertEqual(stderr.getvalue(), | ||||
|                              " launching a new Tor process, this may take a while..\n") | ||||
|             self.assertEqual( | ||||
|                 stderr.getvalue(), | ||||
|                 " launching a new Tor process, this may take a while..\n") | ||||
| 
 | ||||
|     def test_connect(self): | ||||
|         reactor = object() | ||||
|         my_tor = X() # object() didn't like providedBy() | ||||
|         my_tor = X()  # object() didn't like providedBy() | ||||
|         connect_d = defer.Deferred() | ||||
|         stderr = io.StringIO() | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon.connect", | ||||
|                         side_effect=connect_d) as connect: | ||||
|             with mock.patch("wormhole.tor_manager.clientFromString", | ||||
|                             side_effect=["foo"]) as sfs: | ||||
|         with mock.patch( | ||||
|                 "wormhole.tor_manager.txtorcon.connect", | ||||
|                 side_effect=connect_d) as connect: | ||||
|             with mock.patch( | ||||
|                     "wormhole.tor_manager.clientFromString", | ||||
|                     side_effect=["foo"]) as sfs: | ||||
|                 d = get_tor(reactor, stderr=stderr) | ||||
|         self.assertEqual(sfs.mock_calls, []) | ||||
|         self.assertNoResult(d) | ||||
|  | @ -71,10 +83,12 @@ class Tor(unittest.TestCase): | |||
|         reactor = object() | ||||
|         connect_d = defer.Deferred() | ||||
|         stderr = io.StringIO() | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon.connect", | ||||
|                         side_effect=connect_d) as connect: | ||||
|             with mock.patch("wormhole.tor_manager.clientFromString", | ||||
|                             side_effect=["foo"]) as sfs: | ||||
|         with mock.patch( | ||||
|                 "wormhole.tor_manager.txtorcon.connect", | ||||
|                 side_effect=connect_d) as connect: | ||||
|             with mock.patch( | ||||
|                     "wormhole.tor_manager.clientFromString", | ||||
|                     side_effect=["foo"]) as sfs: | ||||
|                 d = get_tor(reactor, stderr=stderr) | ||||
|         self.assertEqual(sfs.mock_calls, []) | ||||
|         self.assertNoResult(d) | ||||
|  | @ -85,20 +99,23 @@ class Tor(unittest.TestCase): | |||
|         self.assertIsInstance(tor, SocksOnlyTor) | ||||
|         self.assert_(ITorManager.providedBy(tor)) | ||||
|         self.assertEqual(tor._reactor, reactor) | ||||
|         self.assertEqual(stderr.getvalue(), | ||||
|                          " unable to find default Tor control port, using SOCKS\n") | ||||
|         self.assertEqual( | ||||
|             stderr.getvalue(), | ||||
|             " unable to find default Tor control port, using SOCKS\n") | ||||
| 
 | ||||
|     def test_connect_custom_control_port(self): | ||||
|         reactor = object() | ||||
|         my_tor = X() # object() didn't like providedBy() | ||||
|         my_tor = X()  # object() didn't like providedBy() | ||||
|         tcp = "PORT" | ||||
|         ep = object() | ||||
|         connect_d = defer.Deferred() | ||||
|         stderr = io.StringIO() | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon.connect", | ||||
|                         side_effect=connect_d) as connect: | ||||
|             with mock.patch("wormhole.tor_manager.clientFromString", | ||||
|                             side_effect=[ep]) as sfs: | ||||
|         with mock.patch( | ||||
|                 "wormhole.tor_manager.txtorcon.connect", | ||||
|                 side_effect=connect_d) as connect: | ||||
|             with mock.patch( | ||||
|                     "wormhole.tor_manager.clientFromString", | ||||
|                     side_effect=[ep]) as sfs: | ||||
|                 d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) | ||||
|         self.assertEqual(sfs.mock_calls, [mock.call(reactor, tcp)]) | ||||
|         self.assertNoResult(d) | ||||
|  | @ -116,10 +133,12 @@ class Tor(unittest.TestCase): | |||
|         ep = object() | ||||
|         connect_d = defer.Deferred() | ||||
|         stderr = io.StringIO() | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon.connect", | ||||
|                         side_effect=connect_d) as connect: | ||||
|             with mock.patch("wormhole.tor_manager.clientFromString", | ||||
|                             side_effect=[ep]) as sfs: | ||||
|         with mock.patch( | ||||
|                 "wormhole.tor_manager.txtorcon.connect", | ||||
|                 side_effect=connect_d) as connect: | ||||
|             with mock.patch( | ||||
|                     "wormhole.tor_manager.clientFromString", | ||||
|                     side_effect=[ep]) as sfs: | ||||
|                 d = get_tor(reactor, tor_control_port=tcp, stderr=stderr) | ||||
|         self.assertEqual(sfs.mock_calls, [mock.call(reactor, tcp)]) | ||||
|         self.assertNoResult(d) | ||||
|  | @ -129,18 +148,22 @@ class Tor(unittest.TestCase): | |||
|         self.failureResultOf(d, ConnectError) | ||||
|         self.assertEqual(stderr.getvalue(), "") | ||||
| 
 | ||||
| 
 | ||||
| class SocksOnly(unittest.TestCase): | ||||
|     def test_tor(self): | ||||
|         reactor = object() | ||||
|         sot = SocksOnlyTor(reactor) | ||||
|         fake_ep = object() | ||||
|         with mock.patch("wormhole.tor_manager.txtorcon.TorClientEndpoint", | ||||
|                         return_value=fake_ep) as tce: | ||||
|         with mock.patch( | ||||
|                 "wormhole.tor_manager.txtorcon.TorClientEndpoint", | ||||
|                 return_value=fake_ep) as tce: | ||||
|             ep = sot.stream_via("host", "port") | ||||
|         self.assertIs(ep, fake_ep) | ||||
|         self.assertEqual(tce.mock_calls, [mock.call("host", "port", | ||||
|                                                     socks_endpoint=None, | ||||
|                                                     tls=False, | ||||
|                                                     reactor=reactor)]) | ||||
| 
 | ||||
| 
 | ||||
|         self.assertEqual(tce.mock_calls, [ | ||||
|             mock.call( | ||||
|                 "host", | ||||
|                 "port", | ||||
|                 socks_endpoint=None, | ||||
|                 tls=False, | ||||
|                 reactor=reactor) | ||||
|         ]) | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,10 +1,15 @@ | |||
| from __future__ import unicode_literals | ||||
| import six | ||||
| import mock | ||||
| 
 | ||||
| import unicodedata | ||||
| 
 | ||||
| import six | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from .. import util | ||||
| 
 | ||||
| 
 | ||||
| class Utils(unittest.TestCase): | ||||
|     def test_to_bytes(self): | ||||
|         b = util.to_bytes("abc") | ||||
|  | @ -41,11 +46,12 @@ class Utils(unittest.TestCase): | |||
|         self.assertIsInstance(d, dict) | ||||
|         self.assertEqual(d, {"a": "b", "c": 2}) | ||||
| 
 | ||||
| 
 | ||||
| class Space(unittest.TestCase): | ||||
|     def test_free_space(self): | ||||
|         free = util.estimate_free_space(".") | ||||
|         self.assert_(isinstance(free, six.integer_types + (type(None),)), | ||||
|                      repr(free)) | ||||
|         self.assert_( | ||||
|             isinstance(free, six.integer_types + (type(None), )), repr(free)) | ||||
|         # some platforms (I think the VMs used by travis are in this | ||||
|         # category) return 0, and windows will return None, so don't assert | ||||
|         # anything more specific about the return value | ||||
|  | @ -56,5 +62,5 @@ class Space(unittest.TestCase): | |||
|         try: | ||||
|             with mock.patch("os.statvfs", side_effect=AttributeError()): | ||||
|                 self.assertEqual(util.estimate_free_space("."), None) | ||||
|         except AttributeError: # raised by mock.get_original() | ||||
|         except AttributeError:  # raised by mock.get_original() | ||||
|             pass | ||||
|  |  | |||
|  | @ -1,8 +1,12 @@ | |||
| from __future__ import print_function, unicode_literals | ||||
| import mock | ||||
| 
 | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from .._wordlist import PGPWordList | ||||
| 
 | ||||
| 
 | ||||
| class Completions(unittest.TestCase): | ||||
|     def test_completions(self): | ||||
|         wl = PGPWordList() | ||||
|  | @ -14,16 +18,21 @@ class Completions(unittest.TestCase): | |||
|         self.assertEqual(len(lots), 256, lots) | ||||
|         first = list(lots)[0] | ||||
|         self.assert_(first.startswith("armistice-"), first) | ||||
|         self.assertEqual(gc("armistice-ba", 2), | ||||
|                          {"armistice-baboon", "armistice-backfield", | ||||
|                           "armistice-backward", "armistice-banjo"}) | ||||
|         self.assertEqual(gc("armistice-ba", 3), | ||||
|                          {"armistice-baboon-", "armistice-backfield-", | ||||
|                           "armistice-backward-", "armistice-banjo-"}) | ||||
|         self.assertEqual( | ||||
|             gc("armistice-ba", 2), { | ||||
|                 "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", 3), {"armistice-baboon-"}) | ||||
|         self.assertEqual(gc("armistice-baboon", 4), {"armistice-baboon-"}) | ||||
| 
 | ||||
| 
 | ||||
| class Choose(unittest.TestCase): | ||||
|     def test_choose_words(self): | ||||
|         wl = PGPWordList() | ||||
|  |  | |||
|  | @ -1,17 +1,22 @@ | |||
| from __future__ import print_function, unicode_literals | ||||
| import io, re | ||||
| import mock | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import io | ||||
| import re | ||||
| 
 | ||||
| from twisted.internet import reactor | ||||
| from twisted.internet.defer import gatherResults, inlineCallbacks, returnValue | ||||
| from twisted.internet.error import ConnectionRefusedError | ||||
| from .common import ServerBase, poll_until | ||||
| from .. import wormhole, _rendezvous | ||||
| from ..errors import (WrongPasswordError, ServerConnectionError, | ||||
|                       KeyFormatError, WormholeClosed, LonelyError, | ||||
|                       NoKeyError, OnlyOneCodeError) | ||||
| from ..transit import allocate_tcp_port | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| import mock | ||||
| 
 | ||||
| from .. import _rendezvous, wormhole | ||||
| from ..errors import (KeyFormatError, LonelyError, NoKeyError, | ||||
|                       OnlyOneCodeError, ServerConnectionError, WormholeClosed, | ||||
|                       WrongPasswordError) | ||||
| from ..eventual import EventualQueue | ||||
| from ..transit import allocate_tcp_port | ||||
| from .common import ServerBase, poll_until | ||||
| 
 | ||||
| APPID = "appid" | ||||
| 
 | ||||
|  | @ -26,6 +31,7 @@ APPID = "appid" | |||
| # * set_code, then connected | ||||
| # * connected, receive_pake, send_phase, set_code | ||||
| 
 | ||||
| 
 | ||||
| class Delegate: | ||||
|     def __init__(self): | ||||
|         self.welcome = None | ||||
|  | @ -35,28 +41,35 @@ class Delegate: | |||
|         self.versions = None | ||||
|         self.messages = [] | ||||
|         self.closed = None | ||||
| 
 | ||||
|     def wormhole_got_welcome(self, welcome): | ||||
|         self.welcome = welcome | ||||
| 
 | ||||
|     def wormhole_got_code(self, code): | ||||
|         self.code = code | ||||
| 
 | ||||
|     def wormhole_got_unverified_key(self, key): | ||||
|         self.key = key | ||||
| 
 | ||||
|     def wormhole_got_verifier(self, verifier): | ||||
|         self.verifier = verifier | ||||
| 
 | ||||
|     def wormhole_got_versions(self, versions): | ||||
|         self.versions = versions | ||||
| 
 | ||||
|     def wormhole_got_message(self, data): | ||||
|         self.messages.append(data) | ||||
| 
 | ||||
|     def wormhole_closed(self, result): | ||||
|         self.closed = result | ||||
| 
 | ||||
| class Delegated(ServerBase, unittest.TestCase): | ||||
| 
 | ||||
| class Delegated(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_delegated(self): | ||||
|         dg = Delegate() | ||||
|         w1 = wormhole.create(APPID, self.relayurl, reactor, delegate=dg) | ||||
|         #w1.debug_set_trace("W1") | ||||
|         # w1.debug_set_trace("W1") | ||||
|         with self.assertRaises(NoKeyError): | ||||
|             w1.derive_key("purpose", 12) | ||||
|         w1.set_code("1-abc") | ||||
|  | @ -103,6 +116,7 @@ class Delegated(ServerBase, unittest.TestCase): | |||
|         yield poll_until(lambda: dg.code is not None) | ||||
|         w1.close() | ||||
| 
 | ||||
| 
 | ||||
| class Wormholes(ServerBase, unittest.TestCase): | ||||
|     # integration test, with a real server | ||||
| 
 | ||||
|  | @ -135,12 +149,12 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|     @inlineCallbacks | ||||
|     def test_basic(self): | ||||
|         w1 = wormhole.create(APPID, self.relayurl, reactor) | ||||
|         #w1.debug_set_trace("W1") | ||||
|         # w1.debug_set_trace("W1") | ||||
|         with self.assertRaises(NoKeyError): | ||||
|             w1.derive_key("purpose", 12) | ||||
| 
 | ||||
|         w2 = wormhole.create(APPID, self.relayurl, reactor) | ||||
|         #w2.debug_set_trace("  W2") | ||||
|         # w2.debug_set_trace("  W2") | ||||
|         w1.allocate_code() | ||||
|         code = yield w1.get_code() | ||||
|         w2.set_code(code) | ||||
|  | @ -302,7 +316,6 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         yield w1.close() | ||||
|         yield w2.close() | ||||
| 
 | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def test_multiple_messages(self): | ||||
|         w1 = wormhole.create(APPID, self.relayurl, reactor) | ||||
|  | @ -322,7 +335,6 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         yield w1.close() | ||||
|         yield w2.close() | ||||
| 
 | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def test_closed(self): | ||||
|         eq = EventualQueue(reactor) | ||||
|  | @ -377,7 +389,7 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         w2 = wormhole.create(APPID, self.relayurl, reactor, _eventual_queue=eq) | ||||
|         w1.allocate_code() | ||||
|         code = yield w1.get_code() | ||||
|         w2.set_code(code+"not") | ||||
|         w2.set_code(code + "not") | ||||
|         code2 = yield w2.get_code() | ||||
|         self.assertNotEqual(code, code2) | ||||
|         # 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") | ||||
|         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 | ||||
|         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 | ||||
|         # then will receive w2.VERSION. When it sees w2.VERSION, it will | ||||
|         # learn about the WrongPasswordError. | ||||
|  | @ -451,7 +463,7 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         badcode = "4 oops spaces" | ||||
|         with self.assertRaises(KeyFormatError) as ex: | ||||
|             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)) | ||||
|         yield self.assertFailure(w.close(), LonelyError) | ||||
| 
 | ||||
|  | @ -461,7 +473,7 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         badcode = " 4-oops-space" | ||||
|         with self.assertRaises(KeyFormatError) as ex: | ||||
|             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)) | ||||
|         yield self.assertFailure(w.close(), LonelyError) | ||||
| 
 | ||||
|  | @ -478,8 +490,8 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|     @inlineCallbacks | ||||
|     def test_welcome(self): | ||||
|         w1 = wormhole.create(APPID, self.relayurl, reactor) | ||||
|         wel1 = yield w1.get_welcome() # early: before connection established | ||||
|         wel2 = yield w1.get_welcome() # late: already received welcome | ||||
|         wel1 = yield w1.get_welcome()  # early: before connection established | ||||
|         wel2 = yield w1.get_welcome()  # late: already received welcome | ||||
|         self.assertEqual(wel1, wel2) | ||||
|         self.assertIn("current_cli_version", wel1) | ||||
| 
 | ||||
|  | @ -489,7 +501,7 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         w2.set_code("123-NOT") | ||||
|         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(w2.close(), WrongPasswordError) | ||||
|  | @ -502,7 +514,7 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         w1.allocate_code() | ||||
|         code = yield w1.get_code() | ||||
|         w2.set_code(code) | ||||
|         v1 = yield w1.get_verifier() # early | ||||
|         v1 = yield w1.get_verifier()  # early | ||||
|         v2 = yield w2.get_verifier() | ||||
|         self.failUnlessEqual(type(v1), type(b"")) | ||||
|         self.failUnlessEqual(v1, v2) | ||||
|  | @ -525,10 +537,10 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|     @inlineCallbacks | ||||
|     def test_versions(self): | ||||
|         # there's no API for this yet, but make sure the internals work | ||||
|         w1 = wormhole.create(APPID, self.relayurl, reactor, | ||||
|                              versions={"w1": 123}) | ||||
|         w2 = wormhole.create(APPID, self.relayurl, reactor, | ||||
|                              versions={"w2": 456}) | ||||
|         w1 = wormhole.create( | ||||
|             APPID, self.relayurl, reactor, versions={"w1": 123}) | ||||
|         w2 = wormhole.create( | ||||
|             APPID, self.relayurl, reactor, versions={"w2": 456}) | ||||
|         w1.allocate_code() | ||||
|         code = yield w1.get_code() | ||||
|         w2.set_code(code) | ||||
|  | @ -564,17 +576,19 @@ class Wormholes(ServerBase, unittest.TestCase): | |||
|         yield w1.close() | ||||
|         yield w2.close() | ||||
| 
 | ||||
| 
 | ||||
| class MessageDoubler(_rendezvous.RendezvousConnector): | ||||
|     # we could double messages on the sending side, but a future server will | ||||
|     # strip those duplicates, so to really exercise the receiver, we must | ||||
|     # double them on the inbound side instead | ||||
|     #def _msg_send(self, phase, body): | ||||
|     #    wormhole._Wormhole._msg_send(self, phase, body) | ||||
|     #    self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) | ||||
|     # def _msg_send(self, phase, body): | ||||
|     #     wormhole._Wormhole._msg_send(self, phase, body) | ||||
|     #     self._ws_send_command("add", phase=phase, body=bytes_to_hexstr(body)) | ||||
|     def _response_handle_message(self, msg): | ||||
|         _rendezvous.RendezvousConnector._response_handle_message(self, msg) | ||||
|         _rendezvous.RendezvousConnector._response_handle_message(self, msg) | ||||
| 
 | ||||
| 
 | ||||
| class Errors(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_derive_key_early(self): | ||||
|  | @ -602,16 +616,17 @@ class Errors(ServerBase, unittest.TestCase): | |||
|             w.set_code("123-nope") | ||||
|         yield self.assertFailure(w.close(), LonelyError) | ||||
| 
 | ||||
| 
 | ||||
| class Reconnection(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_basic(self): | ||||
|         w1 = wormhole.create(APPID, self.relayurl, reactor) | ||||
|         w1_in = [] | ||||
|         w1._boss._RC._debug_record_inbound_f = w1_in.append | ||||
|         #w1.debug_set_trace("W1") | ||||
|         # w1.debug_set_trace("W1") | ||||
|         w1.allocate_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 | ||||
|         def seen_our_pake(): | ||||
|  | @ -619,6 +634,7 @@ class Reconnection(ServerBase, unittest.TestCase): | |||
|                 if m["type"] == "message" and m["phase"] == "pake": | ||||
|                     return True | ||||
|             return False | ||||
| 
 | ||||
|         yield poll_until(seen_our_pake) | ||||
| 
 | ||||
|         w1_in[:] = [] | ||||
|  | @ -634,7 +650,7 @@ class Reconnection(ServerBase, unittest.TestCase): | |||
|         # receiver has started | ||||
| 
 | ||||
|         w2 = wormhole.create(APPID, self.relayurl, reactor) | ||||
|         #w2.debug_set_trace("  W2") | ||||
|         # w2.debug_set_trace("  W2") | ||||
|         w2.set_code(code) | ||||
| 
 | ||||
|         dataY = yield w2.get_message() | ||||
|  | @ -649,6 +665,7 @@ class Reconnection(ServerBase, unittest.TestCase): | |||
|         c2 = yield w2.close() | ||||
|         self.assertEqual(c2, "happy") | ||||
| 
 | ||||
| 
 | ||||
| class InitialFailure(unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def assertSCEFailure(self, eq, d, innerType): | ||||
|  | @ -662,8 +679,8 @@ class InitialFailure(unittest.TestCase): | |||
|     def test_bad_dns(self): | ||||
|         eq = EventualQueue(reactor) | ||||
|         # point at a URL that will never connect | ||||
|         w = wormhole.create(APPID, "ws://%%%.example.org:4000/v1", | ||||
|                             reactor, _eventual_queue=eq) | ||||
|         w = wormhole.create( | ||||
|             APPID, "ws://%%%.example.org:4000/v1", reactor, _eventual_queue=eq) | ||||
|         # that should have already received an error, when it tried to | ||||
|         # 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(d5, ConnectionRefusedError) | ||||
| 
 | ||||
| 
 | ||||
| class Trace(unittest.TestCase): | ||||
|     def test_basic(self): | ||||
|         w1 = wormhole.create(APPID, "ws://localhost:1", reactor) | ||||
|  | @ -734,13 +752,11 @@ class Trace(unittest.TestCase): | |||
|                          ["C1.M1[OLD].IN -> [NEW]"]) | ||||
|         out("OUT1") | ||||
|         self.assertEqual(stderr.getvalue().splitlines(), | ||||
|                          ["C1.M1[OLD].IN -> [NEW]", | ||||
|                           " C1.M1.OUT1()"]) | ||||
|                          ["C1.M1[OLD].IN -> [NEW]", " C1.M1.OUT1()"]) | ||||
|         w1._boss._print_trace("", "R.connected", "", "C1", "RC1", stderr) | ||||
|         self.assertEqual(stderr.getvalue().splitlines(), | ||||
|                          ["C1.M1[OLD].IN -> [NEW]", | ||||
|                           " C1.M1.OUT1()", | ||||
|                           "C1.RC1.R.connected"]) | ||||
|         self.assertEqual( | ||||
|             stderr.getvalue().splitlines(), | ||||
|             ["C1.M1[OLD].IN -> [NEW]", " C1.M1.OUT1()", "C1.RC1.R.connected"]) | ||||
| 
 | ||||
|     def test_delegated(self): | ||||
|         dg = Delegate() | ||||
|  |  | |||
|  | @ -1,11 +1,13 @@ | |||
| from twisted.trial import unittest | ||||
| from twisted.internet import reactor, defer | ||||
| from twisted.internet import defer, reactor | ||||
| from twisted.internet.defer import inlineCallbacks | ||||
| from twisted.trial import unittest | ||||
| 
 | ||||
| from .. import xfer_util | ||||
| from .common import ServerBase | ||||
| 
 | ||||
| APPID = u"appid" | ||||
| 
 | ||||
| 
 | ||||
| class Xfer(ServerBase, unittest.TestCase): | ||||
|     @inlineCallbacks | ||||
|     def test_xfer(self): | ||||
|  | @ -24,10 +26,15 @@ class Xfer(ServerBase, unittest.TestCase): | |||
|         data = u"data" | ||||
|         send_code = [] | ||||
|         receive_code = [] | ||||
|         d1 = xfer_util.send(reactor, APPID, self.relayurl, data, code, | ||||
|                             on_code=send_code.append) | ||||
|         d2 = xfer_util.receive(reactor, APPID, self.relayurl, code, | ||||
|                                on_code=receive_code.append) | ||||
|         d1 = xfer_util.send( | ||||
|             reactor, | ||||
|             APPID, | ||||
|             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 | ||||
|         receive_result = yield d2 | ||||
|         self.assertEqual(send_code, [code]) | ||||
|  | @ -39,8 +46,13 @@ class Xfer(ServerBase, unittest.TestCase): | |||
|     def test_make_code(self): | ||||
|         data = u"data" | ||||
|         got_code = defer.Deferred() | ||||
|         d1 = xfer_util.send(reactor, APPID, self.relayurl, data, code=None, | ||||
|                             on_code=got_code.callback) | ||||
|         d1 = xfer_util.send( | ||||
|             reactor, | ||||
|             APPID, | ||||
|             self.relayurl, | ||||
|             data, | ||||
|             code=None, | ||||
|             on_code=got_code.callback) | ||||
|         code = yield got_code | ||||
|         d2 = xfer_util.receive(reactor, APPID, self.relayurl, code) | ||||
|         send_result = yield d1 | ||||
|  |  | |||
|  | @ -1,8 +1,13 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| import json, time | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| import json | ||||
| import time | ||||
| 
 | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from ._interfaces import ITiming | ||||
| 
 | ||||
| 
 | ||||
| class Event: | ||||
|     def __init__(self, name, when, **details): | ||||
|         # data fields that will be dumped to JSON later | ||||
|  | @ -35,6 +40,7 @@ class Event: | |||
|         else: | ||||
|             self.finish() | ||||
| 
 | ||||
| 
 | ||||
| @implementer(ITiming) | ||||
| class DebugTiming: | ||||
|     def __init__(self): | ||||
|  | @ -47,11 +53,14 @@ class DebugTiming: | |||
| 
 | ||||
|     def write(self, fn, stderr): | ||||
|         with open(fn, "wt") as f: | ||||
|             data = [ dict(name=e._name, | ||||
|                           start=e._start, stop=e._stop, | ||||
|                           details=e._details, | ||||
|                           ) | ||||
|                      for e in self._events ] | ||||
|             data = [ | ||||
|                 dict( | ||||
|                     name=e._name, | ||||
|                     start=e._start, | ||||
|                     stop=e._stop, | ||||
|                     details=e._details, | ||||
|                 ) for e in self._events | ||||
|             ] | ||||
|             json.dump(data, f, indent=1) | ||||
|             f.write("\n") | ||||
|         print("Timing data written to %s" % fn, file=stderr) | ||||
|  |  | |||
|  | @ -1,15 +1,20 @@ | |||
| from __future__ import print_function, unicode_literals | ||||
| 
 | ||||
| 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.endpoints import clientFromString | ||||
| from zope.interface.declarations import directlyProvides | ||||
| 
 | ||||
| from . import _interfaces, errors | ||||
| from .timing import DebugTiming | ||||
| 
 | ||||
| try: | ||||
|     import txtorcon | ||||
| except ImportError: | ||||
|     txtorcon = None | ||||
| from . import _interfaces, errors | ||||
| from .timing import DebugTiming | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| class SocksOnlyTor(object): | ||||
|  | @ -17,15 +22,20 @@ class SocksOnlyTor(object): | |||
| 
 | ||||
|     def stream_via(self, host, port, tls=False): | ||||
|         return txtorcon.TorClientEndpoint( | ||||
|             host, port, | ||||
|             socks_endpoint=None, # tries localhost:9050 and 9150 | ||||
|             host, | ||||
|             port, | ||||
|             socks_endpoint=None,  # tries localhost:9050 and 9150 | ||||
|             tls=tls, | ||||
|             reactor=self._reactor, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| @inlineCallbacks | ||||
| def get_tor(reactor, launch_tor=False, tor_control_port=None, | ||||
|             timing=None, stderr=sys.stderr): | ||||
| def get_tor(reactor, | ||||
|             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 | ||||
|     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: | ||||
|         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") | ||||
|     if not isinstance(tor_control_port, (type(""), type(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. | ||||
| 
 | ||||
|     if launch_tor: | ||||
|         print(" launching a new Tor process, this may take a while..", | ||||
|               file=stderr) | ||||
|         print( | ||||
|             " launching a new Tor process, this may take a while..", | ||||
|             file=stderr) | ||||
|         with timing.add("launch tor"): | ||||
|             tor = yield txtorcon.launch(reactor, | ||||
|                                         #data_directory=, | ||||
|                                         #tor_binary=, | ||||
|                                         # data_directory=, | ||||
|                                         # tor_binary=, | ||||
|                                         ) | ||||
|     elif tor_control_port: | ||||
|         with timing.add("find tor"): | ||||
|             control_ep = clientFromString(reactor, tor_control_port) | ||||
|             tor = yield txtorcon.connect(reactor, control_ep) # might raise | ||||
|             print(" using Tor via control port at %s" % tor_control_port, | ||||
|                   file=stderr) | ||||
|             tor = yield txtorcon.connect(reactor, control_ep)  # might raise | ||||
|             print( | ||||
|                 " using Tor via control port at %s" % tor_control_port, | ||||
|                 file=stderr) | ||||
|     else: | ||||
|         # Let txtorcon look through a list of usual places. If that fails, | ||||
|         # 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 | ||||
|                 # likely to throw a reactor.connectTCP -type error, like | ||||
|                 # ConnectionFailed or ConnectionRefused or something | ||||
|                 print(" unable to find default Tor control port, using SOCKS", | ||||
|                       file=stderr) | ||||
|                 print( | ||||
|                     " unable to find default Tor control port, using SOCKS", | ||||
|                     file=stderr) | ||||
|                 tor = SocksOnlyTor(reactor) | ||||
|     directlyProvides(tor, _interfaces.ITorManager) | ||||
|     returnValue(tor) | ||||
|  |  | |||
|  | @ -1,38 +1,51 @@ | |||
| # no unicode_literals, revisit after twisted patch | ||||
| from __future__ import print_function, absolute_import | ||||
| import os, re, sys, time, socket | ||||
| from collections import namedtuple, deque | ||||
| from __future__ import absolute_import, print_function | ||||
| 
 | ||||
| import os | ||||
| import re | ||||
| import socket | ||||
| import sys | ||||
| import time | ||||
| from binascii import hexlify, unhexlify | ||||
| from collections import deque, namedtuple | ||||
| 
 | ||||
| import six | ||||
| from zope.interface import implementer | ||||
| from twisted.python import log | ||||
| from twisted.python.runtime import platformType | ||||
| from twisted.internet import (reactor, interfaces, defer, protocol, | ||||
|                               endpoints, task, address, error) | ||||
| from hkdf import Hkdf | ||||
| from nacl.secret import SecretBox | ||||
| from twisted.internet import (address, defer, endpoints, error, interfaces, | ||||
|                               protocol, reactor, task) | ||||
| from twisted.internet.defer import inlineCallbacks, returnValue | ||||
| from twisted.protocols import policies | ||||
| from nacl.secret import SecretBox | ||||
| from hkdf import Hkdf | ||||
| from twisted.python import log | ||||
| from twisted.python.runtime import platformType | ||||
| from zope.interface import implementer | ||||
| 
 | ||||
| from . import ipaddrs | ||||
| from .errors import InternalError | ||||
| from .timing import DebugTiming | ||||
| from .util import bytes_to_hexstr | ||||
| from . import ipaddrs | ||||
| 
 | ||||
| 
 | ||||
| def HKDF(skm, outlen, salt=None, CTXinfo=b""): | ||||
|     return Hkdf(salt, skm).expand(CTXinfo, outlen) | ||||
| 
 | ||||
| 
 | ||||
| class TransitError(Exception): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class BadHandshake(Exception): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class TransitClosed(TransitError): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| class BadNonce(TransitError): | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| # The beginning of each TCP connection consists of the following handshake | ||||
| # messages. The sender transmits the same text regardless of whether it is on | ||||
| # 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 | ||||
| # "go\n" or "nevermind\n"+close(). | ||||
| 
 | ||||
| 
 | ||||
| def build_receiver_handshake(key): | ||||
|     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): | ||||
|     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): | ||||
|     assert isinstance(side, type(u"")) | ||||
|     assert len(side) == 8*2 | ||||
|     assert len(side) == 8 * 2 | ||||
|     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 | ||||
|  | @ -87,7 +104,8 @@ def build_sided_relay_handshake(key, 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 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"]) | ||||
| # 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 | ||||
|  | @ -95,6 +113,7 @@ TorTCPV1Hint = namedtuple("TorTCPV1Hint", ["hostname", "port", "priority"]) | |||
| # rest of the V1 protocol. Only one hint per relay is useful. | ||||
| RelayV1Hint = namedtuple("RelayV1Hint", ["hints"]) | ||||
| 
 | ||||
| 
 | ||||
| def describe_hint_obj(hint): | ||||
|     if isinstance(hint, DirectTCPV1Hint): | ||||
|         return u"tcp:%s:%d" % (hint.hostname, hint.port) | ||||
|  | @ -103,27 +122,30 @@ def describe_hint_obj(hint): | |||
|     else: | ||||
|         return str(hint) | ||||
| 
 | ||||
| 
 | ||||
| def parse_hint_argv(hint, stderr=sys.stderr): | ||||
|     assert isinstance(hint, type(u"")) | ||||
|     # return tuple or None for an unparseable hint | ||||
|     priority = 0.0 | ||||
|     mo = re.search(r'^([a-zA-Z0-9]+):(.*)$', hint) | ||||
|     if not mo: | ||||
|         print("unparseable hint '%s'" % (hint,), file=stderr) | ||||
|         print("unparseable hint '%s'" % (hint, ), file=stderr) | ||||
|         return None | ||||
|     hint_type = mo.group(1) | ||||
|     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 | ||||
|     hint_value = mo.group(2) | ||||
|     pieces = hint_value.split(":") | ||||
|     if len(pieces) < 2: | ||||
|         print("unparseable TCP hint (need more colons) '%s'" % (hint,), | ||||
|               file=stderr) | ||||
|         print( | ||||
|             "unparseable TCP hint (need more colons) '%s'" % (hint, ), | ||||
|             file=stderr) | ||||
|         return None | ||||
|     mo = re.search(r'^(\d+)$', pieces[1]) | ||||
|     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 | ||||
|     hint_host = pieces[0] | ||||
|     hint_port = int(pieces[1]) | ||||
|  | @ -133,12 +155,15 @@ def parse_hint_argv(hint, stderr=sys.stderr): | |||
|             try: | ||||
|                 priority = float(more_pieces[1]) | ||||
|             except ValueError: | ||||
|                 print("non-float priority= in TCP hint '%s'" % (hint,), | ||||
|                       file=stderr) | ||||
|                 print( | ||||
|                     "non-float priority= in TCP hint '%s'" % (hint, ), | ||||
|                     file=stderr) | ||||
|                 return None | ||||
|     return DirectTCPV1Hint(hint_host, hint_port, priority) | ||||
| 
 | ||||
| TIMEOUT = 60 # seconds | ||||
| 
 | ||||
| TIMEOUT = 60  # seconds | ||||
| 
 | ||||
| 
 | ||||
| @implementer(interfaces.IProducer, interfaces.IConsumer) | ||||
| class Connection(protocol.Protocol, policies.TimeoutMixin): | ||||
|  | @ -159,7 +184,7 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|         self._waiting_reads = deque() | ||||
| 
 | ||||
|     def connectionMade(self): | ||||
|         self.setTimeout(TIMEOUT) # does timeoutConnection() when it expires | ||||
|         self.setTimeout(TIMEOUT)  # does timeoutConnection() when it expires | ||||
|         self.factory.connectionWasMade(self) | ||||
| 
 | ||||
|     def startNegotiation(self): | ||||
|  | @ -168,11 +193,11 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|             self.state = "relay" | ||||
|         else: | ||||
|             self.state = "start" | ||||
|         self.dataReceived(b"") # cycle the state machine | ||||
|         self.dataReceived(b"")  # cycle the state machine | ||||
|         return self._negotiation_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.transport.loseConnection() | ||||
|         # if connectionLost isn't called synchronously, then our | ||||
|  | @ -181,7 +206,6 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|         if self._negotiation_d: | ||||
|             self._negotiation_d = None | ||||
| 
 | ||||
| 
 | ||||
|     def dataReceived(self, data): | ||||
|         try: | ||||
|             self._dataReceived(data) | ||||
|  | @ -198,7 +222,7 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|         if not self.buf.startswith(expected[:len(self.buf)]): | ||||
|             raise BadHandshake("got %r want %r" % (self.buf, expected)) | ||||
|         if len(self.buf) < len(expected): | ||||
|             return False # keep waiting | ||||
|             return False  # keep waiting | ||||
|         self.buf = self.buf[len(expected):] | ||||
|         return True | ||||
| 
 | ||||
|  | @ -245,9 +269,9 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|             return self.dataReceivedRECORDS() | ||||
|         if self.state == "hung up": | ||||
|             return | ||||
|         if isinstance(self.state, Exception): # for tests | ||||
|         if isinstance(self.state, Exception):  # for tests | ||||
|             raise self.state | ||||
|         raise ValueError("internal error: unknown state %s" % (self.state,)) | ||||
|         raise ValueError("internal error: unknown state %s" % (self.state, )) | ||||
| 
 | ||||
|     def _negotiationSuccessful(self): | ||||
|         self.state = "records" | ||||
|  | @ -266,19 +290,20 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|             if len(self.buf) < 4: | ||||
|                 return | ||||
|             length = int(hexlify(self.buf[:4]), 16) | ||||
|             if len(self.buf) < 4+length: | ||||
|             if len(self.buf) < 4 + length: | ||||
|                 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) | ||||
|             self.recordReceived(record) | ||||
| 
 | ||||
|     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) | ||||
|         if nonce != self.next_receive_nonce: | ||||
|             raise BadNonce("received out-of-order record: got %d, expected %d" | ||||
|                            % (nonce, self.next_receive_nonce)) | ||||
|             raise BadNonce( | ||||
|                 "received out-of-order record: got %d, expected %d" % | ||||
|                 (nonce, self.next_receive_nonce)) | ||||
|         self.next_receive_nonce += 1 | ||||
|         record = self.receive_box.decrypt(encrypted) | ||||
|         return record | ||||
|  | @ -287,14 +312,15 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|         return self._description | ||||
| 
 | ||||
|     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 self.send_nonce < 2**(8*24) | ||||
|         assert len(record) < 2**(8*4) | ||||
|         nonce = unhexlify("%048x" % self.send_nonce) # big-endian | ||||
|         assert self.send_nonce < 2**(8 * 24) | ||||
|         assert len(record) < 2**(8 * 4) | ||||
|         nonce = unhexlify("%048x" % self.send_nonce)  # big-endian | ||||
|         self.send_nonce += 1 | ||||
|         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(encrypted) | ||||
| 
 | ||||
|  | @ -353,8 +379,10 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|     def registerProducer(self, producer, streaming): | ||||
|         assert interfaces.IConsumer.providedBy(self.transport) | ||||
|         self.transport.registerProducer(producer, streaming) | ||||
| 
 | ||||
|     def unregisterProducer(self): | ||||
|         self.transport.unregisterProducer() | ||||
| 
 | ||||
|     def write(self, data): | ||||
|         self.send_record(data) | ||||
| 
 | ||||
|  | @ -362,8 +390,10 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|     # the transport. | ||||
|     def stopProducing(self): | ||||
|         self.transport.stopProducing() | ||||
| 
 | ||||
|     def pauseProducing(self): | ||||
|         self.transport.pauseProducing() | ||||
| 
 | ||||
|     def resumeProducing(self): | ||||
|         self.transport.resumeProducing() | ||||
| 
 | ||||
|  | @ -384,8 +414,8 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|         Deferred, and you must call disconnectConsumer() when you are done.""" | ||||
| 
 | ||||
|         if self._consumer: | ||||
|             raise RuntimeError("A consumer is already attached: %r" % | ||||
|                                self._consumer) | ||||
|             raise RuntimeError( | ||||
|                 "A consumer is already attached: %r" % self._consumer) | ||||
| 
 | ||||
|         # be aware of an ordering hazard: when we call the consumer's | ||||
|         # .registerProducer method, they are likely to immediately call | ||||
|  | @ -440,6 +470,7 @@ class Connection(protocol.Protocol, policies.TimeoutMixin): | |||
|         fc = FileConsumer(f, progress, hasher) | ||||
|         return self.connectConsumer(fc, expected) | ||||
| 
 | ||||
| 
 | ||||
| class OutboundConnectionFactory(protocol.ClientFactory): | ||||
|     protocol = Connection | ||||
| 
 | ||||
|  | @ -478,7 +509,7 @@ class InboundConnectionFactory(protocol.ClientFactory): | |||
| 
 | ||||
|     def _shutdown(self): | ||||
|         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): | ||||
|         if isinstance(addr, address.HostnameAddress): | ||||
|  | @ -511,6 +542,7 @@ class InboundConnectionFactory(protocol.ClientFactory): | |||
|         # ignore these two, let Twisted log everything else | ||||
|         f.trap(BadHandshake, defer.CancelledError) | ||||
| 
 | ||||
| 
 | ||||
| def allocate_tcp_port(): | ||||
|     """Return an (integer) available TCP port on localhost. This briefly | ||||
|     listens on the port in question, then closes it right away.""" | ||||
|  | @ -527,6 +559,7 @@ def allocate_tcp_port(): | |||
|     s.close() | ||||
|     return port | ||||
| 
 | ||||
| 
 | ||||
| class _ThereCanBeOnlyOne: | ||||
|     """Accept a list of contender Deferreds, and return a summary Deferred. | ||||
|     When the first contender fires successfully, cancel the rest and fire the | ||||
|  | @ -535,6 +568,7 @@ class _ThereCanBeOnlyOne: | |||
| 
 | ||||
|     status_cb=? | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, contenders): | ||||
|         self._remaining = set(contenders) | ||||
|         self._winner_d = defer.Deferred(self._cancel) | ||||
|  | @ -581,26 +615,32 @@ class _ThereCanBeOnlyOne: | |||
|         else: | ||||
|             self._winner_d.errback(self._first_failure) | ||||
| 
 | ||||
| 
 | ||||
| def there_can_be_only_one(contenders): | ||||
|     return _ThereCanBeOnlyOne(contenders).run() | ||||
| 
 | ||||
| 
 | ||||
| class Common: | ||||
|     RELAY_DELAY = 2.0 | ||||
|     TRANSIT_KEY_LENGTH = SecretBox.KEY_SIZE | ||||
| 
 | ||||
|     def __init__(self, transit_relay, no_listen=False, tor=None, | ||||
|                  reactor=reactor, timing=None): | ||||
|         self._side = bytes_to_hexstr(os.urandom(8)) # unicode | ||||
|     def __init__(self, | ||||
|                  transit_relay, | ||||
|                  no_listen=False, | ||||
|                  tor=None, | ||||
|                  reactor=reactor, | ||||
|                  timing=None): | ||||
|         self._side = bytes_to_hexstr(os.urandom(8))  # unicode | ||||
|         if transit_relay: | ||||
|             if not isinstance(transit_relay, type(u"")): | ||||
|                 raise InternalError | ||||
|             # TODO: allow multiple hints for a single relay | ||||
|             relay_hint = parse_hint_argv(transit_relay) | ||||
|             relay = RelayV1Hint(hints=(relay_hint,)) | ||||
|             relay = RelayV1Hint(hints=(relay_hint, )) | ||||
|             self._transit_relays = [relay] | ||||
|         else: | ||||
|             self._transit_relays = [] | ||||
|         self._their_direct_hints = [] # hintobjs | ||||
|         self._their_direct_hints = []  # hintobjs | ||||
|         self._our_relay_hints = set(self._transit_relays) | ||||
|         self._tor = tor | ||||
|         self._transit_key = None | ||||
|  | @ -622,33 +662,42 @@ class Common: | |||
|             # some test hosts, including the appveyor VMs, *only* have | ||||
|             # 127.0.0.1, and the tests will hang badly if we remove it. | ||||
|             addresses = non_loopback_addresses | ||||
|         direct_hints = [DirectTCPV1Hint(six.u(addr), portnum, 0.0) | ||||
|                         for addr in addresses] | ||||
|         direct_hints = [ | ||||
|             DirectTCPV1Hint(six.u(addr), portnum, 0.0) for addr in addresses | ||||
|         ] | ||||
|         ep = endpoints.serverFromString(reactor, "tcp:%d" % portnum) | ||||
|         return direct_hints, ep | ||||
| 
 | ||||
|     def get_connection_abilities(self): | ||||
|         return [{u"type": u"direct-tcp-v1"}, | ||||
|                 {u"type": u"relay-v1"}, | ||||
|                 ] | ||||
|         return [ | ||||
|             { | ||||
|                 u"type": u"direct-tcp-v1" | ||||
|             }, | ||||
|             { | ||||
|                 u"type": u"relay-v1" | ||||
|             }, | ||||
|         ] | ||||
| 
 | ||||
|     @inlineCallbacks | ||||
|     def get_connection_hints(self): | ||||
|         hints = [] | ||||
|         direct_hints = yield self._get_direct_hints() | ||||
|         for dh in direct_hints: | ||||
|             hints.append({u"type": u"direct-tcp-v1", | ||||
|                           u"priority": dh.priority, | ||||
|                           u"hostname": dh.hostname, | ||||
|                           u"port": dh.port, # integer | ||||
|                           }) | ||||
|             hints.append({ | ||||
|                 u"type": u"direct-tcp-v1", | ||||
|                 u"priority": dh.priority, | ||||
|                 u"hostname": dh.hostname, | ||||
|                 u"port": dh.port,  # integer | ||||
|             }) | ||||
|         for relay in self._transit_relays: | ||||
|             rhint = {u"type": u"relay-v1", u"hints": []} | ||||
|             for rh in relay.hints: | ||||
|                 rhint[u"hints"].append({u"type": u"direct-tcp-v1", | ||||
|                                         u"priority": rh.priority, | ||||
|                                         u"hostname": rh.hostname, | ||||
|                                         u"port": rh.port}) | ||||
|                 rhint[u"hints"].append({ | ||||
|                     u"type": u"direct-tcp-v1", | ||||
|                     u"priority": rh.priority, | ||||
|                     u"hostname": rh.hostname, | ||||
|                     u"port": rh.port | ||||
|                 }) | ||||
|             hints.append(rhint) | ||||
|         returnValue(hints) | ||||
| 
 | ||||
|  | @ -665,24 +714,27 @@ class Common: | |||
|         # listener will win. | ||||
|         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 | ||||
|             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 | ||||
|         # connect to the direct hints we return. | ||||
|         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() | ||||
|         d = self._listener.listen(f) | ||||
| 
 | ||||
|         def _listening(lp): | ||||
|             # lp is an IListeningPort | ||||
|             #self._listener_port = lp # for tests | ||||
|             # self._listener_port = lp # for tests | ||||
|             def _stop_listening(res): | ||||
|                 lp.stopListening() | ||||
|                 return res | ||||
| 
 | ||||
|             self._listener_d.addBoth(_stop_listening) | ||||
|             return self._my_direct_hints | ||||
| 
 | ||||
|         d.addCallback(_listening) | ||||
|         return d | ||||
| 
 | ||||
|  | @ -694,18 +746,18 @@ class Common: | |||
|         self._listener_d.addErrback(lambda f: None) | ||||
|         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"") | ||||
|         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 | ||||
|         if not(u"hostname" in hint | ||||
|                and isinstance(hint[u"hostname"], type(u""))): | ||||
|             log.msg("invalid hostname in hint: %r" % (hint,)) | ||||
|         if not (u"hostname" in hint | ||||
|                 and isinstance(hint[u"hostname"], type(u""))): | ||||
|             log.msg("invalid hostname in hint: %r" % (hint, )) | ||||
|             return None | ||||
|         if not(u"port" in hint | ||||
|                and isinstance(hint[u"port"], six.integer_types)): | ||||
|             log.msg("invalid port in hint: %r" % (hint,)) | ||||
|         if not (u"port" in hint | ||||
|                 and isinstance(hint[u"port"], six.integer_types)): | ||||
|             log.msg("invalid port in hint: %r" % (hint, )) | ||||
|             return None | ||||
|         priority = hint.get(u"priority", 0.0) | ||||
|         if hint_type == u"direct-tcp-v1": | ||||
|  | @ -714,12 +766,12 @@ class Common: | |||
|             return TorTCPV1Hint(hint[u"hostname"], hint[u"port"], priority) | ||||
| 
 | ||||
|     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"") | ||||
|             if hint_type in [u"direct-tcp-v1", u"tor-tcp-v1"]: | ||||
|                 dh = self._parse_tcp_v1_hint(h) | ||||
|                 if dh: | ||||
|                     self._their_direct_hints.append(dh) # hint_obj | ||||
|                     self._their_direct_hints.append(dh)  # hint_obj | ||||
|             elif hint_type == u"relay-v1": | ||||
|                 # TODO: each relay-v1 clause describes a different relay, | ||||
|                 # 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))) | ||||
|                     self._our_relay_hints.add(rh) | ||||
|             else: | ||||
|                 log.msg("unknown hint type: %r" % (h,)) | ||||
|                 log.msg("unknown hint type: %r" % (h, )) | ||||
| 
 | ||||
|     def _send_this(self): | ||||
|         assert self._transit_key | ||||
|  | @ -748,25 +800,33 @@ class Common: | |||
|         if self.is_sender: | ||||
|             return build_receiver_handshake(self._transit_key) | ||||
|         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): | ||||
|         assert self._transit_key | ||||
|         if self.is_sender: | ||||
|             return HKDF(self._transit_key, SecretBox.KEY_SIZE, | ||||
|                         CTXinfo=b"transit_record_sender_key") | ||||
|             return HKDF( | ||||
|                 self._transit_key, | ||||
|                 SecretBox.KEY_SIZE, | ||||
|                 CTXinfo=b"transit_record_sender_key") | ||||
|         else: | ||||
|             return HKDF(self._transit_key, SecretBox.KEY_SIZE, | ||||
|                         CTXinfo=b"transit_record_receiver_key") | ||||
|             return HKDF( | ||||
|                 self._transit_key, | ||||
|                 SecretBox.KEY_SIZE, | ||||
|                 CTXinfo=b"transit_record_receiver_key") | ||||
| 
 | ||||
|     def _receiver_record_key(self): | ||||
|         assert self._transit_key | ||||
|         if self.is_sender: | ||||
|             return HKDF(self._transit_key, SecretBox.KEY_SIZE, | ||||
|                         CTXinfo=b"transit_record_receiver_key") | ||||
|             return HKDF( | ||||
|                 self._transit_key, | ||||
|                 SecretBox.KEY_SIZE, | ||||
|                 CTXinfo=b"transit_record_receiver_key") | ||||
|         else: | ||||
|             return HKDF(self._transit_key, SecretBox.KEY_SIZE, | ||||
|                         CTXinfo=b"transit_record_sender_key") | ||||
|             return HKDF( | ||||
|                 self._transit_key, | ||||
|                 SecretBox.KEY_SIZE, | ||||
|                 CTXinfo=b"transit_record_sender_key") | ||||
| 
 | ||||
|     def set_transit_key(self, key): | ||||
|         assert isinstance(key, type(b"")), type(key) | ||||
|  | @ -848,9 +908,13 @@ class Common: | |||
|                 description = "->relay:%s" % describe_hint_obj(hint_obj) | ||||
|                 if self._tor: | ||||
|                     description = "tor" + description | ||||
|                 d = task.deferLater(self._reactor, relay_delay, | ||||
|                                     self._start_connector, ep, description, | ||||
|                                     is_relay=True) | ||||
|                 d = task.deferLater( | ||||
|                     self._reactor, | ||||
|                     relay_delay, | ||||
|                     self._start_connector, | ||||
|                     ep, | ||||
|                     description, | ||||
|                     is_relay=True) | ||||
|                 contenders.append(d) | ||||
|             relay_delay += self.RELAY_DELAY | ||||
| 
 | ||||
|  | @ -858,16 +922,18 @@ class Common: | |||
|             raise TransitError("No contenders for connection") | ||||
| 
 | ||||
|         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): | ||||
|         """If the timer fires first, cancel the deferred. If the deferred fires | ||||
|         first, cancel the timer.""" | ||||
|         t = self._reactor.callLater(timeout, d.cancel) | ||||
| 
 | ||||
|         def _done(res): | ||||
|             if t.active(): | ||||
|                 t.cancel() | ||||
|             return res | ||||
| 
 | ||||
|         d.addBoth(_done) | ||||
|         return d | ||||
| 
 | ||||
|  | @ -896,8 +962,8 @@ class Common: | |||
|                     return None | ||||
|             return None | ||||
|         if isinstance(hint, DirectTCPV1Hint): | ||||
|             return endpoints.HostnameEndpoint(self._reactor, | ||||
|                                               hint.hostname, hint.port) | ||||
|             return endpoints.HostnameEndpoint(self._reactor, hint.hostname, | ||||
|                                               hint.port) | ||||
|         return None | ||||
| 
 | ||||
|     def connection_ready(self, p): | ||||
|  | @ -915,9 +981,11 @@ class Common: | |||
|         self._winner = p | ||||
|         return "go" | ||||
| 
 | ||||
| 
 | ||||
| class TransitSender(Common): | ||||
|     is_sender = True | ||||
| 
 | ||||
| 
 | ||||
| class TransitReceiver(Common): | ||||
|     is_sender = False | ||||
| 
 | ||||
|  | @ -926,6 +994,7 @@ class TransitReceiver(Common): | |||
| # 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. | ||||
| 
 | ||||
| 
 | ||||
| @implementer(interfaces.IConsumer) | ||||
| class FileConsumer: | ||||
|     def __init__(self, f, progress=None, hasher=None): | ||||
|  | @ -950,6 +1019,7 @@ class FileConsumer: | |||
|         assert self._producer | ||||
|         self._producer = None | ||||
| 
 | ||||
| 
 | ||||
| # 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 | ||||
| # inbound records? get a Deferred for the next record? The producer/consumer | ||||
|  |  | |||
|  | @ -1,30 +1,42 @@ | |||
| # No unicode_literals | ||||
| import os, json, unicodedata | ||||
| import json | ||||
| import os | ||||
| import unicodedata | ||||
| from binascii import hexlify, unhexlify | ||||
| 
 | ||||
| 
 | ||||
| def to_bytes(u): | ||||
|     return unicodedata.normalize("NFC", u).encode("utf-8") | ||||
| 
 | ||||
| 
 | ||||
| def bytes_to_hexstr(b): | ||||
|     assert isinstance(b, type(b"")) | ||||
|     hexstr = hexlify(b).decode("ascii") | ||||
|     assert isinstance(hexstr, type(u"")) | ||||
|     return hexstr | ||||
| 
 | ||||
| 
 | ||||
| def hexstr_to_bytes(hexstr): | ||||
|     assert isinstance(hexstr, type(u"")) | ||||
|     b = unhexlify(hexstr.encode("ascii")) | ||||
|     assert isinstance(b, type(b"")) | ||||
|     return b | ||||
| 
 | ||||
| 
 | ||||
| def dict_to_bytes(d): | ||||
|     assert isinstance(d, dict) | ||||
|     b = json.dumps(d).encode("utf-8") | ||||
|     assert isinstance(b, type(b"")) | ||||
|     return b | ||||
| 
 | ||||
| 
 | ||||
| def bytes_to_dict(b): | ||||
|     assert isinstance(b, type(b"")) | ||||
|     d = json.loads(b.decode("utf-8")) | ||||
|     assert isinstance(d, dict) | ||||
|     return d | ||||
| 
 | ||||
| 
 | ||||
| def estimate_free_space(target): | ||||
|     # 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 | ||||
|  |  | |||
|  | @ -1,19 +1,25 @@ | |||
| from __future__ import print_function, absolute_import, unicode_literals | ||||
| import os, sys | ||||
| from attr import attrs, attrib | ||||
| from zope.interface import implementer | ||||
| from __future__ import absolute_import, print_function, unicode_literals | ||||
| 
 | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| from attr import attrib, attrs | ||||
| from twisted.python import failure | ||||
| from . import __version__ | ||||
| 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 zope.interface import implementer | ||||
| 
 | ||||
| from ._boss import Boss | ||||
| from ._interfaces import IDeferredWormhole, IWormhole | ||||
| from ._key import derive_key | ||||
| 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: | ||||
| # * Deferreds | ||||
|  | @ -36,6 +42,7 @@ from .util import to_bytes | |||
| #   wormhole(delegate=app, delegate_prefix="wormhole_", | ||||
| #            delegate_args=(args, kwargs)) | ||||
| 
 | ||||
| 
 | ||||
| @attrs | ||||
| @implementer(IWormhole) | ||||
| class _DelegatedWormhole(object): | ||||
|  | @ -51,16 +58,18 @@ class _DelegatedWormhole(object): | |||
| 
 | ||||
|     def allocate_code(self, code_length=2): | ||||
|         self._boss.allocate_code(code_length) | ||||
| 
 | ||||
|     def input_code(self): | ||||
|         return self._boss.input_code() | ||||
| 
 | ||||
|     def set_code(self, code): | ||||
|         self._boss.set_code(code) | ||||
| 
 | ||||
|     ## def serialize(self): | ||||
|     ##     s = {"serialized_wormhole_version": 1, | ||||
|     ##          "boss": self._boss.serialize(), | ||||
|     ##          } | ||||
|     ##     return s | ||||
|     # def serialize(self): | ||||
|     #     s = {"serialized_wormhole_version": 1, | ||||
|     #          "boss": self._boss.serialize(), | ||||
|     #          } | ||||
|     #     return s | ||||
| 
 | ||||
|     def send_message(self, plaintext): | ||||
|         self._boss.send(plaintext) | ||||
|  | @ -72,34 +81,45 @@ class _DelegatedWormhole(object): | |||
|         cannot be called until when_verifier() has fired, nor after close() | ||||
|         was called. | ||||
|         """ | ||||
|         if not isinstance(purpose, type("")): raise TypeError(type(purpose)) | ||||
|         if not self._key: raise NoKeyError() | ||||
|         if not isinstance(purpose, type("")): | ||||
|             raise TypeError(type(purpose)) | ||||
|         if not self._key: | ||||
|             raise NoKeyError() | ||||
|         return derive_key(self._key, to_bytes(purpose), length) | ||||
| 
 | ||||
|     def close(self): | ||||
|         self._boss.close() | ||||
| 
 | ||||
|     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): | ||||
|         self._boss._set_trace(client_name, which, file) | ||||
| 
 | ||||
|     # from below | ||||
|     def got_welcome(self, welcome): | ||||
|         self._delegate.wormhole_got_welcome(welcome) | ||||
| 
 | ||||
|     def got_code(self, code): | ||||
|         self._delegate.wormhole_got_code(code) | ||||
| 
 | ||||
|     def got_key(self, 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): | ||||
|         self._delegate.wormhole_got_verifier(verifier) | ||||
| 
 | ||||
|     def got_versions(self, versions): | ||||
|         self._delegate.wormhole_got_versions(versions) | ||||
| 
 | ||||
|     def received(self, plaintext): | ||||
|         self._delegate.wormhole_got_message(plaintext) | ||||
| 
 | ||||
|     def closed(self, result): | ||||
|         self._delegate.wormhole_closed(result) | ||||
| 
 | ||||
| 
 | ||||
| @implementer(IWormhole, IDeferredWormhole) | ||||
| class _DeferredWormhole(object): | ||||
|     def __init__(self, eq): | ||||
|  | @ -142,8 +162,10 @@ class _DeferredWormhole(object): | |||
| 
 | ||||
|     def allocate_code(self, code_length=2): | ||||
|         self._boss.allocate_code(code_length) | ||||
| 
 | ||||
|     def input_code(self): | ||||
|         return self._boss.input_code() | ||||
| 
 | ||||
|     def set_code(self, code): | ||||
|         self._boss.set_code(code) | ||||
| 
 | ||||
|  | @ -159,20 +181,23 @@ class _DeferredWormhole(object): | |||
|         cannot be called until when_verified() has fired, nor after close() | ||||
|         was called. | ||||
|         """ | ||||
|         if not isinstance(purpose, type("")): raise TypeError(type(purpose)) | ||||
|         if not self._key: raise NoKeyError() | ||||
|         if not isinstance(purpose, type("")): | ||||
|             raise TypeError(type(purpose)) | ||||
|         if not self._key: | ||||
|             raise NoKeyError() | ||||
|         return derive_key(self._key, to_bytes(purpose), length) | ||||
| 
 | ||||
|     def close(self): | ||||
|         # fails with WormholeError unless we established a connection | ||||
|         # (state=="happy"). Fails with WrongPasswordError (a subclass of | ||||
|         # WormholeError) if state=="scary". | ||||
|         d = self._closed_observer.when_fired() # maybe Failure | ||||
|         d = self._closed_observer.when_fired()  # maybe Failure | ||||
|         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 | ||||
| 
 | ||||
|     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", | ||||
|                         file=sys.stderr): | ||||
|         self._boss._set_trace(client_name, which, file) | ||||
|  | @ -180,14 +205,17 @@ class _DeferredWormhole(object): | |||
|     # from below | ||||
|     def got_welcome(self, welcome): | ||||
|         self._welcome_observer.fire_if_not_fired(welcome) | ||||
| 
 | ||||
|     def got_code(self, code): | ||||
|         self._code_observer.fire_if_not_fired(code) | ||||
| 
 | ||||
|     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) | ||||
| 
 | ||||
|     def got_verifier(self, verifier): | ||||
|         self._verifier_observer.fire_if_not_fired(verifier) | ||||
| 
 | ||||
|     def got_versions(self, versions): | ||||
|         self._version_observer.fire_if_not_fired(versions) | ||||
| 
 | ||||
|  | @ -196,7 +224,7 @@ class _DeferredWormhole(object): | |||
| 
 | ||||
|     def closed(self, result): | ||||
|         self._closed = True | ||||
|         #print("closed", result, type(result), file=sys.stderr) | ||||
|         # print("closed", result, type(result), file=sys.stderr) | ||||
|         if isinstance(result, Exception): | ||||
|             # everything pending gets an error, including close() | ||||
|             f = failure.Failure(result) | ||||
|  | @ -215,12 +243,17 @@ class _DeferredWormhole(object): | |||
|         self._received_observer.fire(f) | ||||
| 
 | ||||
| 
 | ||||
| def create(appid, relay_url, reactor, # use keyword args for everything else | ||||
|            versions={}, | ||||
|            delegate=None, journal=None, tor=None, | ||||
|            timing=None, | ||||
|            stderr=sys.stderr, | ||||
|            _eventual_queue=None): | ||||
| def create( | ||||
|         appid, | ||||
|         relay_url, | ||||
|         reactor,  # use keyword args for everything else | ||||
|         versions={}, | ||||
|         delegate=None, | ||||
|         journal=None, | ||||
|         tor=None, | ||||
|         timing=None, | ||||
|         stderr=sys.stderr, | ||||
|         _eventual_queue=None): | ||||
|     timing = timing or DebugTiming() | ||||
|     side = bytes_to_hexstr(os.urandom(5)) | ||||
|     journal = journal or ImmediateJournal() | ||||
|  | @ -229,27 +262,28 @@ def create(appid, relay_url, reactor, # use keyword args for everything else | |||
|         w = _DelegatedWormhole(delegate) | ||||
|     else: | ||||
|         w = _DeferredWormhole(eq) | ||||
|     wormhole_versions = {} # will be used to indicate Wormhole capabilities | ||||
|     wormhole_versions["app_versions"] = versions # app-specific capabilities | ||||
|     wormhole_versions = {}  # will be used to indicate Wormhole capabilities | ||||
|     wormhole_versions["app_versions"] = versions  # app-specific capabilities | ||||
|     v = __version__ | ||||
|     if isinstance(v, type(b"")): | ||||
|         v = v.decode("utf-8", errors="replace") | ||||
|     client_version = ("python", v) | ||||
|     b = Boss(w, side, relay_url, appid, wormhole_versions, client_version, | ||||
|                 reactor, journal, tor, timing) | ||||
|              reactor, journal, tor, timing) | ||||
|     w._set_boss(b) | ||||
|     b.start() | ||||
|     return w | ||||
| 
 | ||||
| ## def from_serialized(serialized, reactor, delegate, | ||||
| ##                     journal=None, tor=None, | ||||
| ##                     timing=None, stderr=sys.stderr): | ||||
| ##     assert serialized["serialized_wormhole_version"] == 1 | ||||
| ##     timing = timing or DebugTiming() | ||||
| ##     w = _DelegatedWormhole(delegate) | ||||
| ##     # now unpack state machines, including the SPAKE2 in Key | ||||
| ##     b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing) | ||||
| ##     w._set_boss(b) | ||||
| ##     b.start() # ?? | ||||
| ##     raise NotImplemented | ||||
| ##     # should the new Wormhole call got_code? only if it wasn't called before. | ||||
| 
 | ||||
| # def from_serialized(serialized, reactor, delegate, | ||||
| #                     journal=None, tor=None, | ||||
| #                     timing=None, stderr=sys.stderr): | ||||
| #     assert serialized["serialized_wormhole_version"] == 1 | ||||
| #     timing = timing or DebugTiming() | ||||
| #     w = _DelegatedWormhole(delegate) | ||||
| #     # now unpack state machines, including the SPAKE2 in Key | ||||
| #     b = Boss.from_serialized(w, serialized["boss"], reactor, journal, timing) | ||||
| #     w._set_boss(b) | ||||
| #     b.start() # ?? | ||||
| #     raise NotImplemented | ||||
| #     # should the new Wormhole call got_code? only if it wasn't called before. | ||||
|  |  | |||
|  | @ -1,12 +1,19 @@ | |||
| import json | ||||
| 
 | ||||
| from twisted.internet.defer import inlineCallbacks, returnValue | ||||
| 
 | ||||
| from . import wormhole | ||||
| from .tor_manager import get_tor | ||||
| 
 | ||||
| 
 | ||||
| @inlineCallbacks | ||||
| def receive(reactor, appid, relay_url, code, | ||||
|             use_tor=False, launch_tor=False, tor_control_port=None, | ||||
| def receive(reactor, | ||||
|             appid, | ||||
|             relay_url, | ||||
|             code, | ||||
|             use_tor=False, | ||||
|             launch_tor=False, | ||||
|             tor_control_port=None, | ||||
|             on_code=None): | ||||
|     """ | ||||
|     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 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 | ||||
|     """ | ||||
|     tor = None | ||||
|  | @ -48,27 +57,33 @@ def receive(reactor, appid, relay_url, code, | |||
|     data = json.loads(data.decode("utf-8")) | ||||
|     offer = data.get('offer', None) | ||||
|     if not offer: | ||||
|         raise Exception( | ||||
|             "Do not understand response: {}".format(data) | ||||
|         ) | ||||
|         raise Exception("Do not understand response: {}".format(data)) | ||||
|     msg = None | ||||
|     if 'message' in offer: | ||||
|         msg = offer['message'] | ||||
|         wh.send_message(json.dumps({"answer": | ||||
|                                     {"message_ack": "ok"}}).encode("utf-8")) | ||||
|         wh.send_message( | ||||
|             json.dumps({ | ||||
|                 "answer": { | ||||
|                     "message_ack": "ok" | ||||
|                 } | ||||
|             }).encode("utf-8")) | ||||
| 
 | ||||
|     else: | ||||
|         raise Exception( | ||||
|             "Unknown offer type: {}".format(offer.keys()) | ||||
|         ) | ||||
|         raise Exception("Unknown offer type: {}".format(offer.keys())) | ||||
| 
 | ||||
|     yield wh.close() | ||||
|     returnValue(msg) | ||||
| 
 | ||||
| 
 | ||||
| @inlineCallbacks | ||||
| def send(reactor, appid, relay_url, data, code, | ||||
|          use_tor=False, launch_tor=False, tor_control_port=None, | ||||
| def send(reactor, | ||||
|          appid, | ||||
|          relay_url, | ||||
|          data, | ||||
|          code, | ||||
|          use_tor=False, | ||||
|          launch_tor=False, | ||||
|          tor_control_port=None, | ||||
|          on_code=None): | ||||
|     """ | ||||
|     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 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 | ||||
|     """ | ||||
|     tor = None | ||||
|  | @ -104,13 +122,7 @@ def send(reactor, appid, relay_url, data, code, | |||
|     if on_code: | ||||
|         on_code(code) | ||||
| 
 | ||||
|     wh.send_message( | ||||
|         json.dumps({ | ||||
|             "offer": { | ||||
|                 "message": data | ||||
|             } | ||||
|         }).encode("utf-8") | ||||
|     ) | ||||
|     wh.send_message(json.dumps({"offer": {"message": data}}).encode("utf-8")) | ||||
|     data = yield wh.get_message() | ||||
|     data = json.loads(data.decode("utf-8")) | ||||
|     answer = data.get('answer', None) | ||||
|  | @ -118,6 +130,4 @@ def send(reactor, appid, relay_url, data, code, | |||
|     if answer: | ||||
|         returnValue(None) | ||||
|     else: | ||||
|         raise Exception( | ||||
|             "Unknown answer: {}".format(data) | ||||
|         ) | ||||
|         raise Exception("Unknown answer: {}".format(data)) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user