Example #1
0
class LightSwitch(object):
    machine = MethodicalMachine()

    @machine.state(serialized="on")
    def on_state(self):
        "the switch is on"

    @machine.state(serialized="off", initial=True)
    def off_state(self):
        "the switch is off"

    @machine.input()
    def flip(self):
        "flip the switch"

    on_state.upon(flip, enter=off_state, outputs=[])
    off_state.upon(flip, enter=on_state, outputs=[])

    @machine.input()
    def query_power(self):
        "return True if powered, False otherwise"

    @machine.output()
    def _is_powered(self):
        return True

    @machine.output()
    def _not_powered(self):
        return False

    on_state.upon(query_power,
                  enter=on_state,
                  outputs=[_is_powered],
                  collector=next)
    off_state.upon(query_power,
                   enter=off_state,
                   outputs=[_not_powered],
                   collector=next)

    @machine.serializer()
    def save(self, state):
        return {"is-it-on": state}

    @machine.unserializer()
    def _restore(self, blob):
        return blob["is-it-on"]

    @classmethod
    def from_blob(cls, blob):
        self = cls()
        self._restore(blob)
        return self
Example #2
0
class Led(object):
    _machine = MethodicalMachine()

    @_machine.state()
    def led_on(self):
        "led is on"

    @_machine.state(initial=True)
    def led_off(self):
        "led is off"

    @_machine.input()
    def turn_on(self):
        "turn the led on"

    @_machine.output()
    def _light(self):
        print("light")

    led_off.upon(turn_on, enter=led_on, outputs=[_light])
Example #3
0
class AnotherMachine(object):
    _machine = MethodicalMachine()

    @_machine.state(initial=True)
    def state1(self):
        pass

    @_machine.state()
    def state2(self):
        pass

    @_machine.state()
    def state3(self):
        pass

    @_machine.input()
    def input1(self):
        pass

    @_machine.input()
    def input2(self):
        pass

    @_machine.input()
    def input3(self):
        pass

    @_machine.output()
    def _move_to_state2(self):
        print('called2')
        self.input2()

    @_machine.output()
    def on_state3(self):
        print('called3')

    state1.upon(input1, enter=state2, outputs=[_move_to_state2])
    state2.upon(input2, enter=state3, outputs=[on_state3])
class CoffeeBrewer(object):
    _machine = MethodicalMachine()

    @_machine.input()
    def brew_button(self):
        "The user pressed the 'brew' button."

    @_machine.output()
    def _heat_the_heating_element(self):
        "Heat up the heating element, which should cause coffee to happen."
        # self._heating_element.turn_on()
    @_machine.state()
    def have_beans(self):
        "In this state, you have some beans."

    @_machine.state(initial=True)
    def dont_have_beans(self):
        "In this state, you don't have any beans."

    @_machine.input()
    def put_in_beans(self, beans):
        "The user put in some beans."

    @_machine.output()
    def _save_beans(self, beans):
        "The beans are now in the machine; save them."
        self._beans = beans

    @_machine.output()
    def _describe_coffee(self):
        return "A cup of coffee made with {}.".format(self._beans)

    dont_have_beans.upon(put_in_beans, enter=have_beans, outputs=[_save_beans])
    have_beans.upon(brew_button,
                    enter=dont_have_beans,
                    outputs=[_heat_the_heating_element, _describe_coffee],
                    collector=lambda iterable: list(iterable)[-1])
class Turnstile(object):
    machine = MethodicalMachine()

    def __init__(self, lock):
        self.lock = lock

    @machine.input()
    def arm_turned(self):
        "The arm was turned."

    @machine.input()
    def fare_paid(self):
        "The fare was paid."

    @machine.output()
    def _engage_lock(self):
        self.lock.engage()

    @machine.output()
    def _disengage_lock(self):
        self.lock.disengage()

    @machine.output()
    def _nope(self):
        print("**Clunk!**  The turnstile doesn't move.")

    @machine.state(initial=True)
    def _locked(self):
        "The turnstile is locked."

    @machine.state()
    def _unlocked(self):
        "The turnstile is unlocked."

    _locked.upon(fare_paid, enter=_unlocked, outputs=[_disengage_lock])
    _unlocked.upon(arm_turned, enter=_locked, outputs=[_engage_lock])
    _locked.upon(arm_turned, enter=_locked, outputs=[_nope])
Example #6
0
class _Record(object):
    _framer = attrib(validator=provides(IFramer))
    _noise = attrib()
    _role = attrib(default="unspecified", validator=_is_role)  # for debugging

    n = MethodicalMachine()

    # TODO: set_trace

    def __attrs_post_init__(self):
        self._noise.start_handshake()

    # in: role=
    # in: prologue_received, frame_received
    # out: handshake_received, record_received
    # out: transport.write (noise handshake, encrypted records)
    # states: want_prologue, want_handshake, want_record

    @n.state(initial=True)
    def no_role_set(self):
        pass  # pragma: no cover

    @n.state()
    def want_prologue_leader(self):
        pass  # pragma: no cover

    @n.state()
    def want_prologue_follower(self):
        pass  # pragma: no cover

    @n.state()
    def want_handshake_leader(self):
        pass  # pragma: no cover

    @n.state()
    def want_handshake_follower(self):
        pass  # pragma: no cover

    @n.state()
    def want_message(self):
        pass  # pragma: no cover

    @n.input()
    def set_role_leader(self):
        pass

    @n.input()
    def set_role_follower(self):
        pass

    @n.input()
    def got_prologue(self):
        pass

    @n.input()
    def got_frame(self, frame):
        pass

    @n.output()
    def ignore_and_send_handshake(self, frame):
        self._send_handshake()

    @n.output()
    def send_handshake(self):
        self._send_handshake()

    def _send_handshake(self):
        try:
            handshake = self._noise.write_message(
            )  # generate the ephemeral key
        except NoiseHandshakeError as e:
            log.err(e, "noise error during handshake")
            raise
        self._framer.send_frame(handshake)

    @n.output()
    def process_handshake(self, frame):
        try:
            payload = self._noise.read_message(frame)
            # Noise can include unencrypted data in the handshake, but we don't
            # use it
            del payload
        except NoiseInvalidMessage as e:
            log.err(e, "bad inbound noise handshake")
            raise Disconnect()
        return Handshake()

    @n.output()
    def decrypt_message(self, frame):
        try:
            message = self._noise.decrypt(frame)
        except NoiseInvalidMessage as e:
            # if this happens during tests, flunk the test
            log.err(e, "bad inbound noise frame")
            raise Disconnect()
        return parse_record(message)

    no_role_set.upon(set_role_leader, outputs=[], enter=want_prologue_leader)
    want_prologue_leader.upon(got_prologue,
                              outputs=[send_handshake],
                              enter=want_handshake_leader)
    want_handshake_leader.upon(got_frame,
                               outputs=[process_handshake],
                               collector=first,
                               enter=want_message)

    no_role_set.upon(set_role_follower,
                     outputs=[],
                     enter=want_prologue_follower)
    want_prologue_follower.upon(got_prologue,
                                outputs=[],
                                enter=want_handshake_follower)
    want_handshake_follower.upon(
        got_frame,
        outputs=[process_handshake, ignore_and_send_handshake],
        collector=first,
        enter=want_message)

    want_message.upon(got_frame,
                      outputs=[decrypt_message],
                      collector=first,
                      enter=want_message)

    # external API is: connectionMade, dataReceived, send_record

    def connectionMade(self):
        self._framer.connectionMade()

    def add_and_unframe(self, data):
        for token in self._framer.add_and_parse(data):
            if isinstance(token, Prologue):
                self.got_prologue()  # triggers send_handshake
            else:
                assert isinstance(token, Frame)
                yield self.got_frame(token.frame)  # Handshake or a Record type

    def send_record(self, r):
        message = encode_record(r)
        frame = self._noise.encrypt(message)
        self._framer.send_frame(frame)
Example #7
0
class Terminator(object):
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __init__(self):
        self._mood = None

    def wire(self, boss, rendezvous_connector, nameplate, mailbox):
        self._B = _interfaces.IBoss(boss)
        self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
        self._N = _interfaces.INameplate(nameplate)
        self._M = _interfaces.IMailbox(mailbox)

    # 4*2-1 main states:
    # (nm, m, n, 0): nameplate and/or mailbox is active
    # (o, ""): open (not-yet-closing), or trying to close
    # S0 is special: we don't hang out in it

    # TODO: rename o to 0, "" to 1. "S1" is special/terminal
    # so S0nm/S0n/S0m/S0, S1nm/S1n/S1m/(S1)

    # We start in Snmo (non-closing). When both nameplate and mailboxes are
    # done, and we're closing, then we stop the RendezvousConnector

    @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

    @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

    # from Nameplate
    @m.input()
    def nameplate_done(self):
        pass

    # from Mailbox
    @m.input()
    def mailbox_done(self):
        pass

    # from RendezvousConnector
    @m.input()
    def stopped(self):
        pass

    @m.output()
    def close_nameplate(self, mood):
        self._N.close()  # ignores mood

    @m.output()
    def close_mailbox(self, mood):
        self._M.close(mood)

    @m.output()
    def ignore_mood_and_RC_stop(self, mood):
        self._RC.stop()

    @m.output()
    def RC_stop(self):
        self._RC.stop()

    @m.output()
    def B_closed(self):
        self._B.closed()

    Snmo.upon(mailbox_done, enter=Sno, outputs=[])
    Snmo.upon(close, enter=Snm, outputs=[close_nameplate, close_mailbox])
    Snmo.upon(nameplate_done, enter=Smo, outputs=[])

    Sno.upon(close, enter=Sn, outputs=[close_nameplate, close_mailbox])
    Sno.upon(nameplate_done, enter=S0o, outputs=[])

    Smo.upon(close, enter=Sm, outputs=[close_nameplate, close_mailbox])
    Smo.upon(mailbox_done, enter=S0o, outputs=[])

    Snm.upon(mailbox_done, enter=Sn, outputs=[])
    Snm.upon(nameplate_done, enter=Sm, outputs=[])

    Sn.upon(nameplate_done, enter=S_stopping, outputs=[RC_stop])
    S0o.upon(
        close,
        enter=S_stopping,
        outputs=[close_nameplate, close_mailbox, ignore_mood_and_RC_stop])
    Sm.upon(mailbox_done, enter=S_stopping, outputs=[RC_stop])

    S_stopping.upon(stopped, enter=S_stopped, outputs=[B_closed])
Example #8
0
class _ClientMachine(object):
    """
    State machine for maintaining a single outgoing connection to an endpoint.

    @see: L{ClientService}
    """

    _machine = MethodicalMachine()

    def __init__(self, endpoint, factory, retryPolicy, clock, log):
        """
        @see: L{ClientService.__init__}

        @param log: The logger for the L{ClientService} instance this state
            machine is associated to.
        @type log: L{Logger}
        """
        self._endpoint = endpoint
        self._failedAttempts = 0
        self._stopped = False
        self._factory = factory
        self._timeoutForAttempt = retryPolicy
        self._clock = clock
        self._connectionInProgress = succeed(None)

        self._awaitingConnected = []

        self._stopWaiters = []
        self._log = log

    @_machine.state(initial=True)
    def _init(self):
        """
        The service has not been started.
        """

    @_machine.state()
    def _connecting(self):
        """
        The service has started connecting.
        """

    @_machine.state()
    def _waiting(self):
        """
        The service is waiting for the reconnection period
        before reconnecting.
        """

    @_machine.state()
    def _connected(self):
        """
        The service is connected.
        """

    @_machine.state()
    def _disconnecting(self):
        """
        The service is disconnecting after being asked to shutdown.
        """

    @_machine.state()
    def _restarting(self):
        """
        The service is disconnecting and has been asked to restart.
        """

    @_machine.state()
    def _stopped(self):
        """
        The service has been stopped an is disconnected.
        """

    @_machine.input()
    def start(self):
        """
        Start this L{ClientService}, initiating the connection retry loop.
        """

    @_machine.output()
    def _connect(self):
        """
        Start a connection attempt.
        """
        factoryProxy = _DisconnectFactory(self._factory,
                                          lambda _: self._clientDisconnected())

        self._connectionInProgress = (
            self._endpoint.connect(factoryProxy).addCallback(
                self._connectionMade).addErrback(
                    lambda _: self._connectionFailed()))

    @_machine.output()
    def _resetFailedAttempts(self):
        """
        Reset the number of failed attempts.
        """
        self._failedAttempts = 0

    @_machine.input()
    def stop(self):
        """
        Stop trying to connect and disconnect any current connection.

        @return: a L{Deferred} that fires when all outstanding connections are
            closed and all in-progress connection attempts halted.
        """

    @_machine.output()
    def _waitForStop(self):
        """
        Return a deferred that will fire when the service has finished
        disconnecting.

        @return: L{Deferred} that fires when the service has finished
            disconnecting.
        """
        self._stopWaiters.append(Deferred())
        return self._stopWaiters[-1]

    @_machine.output()
    def _stopConnecting(self):
        """
        Stop pending connection attempt.
        """
        self._connectionInProgress.cancel()

    @_machine.output()
    def _stopRetrying(self):
        """
        Stop pending attempt to reconnect.
        """
        self._retryCall.cancel()
        del self._retryCall

    @_machine.output()
    def _disconnect(self):
        """
        Disconnect the current connection.
        """
        self._currentConnection.transport.loseConnection()

    @_machine.input()
    def _connectionMade(self, protocol):
        """
        A connection has been made.

        @param protocol: The protocol of the connection.
        @type protocol: L{IProtocol}
        """

    @_machine.output()
    def _notifyWaiters(self, protocol):
        """
        Notify all pending requests for a connection that a connection has been
        made.

        @param protocol: The protocol of the connection.
        @type protocol: L{IProtocol}
        """
        # This should be in _resetFailedAttempts but the signature doesn't
        # match.
        self._failedAttempts = 0

        self._currentConnection = protocol._protocol
        self._unawait(self._currentConnection)

    @_machine.input()
    def _connectionFailed(self):
        """
        The current connection attempt failed.
        """

    @_machine.output()
    def _wait(self):
        """
        Schedule a retry attempt.
        """
        self._failedAttempts += 1
        delay = self._timeoutForAttempt(self._failedAttempts)
        self._log.info(
            "Scheduling retry {attempt} to connect {endpoint} "
            "in {delay} seconds.",
            attempt=self._failedAttempts,
            endpoint=self._endpoint,
            delay=delay)
        self._retryCall = self._clock.callLater(delay, self._reconnect)

    @_machine.input()
    def _reconnect(self):
        """
        The wait between connection attempts is done.
        """

    @_machine.input()
    def _clientDisconnected(self):
        """
        The current connection has been disconnected.
        """

    @_machine.output()
    def _forgetConnection(self):
        """
        Forget the current connection.
        """
        del self._currentConnection

    @_machine.output()
    def _cancelConnectWaiters(self):
        """
        Notify all pending requests for a connection that no more connections
        are expected.
        """
        self._unawait(Failure(CancelledError()))

    @_machine.output()
    def _finishStopping(self):
        """
        Notify all deferreds waiting on the service stopping.
        """
        self._stopWaiters, waiting = [], self._stopWaiters
        for w in waiting:
            w.callback(None)

    @_machine.input()
    def whenConnected(self):
        """
        Retrieve the currently-connected L{Protocol}, or the next one to
        connect.

        @return: a Deferred that fires with a protocol produced by the factory
            passed to C{__init__}
        @rtype: L{Deferred} firing with L{IProtocol} or failing with
            L{CancelledError} the service is stopped.
        """

    @_machine.output()
    def _currentConnection(self):
        """
        Return the currently connected protocol.

        @return: L{Deferred} that is fired with currently connected protocol.
        """
        return succeed(self._currentConnection)

    @_machine.output()
    def _noConnection(self):
        """
        Notify the caller that no connection is expected.

        @return: L{Deferred} that is fired with L{CancelledError}.
        """
        return fail(CancelledError())

    @_machine.output()
    def _awaitingConnection(self):
        """
        Return a deferred that will fire with the next connected protocol.

        @return: L{Deferred} that will fire with the next connected protocol.
        """
        result = Deferred()
        self._awaitingConnected.append(result)
        return result

    @_machine.output()
    def _deferredSucceededWithNone(self):
        """
        Return a deferred that has already fired with L{None}.

        @return: A L{Deferred} that has already fired with L{None}.
        """
        return succeed(None)

    def _unawait(self, value):
        """
        Fire all outstanding L{ClientService.whenConnected} L{Deferred}s.

        @param value: the value to fire the L{Deferred}s with.
        """
        self._awaitingConnected, waiting = [], self._awaitingConnected
        for w in waiting:
            w.callback(value)

    # State Transitions

    _init.upon(start, enter=_connecting, outputs=[_connect])
    _init.upon(stop,
               enter=_stopped,
               outputs=[_deferredSucceededWithNone],
               collector=_firstResult)

    _connecting.upon(start, enter=_connecting, outputs=[])
    # Note that this synchonously triggers _connectionFailed in the
    # _disconnecting state.
    _connecting.upon(stop,
                     enter=_disconnecting,
                     outputs=[_waitForStop, _stopConnecting],
                     collector=_firstResult)
    _connecting.upon(_connectionMade,
                     enter=_connected,
                     outputs=[_notifyWaiters])
    _connecting.upon(_connectionFailed, enter=_waiting, outputs=[_wait])

    _waiting.upon(start, enter=_waiting, outputs=[])
    _waiting.upon(stop,
                  enter=_stopped,
                  outputs=[
                      _waitForStop, _cancelConnectWaiters, _stopRetrying,
                      _finishStopping
                  ],
                  collector=_firstResult)
    _waiting.upon(_reconnect, enter=_connecting, outputs=[_connect])

    _connected.upon(start, enter=_connected, outputs=[])
    _connected.upon(stop,
                    enter=_disconnecting,
                    outputs=[_waitForStop, _disconnect],
                    collector=_firstResult)
    _connected.upon(_clientDisconnected,
                    enter=_waiting,
                    outputs=[_forgetConnection, _wait])

    _disconnecting.upon(start,
                        enter=_restarting,
                        outputs=[_resetFailedAttempts])
    _disconnecting.upon(stop,
                        enter=_disconnecting,
                        outputs=[_waitForStop],
                        collector=_firstResult)
    _disconnecting.upon(
        _clientDisconnected,
        enter=_stopped,
        outputs=[_cancelConnectWaiters, _finishStopping, _forgetConnection])
    # Note that this is triggered synchonously with the transition from
    # _connecting
    _disconnecting.upon(_connectionFailed,
                        enter=_stopped,
                        outputs=[_cancelConnectWaiters, _finishStopping])

    _restarting.upon(start, enter=_restarting, outputs=[])
    _restarting.upon(stop,
                     enter=_disconnecting,
                     outputs=[_waitForStop],
                     collector=_firstResult)
    _restarting.upon(_clientDisconnected,
                     enter=_connecting,
                     outputs=[_finishStopping, _connect])

    _stopped.upon(start, enter=_connecting, outputs=[_connect])
    _stopped.upon(stop,
                  enter=_stopped,
                  outputs=[_deferredSucceededWithNone],
                  collector=_firstResult)

    _init.upon(whenConnected,
               enter=_init,
               outputs=[_awaitingConnection],
               collector=_firstResult)
    _connecting.upon(whenConnected,
                     enter=_connecting,
                     outputs=[_awaitingConnection],
                     collector=_firstResult)
    _waiting.upon(whenConnected,
                  enter=_waiting,
                  outputs=[_awaitingConnection],
                  collector=_firstResult)
    _connected.upon(whenConnected,
                    enter=_connected,
                    outputs=[_currentConnection],
                    collector=_firstResult)
    _disconnecting.upon(whenConnected,
                        enter=_disconnecting,
                        outputs=[_awaitingConnection],
                        collector=_firstResult)
    _restarting.upon(whenConnected,
                     enter=_restarting,
                     outputs=[_awaitingConnection],
                     collector=_firstResult)
    _stopped.upon(whenConnected,
                  enter=_stopped,
                  outputs=[_noConnection],
                  collector=_firstResult)
Example #9
0
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

    def __attrs_post_init__(self):
        self._key = None

    def wire(self, boss, send):
        self._B = _interfaces.IBoss(boss)
        self._S = _interfaces.ISend(send)

    @m.state(initial=True)
    def S0_unknown_key(self):
        pass  # pragma: no cover

    @m.state()
    def S1_unverified_key(self):
        pass  # pragma: no cover

    @m.state()
    def S2_verified_key(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def S3_scared(self):
        pass  # pragma: no cover

    # from Ordering
    def got_message(self, side, phase, body):
        assert isinstance(side, type("")), type(phase)
        assert isinstance(phase, type("")), type(phase)
        assert isinstance(body, type(b"")), type(body)
        assert self._key
        data_key = derive_phase_key(self._key, side, phase)
        try:
            plaintext = decrypt_data(data_key, body)
        except CryptoError:
            self.got_message_bad()
            return
        self.got_message_good(phase, plaintext)

    @m.input()
    def got_message_good(self, phase, plaintext):
        pass

    @m.input()
    def got_message_bad(self):
        pass

    # from Key
    @m.input()
    def got_key(self, key):
        pass

    @m.output()
    def record_key(self, key):
        self._key = key

    @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])
    S3_scared.upon(got_message_good, enter=S3_scared, outputs=[])
    S3_scared.upon(got_message_bad, enter=S3_scared, outputs=[])
Example #10
0
class _ClientMachine:
    """
    State machine for maintaining a single outgoing connection to an endpoint.

    @see: L{ClientService}
    """

    _machine = MethodicalMachine()

    def __init__(self, endpoint, factory, retryPolicy, clock,
                 prepareConnection, log):
        """
        @see: L{ClientService.__init__}

        @param log: The logger for the L{ClientService} instance this state
            machine is associated to.
        @type log: L{Logger}

        @ivar _awaitingConnected: notifications to make when connection
            succeeds, fails, or is cancelled
        @type _awaitingConnected: list of (Deferred, count) tuples
        """
        self._endpoint = endpoint
        self._failedAttempts = 0
        self._stopped = False
        self._factory = factory
        self._timeoutForAttempt = retryPolicy
        self._clock = clock
        self._prepareConnection = prepareConnection
        self._connectionInProgress = succeed(None)

        self._awaitingConnected = []

        self._stopWaiters = []
        self._log = log

    @_machine.state(initial=True)
    def _init(self):
        """
        The service has not been started.
        """

    @_machine.state()
    def _connecting(self):
        """
        The service has started connecting.
        """

    @_machine.state()
    def _waiting(self):
        """
        The service is waiting for the reconnection period
        before reconnecting.
        """

    @_machine.state()
    def _connected(self):
        """
        The service is connected.
        """

    @_machine.state()
    def _disconnecting(self):
        """
        The service is disconnecting after being asked to shutdown.
        """

    @_machine.state()
    def _restarting(self):
        """
        The service is disconnecting and has been asked to restart.
        """

    @_machine.state()
    def _stopped(self):
        """
        The service has been stopped and is disconnected.
        """

    @_machine.input()
    def start(self):
        """
        Start this L{ClientService}, initiating the connection retry loop.
        """

    @_machine.output()
    def _connect(self):
        """
        Start a connection attempt.
        """
        factoryProxy = _DisconnectFactory(self._factory,
                                          lambda _: self._clientDisconnected())

        self._connectionInProgress = (
            self._endpoint.connect(factoryProxy).addCallback(
                self._runPrepareConnection).addCallback(
                    self._connectionMade).addErrback(self._connectionFailed))

    def _runPrepareConnection(self, protocol):
        """
        Run any C{prepareConnection} callback with the connected protocol,
        ignoring its return value but propagating any failure.

        @param protocol: The protocol of the connection.
        @type protocol: L{IProtocol}

        @return: Either:

            - A L{Deferred} that succeeds with the protocol when the
              C{prepareConnection} callback has executed successfully.

            - A L{Deferred} that fails when the C{prepareConnection} callback
              throws or returns a failed L{Deferred}.

            - The protocol, when no C{prepareConnection} callback is defined.
        """
        if self._prepareConnection:
            return maybeDeferred(self._prepareConnection,
                                 protocol).addCallback(lambda _: protocol)
        return protocol

    @_machine.output()
    def _resetFailedAttempts(self):
        """
        Reset the number of failed attempts.
        """
        self._failedAttempts = 0

    @_machine.input()
    def stop(self):
        """
        Stop trying to connect and disconnect any current connection.

        @return: a L{Deferred} that fires when all outstanding connections are
            closed and all in-progress connection attempts halted.
        """

    @_machine.output()
    def _waitForStop(self):
        """
        Return a deferred that will fire when the service has finished
        disconnecting.

        @return: L{Deferred} that fires when the service has finished
            disconnecting.
        """
        self._stopWaiters.append(Deferred())
        return self._stopWaiters[-1]

    @_machine.output()
    def _stopConnecting(self):
        """
        Stop pending connection attempt.
        """
        self._connectionInProgress.cancel()

    @_machine.output()
    def _stopRetrying(self):
        """
        Stop pending attempt to reconnect.
        """
        self._retryCall.cancel()
        del self._retryCall

    @_machine.output()
    def _disconnect(self):
        """
        Disconnect the current connection.
        """
        self._currentConnection.transport.loseConnection()

    @_machine.input()
    def _connectionMade(self, protocol):
        """
        A connection has been made.

        @param protocol: The protocol of the connection.
        @type protocol: L{IProtocol}
        """

    @_machine.output()
    def _notifyWaiters(self, protocol):
        """
        Notify all pending requests for a connection that a connection has been
        made.

        @param protocol: The protocol of the connection.
        @type protocol: L{IProtocol}
        """
        # This should be in _resetFailedAttempts but the signature doesn't
        # match.
        self._failedAttempts = 0

        self._currentConnection = protocol._protocol
        self._unawait(self._currentConnection)

    @_machine.input()
    def _connectionFailed(self, f):
        """
        The current connection attempt failed.
        """

    @_machine.output()
    def _wait(self):
        """
        Schedule a retry attempt.
        """
        self._doWait()

    @_machine.output()
    def _ignoreAndWait(self, f):
        """
        Schedule a retry attempt, and ignore the Failure passed in.
        """
        return self._doWait()

    def _doWait(self):
        self._failedAttempts += 1
        delay = self._timeoutForAttempt(self._failedAttempts)
        self._log.info(
            "Scheduling retry {attempt} to connect {endpoint} "
            "in {delay} seconds.",
            attempt=self._failedAttempts,
            endpoint=self._endpoint,
            delay=delay,
        )
        self._retryCall = self._clock.callLater(delay, self._reconnect)

    @_machine.input()
    def _reconnect(self):
        """
        The wait between connection attempts is done.
        """

    @_machine.input()
    def _clientDisconnected(self):
        """
        The current connection has been disconnected.
        """

    @_machine.output()
    def _forgetConnection(self):
        """
        Forget the current connection.
        """
        del self._currentConnection

    @_machine.output()
    def _cancelConnectWaiters(self):
        """
        Notify all pending requests for a connection that no more connections
        are expected.
        """
        self._unawait(Failure(CancelledError()))

    @_machine.output()
    def _ignoreAndCancelConnectWaiters(self, f):
        """
        Notify all pending requests for a connection that no more connections
        are expected, after ignoring the Failure passed in.
        """
        self._unawait(Failure(CancelledError()))

    @_machine.output()
    def _finishStopping(self):
        """
        Notify all deferreds waiting on the service stopping.
        """
        self._doFinishStopping()

    @_machine.output()
    def _ignoreAndFinishStopping(self, f):
        """
        Notify all deferreds waiting on the service stopping, and ignore the
        Failure passed in.
        """
        self._doFinishStopping()

    def _doFinishStopping(self):
        self._stopWaiters, waiting = [], self._stopWaiters
        for w in waiting:
            w.callback(None)

    @_machine.input()
    def whenConnected(self, failAfterFailures=None):
        """
        Retrieve the currently-connected L{Protocol}, or the next one to
        connect.

        @param failAfterFailures: number of connection failures after which
            the Deferred will deliver a Failure (None means the Deferred will
            only fail if/when the service is stopped).  Set this to 1 to make
            the very first connection failure signal an error.  Use 2 to
            allow one failure but signal an error if the subsequent retry
            then fails.
        @type failAfterFailures: L{int} or None

        @return: a Deferred that fires with a protocol produced by the
            factory passed to C{__init__}
        @rtype: L{Deferred} that may:

            - fire with L{IProtocol}

            - fail with L{CancelledError} when the service is stopped

            - fail with e.g.
              L{DNSLookupError<twisted.internet.error.DNSLookupError>} or
              L{ConnectionRefusedError<twisted.internet.error.ConnectionRefusedError>}
              when the number of consecutive failed connection attempts
              equals the value of "failAfterFailures"
        """

    @_machine.output()
    def _currentConnection(self, failAfterFailures=None):
        """
        Return the currently connected protocol.

        @return: L{Deferred} that is fired with currently connected protocol.
        """
        return succeed(self._currentConnection)

    @_machine.output()
    def _noConnection(self, failAfterFailures=None):
        """
        Notify the caller that no connection is expected.

        @return: L{Deferred} that is fired with L{CancelledError}.
        """
        return fail(CancelledError())

    @_machine.output()
    def _awaitingConnection(self, failAfterFailures=None):
        """
        Return a deferred that will fire with the next connected protocol.

        @return: L{Deferred} that will fire with the next connected protocol.
        """
        result = Deferred()
        self._awaitingConnected.append((result, failAfterFailures))
        return result

    @_machine.output()
    def _deferredSucceededWithNone(self):
        """
        Return a deferred that has already fired with L{None}.

        @return: A L{Deferred} that has already fired with L{None}.
        """
        return succeed(None)

    def _unawait(self, value):
        """
        Fire all outstanding L{ClientService.whenConnected} L{Deferred}s.

        @param value: the value to fire the L{Deferred}s with.
        """
        self._awaitingConnected, waiting = [], self._awaitingConnected
        for (w, remaining) in waiting:
            w.callback(value)

    @_machine.output()
    def _deliverConnectionFailure(self, f):
        """
        Deliver connection failures to any L{ClientService.whenConnected}
        L{Deferred}s that have met their failAfterFailures threshold.

        @param f: the Failure to fire the L{Deferred}s with.
        """
        ready = []
        notReady = []
        for (w, remaining) in self._awaitingConnected:
            if remaining is None:
                notReady.append((w, remaining))
            elif remaining <= 1:
                ready.append(w)
            else:
                notReady.append((w, remaining - 1))
        self._awaitingConnected = notReady
        for w in ready:
            w.callback(f)

    # State Transitions

    _init.upon(start, enter=_connecting, outputs=[_connect])
    _init.upon(
        stop,
        enter=_stopped,
        outputs=[_deferredSucceededWithNone],
        collector=_firstResult,
    )

    _connecting.upon(start, enter=_connecting, outputs=[])
    # Note that this synchonously triggers _connectionFailed in the
    # _disconnecting state.
    _connecting.upon(
        stop,
        enter=_disconnecting,
        outputs=[_waitForStop, _stopConnecting],
        collector=_firstResult,
    )
    _connecting.upon(_connectionMade,
                     enter=_connected,
                     outputs=[_notifyWaiters])
    _connecting.upon(
        _connectionFailed,
        enter=_waiting,
        outputs=[_ignoreAndWait, _deliverConnectionFailure],
    )

    _waiting.upon(start, enter=_waiting, outputs=[])
    _waiting.upon(
        stop,
        enter=_stopped,
        outputs=[
            _waitForStop, _cancelConnectWaiters, _stopRetrying, _finishStopping
        ],
        collector=_firstResult,
    )
    _waiting.upon(_reconnect, enter=_connecting, outputs=[_connect])

    _connected.upon(start, enter=_connected, outputs=[])
    _connected.upon(
        stop,
        enter=_disconnecting,
        outputs=[_waitForStop, _disconnect],
        collector=_firstResult,
    )
    _connected.upon(_clientDisconnected,
                    enter=_waiting,
                    outputs=[_forgetConnection, _wait])

    _disconnecting.upon(start,
                        enter=_restarting,
                        outputs=[_resetFailedAttempts])
    _disconnecting.upon(stop,
                        enter=_disconnecting,
                        outputs=[_waitForStop],
                        collector=_firstResult)
    _disconnecting.upon(
        _clientDisconnected,
        enter=_stopped,
        outputs=[_cancelConnectWaiters, _finishStopping, _forgetConnection],
    )
    # Note that this is triggered synchonously with the transition from
    # _connecting
    _disconnecting.upon(
        _connectionFailed,
        enter=_stopped,
        outputs=[_ignoreAndCancelConnectWaiters, _ignoreAndFinishStopping],
    )

    _restarting.upon(start, enter=_restarting, outputs=[])
    _restarting.upon(stop,
                     enter=_disconnecting,
                     outputs=[_waitForStop],
                     collector=_firstResult)
    _restarting.upon(_clientDisconnected,
                     enter=_connecting,
                     outputs=[_finishStopping, _connect])

    _stopped.upon(start, enter=_connecting, outputs=[_connect])
    _stopped.upon(
        stop,
        enter=_stopped,
        outputs=[_deferredSucceededWithNone],
        collector=_firstResult,
    )

    _init.upon(
        whenConnected,
        enter=_init,
        outputs=[_awaitingConnection],
        collector=_firstResult,
    )
    _connecting.upon(
        whenConnected,
        enter=_connecting,
        outputs=[_awaitingConnection],
        collector=_firstResult,
    )
    _waiting.upon(
        whenConnected,
        enter=_waiting,
        outputs=[_awaitingConnection],
        collector=_firstResult,
    )
    _connected.upon(
        whenConnected,
        enter=_connected,
        outputs=[_currentConnection],
        collector=_firstResult,
    )
    _disconnecting.upon(
        whenConnected,
        enter=_disconnecting,
        outputs=[_awaitingConnection],
        collector=_firstResult,
    )
    _restarting.upon(
        whenConnected,
        enter=_restarting,
        outputs=[_awaitingConnection],
        collector=_firstResult,
    )
    _stopped.upon(whenConnected,
                  enter=_stopped,
                  outputs=[_noConnection],
                  collector=_firstResult)
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)

    def __attrs_post_init__(self):
        self._queue = []

    def wire(self, mailbox):
        self._M = _interfaces.IMailbox(mailbox)

    @m.state(initial=True)
    def S0_no_key(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def S1_verified_key(self):
        pass  # pragma: no cover

    # from Receive
    @m.input()
    def got_verified_key(self, key):
        pass
    # from Boss
    @m.input()
    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)
        assert isinstance(plaintext, type(b"")), type(plaintext)
        self._encrypt_and_send(phase, plaintext)

    def _encrypt_and_send(self, phase, plaintext):
        assert self._key
        data_key = derive_phase_key(self._key, self._side, phase)
        encrypted = encrypt_data(data_key, plaintext)
        self._M.add_message(phase, encrypted)

    S0_no_key.upon(send, enter=S0_no_key, outputs=[queue])
    S0_no_key.upon(got_verified_key,
                   enter=S1_verified_key,
                   outputs=[record_key, drain])
    S1_verified_key.upon(send, enter=S1_verified_key, outputs=[deliver])
Example #12
0
class Nameplate(object):
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace", lambda self, f: None)

    def __init__(self):
        self._nameplate = None

    def wire(self, mailbox, input, rendezvous_connector, terminator):
        self._M = _interfaces.IMailbox(mailbox)
        self._I = _interfaces.IInput(input)
        self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
        self._T = _interfaces.ITerminator(terminator)

    # all -A states: not connected
    # all -B states: yes connected
    # B states serialize as A, so they deserialize as unconnected

    # S0: know nothing
    @m.state(initial=True)
    def S0A(self):
        pass  # pragma: no cover

    @m.state()
    def S0B(self):
        pass  # pragma: no cover

    # S1: nameplate known, never claimed
    @m.state()
    def S1A(self):
        pass  # pragma: no cover

    # S2: nameplate known, maybe claimed
    @m.state()
    def S2A(self):
        pass  # pragma: no cover

    @m.state()
    def S2B(self):
        pass  # pragma: no cover

    # S3: nameplate claimed
    @m.state()
    def S3A(self):
        pass  # pragma: no cover

    @m.state()
    def S3B(self):
        pass  # pragma: no cover

    # S4: maybe released
    @m.state()
    def S4A(self):
        pass  # pragma: no cover

    @m.state()
    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 S5(self):
        pass  # pragma: no cover

    S5A = S5
    S5B = S5

    # from Boss
    @m.input()
    def set_nameplate(self, nameplate):
        pass

    # from Mailbox
    @m.input()
    def release(self):
        pass

    # from Terminator
    @m.input()
    def close(self):
        pass

    # from RendezvousConnector
    @m.input()
    def connected(self):
        pass

    @m.input()
    def lost(self):
        pass

    @m.input()
    def rx_claimed(self, mailbox):
        pass

    @m.input()
    def rx_released(self):
        pass

    @m.output()
    def record_nameplate(self, nameplate):
        self._nameplate = nameplate

    @m.output()
    def record_nameplate_and_RC_tx_claim(self, nameplate):
        self._nameplate = nameplate
        self._RC.tx_claim(self._nameplate)

    @m.output()
    def RC_tx_claim(self):
        # when invoked via M.connected(), we must use the stored nameplate
        self._RC.tx_claim(self._nameplate)

    @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()

    S0A.upon(set_nameplate, enter=S1A, outputs=[record_nameplate])
    S0A.upon(connected, enter=S0B, outputs=[])
    S0A.upon(close, enter=S5A, outputs=[T_nameplate_done])
    S0B.upon(set_nameplate,
             enter=S2B,
             outputs=[record_nameplate_and_RC_tx_claim])
    S0B.upon(lost, enter=S0A, outputs=[])
    S0B.upon(close, enter=S5A, outputs=[T_nameplate_done])

    S1A.upon(connected, enter=S2B, outputs=[RC_tx_claim])
    S1A.upon(close, enter=S5A, outputs=[T_nameplate_done])

    S2A.upon(connected, enter=S2B, outputs=[RC_tx_claim])
    S2A.upon(close, enter=S4A, outputs=[])
    S2B.upon(lost, enter=S2A, outputs=[])
    S2B.upon(rx_claimed, enter=S3B, outputs=[I_got_wordlist, M_got_mailbox])
    S2B.upon(close, enter=S4B, outputs=[RC_tx_release])

    S3A.upon(connected, enter=S3B, outputs=[])
    S3A.upon(close, enter=S4A, outputs=[])
    S3B.upon(lost, enter=S3A, outputs=[])
    #S3B.upon(rx_claimed, enter=S3B, outputs=[]) # shouldn't happen
    S3B.upon(release, enter=S4B, outputs=[RC_tx_release])
    S3B.upon(close, enter=S4B, outputs=[RC_tx_release])

    S4A.upon(connected, enter=S4B, outputs=[RC_tx_release])
    S4A.upon(close, enter=S4A, outputs=[])
    S4B.upon(lost, enter=S4A, outputs=[])
    S4B.upon(rx_claimed, enter=S4B, outputs=[])
    S4B.upon(rx_released, enter=S5B, outputs=[T_nameplate_done])
    S4B.upon(release, enter=S4B, outputs=[])  # mailbox is lazy
    # Mailbox doesn't remember how many times it's sent a release, and will
    # re-send a new one for each peer message it receives. Ignoring it here
    # is easier than adding a new pair of states to Mailbox.
    S4B.upon(close, enter=S4B, outputs=[])

    S5A.upon(connected, enter=S5B, outputs=[])
    S5B.upon(lost, enter=S5A, outputs=[])
    S5.upon(release, enter=S5, outputs=[])  # mailbox is lazy
    S5.upon(close, enter=S5, outputs=[])
class SubChannel(object):
    _scid = attrib(validator=instance_of(six.integer_types))
    _manager = attrib(validator=provides(IDilationManager))
    _host_addr = attrib(validator=instance_of(_WormholeAddress))
    _peer_addr = attrib(validator=instance_of(_SubchannelAddress))

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        # self._mailbox = None
        # self._pending_outbound = {}
        # self._processed = set()
        self._protocol = None
        self._pending_remote_data = []
        self._pending_remote_close = False

    @m.state(initial=True)
    def unconnected(self):
        pass  # pragma: no cover

    # once we get the IProtocol, it's either a IHalfCloseableProtocol, or it
    # can only be fully closed
    @m.state()
    def open_half(self):
        pass  # pragma: no cover

    @m.state()
    def read_closed():
        pass  # pragma: no cover

    @m.state()
    def write_closed():
        pass  # pragma: no cover

    @m.state()
    def open_full(self):
        pass  # pragma: no cover

    @m.state()
    def closing():
        pass  # pragma: no cover

    @m.state()
    def closed():
        pass  # pragma: no cover

    @m.input()
    def connect_protocol_half(self):
        pass

    @m.input()
    def connect_protocol_full(self):
        pass

    @m.input()
    def remote_data(self, data):
        pass

    @m.input()
    def remote_close(self):
        pass

    @m.input()
    def local_data(self, data):
        pass

    @m.input()
    def local_close(self):
        pass

    @m.output()
    def queue_remote_data(self, data):
        self._pending_remote_data.append(data)

    @m.output()
    def queue_remote_close(self):
        self._pending_remote_close = True

    @m.output()
    def send_data(self, data):
        self._manager.send_data(self._scid, data)

    @m.output()
    def send_close(self):
        self._manager.send_close(self._scid)

    @m.output()
    def signal_dataReceived(self, data):
        assert self._protocol
        self._protocol.dataReceived(data)

    @m.output()
    def signal_readConnectionLost(self):
        IHalfCloseableProtocol(self._protocol).readConnectionLost()

    @m.output()
    def signal_writeConnectionLost(self):
        IHalfCloseableProtocol(self._protocol).writeConnectionLost()

    @m.output()
    def signal_connectionLost(self):
        assert self._protocol
        self._protocol.connectionLost(ConnectionDone())

    @m.output()
    def close_subchannel(self):
        self._manager.subchannel_closed(self._scid, self)
        # we're deleted momentarily

    @m.output()
    def error_closed_write(self, data):
        raise AlreadyClosedError("write not allowed on closed subchannel")

    @m.output()
    def error_closed_close(self):
        raise AlreadyClosedError(
            "loseConnection not allowed on closed subchannel")

    # stuff that arrives before we have a protocol connected
    unconnected.upon(remote_data,
                     enter=unconnected,
                     outputs=[queue_remote_data])
    unconnected.upon(remote_close,
                     enter=unconnected,
                     outputs=[queue_remote_close])

    # IHalfCloseableProtocol flow
    unconnected.upon(connect_protocol_half, enter=open_half, outputs=[])
    open_half.upon(remote_data, enter=open_half, outputs=[signal_dataReceived])
    open_half.upon(local_data, enter=open_half, outputs=[send_data])
    # remote closes first
    open_half.upon(remote_close,
                   enter=read_closed,
                   outputs=[signal_readConnectionLost])
    read_closed.upon(local_data, enter=read_closed, outputs=[send_data])
    read_closed.upon(
        local_close,
        enter=closed,
        outputs=[
            send_close,
            close_subchannel,
            # TODO: eventual-signal this?
            signal_writeConnectionLost,
        ])
    # local closes first
    open_half.upon(local_close,
                   enter=write_closed,
                   outputs=[signal_writeConnectionLost, send_close])
    write_closed.upon(local_data,
                      enter=write_closed,
                      outputs=[error_closed_write])
    write_closed.upon(remote_data,
                      enter=write_closed,
                      outputs=[signal_dataReceived])
    write_closed.upon(remote_close,
                      enter=closed,
                      outputs=[
                          close_subchannel,
                          signal_readConnectionLost,
                      ])
    # error cases
    write_closed.upon(local_close,
                      enter=write_closed,
                      outputs=[error_closed_close])

    # fully-closeable-only flow
    unconnected.upon(connect_protocol_full, enter=open_full, outputs=[])
    open_full.upon(remote_data, enter=open_full, outputs=[signal_dataReceived])
    open_full.upon(local_data, enter=open_full, outputs=[send_data])
    open_full.upon(
        remote_close,
        enter=closed,
        outputs=[send_close, close_subchannel, signal_connectionLost])
    open_full.upon(local_close, enter=closing, outputs=[send_close])
    closing.upon(remote_data, enter=closing, outputs=[signal_dataReceived])
    closing.upon(remote_close,
                 enter=closed,
                 outputs=[close_subchannel, signal_connectionLost])
    # error cases
    # we won't ever see an OPEN, since L4 will log+ignore those for us
    closing.upon(local_data, enter=closing, outputs=[error_closed_write])
    closing.upon(local_close, enter=closing, outputs=[error_closed_close])

    # the CLOSED state won't ever see messages, since we'll be deleted

    # our endpoints use these

    def _set_protocol(self, protocol):
        assert not self._protocol
        self._protocol = protocol
        if IHalfCloseableProtocol.providedBy(protocol):
            self.connect_protocol_half()
        else:
            # move from UNCONNECTED to OPEN
            self.connect_protocol_full()

    def _deliver_queued_data(self):
        for data in self._pending_remote_data:
            self.remote_data(data)
        del self._pending_remote_data
        if self._pending_remote_close:
            self.remote_close()
            del self._pending_remote_close

    # ITransport
    def write(self, data):
        assert isinstance(data, type(b""))
        assert len(data) <= MAX_FRAME_LENGTH
        self.local_data(data)

    def writeSequence(self, iovec):
        self.write(b"".join(iovec))

    def loseWriteConnection(self):
        if not IHalfCloseableProtocol.providedBy(self._protocol):
            # this is a clear error
            raise HalfCloseUsedOnNonHalfCloseable()
        self.local_close()

    def loseConnection(self):
        # TODO: what happens if an IHalfCloseableProtocol calls normal
        # loseConnection()? I think we need to close the read side too.
        if IHalfCloseableProtocol.providedBy(self._protocol):
            # I don't know is correct, so avoid this for now
            raise NormalCloseUsedOnHalfCloseable()
        self.local_close()

    def getHost(self):
        # we define "host addr" as the overall wormhole
        return self._host_addr

    def getPeer(self):
        # and "peer addr" as the subchannel within that wormhole
        return self._peer_addr

    # IProducer: throttle inbound data (wormhole "up" to local app's Protocol)
    def stopProducing(self):
        self._manager.subchannel_stopProducing(self)

    def pauseProducing(self):
        self._manager.subchannel_pauseProducing(self)

    def resumeProducing(self):
        self._manager.subchannel_resumeProducing(self)

    # IConsumer: allow the wormhole to throttle outbound data (app->wormhole)
    def registerProducer(self, producer, streaming):
        self._manager.subchannel_registerProducer(self, producer, streaming)

    def unregisterProducer(self):
        self._manager.subchannel_unregisterProducer(self)
Example #14
0
class Lister(object):
    _timing = attrib(validator=provides(_interfaces.ITiming))
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def wire(self, rendezvous_connector, input):
        self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
        self._I = _interfaces.IInput(input)

    # Ideally, each API request would spawn a new "list_nameplates" message
    # to the server, so the response would be maximally fresh, but that would
    # require correlating server request+response messages, and the protocol
    # is intended to be less stateful than that. So we offer a weaker
    # freshness property: if no server requests are in flight, then a new API
    # request will provoke a new server request, and the result will be
    # fresh. But if a server request is already in flight when a second API
    # request arrives, both requests will be satisfied by the same response.

    @m.state(initial=True)
    def S0A_idle_disconnected(self):
        pass  # pragma: no cover

    @m.state()
    def S1A_wanting_disconnected(self):
        pass  # pragma: no cover

    @m.state()
    def S0B_idle_connected(self):
        pass  # pragma: no cover

    @m.state()
    def S1B_wanting_connected(self):
        pass  # pragma: no cover

    @m.input()
    def connected(self):
        pass

    @m.input()
    def lost(self):
        pass

    @m.input()
    def refresh(self):
        pass

    @m.input()
    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
        # future: change RendezvousConnector._response_handle_nameplates to
        # get them
        self._I.got_nameplates(all_nameplates)

    S0A_idle_disconnected.upon(connected, enter=S0B_idle_connected, outputs=[])
    S0B_idle_connected.upon(lost, enter=S0A_idle_disconnected, outputs=[])

    S0A_idle_disconnected.upon(refresh,
                               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])
Example #15
0
class DilatedConnectionProtocol(Protocol, object):
    """I manage an L2 connection.

    When a new L2 connection is needed (as determined by the Leader),
    both Leader and Follower will initiate many simultaneous connections
    (probably TCP, but conceivably others). A subset will actually
    connect. A subset of those will successfully pass negotiation by
    exchanging handshakes to demonstrate knowledge of the session key.
    One of the negotiated connections will be selected by the Leader for
    active use, and the others will be dropped.

    At any given time, there is at most one active L2 connection.
    """

    _eventual_queue = attrib()
    _role = attrib()
    _connector = attrib(validator=provides(IDilationConnector))
    _noise = attrib()
    _outbound_prologue = attrib(validator=instance_of(bytes))
    _inbound_prologue = attrib(validator=instance_of(bytes))

    _use_relay = False
    _relay_handshake = None

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        self._manager = None  # set if/when we are selected
        self._disconnected = OneShotObserver(self._eventual_queue)
        self._can_send_records = False

    @m.state(initial=True)
    def unselected(self):
        pass  # pragma: no cover

    @m.state()
    def selecting(self):
        pass  # pragma: no cover

    @m.state()
    def selected(self):
        pass  # pragma: no cover

    @m.input()
    def got_kcm(self):
        pass

    @m.input()
    def select(self, manager):
        pass  # fires set_manager()

    @m.input()
    def got_record(self, record):
        pass

    @m.output()
    def add_candidate(self):
        self._connector.add_candidate(self)

    @m.output()
    def set_manager(self, manager):
        self._manager = manager

    @m.output()
    def can_send_records(self, manager):
        self._can_send_records = True

    @m.output()
    def deliver_record(self, record):
        self._manager.got_record(record)

    unselected.upon(got_kcm, outputs=[add_candidate], enter=selecting)
    selecting.upon(select,
                   outputs=[set_manager, can_send_records],
                   enter=selected)
    selected.upon(got_record, outputs=[deliver_record], enter=selected)

    # called by Connector

    def use_relay(self, relay_handshake):
        assert isinstance(relay_handshake, bytes)
        self._use_relay = True
        self._relay_handshake = relay_handshake

    def when_disconnected(self):
        return self._disconnected.when_fired()

    def disconnect(self):
        self.transport.loseConnection()

    # select() called by Connector

    # called by Manager
    def send_record(self, record):
        assert self._can_send_records
        self._record.send_record(record)

    # IProtocol methods

    def connectionMade(self):
        framer = _Framer(self.transport, self._outbound_prologue,
                         self._inbound_prologue)
        if self._use_relay:
            framer.use_relay(self._relay_handshake)
        self._record = _Record(framer, self._noise)
        self._record.connectionMade()

    def dataReceived(self, data):
        try:
            for token in self._record.add_and_unframe(data):
                assert isinstance(token, Handshake_or_Records)
                if isinstance(token, Handshake):
                    if self._role is FOLLOWER:
                        self._record.send_record(KCM())
                elif isinstance(token, KCM):
                    # if we're the leader, add this connection as a candiate.
                    # if we're the follower, accept this connection.
                    self.got_kcm()  # connector.add_candidate()
                else:
                    self.got_record(token)  # manager.got_record()
        except Disconnect:
            self.transport.loseConnection()

    def connectionLost(self, why=None):
        self._disconnected.fire(self)
Example #16
0
class Code(object):
    _timing = attrib(validator=provides(_interfaces.ITiming))
    m = MethodicalMachine()
    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)
        self._A = _interfaces.IAllocator(allocator)
        self._N = _interfaces.INameplate(nameplate)
        self._K = _interfaces.IKey(key)
        self._I = _interfaces.IInput(input)

    @m.state(initial=True)
    def S0_idle(self):
        pass  # pragma: no cover

    @m.state()
    def S1_inputting_nameplate(self):
        pass  # pragma: no cover

    @m.state()
    def S2_inputting_words(self):
        pass  # pragma: no cover

    @m.state()
    def S3_allocating(self):
        pass  # pragma: no cover

    @m.state()
    def S4_known(self):
        pass  # pragma: no cover

    # from App
    @m.input()
    def allocate_code(self, length, wordlist):
        pass

    @m.input()
    def input_code(self):
        pass

    def set_code(self, code):
        validate_code(code)  # can raise KeyFormatError
        self._set_code(code)

    @m.input()
    def _set_code(self, code):
        pass

    # from Allocator
    @m.input()
    def allocated(self, nameplate, code):
        pass

    # from Input
    @m.input()
    def got_nameplate(self, nameplate):
        pass

    @m.input()
    def finished_input(self, code):
        pass

    @m.output()
    def do_set_code(self, code):
        nameplate = code.split("-", 2)[0]
        self._N.set_nameplate(nameplate)
        self._B.got_code(code)
        self._K.got_code(code)

    @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)
        self._K.got_code(code)

    @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)
        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])
    S3_allocating.upon(allocated, enter=S4_known, outputs=[do_finish_allocate])
Example #17
0
class LobbyMachine(object):
    _machine = MethodicalMachine()

    # ---------------
    # Protocol states
    # ---------------

    TOKEN_START = "start"
    TOKEN_UNJOINED = "unjoined"
    TOKEN_WAITING_FOR_ACCEPTS = "waiting_for_accepts"
    TOKEN_INVITED = "invited"
    TOKEN_ACCEPTED = "accepted"
    TOKEN_SESSION_STARTED = "session_started"

    @_machine.state(serialized=TOKEN_START, initial=True)
    def start(self):
        """
        Initial state.
        """

    @_machine.state(serialized=TOKEN_UNJOINED)
    def unjoined(self):
        """
        Avatar has not joined a group session yet. 
        """

    @_machine.state(serialized=TOKEN_WAITING_FOR_ACCEPTS)
    def waiting_for_accepts(self):
        """
        User has invited others to a session, is waiting for them to accept.
        May cancel session and all accepts and outstanding invites.
        May choose to start session, cancelling any outstanding invites.
        """

    @_machine.state(serialized=TOKEN_INVITED)
    def invited(self):
        """
        User has been invited to a session.
        May accept or reject.
        """

    @_machine.state(serialized=TOKEN_ACCEPTED)
    def accepted(self):
        """
        User has accepted invitation to a session.  
        Is waiting for session to start.
        May still withdraw from invitation at this point.
        """

    @_machine.state(serialized=TOKEN_SESSION_STARTED)
    def session_started(self):
        """
        The session has started.
        A new protcol can take over at this point.
        """

    # ------
    # Inputs
    # ------

    @_machine.input()
    def initialize(self):
        """
        Initialize the machine.
        """

    @_machine.input()
    def create_session(self):
        """
        Send an invitation to another player.
        """

    @_machine.input()
    def send_invitation(self):
        """
        Send an invitation to another player.
        """

    @_machine.input()
    def receive_invitation(self):
        """
        Receive an invitation to join a session.
        """

    @_machine.input()
    def revoke_invitation(self):
        """
        Revoke an invitation to join a session.
        """

    @_machine.input()
    def cancel(self):
        """
        Cancel a session that has not yet been started if you are the owner.
        OR
        Cancel acceptance into a session that has not yet been started.
        """

    @_machine.input()
    def accept(self):
        """
        Accept an invitation.
        """

    @_machine.input()
    def reject(self):
        """
        Reject an invitation.
        """

    @_machine.input()
    def start_session(self):
        """
        Start a session.
        """

    # -------
    # Outputs
    # -------

    @_machine.output()
    def _enter_unjoined(self):
        self._call_handler(self.handle_unjoined)

    @_machine.output()
    def _enter_invited(self):
        self._call_handler(self.handle_invited)

    @_machine.output()
    def _enter_accepted(self):
        self._call_handler(self.handle_accepted)

    @_machine.output()
    def _enter_waiting_for_accepts(self):
        self._call_handler(self.handle_waiting_for_accepts)

    @_machine.output()
    def _enter_session_started(self):
        self._call_handler(self.handle_session_started)

    @_machine.output()
    def _enter_invited(self):
        self._call_handler(self.handle_invited)

    # --------------
    # Event handlers
    # --------------

    @staticmethod
    def _call_handler(handler):
        if handler is not None:
            handler()

    handle_unjoined = None
    handle_invited = None
    handle_accepted = None
    handle_waiting_for_accepts = None
    handle_session_started = None
    handle_invited = None

    # -----------
    # Transitions
    # -----------
    start.upon(initialize, enter=unjoined, outputs=[_enter_unjoined])
    unjoined.upon(create_session,
                  enter=waiting_for_accepts,
                  outputs=[_enter_waiting_for_accepts])
    unjoined.upon(receive_invitation, enter=invited, outputs=[_enter_invited])
    waiting_for_accepts.upon(start_session,
                             enter=session_started,
                             outputs=[_enter_session_started])
    waiting_for_accepts.upon(cancel, enter=unjoined, outputs=[_enter_unjoined])
    waiting_for_accepts.upon(send_invitation,
                             enter=waiting_for_accepts,
                             outputs=[_enter_waiting_for_accepts])
    invited.upon(accept, enter=accepted, outputs=[_enter_accepted])
    invited.upon(reject, enter=unjoined, outputs=[_enter_unjoined])
    invited.upon(revoke_invitation, enter=unjoined, outputs=[_enter_unjoined])
    accepted.upon(start_session,
                  enter=session_started,
                  outputs=[_enter_session_started])
    accepted.upon(cancel, enter=unjoined, outputs=[_enter_unjoined])

    # -------------
    # Serialization
    # -------------

    @_machine.serializer()
    def savestate(self, state):
        return state

    @_machine.unserializer()
    def restorestate(self, state):
        log.msg("Restoring state `{}`.".format(state))
        self._fire_handler(state)
        return state

    def _fire_handler(self, state):
        log.msg("Entered _fire_handler()")
        if state == self.TOKEN_START:
            pass
        elif state == self.TOKEN_UNJOINED:
            LobbyMachine._call_handler(self.handle_unjoined)
            log.msg("fired handle_unjoined().")
        elif state == self.TOKEN_WAITING_FOR_ACCEPTS:
            log.msg("about to fire handle_waiting_for_accepts().")
            LobbyMachine._call_handler(self.handle_waiting_for_accepts)
            log.msg("fired handle_waiting_for_accepts().")
        elif state == self.TOKEN_INVITED:
            LobbyMachine._call_handler(self.handle_invited)
            log.msg("fired handle_invited().")
        elif state == self.TOKEN_ACCEPTED:
            LobbyMachine._call_handler(self.handle_accepted)
            log.msg("fired handle_accepted().")
        elif state == self.TOKEN_SESSION_STARTED:
            LobbyMachine._call_handler(self.handle_session_started)
            log.msg("fired handle_session_started().")
        else:
            raise Exception("Unrecognized state '{}'.".format(state))
Example #18
0
class Game(object):
    _machine = MethodicalMachine()

    def __init__(self):
        self.go_to_main_menu()

    # Game States
    @_machine.state(initial=True)
    def initial(self):
        """Initial state for the game. Allows calling `_show_main_menu` at start of game."""

    @_machine.state()
    def main_menu(self):
        """The game is on the main menu, waiting for the player to press start."""

    @_machine.state()
    def running_game(self):
        """The game is in progress."""

    @_machine.state()
    def game_over(self):
        """The game has finished."""

    @_machine.state()
    def quit(self):
        """State when quitting the game."""

    # Inputs
    @_machine.input()
    def start_game(self):
        """Start playing the game."""

    @_machine.input()
    def finish_game(self):
        """Finish playing the game."""

    @_machine.input()
    def go_to_main_menu(self):
        """Quit out of the game and return to the main menu."""

    @_machine.input()
    def quit_game(self):
        """Quit out of the game from the main menu."""

    # Outputs
    @_machine.output()
    def _show_main_menu(self):
        """Show the menu menu to the player."""
        answer = input("Do you want to play the game? [y/n] ")
        if answer == 'y':
            self.start_game()
        elif answer == 'n':
            self.quit_game()

    @_machine.output()
    def _play_game(self):
        """Main game loop."""
        answer = None
        while answer != 'y':
            answer = input("Is this a great game? [y/n] ")

        self.finish_game()

    @_machine.output()
    def _show_end_screen(self):
        """Display end game message."""
        print("Nice work!")
        self.go_to_main_menu()

    @_machine.output()
    def _quit_game(self):
        """Display message and close the game."""
        print("Goodbye.")
        sleep(1.5)
        print("Loser.")
        sys.exit(0)

    # Connections
    initial.upon(go_to_main_menu, enter=main_menu, outputs=[_show_main_menu])
    main_menu.upon(start_game, enter=running_game, outputs=[_play_game])
    main_menu.upon(quit_game, enter=quit, outputs=[_quit_game])
    running_game.upon(finish_game, enter=game_over, outputs=[_show_end_screen])
    game_over.upon(go_to_main_menu, enter=main_menu, outputs=[_show_main_menu])
Example #19
0
class Manager(object):
    _S = attrib(validator=provides(ISend), repr=False)
    _my_side = attrib(validator=instance_of(type(u"")))
    _transit_key = attrib(validator=instance_of(bytes), repr=False)
    _transit_relay_location = attrib(validator=optional(instance_of(str)))
    _reactor = attrib(repr=False)
    _eventual_queue = attrib(repr=False)
    _cooperator = attrib(repr=False)
    _no_listen = attrib(default=False)
    _tor = None  # TODO
    _timing = None  # TODO
    _next_subchannel_id = None  # initialized in choose_role

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        self._got_versions_d = Deferred()

        self._my_role = None  # determined upon rx_PLEASE

        self._connection = None
        self._made_first_connection = False
        self._first_connected = OneShotObserver(self._eventual_queue)
        self._stopped = OneShotObserver(self._eventual_queue)
        self._host_addr = _WormholeAddress()

        self._next_dilation_phase = 0

        # I kept getting confused about which methods were for inbound data
        # (and thus flow-control methods go "out") and which were for
        # outbound data (with flow-control going "in"), so I split them up
        # into separate pieces.
        self._inbound = Inbound(self, self._host_addr)
        self._outbound = Outbound(self, self._cooperator)  # from us to peer

    def set_listener_endpoint(self, listener_endpoint):
        self._inbound.set_listener_endpoint(listener_endpoint)

    def set_subchannel_zero(self, scid0, sc0):
        self._inbound.set_subchannel_zero(scid0, sc0)

    def when_first_connected(self):
        return self._first_connected.when_fired()

    def when_stopped(self):
        return self._stopped.when_fired()

    def send_dilation_phase(self, **fields):
        dilation_phase = self._next_dilation_phase
        self._next_dilation_phase += 1
        self._S.send("dilate-%d" % dilation_phase, dict_to_bytes(fields))

    def send_hints(self, hints):  # from Connector
        self.send_dilation_phase(type="connection-hints", hints=hints)

    # forward inbound-ish things to _Inbound

    def subchannel_pauseProducing(self, sc):
        self._inbound.subchannel_pauseProducing(sc)

    def subchannel_resumeProducing(self, sc):
        self._inbound.subchannel_resumeProducing(sc)

    def subchannel_stopProducing(self, sc):
        self._inbound.subchannel_stopProducing(sc)

    # forward outbound-ish things to _Outbound
    def subchannel_registerProducer(self, sc, producer, streaming):
        self._outbound.subchannel_registerProducer(sc, producer, streaming)

    def subchannel_unregisterProducer(self, sc):
        self._outbound.subchannel_unregisterProducer(sc)

    def send_open(self, scid):
        assert isinstance(scid, bytes)
        self._queue_and_send(Open, scid)

    def send_data(self, scid, data):
        assert isinstance(scid, bytes)
        self._queue_and_send(Data, scid, data)

    def send_close(self, scid):
        assert isinstance(scid, bytes)
        self._queue_and_send(Close, scid)

    def _queue_and_send(self, record_type, *args):
        r = self._outbound.build_record(record_type, *args)
        # Outbound owns the send_record() pipe, so that it can stall new
        # writes after a new connection is made until after all queued
        # messages are written (to preserve ordering).
        self._outbound.queue_and_send_record(r)  # may trigger pauseProducing

    def subchannel_closed(self, scid, sc):
        # let everyone clean up. This happens just after we delivered
        # connectionLost to the Protocol, except for the control channel,
        # which might get connectionLost later after they use ep.connect.
        # TODO: is this inversion a problem?
        self._inbound.subchannel_closed(scid, sc)
        self._outbound.subchannel_closed(scid, sc)

    # our Connector calls these

    def connector_connection_made(self, c):
        self.connection_made()  # state machine update
        self._connection = c
        self._inbound.use_connection(c)
        self._outbound.use_connection(c)  # does c.registerProducer
        if not self._made_first_connection:
            self._made_first_connection = True
            self._first_connected.fire(None)
        pass

    def connector_connection_lost(self):
        self._stop_using_connection()
        if self._my_role is LEADER:
            self.connection_lost_leader()  # state machine
        else:
            self.connection_lost_follower()

    def _stop_using_connection(self):
        # the connection is already lost by this point
        self._connection = None
        self._inbound.stop_using_connection()
        self._outbound.stop_using_connection()  # does c.unregisterProducer

    # from our active Connection

    def got_record(self, r):
        # records with sequence numbers: always ack, ignore old ones
        if isinstance(r, (Open, Data, Close)):
            self.send_ack(r.seqnum)  # always ack, even for old ones
            if self._inbound.is_record_old(r):
                return
            self._inbound.update_ack_watermark(r.seqnum)
            if isinstance(r, Open):
                self._inbound.handle_open(r.scid)
            elif isinstance(r, Data):
                self._inbound.handle_data(r.scid, r.data)
            else:  # isinstance(r, Close)
                self._inbound.handle_close(r.scid)
            return
        if isinstance(r, KCM):
            log.err(UnexpectedKCM())
        elif isinstance(r, Ping):
            self.handle_ping(r.ping_id)
        elif isinstance(r, Pong):
            self.handle_pong(r.ping_id)
        elif isinstance(r, Ack):
            self._outbound.handle_ack(r.resp_seqnum)  # retire queued messages
        else:
            log.err(UnknownMessageType("{}".format(r)))

    # pings, pongs, and acks are not queued
    def send_ping(self, ping_id):
        self._outbound.send_if_connected(Ping(ping_id))

    def send_pong(self, ping_id):
        self._outbound.send_if_connected(Pong(ping_id))

    def send_ack(self, resp_seqnum):
        self._outbound.send_if_connected(Ack(resp_seqnum))

    def handle_ping(self, ping_id):
        self.send_pong(ping_id)

    def handle_pong(self, ping_id):
        # TODO: update is-alive timer
        pass

    # subchannel maintenance
    def allocate_subchannel_id(self):
        scid_num = self._next_subchannel_id
        self._next_subchannel_id += 2
        return to_be4(scid_num)

    # state machine

    # We are born WANTING after the local app calls w.dilate(). We start
    # CONNECTING when we receive PLEASE from the remote side

    def start(self):
        self.send_please()

    def send_please(self):
        self.send_dilation_phase(type="please", side=self._my_side)

    @m.state(initial=True)
    def WANTING(self):
        pass  # pragma: no cover

    @m.state()
    def CONNECTING(self):
        pass  # pragma: no cover

    @m.state()
    def CONNECTED(self):
        pass  # pragma: no cover

    @m.state()
    def FLUSHING(self):
        pass  # pragma: no cover

    @m.state()
    def ABANDONING(self):
        pass  # pragma: no cover

    @m.state()
    def LONELY(self):
        pass  # pragma: no cover

    @m.state()
    def STOPPING(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def STOPPED(self):
        pass  # pragma: no cover

    @m.input()
    def rx_PLEASE(self, message):
        pass  # pragma: no cover

    @m.input()  # only sent by Follower
    def rx_HINTS(self, hint_message):
        pass  # pragma: no cover

    @m.input()  # only Leader sends RECONNECT, so only Follower receives it
    def rx_RECONNECT(self):
        pass  # pragma: no cover

    @m.input()  # only Follower sends RECONNECTING, so only Leader receives it
    def rx_RECONNECTING(self):
        pass  # pragma: no cover

    # Connector gives us connection_made()
    @m.input()
    def connection_made(self):
        pass  # pragma: no cover

    # our connection_lost() fires connection_lost_leader or
    # connection_lost_follower depending upon our role. If either side sees a
    # problem with the connection (timeouts, bad authentication) then they
    # just drop it and let connection_lost() handle the cleanup.
    @m.input()
    def connection_lost_leader(self):
        pass  # pragma: no cover

    @m.input()
    def connection_lost_follower(self):
        pass

    @m.input()
    def stop(self):
        pass  # pragma: no cover

    @m.output()
    def choose_role(self, message):
        their_side = message["side"]
        if self._my_side > their_side:
            self._my_role = LEADER
            # scid 0 is reserved for the control channel. the leader uses odd
            # numbers starting with 1
            self._next_subchannel_id = 1
        elif their_side > self._my_side:
            self._my_role = FOLLOWER
            # the follower uses even numbers starting with 2
            self._next_subchannel_id = 2
        else:
            raise ValueError("their side shouldn't be equal: reflection?")

    # these Outputs behave differently for the Leader vs the Follower

    @m.output()
    def start_connecting_ignore_message(self, message):
        del message  # ignored
        return self._start_connecting()

    @m.output()
    def start_connecting(self):
        self._start_connecting()

    def _start_connecting(self):
        assert self._my_role is not None
        self._connector = Connector(
            self._transit_key,
            self._transit_relay_location,
            self,
            self._reactor,
            self._eventual_queue,
            self._no_listen,
            self._tor,
            self._timing,
            self._my_side,  # needed for relay handshake
            self._my_role)
        self._connector.start()

    @m.output()
    def send_reconnect(self):
        self.send_dilation_phase(type="reconnect")  # TODO: generation number?

    @m.output()
    def send_reconnecting(self):
        self.send_dilation_phase(type="reconnecting")  # TODO: generation?

    @m.output()
    def use_hints(self, hint_message):
        hint_objs = filter(
            lambda h: h,  # ignore None, unrecognizable
            [parse_hint(hs) for hs in hint_message["hints"]])
        hint_objs = list(hint_objs)
        self._connector.got_hints(hint_objs)

    @m.output()
    def stop_connecting(self):
        self._connector.stop()

    @m.output()
    def abandon_connection(self):
        # we think we're still connected, but the Leader disagrees. Or we've
        # been told to shut down.
        self._connection.disconnect()  # let connection_lost do cleanup

    @m.output()
    def notify_stopped(self):
        self._stopped.fire(None)

    # we start CONNECTING when we get rx_PLEASE
    WANTING.upon(rx_PLEASE,
                 enter=CONNECTING,
                 outputs=[choose_role, start_connecting_ignore_message])

    CONNECTING.upon(connection_made, enter=CONNECTED, outputs=[])

    # Leader
    CONNECTED.upon(connection_lost_leader,
                   enter=FLUSHING,
                   outputs=[send_reconnect])
    FLUSHING.upon(rx_RECONNECTING,
                  enter=CONNECTING,
                  outputs=[start_connecting])

    # Follower
    # if we notice a lost connection, just wait for the Leader to notice too
    CONNECTED.upon(connection_lost_follower, enter=LONELY, outputs=[])
    LONELY.upon(rx_RECONNECT,
                enter=CONNECTING,
                outputs=[send_reconnecting, start_connecting])
    # but if they notice it first, abandon our (seemingly functional)
    # connection, then tell them that we're ready to try again
    CONNECTED.upon(rx_RECONNECT,
                   enter=ABANDONING,
                   outputs=[abandon_connection])
    ABANDONING.upon(connection_lost_follower,
                    enter=CONNECTING,
                    outputs=[send_reconnecting, start_connecting])
    # and if they notice a problem while we're still connecting, abandon our
    # incomplete attempt and try again. in this case we don't have to wait
    # for a connection to finish shutdown
    CONNECTING.upon(
        rx_RECONNECT,
        enter=CONNECTING,
        outputs=[stop_connecting, send_reconnecting, start_connecting])

    # rx_HINTS never changes state, they're just accepted or ignored
    WANTING.upon(rx_HINTS, enter=WANTING, outputs=[])  # too early
    CONNECTING.upon(rx_HINTS, enter=CONNECTING, outputs=[use_hints])
    CONNECTED.upon(rx_HINTS, enter=CONNECTED, outputs=[])  # too late, ignore
    FLUSHING.upon(rx_HINTS, enter=FLUSHING, outputs=[])  # stale, ignore
    LONELY.upon(rx_HINTS, enter=LONELY, outputs=[])  # stale, ignore
    ABANDONING.upon(rx_HINTS, enter=ABANDONING, outputs=[])  # shouldn't happen
    STOPPING.upon(rx_HINTS, enter=STOPPING, outputs=[])

    WANTING.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    CONNECTING.upon(stop,
                    enter=STOPPED,
                    outputs=[stop_connecting, notify_stopped])
    CONNECTED.upon(stop, enter=STOPPING, outputs=[abandon_connection])
    ABANDONING.upon(stop, enter=STOPPING, outputs=[])
    FLUSHING.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    LONELY.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    STOPPING.upon(connection_lost_leader,
                  enter=STOPPED,
                  outputs=[notify_stopped])
    STOPPING.upon(connection_lost_follower,
                  enter=STOPPED,
                  outputs=[notify_stopped])
Example #20
0
class DilatedConnectionProtocol(Protocol, object):
    """I manage an L2 connection.

    When a new L2 connection is needed (as determined by the Leader),
    both Leader and Follower will initiate many simultaneous connections
    (probably TCP, but conceivably others). A subset will actually
    connect. A subset of those will successfully pass negotiation by
    exchanging handshakes to demonstrate knowledge of the session key.
    One of the negotiated connections will be selected by the Leader for
    active use, and the others will be dropped.

    At any given time, there is at most one active L2 connection.
    """

    _eventual_queue = attrib(repr=False)
    _role = attrib()
    _description = attrib()
    _connector = attrib(validator=provides(IDilationConnector), repr=False)
    _noise = attrib(repr=False)
    _outbound_prologue = attrib(validator=instance_of(bytes), repr=False)
    _inbound_prologue = attrib(validator=instance_of(bytes), repr=False)

    _use_relay = False
    _relay_handshake = None

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        self._manager = None  # set if/when we are selected
        self._disconnected = OneShotObserver(self._eventual_queue)
        self._can_send_records = False
        self._inbound_record_queue = []

    @m.state(initial=True)
    def unselected(self):
        pass  # pragma: no cover

    @m.state()
    def selecting(self):
        pass  # pragma: no cover

    @m.state()
    def selected(self):
        pass  # pragma: no cover

    @m.input()
    def got_kcm(self):
        pass

    @m.input()
    def select(self, manager):
        pass  # fires set_manager()

    @m.input()
    def got_record(self, record):
        pass

    @m.output()
    def add_candidate(self):
        self._connector.add_candidate(self)

    @m.output()
    def queue_inbound_record(self, record):
        # the Follower will see a dataReceived chunk containing both the KCM
        # (leader says we've been picked) and the first record.
        # Connector.consider takes an eventual-turn to decide to accept this
        # connection, which means the record will arrive before we get
        # .select() and move to the 'selected' state where we can
        # deliver_record. So we need to queue the record for a turn. TODO:
        # when we move to the sans-io event-driven scheme, this queue
        # shouldn't be necessary
        self._inbound_record_queue.append(record)

    @m.output()
    def set_manager(self, manager):
        self._manager = manager
        self.when_disconnected().addCallback(
            lambda c: manager.connector_connection_lost())

    @m.output()
    def can_send_records(self, manager):
        self._can_send_records = True

    @m.output()
    def process_inbound_queue(self, manager):
        while self._inbound_record_queue:
            r = self._inbound_record_queue.pop(0)
            self._manager.got_record(r)

    @m.output()
    def deliver_record(self, record):
        self._manager.got_record(record)

    unselected.upon(got_kcm, outputs=[add_candidate], enter=selecting)
    selecting.upon(got_record, outputs=[queue_inbound_record], enter=selecting)
    selecting.upon(
        select,
        outputs=[set_manager, can_send_records, process_inbound_queue],
        enter=selected)
    selected.upon(got_record, outputs=[deliver_record], enter=selected)

    # called by Connector

    def use_relay(self, relay_handshake):
        assert isinstance(relay_handshake, bytes)
        self._use_relay = True
        self._relay_handshake = relay_handshake

    def when_disconnected(self):
        return self._disconnected.when_fired()

    def disconnect(self):
        self.transport.loseConnection()

    # select() called by Connector

    # called by Manager
    def send_record(self, record):
        assert self._can_send_records
        self._record.send_record(record)

    # IProtocol methods

    def connectionMade(self):
        try:
            framer = _Framer(self.transport, self._outbound_prologue,
                             self._inbound_prologue)
            if self._use_relay:
                framer.use_relay(self._relay_handshake)
            self._record = _Record(framer, self._noise, self._role)
            if self._role is LEADER:
                self._record.set_role_leader()
            else:
                self._record.set_role_follower()
            self._record.connectionMade()
        except:
            log.err()
            raise

    def dataReceived(self, data):
        try:
            for token in self._record.add_and_unframe(data):
                assert isinstance(token, Handshake_or_Records)
                if isinstance(token, Handshake):
                    if self._role is FOLLOWER:
                        self._record.send_record(KCM())
                elif isinstance(token, KCM):
                    # if we're the leader, add this connection as a candidate.
                    # if we're the follower, accept this connection.
                    self.got_kcm()  # connector.add_candidate()
                else:
                    self.got_record(token)  # manager.got_record()
        except Disconnect:
            self.transport.loseConnection()

    def connectionLost(self, why=None):
        self._disconnected.fire(self)
Example #21
0
class _Framer(object):
    _transport = attrib(validator=provides(ITransport))
    _outbound_prologue = attrib(validator=instance_of(bytes))
    _inbound_prologue = attrib(validator=instance_of(bytes))
    _buffer = b""
    _can_send_frames = False

    # in: use_relay
    # in: connectionMade, dataReceived
    # out: prologue_received, frame_received
    # out (shared): transport.loseConnection
    # out (shared): transport.write (relay handshake, prologue)
    # states: want_relay, want_prologue, want_frame
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    @m.state()
    def want_relay(self):
        pass  # pragma: no cover

    @m.state(initial=True)
    def want_prologue(self):
        pass  # pragma: no cover

    @m.state()
    def want_frame(self):
        pass  # pragma: no cover

    @m.input()
    def use_relay(self, relay_handshake):
        pass

    @m.input()
    def connectionMade(self):
        pass

    @m.input()
    def parse(self):
        pass

    @m.input()
    def got_relay_ok(self):
        pass

    @m.input()
    def got_prologue(self):
        pass

    @m.output()
    def store_relay_handshake(self, relay_handshake):
        self._outbound_relay_handshake = relay_handshake
        self._expected_relay_handshake = b"ok\n"  # TODO: make this configurable

    @m.output()
    def send_relay_handshake(self):
        self._transport.write(self._outbound_relay_handshake)

    @m.output()
    def send_prologue(self):
        self._transport.write(self._outbound_prologue)

    @m.output()
    def parse_relay_ok(self):
        if self._get_expected("relay_ok", self._expected_relay_handshake):
            return RelayOK()

    @m.output()
    def parse_prologue(self):
        if self._get_expected("prologue", self._inbound_prologue):
            return Prologue()

    @m.output()
    def can_send_frames(self):
        self._can_send_frames = True  # for assertion in send_frame()

    @m.output()
    def parse_frame(self):
        if len(self._buffer) < 4:
            return None
        frame_length = from_be4(self._buffer[0:4])
        if len(self._buffer) < 4 + frame_length:
            return None
        frame = self._buffer[4:4 + frame_length]
        self._buffer = self._buffer[4 + frame_length:]  # TODO: avoid copy
        return Frame(frame=frame)

    want_prologue.upon(use_relay,
                       outputs=[store_relay_handshake],
                       enter=want_relay)

    want_relay.upon(connectionMade,
                    outputs=[send_relay_handshake],
                    enter=want_relay)
    want_relay.upon(parse,
                    outputs=[parse_relay_ok],
                    enter=want_relay,
                    collector=first)
    want_relay.upon(got_relay_ok, outputs=[send_prologue], enter=want_prologue)

    want_prologue.upon(connectionMade,
                       outputs=[send_prologue],
                       enter=want_prologue)
    want_prologue.upon(parse,
                       outputs=[parse_prologue],
                       enter=want_prologue,
                       collector=first)
    want_prologue.upon(got_prologue,
                       outputs=[can_send_frames],
                       enter=want_frame)

    want_frame.upon(parse,
                    outputs=[parse_frame],
                    enter=want_frame,
                    collector=first)

    def _get_expected(self, name, expected):
        lb = len(self._buffer)
        le = len(expected)
        if self._buffer.startswith(expected):
            # if the buffer starts with the expected string, consume it and
            # return True
            self._buffer = self._buffer[le:]
            return True
        if not expected.startswith(self._buffer):
            # we're not on track: the data we've received so far does not
            # match the expected value, so this can't possibly be right.
            # Don't complain until we see the expected length, or a newline,
            # so we can capture the weird input in the log for debugging.
            if (b"\n" in self._buffer or lb >= le):
                log.msg("bad {}: {}".format(name, self._buffer[:le]))
                raise Disconnect()
            return False  # wait a bit longer
        # good so far, just waiting for the rest
        return False

    # external API is: connectionMade, add_and_parse, and send_frame

    def add_and_parse(self, data):
        # we can't make this an @m.input because we can't change the state
        # from within an input. Instead, let the state choose the parser to
        # use, then use the parsed token to drive a state transition.
        self._buffer += data
        while True:
            # it'd be nice to use an iterator here, but since self.parse()
            # dispatches to a different parser (depending upon the current
            # state), we'd be using multiple iterators
            token = self.parse()
            if isinstance(token, RelayOK):
                self.got_relay_ok()
            elif isinstance(token, Prologue):
                self.got_prologue()
                yield token  # triggers send_handshake
            elif isinstance(token, Frame):
                yield token
            else:
                break

    def send_frame(self, frame):
        assert self._can_send_frames
        self._transport.write(to_be4(len(frame)) + frame)
Example #22
0
class Boss(object):
    _W = attrib()
    _side = attrib(validator=instance_of(type(u"")))
    _url = attrib(validator=instance_of(type(u"")))
    _appid = attrib(validator=instance_of(type(u"")))
    _versions = attrib(validator=instance_of(dict))
    _client_version = attrib(validator=instance_of(tuple))
    _reactor = attrib()
    _eventual_queue = attrib()
    _cooperator = attrib()
    _journal = attrib(validator=provides(_interfaces.IJournal))
    _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

    def __attrs_post_init__(self):
        self._build_workers()
        self._init_other_state()

    def _build_workers(self):
        self._N = Nameplate()
        self._M = Mailbox(self._side)
        self._S = Send(self._side, self._timing)
        self._O = Order(self._side, self._timing)
        self._K = Key(self._appid, self._versions, self._side, self._timing)
        self._R = Receive(self._side, self._timing)
        self._RC = RendezvousConnector(self._url, self._appid, self._side,
                                       self._reactor, self._journal, self._tor,
                                       self._timing, self._client_version)
        self._L = Lister(self._timing)
        self._A = Allocator(self._timing)
        self._I = Input(self._timing)
        self._C = Code(self._timing)
        self._T = Terminator()
        self._D = Dilator(self._reactor, self._eventual_queue,
                          self._cooperator)

        self._N.wire(self._M, self._I, self._RC, self._T)
        self._M.wire(self._N, self._RC, self._O, self._T)
        self._S.wire(self._M)
        self._O.wire(self._K, self._R)
        self._K.wire(self, self._M, self._R)
        self._R.wire(self, self._S)
        self._RC.wire(self, self._N, self._M, self._A, self._L, self._T)
        self._L.wire(self._RC, self._I)
        self._A.wire(self._RC, self._C)
        self._I.wire(self._C, self._L)
        self._C.wire(self, self._A, self._N, self._K, self._I)
        self._T.wire(self, self._RC, self._N, self._M, self._D)
        self._D.wire(self._S, self._T)

    def _init_other_state(self):
        self._did_start_code = False
        self._next_tx_phase = 0
        self._next_rx_phase = 0
        self._rx_phases = {}  # phase -> plaintext

        self._next_rx_dilate_seqnum = 0
        self._rx_dilate_seqnums = {}  # seqnum -> plaintext

        self._result = "empty"

    # these methods are called from outside
    def start(self):
        self._RC.start()

    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)
        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)
        file.flush()

        def output_tracer(output):
            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
        }
        for machine in which.split():
            t = (lambda old_state, input, new_state, machine=machine: self.
                 _print_trace(old_state,
                              input,
                              new_state,
                              client_name=client_name,
                              machine=machine,
                              file=file))
            names[machine].set_trace(t)
            if machine == "I":
                self._I.set_debug(t)

    # 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

    @m.state()
    def S1_lonely(self):
        pass  # pragma: no cover

    @m.state()
    def S2_happy(self):
        pass  # pragma: no cover

    @m.state()
    def S3_closing(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def S4_closed(self):
        pass  # pragma: no cover

    # from the Wormhole

    # input/allocate/set_code are regular methods, not state-transition
    # inputs. We expect them to be called just after initialization, while
    # we're in the S0_empty state. You must call exactly one of them, and the
    # call must happen while we're in S0_empty, which makes them good
    # candiates for being a proper @m.input, but set_code() will immediately
    # (reentrantly) cause self.got_code() to be fired, which is messy. These
    # are all passthroughs to the Code machine, so one alternative would be
    # to have Wormhole call Code.{input,allocate,set_code} instead, but that
    # would require the Wormhole to be aware of Code (whereas right now
    # Wormhole only knows about this Boss instance, and everything else is
    # hidden away).
    def input_code(self):
        if self._did_start_code:
            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
        if self._did_start_code:
            raise OnlyOneCodeError()
        self._did_start_code = True
        self._C.set_code(code)

    def dilate(self):
        return self._D.dilate()  # fires with endpoints

    @m.input()
    def send(self, plaintext):
        pass

    @m.input()
    def close(self):
        pass

    # from RendezvousConnector:
    # * "rx_welcome" is the Welcome message, which might signal an error, or
    #   our welcome_handler might signal one
    # * "rx_error" is error message from the server (probably because of
    #   something we said badly, or due to CrowdedError)
    # * "error" is when an exception happened while it tried to deliver
    #   something else
    def rx_welcome(self, welcome):
        try:
            if "error" in welcome:
                raise WelcomeError(welcome["error"])
            # TODO: it'd be nice to not call the handler when we're in
            # S3_closing or S4_closed states. I tried to implement this with
            # rx_welcome as an @input, but in the error case I'd be
            # delivering a new input (rx_error or something) while in the
            # middle of processing the rx_welcome input, and I wasn't sure
            # Automat would handle that correctly.
            self._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

    @m.input()
    def rx_error(self, errmsg, orig):
        pass

    @m.input()
    def error(self, err):
        pass

    # from Code (provoked by input/allocate/set_code)
    @m.input()
    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

    @m.input()
    def scared(self):
        pass

    def got_message(self, phase, plaintext):
        assert isinstance(phase, type("")), type(phase)
        assert isinstance(plaintext, type(b"")), type(plaintext)
        d_mo = re.search(r'^dilate-(\d+)$', phase)
        if phase == "version":
            self._got_version(plaintext)
        elif d_mo:
            self._got_dilate(int(d_mo.group(1)), plaintext)
        elif re.search(r'^\d+$', phase):
            self._got_phase(int(phase), plaintext)
        else:
            # 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

    @m.input()
    def _got_phase(self, phase, plaintext):
        pass

    @m.input()
    def _got_dilate(self, seqnum, plaintext):
        pass

    @m.input()
    def got_key(self, key):
        pass

    @m.input()
    def got_verifier(self, verifier):
        pass

    # Terminator sends closed
    @m.input()
    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
        # in the future, this is how Dilation is signalled
        self._their_versions = bytes_to_dict(plaintext)
        self._D.got_wormhole_versions(self._their_versions)
        # but this part is app-to-app
        app_versions = self._their_versions.get("app_versions", {})
        self._W.got_versions(app_versions)

    @m.output()
    def S_send(self, plaintext):
        assert isinstance(plaintext, type(b"")), type(plaintext)
        phase = self._next_tx_phase
        self._next_tx_phase += 1
        self._S.send("%d" % phase, plaintext)

    @m.output()
    def close_unwelcome(self, welcome_error):
        # assert isinstance(err, WelcomeError)
        self._result = welcome_error
        self._T.close("unwelcome")

    @m.output()
    def close_error(self, errmsg, orig):
        self._result = ServerError(errmsg)
        self._T.close("errory")

    @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"
        self._T.close("happy")

    @m.output()
    def W_got_key(self, key):
        self._W.got_key(key)

    @m.output()
    def D_got_key(self, key):
        self._D.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)
        # we call Wormhole.received() in strict phase order, with no gaps
        self._rx_phases[phase] = plaintext
        while self._next_rx_phase in self._rx_phases:
            self._W.received(self._rx_phases.pop(self._next_rx_phase))
            self._next_rx_phase += 1

    @m.output()
    def D_received_dilate(self, seqnum, plaintext):
        assert isinstance(seqnum, six.integer_types), type(seqnum)
        # strict phase order, no gaps
        self._rx_dilate_seqnums[seqnum] = plaintext
        while self._next_rx_dilate_seqnum in self._rx_dilate_seqnums:
            m = self._rx_dilate_seqnums.pop(self._next_rx_dilate_seqnum)
            self._D.received_dilate(m)
            self._next_rx_dilate_seqnum += 1

    @m.output()
    def W_close_with_error(self, err):
        self._result = err  # exception
        self._W.closed(self._result)

    @m.output()
    def W_closed(self):
        # result is either "happy" or a WormholeError of some sort
        self._W.closed(self._result)

    S0_empty.upon(close, enter=S3_closing, outputs=[close_lonely])
    S0_empty.upon(send, enter=S0_empty, outputs=[S_send])
    S0_empty.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
    S0_empty.upon(got_code, enter=S1_lonely, outputs=[do_got_code])
    S0_empty.upon(rx_error, enter=S3_closing, outputs=[close_error])
    S0_empty.upon(error, enter=S4_closed, outputs=[W_close_with_error])

    S1_lonely.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
    S1_lonely.upon(happy, enter=S2_happy, outputs=[])
    S1_lonely.upon(scared, enter=S3_closing, outputs=[close_scared])
    S1_lonely.upon(close, enter=S3_closing, outputs=[close_lonely])
    S1_lonely.upon(send, enter=S1_lonely, outputs=[S_send])
    S1_lonely.upon(got_key, enter=S1_lonely, outputs=[W_got_key, D_got_key])
    S1_lonely.upon(rx_error, enter=S3_closing, outputs=[close_error])
    S1_lonely.upon(error, enter=S4_closed, outputs=[W_close_with_error])

    S2_happy.upon(rx_unwelcome, enter=S3_closing, outputs=[close_unwelcome])
    S2_happy.upon(got_verifier, enter=S2_happy, outputs=[W_got_verifier])
    S2_happy.upon(_got_phase, enter=S2_happy, outputs=[W_received])
    S2_happy.upon(_got_version, enter=S2_happy, outputs=[process_version])
    S2_happy.upon(_got_dilate, enter=S2_happy, outputs=[D_received_dilate])
    S2_happy.upon(scared, enter=S3_closing, outputs=[close_scared])
    S2_happy.upon(close, enter=S3_closing, outputs=[close_happy])
    S2_happy.upon(send, enter=S2_happy, outputs=[S_send])
    S2_happy.upon(rx_error, enter=S3_closing, outputs=[close_error])
    S2_happy.upon(error, enter=S4_closed, outputs=[W_close_with_error])

    S3_closing.upon(rx_unwelcome, enter=S3_closing, outputs=[])
    S3_closing.upon(rx_error, enter=S3_closing, outputs=[])
    S3_closing.upon(got_verifier, enter=S3_closing, outputs=[])
    S3_closing.upon(_got_phase, enter=S3_closing, outputs=[])
    S3_closing.upon(_got_version, enter=S3_closing, outputs=[])
    S3_closing.upon(_got_dilate, enter=S3_closing, outputs=[])
    S3_closing.upon(happy, enter=S3_closing, outputs=[])
    S3_closing.upon(scared, enter=S3_closing, outputs=[])
    S3_closing.upon(close, enter=S3_closing, outputs=[])
    S3_closing.upon(send, enter=S3_closing, outputs=[])
    S3_closing.upon(closed, enter=S4_closed, outputs=[W_closed])
    S3_closing.upon(error, enter=S4_closed, outputs=[W_close_with_error])

    S4_closed.upon(rx_unwelcome, enter=S4_closed, outputs=[])
    S4_closed.upon(got_verifier, enter=S4_closed, outputs=[])
    S4_closed.upon(_got_phase, enter=S4_closed, outputs=[])
    S4_closed.upon(_got_version, enter=S4_closed, outputs=[])
    S4_closed.upon(_got_dilate, enter=S4_closed, outputs=[])
    S4_closed.upon(happy, enter=S4_closed, outputs=[])
    S4_closed.upon(scared, enter=S4_closed, outputs=[])
    S4_closed.upon(close, enter=S4_closed, outputs=[])
    S4_closed.upon(send, enter=S4_closed, outputs=[])
    S4_closed.upon(error, enter=S4_closed, outputs=[])
Example #23
0
class Connector(object):
    """I manage a single generation of connection.

    The Manager creates one of me at a time, whenever it wants a connection
    (which is always, once w.dilate() has been called and we know the remote
    end can dilate, and is expressed by the Manager calling my .start()
    method). I am discarded when my established connection is lost (and if we
    still want to be connected, a new generation is started and a new
    Connector is created). I am also discarded if we stop wanting to be
    connected (which the Manager expresses by calling my .stop() method).

    I manage the race between multiple connections for a specific generation
    of the dilated connection.

    I send connection hints when my InboundConnectionFactory yields addresses
    (self.listener_ready), and I initiate outbond connections (with
    OutboundConnectionFactory) as I receive connection hints from my peer
    (self.got_hints). Both factories use my build_protocol() method to create
    connection.DilatedConnectionProtocol instances. I track these protocol
    instances until one finishes negotiation and wins the race. I then shut
    down the others, remember the winner as self._winning_connection, and
    deliver the winner to manager.connector_connection_made(c).

    When an active connection is lost, we call manager.connector_connection_lost,
    allowing the manager to decide whether it wants to start a new generation
    or not.
    """

    _dilation_key = attrib(validator=instance_of(type(b"")))
    _transit_relay_location = attrib(
        validator=optional(instance_of(type(u""))))
    _manager = attrib(validator=provides(IDilationManager))
    _reactor = attrib()
    _eventual_queue = attrib()
    _no_listen = attrib(validator=instance_of(bool))
    _tor = attrib()
    _timing = attrib()
    _side = attrib(validator=instance_of(type(u"")))
    # was self._side = bytes_to_hexstr(os.urandom(8)) # unicode
    _role = attrib()

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    RELAY_DELAY = 2.0

    def __attrs_post_init__(self):
        if self._transit_relay_location:
            # TODO: allow multiple hints for a single relay
            relay_hint = parse_hint_argv(self._transit_relay_location)
            relay = RelayV1Hint(hints=(relay_hint, ))
            self._transit_relays = [relay]
        else:
            self._transit_relays = []
        self._listeners = set()  # IListeningPorts that can be stopped
        self._pending_connectors = set()  # Deferreds that can be cancelled
        self._pending_connections = EmptyableSet(
            _eventual_queue=self._eventual_queue)  # Protocols to be stopped
        self._contenders = set()  # viable connections
        self._winning_connection = None
        self._timing = self._timing or DebugTiming()
        self._timing.add("transit")

    # this describes what our Connector can do, for the initial advertisement
    @classmethod
    def get_connection_abilities(klass):
        return [
            {
                "type": "direct-tcp-v1"
            },
            {
                "type": "relay-v1"
            },
        ]

    def build_protocol(self, addr, description):
        # encryption: let's use Noise NNpsk0 (or maybe NNpsk2). That uses
        # ephemeral keys plus a pre-shared symmetric key (the Transit key), a
        # different one for each potential connection.
        noise = build_noise()
        noise.set_psks(self._dilation_key)
        if self._role is LEADER:
            noise.set_as_initiator()
            outbound_prologue = PROLOGUE_LEADER
            inbound_prologue = PROLOGUE_FOLLOWER
        else:
            noise.set_as_responder()
            outbound_prologue = PROLOGUE_FOLLOWER
            inbound_prologue = PROLOGUE_LEADER
        p = DilatedConnectionProtocol(self._eventual_queue, self._role,
                                      description, self, noise,
                                      outbound_prologue, inbound_prologue)
        return p

    @m.state(initial=True)
    def connecting(self):
        pass  # pragma: no cover

    @m.state()
    def connected(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def stopped(self):
        pass  # pragma: no cover

    # TODO: unify the tense of these method-name verbs

    # add_relay() and got_hints() are called by the Manager as it receives
    # messages from our peer. stop() is called when the Manager shuts down
    @m.input()
    def add_relay(self, hint_objs):
        pass

    @m.input()
    def got_hints(self, hint_objs):
        pass

    @m.input()
    def stop(self):
        pass

    # called by ourselves, when _start_listener() is ready
    @m.input()
    def listener_ready(self, hint_objs):
        pass

    # called when DilatedConnectionProtocol submits itself, after KCM
    # received
    @m.input()
    def add_candidate(self, c):
        pass

    # called by ourselves, via consider()
    @m.input()
    def accept(self, c):
        pass

    @m.output()
    def use_hints(self, hint_objs):
        self._use_hints(hint_objs)

    @m.output()
    def publish_hints(self, hint_objs):
        self._publish_hints(hint_objs)

    def _publish_hints(self, hint_objs):
        self._manager.send_hints([encode_hint(h) for h in hint_objs])

    @m.output()
    def consider(self, c):
        self._contenders.add(c)
        if self._role is LEADER:
            # for now, just accept the first one. TODO: be clever.
            self._eventual_queue.eventually(self.accept, c)
        else:
            # the follower always uses the first contender, since that's the
            # only one the leader picked
            self._eventual_queue.eventually(self.accept, c)

    @m.output()
    def select_and_stop_remaining(self, c):
        self._winning_connection = c
        self._contenders.clear()  # we no longer care who else came close
        # remove this winner from the losers, so we don't shut it down
        self._pending_connections.discard(c)
        # shut down losing connections
        self.stop_listeners()  # TODO: maybe keep it open? NAT/p2p assist
        self.stop_pending_connectors()
        self.stop_pending_connections()

        c.select(self._manager)  # subsequent frames go directly to the manager
        # c.select also wires up when_disconnected() to fire
        # manager.connector_connection_lost(). TODO: rename this, since the
        # Connector is no longer the one calling it
        if self._role is LEADER:
            # TODO: this should live in Connection
            c.send_record(KCM())  # leader sends KCM now
        self._manager.connector_connection_made(
            c)  # manager sends frames to Connection

    @m.output()
    def stop_everything(self):
        self.stop_listeners()
        self.stop_pending_connectors()
        self.stop_pending_connections()
        self.break_cycles()

    def stop_listeners(self):
        d = DeferredList([l.stopListening() for l in self._listeners])
        self._listeners.clear()
        return d  # synchronization for tests

    def stop_pending_connectors(self):
        for d in self._pending_connectors:
            d.cancel()

    def stop_pending_connections(self):
        d = self._pending_connections.when_next_empty()
        [c.disconnect() for c in self._pending_connections]
        return d

    def break_cycles(self):
        # help GC by forgetting references to things that reference us
        self._listeners.clear()
        self._pending_connectors.clear()
        self._pending_connections.clear()
        self._winning_connection = None

    connecting.upon(listener_ready, enter=connecting, outputs=[publish_hints])
    connecting.upon(add_relay,
                    enter=connecting,
                    outputs=[use_hints, publish_hints])
    connecting.upon(got_hints, enter=connecting, outputs=[use_hints])
    connecting.upon(add_candidate, enter=connecting, outputs=[consider])
    connecting.upon(accept,
                    enter=connected,
                    outputs=[select_and_stop_remaining])
    connecting.upon(stop, enter=stopped, outputs=[stop_everything])

    # once connected, we ignore everything except stop
    connected.upon(listener_ready, enter=connected, outputs=[])
    connected.upon(add_relay, enter=connected, outputs=[])
    connected.upon(got_hints, enter=connected, outputs=[])
    # TODO: tell them to disconnect? will they hang out forever? I *think*
    # they'll drop this once they get a KCM on the winning connection.
    connected.upon(add_candidate, enter=connected, outputs=[])
    connected.upon(accept, enter=connected, outputs=[])
    connected.upon(stop, enter=stopped, outputs=[stop_everything])

    # from Manager: start, got_hints, stop
    # maybe add_candidate, accept

    def start(self):
        if not self._no_listen and not self._tor:
            addresses = self._get_listener_addresses()
            self._start_listener(addresses)
        if self._transit_relays:
            self._publish_hints(self._transit_relays)
            self._use_hints(self._transit_relays)

    def _get_listener_addresses(self):
        addresses = ipaddrs.find_addresses()
        non_loopback_addresses = [a for a in addresses if a != "127.0.0.1"]
        if non_loopback_addresses:
            # 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
        return addresses

    def _start_listener(self, addresses):
        # TODO: listen on a fixed port, if possible, for NAT/p2p benefits, also
        # to make firewall configs easier
        # TODO: retain listening port between connection generations?
        ep = serverFromString(self._reactor, "tcp:0")
        f = InboundConnectionFactory(self)
        d = ep.listen(f)

        def _listening(lp):
            # lp is an IListeningPort
            self._listeners.add(lp)  # for shutdown and tests
            portnum = lp.getHost().port
            direct_hints = [
                DirectTCPV1Hint(to_unicode(addr), portnum, 0.0)
                for addr in addresses
            ]
            self.listener_ready(direct_hints)

        d.addCallback(_listening)
        d.addErrback(log.err)

    def _schedule_connection(self, delay, h, is_relay):
        ep = endpoint_from_hint_obj(h, self._tor, self._reactor)
        desc = describe_hint_obj(h, is_relay, self._tor)
        d = deferLater(self._reactor, delay, self._connect, ep, desc, is_relay)
        d.addErrback(lambda f: f.trap(
            ConnectingCancelledError,
            ConnectionRefusedError,
            CancelledError,
        ))
        # TODO: HostnameEndpoint.connect catches CancelledError and replaces
        # it with DNSLookupError. Remove this workaround when
        # https://twistedmatrix.com/trac/ticket/9696 is fixed.
        d.addErrback(lambda f: f.trap(DNSLookupError))
        d.addErrback(log.err)
        self._pending_connectors.add(d)

    def _use_hints(self, hints):
        # first, pull out all the relays, we'll connect to them later
        relays = []
        direct = defaultdict(list)
        for h in hints:
            if isinstance(h, RelayV1Hint):
                relays.append(h)
            else:
                direct[h.priority].append(h)
        delay = 0.0
        made_direct = False
        priorities = sorted(set(direct.keys()), reverse=True)
        for p in priorities:
            for h in direct[p]:
                if isinstance(h, TorTCPV1Hint) and not self._tor:
                    continue
                self._schedule_connection(delay, h, is_relay=False)
                made_direct = True
                # Make all direct connections immediately. Later, we'll change
                # the add_candidate() function to look at the priority when
                # deciding whether to accept a successful connection or not,
                # and it can wait for more options if it sees a higher-priority
                # one still running. But if we bail on that, we might consider
                # putting an inter-direct-hint delay here to influence the
                # process.
                # delay += 1.0

        if made_direct and not self._no_listen:
            # Prefer direct connections by stalling relay connections by a
            # few seconds. We don't wait until direct connections have
            # failed, because many direct hints will be to unused
            # local-network IP address, which won't answer, and can take the
            # full 30s TCP timeout to fail.
            #
            # If we didn't make any direct connections, or we're using
            # --no-listen, then we're probably going to have to use the
            # relay, so don't delay it at all.
            delay += self.RELAY_DELAY

        # It might be nice to wire this so that a failure in the direct hints
        # causes the relay hints to be used right away (fast failover). But
        # none of our current use cases would take advantage of that: if we
        # have any viable direct hints, then they're either going to succeed
        # quickly or hang for a long time.
        for r in relays:
            for h in r.hints:
                self._schedule_connection(delay, h, is_relay=True)
        # TODO:
        # if not contenders:
        #    raise TransitError("No contenders for connection")

    # TODO: add 2*TIMEOUT deadline for first generation, don't wait forever for
    # the initial connection

    def _connect(self, ep, description, is_relay=False):
        relay_handshake = None
        if is_relay:
            relay_handshake = build_sided_relay_handshake(
                self._dilation_key, self._side)
        f = OutboundConnectionFactory(self, relay_handshake, description)
        d = ep.connect(f)

        # fires with protocol, or ConnectError

        def _connected(p):
            self._pending_connections.add(p)
            # c might not be in _pending_connections, if it turned out to be a
            # winner, which is why we use discard() and not remove()
            p.when_disconnected().addCallback(
                self._pending_connections.discard)

        d.addCallback(_connected)
        return d
class Key(object):
    _appid = attrib(validator=instance_of(type(u"")))
    _versions = attrib(validator=instance_of(dict))
    _side = attrib(validator=instance_of(type(u"")))
    _timing = attrib(validator=provides(_interfaces.ITiming))
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace", lambda self, f: None)

    def __attrs_post_init__(self):
        self._SK = _SortedKey(self._appid, self._versions, self._side,
                              self._timing)
        self._debug_pake_stashed = False  # for tests

    def wire(self, boss, mailbox, receive):
        self._SK.wire(boss, mailbox, receive)

    @m.state(initial=True)
    def S00(self):
        pass  # pragma: no cover

    @m.state()
    def S01(self):
        pass  # pragma: no cover

    @m.state()
    def S10(self):
        pass  # pragma: no cover

    @m.state()
    def S11(self):
        pass  # pragma: no cover

    @m.input()
    def got_code(self, code):
        pass

    @m.input()
    def got_pake(self, body):
        pass

    @m.output()
    def stash_pake(self, body):
        self._pake = body
        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)
        self._SK.got_pake(self._pake)

    S00.upon(got_code, enter=S10, outputs=[deliver_code])
    S10.upon(got_pake, enter=S11, outputs=[deliver_pake])
    S00.upon(got_pake, enter=S01, outputs=[stash_pake])
    S01.upon(got_code, enter=S11, outputs=[deliver_code_and_stashed_pake])
Example #25
0
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

    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

    @m.state(terminal=True)
    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))
        assert isinstance(side, type("")), type(phase)
        assert isinstance(phase, type("")), type(phase)
        assert isinstance(body, type(b"")), type(body)
        if phase == "pake":
            self.got_pake(side, phase, body)
        else:
            self.got_non_pake(side, phase, body)

    @m.input()
    def got_pake(self, side, phase, body):
        pass

    @m.input()
    def got_non_pake(self, side, phase, body):
        pass

    @m.output()
    def queue(self, side, phase, body):
        assert isinstance(side, type("")), type(phase)
        assert isinstance(phase, type("")), type(phase)
        assert isinstance(body, type(b"")), type(body)
        self._queue.append((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
        del body
        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)

    def _deliver(self, side, phase, body):
        self._R.got_message(side, phase, body)

    S0_no_pake.upon(got_non_pake, enter=S0_no_pake, outputs=[queue])
    S0_no_pake.upon(got_pake, enter=S1_yes_pake, outputs=[notify_key, drain])
    S1_yes_pake.upon(got_non_pake, enter=S1_yes_pake, outputs=[deliver])
Example #26
0
class Input(object):
    _timing = attrib(validator=provides(_interfaces.ITiming))
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        self._all_nameplates = set()
        self._nameplate = None
        self._wordlist = None
        self._wordlist_waiters = []
        self._trace = None

    def set_debug(self, f):
        self._trace = f

    def _debug(self, what):  # pragma: no cover
        if self._trace:
            self._trace(old_state="", input=what, new_state="")

    def wire(self, code, lister):
        self._C = _interfaces.ICode(code)
        self._L = _interfaces.ILister(lister)

    def when_wordlist_is_available(self):
        if self._wordlist:
            return defer.succeed(None)
        d = defer.Deferred()
        self._wordlist_waiters.append(d)
        return d

    @m.state(initial=True)
    def S0_idle(self):
        pass  # pragma: no cover

    @m.state()
    def S1_typing_nameplate(self):
        pass  # pragma: no cover

    @m.state()
    def S2_typing_code_no_wordlist(self):
        pass  # pragma: no cover

    @m.state()
    def S3_typing_code_yes_wordlist(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def S4_done(self):
        pass  # pragma: no cover

    # from Code
    @m.input()
    def start(self):
        pass

    # from Lister
    @m.input()
    def got_nameplates(self, all_nameplates):
        pass

    # from Nameplate
    @m.input()
    def got_wordlist(self, wordlist):
        pass

    # API provided to app as ICodeInputHelper
    @m.input()
    def refresh_nameplates(self):
        pass

    @m.input()
    def get_nameplate_completions(self, prefix):
        pass

    def choose_nameplate(self, nameplate):
        validate_nameplate(nameplate)  # can raise KeyFormatError
        self._choose_nameplate(nameplate)

    @m.input()
    def _choose_nameplate(self, nameplate):
        pass

    @m.input()
    def get_word_completions(self, prefix):
        pass

    @m.input()
    def choose_words(self, words):
        pass

    @m.output()
    def do_start(self):
        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()
        for nameplate in self._all_nameplates:
            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 + "-")
        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:
            d = self._wordlist_waiters.pop()
            d.callback(None)

    @m.output()
    def no_word_completions(self, prefix):
        return set()

    @m.output()
    def _get_word_completions(self, prefix):
        assert self._wordlist
        return self._wordlist.get_completions(prefix)

    @m.output()
    def raise_must_choose_nameplate1(self, prefix):
        raise errors.MustChooseNameplateFirstError()

    @m.output()
    def raise_must_choose_nameplate2(self, words):
        raise errors.MustChooseNameplateFirstError()

    @m.output()
    def raise_already_chose_nameplate1(self):
        raise errors.AlreadyChoseNameplateError()

    @m.output()
    def raise_already_chose_nameplate2(self, prefix):
        raise errors.AlreadyChoseNameplateError()

    @m.output()
    def raise_already_chose_nameplate3(self, nameplate):
        raise errors.AlreadyChoseNameplateError()

    @m.output()
    def raise_already_chose_words1(self, prefix):
        raise errors.AlreadyChoseWordsError()

    @m.output()
    def raise_already_chose_words2(self, words):
        raise errors.AlreadyChoseWordsError()

    @m.output()
    def do_words(self, words):
        code = self._nameplate + "-" + words
        self._start_timing.finish()
        self._C.finished_input(code)

    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])
    # 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])

    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=[])
    # got_wordlist: should never happen
    S3_typing_code_yes_wordlist.upon(refresh_nameplates,
                                     enter=S3_typing_code_yes_wordlist,
                                     outputs=[raise_already_chose_nameplate1])
    S3_typing_code_yes_wordlist.upon(get_nameplate_completions,
                                     enter=S3_typing_code_yes_wordlist,
                                     outputs=[raise_already_chose_nameplate2])
    S3_typing_code_yes_wordlist.upon(_choose_nameplate,
                                     enter=S3_typing_code_yes_wordlist,
                                     outputs=[raise_already_chose_nameplate3])
    S3_typing_code_yes_wordlist.upon(get_word_completions,
                                     enter=S3_typing_code_yes_wordlist,
                                     outputs=[_get_word_completions],
                                     collector=first)
    S3_typing_code_yes_wordlist.upon(choose_words,
                                     enter=S4_done,
                                     outputs=[do_words])

    S4_done.upon(got_nameplates, enter=S4_done, outputs=[])
    S4_done.upon(got_wordlist, enter=S4_done, outputs=[])
    S4_done.upon(refresh_nameplates,
                 enter=S4_done,
                 outputs=[raise_already_chose_nameplate1])
    S4_done.upon(get_nameplate_completions,
                 enter=S4_done,
                 outputs=[raise_already_chose_nameplate2])
    S4_done.upon(_choose_nameplate,
                 enter=S4_done,
                 outputs=[raise_already_chose_nameplate3])
    S4_done.upon(get_word_completions,
                 enter=S4_done,
                 outputs=[raise_already_chose_words1])
    S4_done.upon(choose_words,
                 enter=S4_done,
                 outputs=[raise_already_chose_words2])
class _SortedKey(object):
    _appid = attrib(validator=instance_of(type(u"")))
    _versions = attrib(validator=instance_of(dict))
    _side = attrib(validator=instance_of(type(u"")))
    _timing = attrib(validator=provides(_interfaces.ITiming))
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace", lambda self, f: None)

    def wire(self, boss, mailbox, receive):
        self._B = _interfaces.IBoss(boss)
        self._M = _interfaces.IMailbox(mailbox)
        self._R = _interfaces.IReceive(receive)

    @m.state(initial=True)
    def S0_know_nothing(self):
        pass  # pragma: no cover

    @m.state()
    def S1_know_code(self):
        pass  # pragma: no cover

    @m.state()
    def S2_know_key(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def S3_scared(self):
        pass  # pragma: no cover

    # from Boss
    @m.input()
    def got_code(self, code):
        pass

    # from Ordering
    def got_pake(self, body):
        assert isinstance(body, type(b"")), type(body)
        payload = bytes_to_dict(body)
        if "pake_v1" in payload:
            self.got_pake_good(hexstr_to_bytes(payload["pake_v1"]))
        else:
            self.got_pake_bad()

    @m.input()
    def got_pake_good(self, msg2):
        pass

    @m.input()
    def got_pake_bad(self):
        pass

    @m.output()
    def build_pake(self, code):
        with self._timing.add("pake1", waiting="crypto"):
            self._sp = SPAKE2_Symmetric(to_bytes(code),
                                        idSymmetric=to_bytes(self._appid))
            msg1 = self._sp.start()
        body = dict_to_bytes({"pake_v1": bytes_to_hexstr(msg1)})
        self._M.add_message("pake", body)

    @m.output()
    def scared(self):
        self._B.scared()

    @m.output()
    def compute_key(self, msg2):
        assert isinstance(msg2, type(b""))
        with self._timing.add("pake2", waiting="crypto"):
            key = self._sp.finish(msg2)
        self._B.got_key(key)
        phase = "version"
        data_key = derive_phase_key(key, self._side, phase)
        plaintext = dict_to_bytes(self._versions)
        encrypted = encrypt_data(data_key, plaintext)
        self._M.add_message(phase, encrypted)
        self._R.got_key(key)

    S0_know_nothing.upon(got_code, enter=S1_know_code, outputs=[build_pake])
    S1_know_code.upon(got_pake_good, enter=S2_know_key, outputs=[compute_key])
    S1_know_code.upon(got_pake_bad, enter=S3_scared, outputs=[scared])
Example #28
0
class Mailbox(object):
    _side = attrib(validator=instance_of(type(u"")))
    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        self._mailbox = None
        self._pending_outbound = {}
        self._processed = set()

    def wire(self, nameplate, rendezvous_connector, ordering, terminator):
        self._N = _interfaces.INameplate(nameplate)
        self._RC = _interfaces.IRendezvousConnector(rendezvous_connector)
        self._O = _interfaces.IOrder(ordering)
        self._T = _interfaces.ITerminator(terminator)

    # all -A states: not connected
    # all -B states: yes connected
    # B states serialize as A, so they deserialize as unconnected

    # S0: know nothing
    @m.state(initial=True)
    def S0A(self):
        pass  # pragma: no cover

    @m.state()
    def S0B(self):
        pass  # pragma: no cover

    # S1: mailbox known, not opened
    @m.state()
    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

    @m.state()
    def S2B(self):
        pass  # pragma: no cover

    # S3: closing
    @m.state()
    def S3A(self):
        pass  # pragma: no cover

    @m.state()
    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(terminal=True)
    def S4(self):
        pass  # pragma: no cover

    S4A = S4
    S4B = S4

    # from Terminator
    @m.input()
    def close(self, mood):
        pass

    # from Nameplate
    @m.input()
    def got_mailbox(self, mailbox):
        pass

    # from RendezvousConnector
    @m.input()
    def connected(self):
        pass

    @m.input()
    def lost(self):
        pass

    def rx_message(self, side, phase, body):
        assert isinstance(side, type("")), type(side)
        assert isinstance(phase, type("")), type(phase)
        assert isinstance(body, type(b"")), type(body)
        if side == self._side:
            self.rx_message_ours(phase, body)
        else:
            self.rx_message_theirs(side, phase, body)

    @m.input()
    def rx_message_ours(self, phase, body):
        pass

    @m.input()
    def rx_message_theirs(self, side, phase, body):
        pass

    @m.input()
    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()

    S0A.upon(connected, enter=S0B, outputs=[])
    S0A.upon(got_mailbox, enter=S1A, outputs=[record_mailbox])
    S0A.upon(add_message, enter=S0A, outputs=[queue])
    S0A.upon(close, enter=S4A, outputs=[ignore_mood_and_T_mailbox_done])
    S0B.upon(lost, enter=S0A, outputs=[])
    S0B.upon(add_message, enter=S0B, outputs=[queue])
    S0B.upon(close, enter=S4B, outputs=[ignore_mood_and_T_mailbox_done])
    S0B.upon(got_mailbox,
             enter=S2B,
             outputs=[record_mailbox_and_RC_tx_open_and_drain])

    S1A.upon(connected, enter=S2B, outputs=[RC_tx_open, drain])
    S1A.upon(add_message, enter=S1A, outputs=[queue])
    S1A.upon(close, enter=S4A, outputs=[ignore_mood_and_T_mailbox_done])

    S2A.upon(connected, enter=S2B, outputs=[RC_tx_open, drain])
    S2A.upon(add_message, enter=S2A, outputs=[queue])
    S2A.upon(close, enter=S3A, outputs=[record_mood])
    S2B.upon(lost, enter=S2A, outputs=[])
    S2B.upon(add_message, enter=S2B, outputs=[queue, RC_tx_add])
    S2B.upon(rx_message_theirs, enter=S2B, outputs=[N_release_and_accept])
    S2B.upon(rx_message_ours, enter=S2B, outputs=[dequeue])
    S2B.upon(close, enter=S3B, outputs=[record_mood_and_RC_tx_close])

    S3A.upon(connected, enter=S3B, outputs=[RC_tx_close])
    S3B.upon(lost, enter=S3A, outputs=[])
    S3B.upon(rx_closed, enter=S4B, outputs=[T_mailbox_done])
    S3B.upon(add_message, enter=S3B, outputs=[])
    S3B.upon(rx_message_theirs, enter=S3B, outputs=[])
    S3B.upon(rx_message_ours, enter=S3B, outputs=[])
    S3B.upon(close, enter=S3B, outputs=[])

    S4A.upon(connected, enter=S4B, outputs=[])
    S4B.upon(lost, enter=S4A, outputs=[])
    S4.upon(add_message, enter=S4, outputs=[])
    S4.upon(rx_message_theirs, enter=S4, outputs=[])
    S4.upon(rx_message_ours, enter=S4, outputs=[])
    S4.upon(close, enter=S4, outputs=[])
Example #29
0
class Manager(object):
    _S = attrib(validator=provides(ISend), repr=False)
    _my_side = attrib(validator=instance_of(type(u"")))
    _transit_relay_location = attrib(validator=optional(instance_of(str)))
    _reactor = attrib(repr=False)
    _eventual_queue = attrib(repr=False)
    _cooperator = attrib(repr=False)
    # TODO: can this validator work when the parameter is optional?
    _no_listen = attrib(validator=instance_of(bool), default=False)

    _dilation_key = None
    _tor = None  # TODO
    _timing = None  # TODO
    _next_subchannel_id = None  # initialized in choose_role

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        self._got_versions_d = Deferred()

        self._my_role = None  # determined upon rx_PLEASE
        self._host_addr = _WormholeAddress()

        self._connection = None
        self._made_first_connection = False
        self._stopped = OneShotObserver(self._eventual_queue)
        self._debug_stall_connector = False

        self._next_dilation_phase = 0

        # I kept getting confused about which methods were for inbound data
        # (and thus flow-control methods go "out") and which were for
        # outbound data (with flow-control going "in"), so I split them up
        # into separate pieces.
        self._inbound = Inbound(self, self._host_addr)
        self._outbound = Outbound(self, self._cooperator)  # from us to peer

        # We must open subchannel0 early, since messages may arrive very
        # quickly once the connection is established. This subchannel may or
        # may not ever get revealed to the caller, since the peer might not
        # even be capable of dilation.
        scid0 = 0
        peer_addr0 = _SubchannelAddress(scid0)
        sc0 = SubChannel(scid0, self, self._host_addr, peer_addr0)
        self._inbound.set_subchannel_zero(scid0, sc0)

        # we can open non-zero subchannels as soon as we get our first
        # connection, and we can make the Endpoints even earlier
        control_ep = ControlEndpoint(peer_addr0, sc0, self._eventual_queue)
        connect_ep = SubchannelConnectorEndpoint(self, self._host_addr,
                                                 self._eventual_queue)
        listen_ep = SubchannelListenerEndpoint(self, self._host_addr,
                                               self._eventual_queue)
        # TODO: let inbound/outbound create the endpoints, then return them
        # to us
        self._inbound.set_listener_endpoint(listen_ep)

        self._endpoints = EndpointRecord(control_ep, connect_ep, listen_ep)

    def get_endpoints(self):
        return self._endpoints

    def got_dilation_key(self, key):
        assert isinstance(key, bytes)
        self._dilation_key = key

    def got_wormhole_versions(self, their_wormhole_versions):
        # this always happens before received_dilation_message
        dilation_version = None
        their_dilation_versions = set(
            their_wormhole_versions.get("can-dilate", []))
        my_versions = set(DILATION_VERSIONS)
        shared_versions = my_versions.intersection(their_dilation_versions)
        if "1" in shared_versions:
            dilation_version = "1"

        # dilation_version is the best mutually-compatible version we have
        # with the peer, or None if we have nothing in common

        if not dilation_version:  # "1" or None
            # TODO: be more specific about the error. dilation_version==None
            # means we had no version in common with them, which could either
            # be because they're so old they don't dilate at all, or because
            # they're so new that they no longer accommodate our old version
            self.fail(failure.Failure(OldPeerCannotDilateError()))

        self.start()

    def fail(self, f):
        self._endpoints.control._main_channel_failed(f)
        self._endpoints.connect._main_channel_failed(f)
        self._endpoints.listen._main_channel_failed(f)

    def received_dilation_message(self, plaintext):
        # this receives new in-order DILATE-n payloads, decrypted but not
        # de-JSONed.

        message = bytes_to_dict(plaintext)
        type = message["type"]
        if type == "please":
            self.rx_PLEASE(message)
        elif type == "connection-hints":
            self.rx_HINTS(message)
        elif type == "reconnect":
            self.rx_RECONNECT()
        elif type == "reconnecting":
            self.rx_RECONNECTING()
        else:
            log.err(UnknownDilationMessageType(message))
            return

    def when_stopped(self):
        return self._stopped.when_fired()

    def send_dilation_phase(self, **fields):
        dilation_phase = self._next_dilation_phase
        self._next_dilation_phase += 1
        self._S.send("dilate-%d" % dilation_phase, dict_to_bytes(fields))

    def send_hints(self, hints):  # from Connector
        self.send_dilation_phase(type="connection-hints", hints=hints)

    # forward inbound-ish things to _Inbound

    def subchannel_pauseProducing(self, sc):
        self._inbound.subchannel_pauseProducing(sc)

    def subchannel_resumeProducing(self, sc):
        self._inbound.subchannel_resumeProducing(sc)

    def subchannel_stopProducing(self, sc):
        self._inbound.subchannel_stopProducing(sc)

    def subchannel_local_open(self, scid, sc):
        self._inbound.subchannel_local_open(scid, sc)

    # forward outbound-ish things to _Outbound
    def subchannel_registerProducer(self, sc, producer, streaming):
        self._outbound.subchannel_registerProducer(sc, producer, streaming)

    def subchannel_unregisterProducer(self, sc):
        self._outbound.subchannel_unregisterProducer(sc)

    def send_open(self, scid):
        assert isinstance(scid, six.integer_types)
        self._queue_and_send(Open, scid)

    def send_data(self, scid, data):
        assert isinstance(scid, six.integer_types)
        self._queue_and_send(Data, scid, data)

    def send_close(self, scid):
        assert isinstance(scid, six.integer_types)
        self._queue_and_send(Close, scid)

    def _queue_and_send(self, record_type, *args):
        r = self._outbound.build_record(record_type, *args)
        # Outbound owns the send_record() pipe, so that it can stall new
        # writes after a new connection is made until after all queued
        # messages are written (to preserve ordering).
        self._outbound.queue_and_send_record(r)  # may trigger pauseProducing

    def subchannel_closed(self, scid, sc):
        # let everyone clean up. This happens just after we delivered
        # connectionLost to the Protocol, except for the control channel,
        # which might get connectionLost later after they use ep.connect.
        # TODO: is this inversion a problem?
        self._inbound.subchannel_closed(scid, sc)
        self._outbound.subchannel_closed(scid, sc)

    # our Connector calls these

    def connector_connection_made(self, c):
        self.connection_made()  # state machine update
        self._connection = c
        self._inbound.use_connection(c)
        self._outbound.use_connection(c)  # does c.registerProducer
        if not self._made_first_connection:
            self._made_first_connection = True
            self._endpoints.control._main_channel_ready()
            self._endpoints.connect._main_channel_ready()
            self._endpoints.listen._main_channel_ready()
        pass

    def connector_connection_lost(self):
        self._stop_using_connection()
        if self._my_role is LEADER:
            self.connection_lost_leader()  # state machine
        else:
            self.connection_lost_follower()

    def _stop_using_connection(self):
        # the connection is already lost by this point
        self._connection = None
        self._inbound.stop_using_connection()
        self._outbound.stop_using_connection()  # does c.unregisterProducer

    # from our active Connection

    def got_record(self, r):
        # records with sequence numbers: always ack, ignore old ones
        if isinstance(r, (Open, Data, Close)):
            self.send_ack(r.seqnum)  # always ack, even for old ones
            if self._inbound.is_record_old(r):
                return
            self._inbound.update_ack_watermark(r.seqnum)
            if isinstance(r, Open):
                self._inbound.handle_open(r.scid)
            elif isinstance(r, Data):
                self._inbound.handle_data(r.scid, r.data)
            else:  # isinstance(r, Close)
                self._inbound.handle_close(r.scid)
            return
        if isinstance(r, KCM):
            log.err(UnexpectedKCM())
        elif isinstance(r, Ping):
            self.handle_ping(r.ping_id)
        elif isinstance(r, Pong):
            self.handle_pong(r.ping_id)
        elif isinstance(r, Ack):
            self._outbound.handle_ack(r.resp_seqnum)  # retire queued messages
        else:
            log.err(UnknownMessageType("{}".format(r)))

    # pings, pongs, and acks are not queued
    def send_ping(self, ping_id):
        self._outbound.send_if_connected(Ping(ping_id))

    def send_pong(self, ping_id):
        self._outbound.send_if_connected(Pong(ping_id))

    def send_ack(self, resp_seqnum):
        self._outbound.send_if_connected(Ack(resp_seqnum))

    def handle_ping(self, ping_id):
        self.send_pong(ping_id)

    def handle_pong(self, ping_id):
        # TODO: update is-alive timer
        pass

    # subchannel maintenance
    def allocate_subchannel_id(self):
        scid_num = self._next_subchannel_id
        self._next_subchannel_id += 2
        return scid_num

    # state machine

    @m.state(initial=True)
    def WAITING(self):
        pass  # pragma: no cover

    @m.state()
    def WANTING(self):
        pass  # pragma: no cover

    @m.state()
    def CONNECTING(self):
        pass  # pragma: no cover

    @m.state()
    def CONNECTED(self):
        pass  # pragma: no cover

    @m.state()
    def FLUSHING(self):
        pass  # pragma: no cover

    @m.state()
    def ABANDONING(self):
        pass  # pragma: no cover

    @m.state()
    def LONELY(self):
        pass  # pragma: no cover

    @m.state()
    def STOPPING(self):
        pass  # pragma: no cover

    @m.state(terminal=True)
    def STOPPED(self):
        pass  # pragma: no cover

    @m.input()
    def start(self):
        pass  # pragma: no cover

    @m.input()
    def rx_PLEASE(self, message):
        pass  # pragma: no cover

    @m.input()  # only sent by Follower
    def rx_HINTS(self, hint_message):
        pass  # pragma: no cover

    @m.input()  # only Leader sends RECONNECT, so only Follower receives it
    def rx_RECONNECT(self):
        pass  # pragma: no cover

    @m.input()  # only Follower sends RECONNECTING, so only Leader receives it
    def rx_RECONNECTING(self):
        pass  # pragma: no cover

    # Connector gives us connection_made()
    @m.input()
    def connection_made(self):
        pass  # pragma: no cover

    # our connection_lost() fires connection_lost_leader or
    # connection_lost_follower depending upon our role. If either side sees a
    # problem with the connection (timeouts, bad authentication) then they
    # just drop it and let connection_lost() handle the cleanup.
    @m.input()
    def connection_lost_leader(self):
        pass  # pragma: no cover

    @m.input()
    def connection_lost_follower(self):
        pass

    @m.input()
    def stop(self):
        pass  # pragma: no cover

    @m.output()
    def send_please(self):
        self.send_dilation_phase(type="please", side=self._my_side)

    @m.output()
    def choose_role(self, message):
        their_side = message["side"]
        if self._my_side > their_side:
            self._my_role = LEADER
            # scid 0 is reserved for the control channel. the leader uses odd
            # numbers starting with 1
            self._next_subchannel_id = 1
        elif their_side > self._my_side:
            self._my_role = FOLLOWER
            # the follower uses even numbers starting with 2
            self._next_subchannel_id = 2
        else:
            raise ValueError("their side shouldn't be equal: reflection?")

    # these Outputs behave differently for the Leader vs the Follower

    @m.output()
    def start_connecting_ignore_message(self, message):
        del message  # ignored
        return self._start_connecting()

    @m.output()
    def start_connecting(self):
        self._start_connecting()

    def _start_connecting(self):
        assert self._my_role is not None
        assert self._dilation_key is not None
        self._connector = Connector(
            self._dilation_key,
            self._transit_relay_location,
            self,
            self._reactor,
            self._eventual_queue,
            self._no_listen,
            self._tor,
            self._timing,
            self._my_side,  # needed for relay handshake
            self._my_role)
        if self._debug_stall_connector:
            # unit tests use this hook to send messages while we know we
            # don't have a connection
            self._eventual_queue.eventually(self._debug_stall_connector,
                                            self._connector)
            return
        self._connector.start()

    @m.output()
    def send_reconnect(self):
        self.send_dilation_phase(type="reconnect")  # TODO: generation number?

    @m.output()
    def send_reconnecting(self):
        self.send_dilation_phase(type="reconnecting")  # TODO: generation?

    @m.output()
    def use_hints(self, hint_message):
        hint_objs = filter(
            lambda h: h,  # ignore None, unrecognizable
            [parse_hint(hs) for hs in hint_message["hints"]])
        hint_objs = list(hint_objs)
        self._connector.got_hints(hint_objs)

    @m.output()
    def stop_connecting(self):
        self._connector.stop()

    @m.output()
    def abandon_connection(self):
        # we think we're still connected, but the Leader disagrees. Or we've
        # been told to shut down.
        self._connection.disconnect()  # let connection_lost do cleanup

    @m.output()
    def notify_stopped(self):
        self._stopped.fire(None)

    # We are born WAITING after the local app calls w.dilate(). We enter
    # WANTING (and send a PLEASE) when we learn of a mutually-compatible
    # dilation_version.
    WAITING.upon(start, enter=WANTING, outputs=[send_please])

    # we start CONNECTING when we get rx_PLEASE
    WANTING.upon(rx_PLEASE,
                 enter=CONNECTING,
                 outputs=[choose_role, start_connecting_ignore_message])

    CONNECTING.upon(connection_made, enter=CONNECTED, outputs=[])

    # Leader
    CONNECTED.upon(connection_lost_leader,
                   enter=FLUSHING,
                   outputs=[send_reconnect])
    FLUSHING.upon(rx_RECONNECTING,
                  enter=CONNECTING,
                  outputs=[start_connecting])

    # Follower
    # if we notice a lost connection, just wait for the Leader to notice too
    CONNECTED.upon(connection_lost_follower, enter=LONELY, outputs=[])
    LONELY.upon(rx_RECONNECT,
                enter=CONNECTING,
                outputs=[send_reconnecting, start_connecting])
    # but if they notice it first, abandon our (seemingly functional)
    # connection, then tell them that we're ready to try again
    CONNECTED.upon(rx_RECONNECT,
                   enter=ABANDONING,
                   outputs=[abandon_connection])
    ABANDONING.upon(connection_lost_follower,
                    enter=CONNECTING,
                    outputs=[send_reconnecting, start_connecting])
    # and if they notice a problem while we're still connecting, abandon our
    # incomplete attempt and try again. in this case we don't have to wait
    # for a connection to finish shutdown
    CONNECTING.upon(
        rx_RECONNECT,
        enter=CONNECTING,
        outputs=[stop_connecting, send_reconnecting, start_connecting])

    # rx_HINTS never changes state, they're just accepted or ignored
    WANTING.upon(rx_HINTS, enter=WANTING, outputs=[])  # too early
    CONNECTING.upon(rx_HINTS, enter=CONNECTING, outputs=[use_hints])
    CONNECTED.upon(rx_HINTS, enter=CONNECTED, outputs=[])  # too late, ignore
    FLUSHING.upon(rx_HINTS, enter=FLUSHING, outputs=[])  # stale, ignore
    LONELY.upon(rx_HINTS, enter=LONELY, outputs=[])  # stale, ignore
    ABANDONING.upon(rx_HINTS, enter=ABANDONING, outputs=[])  # shouldn't happen
    STOPPING.upon(rx_HINTS, enter=STOPPING, outputs=[])

    WAITING.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    WANTING.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    CONNECTING.upon(stop,
                    enter=STOPPED,
                    outputs=[stop_connecting, notify_stopped])
    CONNECTED.upon(stop, enter=STOPPING, outputs=[abandon_connection])
    ABANDONING.upon(stop, enter=STOPPING, outputs=[])
    FLUSHING.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    LONELY.upon(stop, enter=STOPPED, outputs=[notify_stopped])
    STOPPING.upon(connection_lost_leader,
                  enter=STOPPED,
                  outputs=[notify_stopped])
    STOPPING.upon(connection_lost_follower,
                  enter=STOPPED,
                  outputs=[notify_stopped])
Example #30
0
class SubChannel(object):
    _id = attrib(validator=instance_of(bytes))
    _manager = attrib(validator=provides(IDilationManager))
    _host_addr = attrib(validator=instance_of(_WormholeAddress))
    _peer_addr = attrib(validator=instance_of(_SubchannelAddress))

    m = MethodicalMachine()
    set_trace = getattr(m, "_setTrace",
                        lambda self, f: None)  # pragma: no cover

    def __attrs_post_init__(self):
        # self._mailbox = None
        # self._pending_outbound = {}
        # self._processed = set()
        self._protocol = None
        self._pending_dataReceived = []
        self._pending_connectionLost = (False, None)

    @m.state(initial=True)
    def open(self):
        pass  # pragma: no cover

    @m.state()
    def closing():
        pass  # pragma: no cover

    @m.state()
    def closed():
        pass  # pragma: no cover

    @m.input()
    def remote_data(self, data):
        pass

    @m.input()
    def remote_close(self):
        pass

    @m.input()
    def local_data(self, data):
        pass

    @m.input()
    def local_close(self):
        pass

    @m.output()
    def send_data(self, data):
        self._manager.send_data(self._id, data)

    @m.output()
    def send_close(self):
        self._manager.send_close(self._id)

    @m.output()
    def signal_dataReceived(self, data):
        if self._protocol:
            self._protocol.dataReceived(data)
        else:
            self._pending_dataReceived.append(data)

    @m.output()
    def signal_connectionLost(self):
        if self._protocol:
            self._protocol.connectionLost(ConnectionDone())
        else:
            self._pending_connectionLost = (True, ConnectionDone())
        self._manager.subchannel_closed(self)
        # we're deleted momentarily

    @m.output()
    def error_closed_write(self, data):
        raise AlreadyClosedError("write not allowed on closed subchannel")

    @m.output()
    def error_closed_close(self):
        raise AlreadyClosedError(
            "loseConnection not allowed on closed subchannel")

    # primary transitions
    open.upon(remote_data, enter=open, outputs=[signal_dataReceived])
    open.upon(local_data, enter=open, outputs=[send_data])
    open.upon(remote_close, enter=closed, outputs=[signal_connectionLost])
    open.upon(local_close, enter=closing, outputs=[send_close])
    closing.upon(remote_data, enter=closing, outputs=[signal_dataReceived])
    closing.upon(remote_close, enter=closed, outputs=[signal_connectionLost])

    # error cases
    # we won't ever see an OPEN, since L4 will log+ignore those for us
    closing.upon(local_data, enter=closing, outputs=[error_closed_write])
    closing.upon(local_close, enter=closing, outputs=[error_closed_close])

    # the CLOSED state won't ever see messages, since we'll be deleted

    # our endpoints use this

    def _set_protocol(self, protocol):
        assert not self._protocol
        self._protocol = protocol
        if self._pending_dataReceived:
            for data in self._pending_dataReceived:
                self._protocol.dataReceived(data)
            self._pending_dataReceived = []
        cl, what = self._pending_connectionLost
        if cl:
            self._protocol.connectionLost(what)

    # ITransport
    def write(self, data):
        assert isinstance(data, type(b""))
        self.local_data(data)

    def writeSequence(self, iovec):
        self.write(b"".join(iovec))

    def loseConnection(self):
        self.local_close()

    def getHost(self):
        # we define "host addr" as the overall wormhole
        return self._host_addr

    def getPeer(self):
        # and "peer addr" as the subchannel within that wormhole
        return self._peer_addr

    # IProducer: throttle inbound data (wormhole "up" to local app's Protocol)
    def stopProducing(self):
        self._manager.subchannel_stopProducing(self)

    def pauseProducing(self):
        self._manager.subchannel_pauseProducing(self)

    def resumeProducing(self):
        self._manager.subchannel_resumeProducing(self)

    # IConsumer: allow the wormhole to throttle outbound data (app->wormhole)
    def registerProducer(self, producer, streaming):
        self._manager.subchannel_registerProducer(self, producer, streaming)

    def unregisterProducer(self):
        self._manager.subchannel_unregisterProducer(self)