Example #1
0
    def start_router_component(self, id, config, details=None):
        """
        Dynamically start an application component to run next to the router in "embedded mode".

        :param id: The ID of the component to start.
        :type id: str
        :param config: The component configuration.
        :type config: obj
        """
        self.log.debug("{}.start_router_component".format(self.__class__.__name__),
                       id=id, config=config)

        # prohibit starting a component twice
        #
        if id in self.components:
            emsg = "Could not start component: a component with ID '{}'' is already running (or starting)".format(id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.already_running', emsg)

        # check configuration
        #
        try:
            checkconfig.check_router_component(config)
        except Exception as e:
            emsg = "Invalid router component configuration: {}".format(e)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)
        else:
            self.log.debug("Starting {type}-component on router.",
                           type=config['type'])

        # resolve references to other entities
        #
        references = {}
        for ref in config.get('references', []):
            ref_type, ref_id = ref.split(':')
            if ref_type == u'connection':
                if ref_id in self._connections:
                    references[ref] = self._connections[ref_id]
                else:
                    emsg = "cannot resolve reference '{}' - no '{}' with ID '{}'".format(ref, ref_type, ref_id)
                    self.log.error(emsg)
                    raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)
            else:
                emsg = "cannot resolve reference '{}' - invalid reference type '{}'".format(ref, ref_type)
                self.log.error(emsg)
                raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)

        # create component config
        #
        realm = config['realm']
        extra = config.get('extra', None)
        component_config = ComponentConfig(realm=realm, extra=extra)
        create_component = _appsession_loader(config)

        # .. and create and add an WAMP application session to
        # run the component next to the router
        #
        try:
            session = create_component(component_config)

            # any exception spilling out from user code in onXXX handlers is fatal!
            def panic(fail, msg):
                self.log.error("Fatal error in component: {} - {}".format(msg, fail.value))
                session.disconnect()
            session._swallow_error = panic
        except Exception as e:
            msg = "{}".format(e).strip()
            self.log.error("Component instantiation failed:\n\n{err}", err=msg)
            raise

        self.components[id] = RouterComponent(id, config, session)
        self._router_session_factory.add(session, authrole=config.get('role', u'anonymous'))
        self.log.debug("Added component {id}", id=id)
Example #2
0
    def start_component(self,
                        component_id,
                        config,
                        reload_modules=False,
                        details=None):
        """
        Starts a component in this container worker.

        :param component_id: The ID under which to start the component.
        :type component_id: str

        :param config: Component configuration.
        :type config: dict

        :param reload_modules: If `True`, enforce reloading of modules (user code)
           that were modified (see: TrackingModuleReloader).
        :type reload_modules: bool

        :param details: Caller details.
        :type details: instance of :class:`autobahn.wamp.types.CallDetails`

        :returns: Component startup information.
        :rtype: dict
        """
        self.log.debug(u'{klass}.start_component({component_id}, {config})',
                       klass=self.__class__.__name__,
                       component_id=component_id,
                       config=config)

        # prohibit starting a component twice
        #
        if component_id in self.components:
            emsg = u'duplicate component "{}" - a component with this ID is already running (or starting)'.format(
                component_id)
            self.log.debug(emsg)
            raise ApplicationError(u'crossbar.error.already_running', emsg)

        # check component configuration
        #
        try:
            self.personality.check_container_component(self.personality,
                                                       config)
        except Exception as e:
            emsg = u'invalid container component configuration: {}'.format(e)
            self.log.debug(emsg)
            raise ApplicationError(u'crossbar.error.invalid_configuration',
                                   emsg)
        else:
            self.log.debug(u'starting component "{component_id}" ..',
                           component_id=component_id)

        # WAMP application component factory
        #
        realm = config.get(u'realm', None)
        assert type(realm) == str

        extra = config.get(u'extra', {})
        assert type(extra) == dict

        # forward crossbar node base directory
        extra['cbdir'] = self.config.extra.cbdir

        # allow access to controller session
        controller = self if self.config.extra.expose_controller else None

        # expose an object shared between components
        shared = self.components_shared if self.config.extra.expose_shared else None

        # this is the component configuration provided to the components ApplicationSession
        component_config = ComponentConfig(realm=realm,
                                           extra=extra,
                                           keyring=None,
                                           controller=controller,
                                           shared=shared)

        # define component ctor function
        try:
            create_component = _appsession_loader(config)
        except ApplicationError as e:
            # for convenience, also log failed component loading
            self.log.error(u'component loading failed', log_failure=Failure())
            if u'No module named' in str(e):
                self.log.error(u'  Python module search paths:')
                for path in e.kwargs['pythonpath']:
                    self.log.error(u'    {path}', path=path)
            raise

        # force reload of modules (user code)
        #
        if reload_modules:
            self._module_tracker.reload()

        # prepare some cleanup code in case this connection goes away
        def _closed(session, was_clean):
            """
            This is moderate hack around the fact that we don't have any way
            to "listen" for a close event on websocket or rawsocket
            objects. Also, the rawsocket implementation doesn't have
            "a" function we can wrap anyway (they are asyncio vs
            Twisted specific), so for both WebSocket and rawsocket
            cases, we actually listen on the WAMP session for
            transport close notifications.

            Ideally we'd listen for "close" on the transport but this
            works fine for cleaning up the components.
            """
            if component_id not in self.components:
                self.log.warn(
                    "Component '{id}' closed, but not in set.",
                    id=component_id,
                )
                return

            if was_clean:
                self.log.info(
                    "Closed connection to '{id}'",
                    id=component_id,
                )
            else:
                self.log.error(
                    "Lost connection to component '{id}' uncleanly",
                    id=component_id,
                )

            component = self.components[component_id]
            del self.components[component_id]
            self._publish_component_stop(component)
            component._stopped.callback(component.marshal())
            del component

            # figure out if we need to shut down the container itself or not
            if not was_clean and self._exit_mode == self.SHUTDOWN_ON_ANY_COMPONENT_FAILED:
                self.log.info(
                    "A component has failed: stopping container in exit mode <{exit_mode}> ...",
                    exit_mode=self._exit_mode,
                )
                self.shutdown()
                return

            if self._exit_mode == self.SHUTDOWN_ON_ANY_COMPONENT_STOPPED:
                self.log.info(
                    "A component has stopped: stopping container in exit mode <{exit_mode}> ...",
                    exit_mode=self._exit_mode,
                )
                self.shutdown()
                return

            if not self.components:
                if self._exit_mode == self.SHUTDOWN_ON_LAST_COMPONENT_STOPPED:
                    self.log.info(
                        "Container is hosting no more components: stopping container in exit mode <{exit_mode}> ...",
                        exit_mode=self._exit_mode,
                    )
                    self.shutdown()
                    return
                else:
                    self.log.info(
                        "Container is hosting no more components: continue running in exit mode <{exit_mode}>",
                        exit_mode=self._exit_mode,
                    )
            else:
                self.log.info(
                    "Container is still hosting {component_count} components: continue running in exit mode <{exit_mode}>",
                    exit_mode=self._exit_mode,
                    component_count=len(self.components),
                )

            # determine if we should re-start the component. Note that
            # we can only arrive here if we *didn't* decide to
            # shutdown above .. so if we have a shutdown mode of
            # SHUTDOWN_ON_ANY_COMPONENT_STOPPED will mean we never try
            # to re-start anything.
            if self._restart_mode and self._restart_mode != self.RESTART_NEVER:

                def restart_component():
                    # Think: if this below start_component() fails,
                    # we'll still schedule *exactly one* new re-start
                    # attempt for it, right?
                    self.log.info(
                        "Restarting component '{component_id}'",
                        component_id=component_id,
                    )
                    return self.start_component(
                        component_id,
                        config,
                        reload_modules=reload_modules,
                        details=details,
                    )

                # note we must yield to the reactor with
                # callLater(0, ..) to avoid infinite recurision if
                # we're stuck in a restart loop
                from twisted.internet import reactor
                if self._restart_mode == self.RESTART_ALWAYS:
                    reactor.callLater(0, restart_component)
                elif self._restart_mode == self.RESTART_FAILED and not was_clean:
                    reactor.callLater(0, restart_component)

        joined_d = Deferred()

        # WAMP application session factory
        #
        def create_session():
            try:
                session = create_component(component_config)

                # any exception spilling out from user code in onXXX handlers is fatal!
                def panic(fail, msg):
                    self.log.error(
                        "Fatal error in component: {msg} - {log_failure.value}",
                        msg=msg,
                        log_failure=fail,
                    )
                    session.disconnect()

                session._swallow_error = panic

                # see note above, for _closed -- we should be
                # listening for "the transport was closed", but
                # "session disconnect" is close enough (since there
                # are no "proper events" from websocket/rawsocket
                # implementations).
                session.on('disconnect', _closed)

                # note, "ready" here means: onJoin and any on('join',
                # ..) handlers have all completed successfully. This
                # is necessary for container-components (as opposed to
                # router-components) to work as expected
                def _ready(s):
                    joined_d.callback(None)

                session.on('ready', _ready)

                def _left(s, details):
                    if not joined_d.called:
                        joined_d.errback(
                            ApplicationError(
                                details.reason,
                                details.message,
                            ))

                session.on('leave', _left)

                return session

            except Exception:
                self.log.failure(
                    u'component instantiation failed: {log_failure.value}')
                raise

        # WAMP transport factory
        #
        transport_config = config[u'transport']

        if transport_config[u'type'] == u'websocket':

            # create a WAMP-over-WebSocket transport client factory
            transport_factory = WampWebSocketClientFactory(
                create_session, transport_config[u'url'])
            transport_factory.noisy = False

            if 'options' in transport_config:
                set_websocket_options(transport_factory,
                                      transport_config['options'])

        elif transport_config[u'type'] == u'rawsocket':

            transport_factory = WampRawSocketClientFactory(
                create_session, transport_config)
            transport_factory.noisy = False

            if 'options' in transport_config:
                set_rawsocket_options(transport_factory,
                                      transport_config['options'])

        else:
            # should not arrive here, since we did check the config before
            raise Exception(u'logic error')

        # create and connect client endpoint
        #
        endpoint = create_connecting_endpoint_from_config(
            transport_config[u'endpoint'], self.config.extra.cbdir,
            self._reactor, self.log)

        # now, actually connect the client
        #
        d = endpoint.connect(transport_factory)

        def on_connect_success(proto):
            component = ContainerComponent(component_id, config, proto, None)
            self.components[component_id] = component

            # publish event "on_component_start" to all but the caller
            #
            uri = self._uri_prefix + u'.on_component_started'

            component_started = {u'id': component_id, u'config': config}

            self.publish(uri,
                         component_started,
                         options=PublishOptions(exclude=details.caller))

            return component_started

        def on_connect_error(err):
            # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html
            if isinstance(err.value, internet.error.ConnectError):
                emsg = u'could not connect container component to router - transport establishment failed ({})'.format(
                    err.value)
                self.log.warn(emsg)
                raise ApplicationError(u'crossbar.error.cannot_connect', emsg)
            else:
                # should not arrive here (since all errors arriving here
                # should be subclasses of ConnectError)
                raise err

        def await_join(arg):
            """
            We don't want to consider this component working until its on_join
            has completed (see create_session() above where this is hooked up)
            """
            return joined_d

        d.addCallbacks(on_connect_success, on_connect_error)
        d.addCallback(await_join)

        return d
Example #3
0
    def sell(self,
             market_maker_adr,
             buyer_pubkey,
             key_id,
             channel_adr,
             channel_seq,
             amount,
             balance,
             signature,
             details=None):
        """
        Called by a XBR Market Maker to buy a data encyption key. The XBR Market Maker here is
        acting for (triggered by) the XBR buyer delegate.

        :param market_maker_adr: The market maker Ethereum address. The technical buyer is usually the
            XBR market maker (== the XBR delegate of the XBR market operator).
        :type market_maker_adr: bytes of length 20

        :param buyer_pubkey: The buyer delegate Ed25519 public key.
        :type buyer_pubkey: bytes of length 32

        :param key_id: The UUID of the data encryption key to buy.
        :type key_id: bytes of length 16

        :param channel_adr: The on-chain channel contract address.
        :type channel_adr: bytes of length 20

        :param channel_seq: Paying channel sequence off-chain transaction number.
        :type channel_seq: int

        :param amount: The amount paid by the XBR Buyer via the XBR Market Maker.
        :type amount: bytes

        :param balance: Balance remaining in the payment channel (from the market maker to the
            seller) after successfully buying the key.
        :type balance: bytes

        :param signature: Signature over the supplied buying information, using the Ethereum
            private key of the market maker (which is the delegate of the marker operator).
        :type signature: bytes of length 65

        :param details: Caller details. The call will come from the XBR Market Maker.
        :type details: :class:`autobahn.wamp.types.CallDetails`

        :return: The data encryption key, itself encrypted to the public key of the original buyer.
        :rtype: bytes
        """
        assert type(market_maker_adr) == bytes and len(
            market_maker_adr) == 20, 'delegate_adr must be bytes[20]'
        assert type(buyer_pubkey) == bytes and len(
            buyer_pubkey) == 32, 'buyer_pubkey must be bytes[32]'
        assert type(key_id) == bytes and len(
            key_id) == 16, 'key_id must be bytes[16]'
        assert type(channel_adr) == bytes and len(
            channel_adr) == 20, 'channel_adr must be bytes[20]'
        assert type(channel_seq) == int, 'channel_seq must be int'
        assert type(amount) == bytes and len(
            amount) == 32, 'amount_paid must be bytes[32], but was {}'.format(
                type(amount))
        assert type(balance) == bytes and len(
            amount) == 32, 'post_balance must be bytes[32], but was {}'.format(
                type(balance))
        assert type(signature) == bytes and len(signature) == (
            32 + 32 + 1), 'signature must be bytes[65]'
        assert details is None or isinstance(
            details,
            CallDetails), 'details must be autobahn.wamp.types.CallDetails'

        amount = unpack_uint256(amount)
        balance = unpack_uint256(balance)

        # check that the delegate_adr fits what we expect for the market maker
        if market_maker_adr != self._market_maker_adr:
            raise ApplicationError(
                'xbr.error.unexpected_marketmaker_adr',
                '{}.sell() - unexpected market maker address: expected 0x{}, but got 0x{}'
                .format(self.__class__.__name__,
                        binascii.b2a_hex(self._market_maker_adr).decode(),
                        binascii.b2a_hex(market_maker_adr).decode()))

        # get the key series given the key_id
        if key_id not in self._keys_map:
            raise ApplicationError(
                'crossbar.error.no_such_object',
                '{}.sell() - no key with ID "{}"'.format(
                    self.__class__.__name__, key_id))
        key_series = self._keys_map[key_id]

        # FIXME: must be the currently active channel .. and we need to track all of these
        if channel_adr != self._channel['channel']:
            self._session.leave()
            raise ApplicationError(
                'xbr.error.unexpected_channel_adr',
                '{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'
                .format(self.__class__.__name__,
                        binascii.b2a_hex(self._channel['channel']).decode(),
                        binascii.b2a_hex(channel_adr).decode()))

        # channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker)
        if channel_seq != self._seq + 1:
            raise ApplicationError(
                'xbr.error.unexpected_channel_seq',
                '{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'
                .format(self.__class__.__name__, self._seq + 1, channel_seq))

        # channel balance: check we have consensus on off-chain channel state with peer (which is the market maker)
        if balance != self._balance - amount:
            raise ApplicationError(
                'xbr.error.unexpected_channel_balance',
                '{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'
                .format(self.__class__.__name__, self._balance - amount,
                        balance))

        # XBRSIG[4/8]: check the signature (over all input data for the buying of the key)
        signer_address = recover_eip712_signer(channel_adr, channel_seq,
                                               balance, False, signature)
        if signer_address != market_maker_adr:
            self.log.warn(
                '{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
                klass=self.__class__.__name__,
                signer_address=hl(binascii.b2a_hex(signer_address).decode()),
                delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode()))
            raise ApplicationError(
                'xbr.error.invalid_signature',
                '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'
                .format(self.__class__.__name__))

        # now actually update our local knowledge of the channel state
        # FIXME: what if code down below fails?
        self._seq += 1
        self._balance -= amount

        # encrypt the data encryption key against the original buyer delegate Ed25519 public key
        sealed_key = key_series.encrypt_key(key_id, buyer_pubkey)

        assert type(sealed_key) == bytes and len(
            sealed_key
        ) == 80, '{}.sell() - unexpected sealed key computed (expected bytes[80]): {}'.format(
            self.__class__.__name__, sealed_key)

        # XBRSIG[5/8]: compute EIP712 typed data signature
        seller_signature = sign_eip712_data(self._pkey_raw,
                                            self._channel['channel'],
                                            self._seq, self._balance)

        receipt = {
            # key ID that has been bought
            'key_id': key_id,

            # seller delegate address that sold the key
            'delegate': self._addr,

            # buyer delegate Ed25519 public key with which the bought key was sealed
            'buyer_pubkey': buyer_pubkey,

            # finally return what the consumer (buyer) was actually interested in:
            # the data encryption key, sealed (public key Ed25519 encrypted) to the
            # public key of the buyer delegate
            'sealed_key': sealed_key,

            # paying channel off-chain transaction sequence numbers
            'channel_seq': self._seq,

            # amount paid for the key
            'amount': amount,

            # paying channel amount remaining
            'balance': self._balance,

            # seller (delegate) signature
            'signature': seller_signature,
        }

        self.log.info(
            '{klass}.sell() - {tx_type} key "{key_id}" sold for {amount_earned} - balance is {balance} [caller={caller}, caller_authid="{caller_authid}", buyer_pubkey="{buyer_pubkey}"]',
            klass=self.__class__.__name__,
            tx_type=hl('XBR SELL  ', color='magenta'),
            key_id=hl(uuid.UUID(bytes=key_id)),
            amount_earned=hl(str(int(amount / 10**18)) + ' XBR',
                             color='magenta'),
            balance=hl(str(int(self._balance / 10**18)) + ' XBR',
                       color='magenta'),
            # paying_channel=hl(binascii.b2a_hex(paying_channel).decode()),
            caller=hl(details.caller),
            caller_authid=hl(details.caller_authid),
            buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()))

        return receipt
Example #4
0
    async def unwrap(self, key_id, serializer, ciphertext):
        """
        Decrypt XBR data. This functions will potentially make the buyer call the
        XBR market maker to buy data encryption keys from the XBR provider.

        :param key_id: ID of the data encryption used for decryption
            of application payload.
        :type key_id: bytes

        :param serializer: Application payload serializer.
        :type serializer: str

        :param ciphertext: Ciphertext of encrypted application payload to
            decrypt.
        :type ciphertext: bytes

        :return: Decrypted application payload.
        :rtype: object
        """
        assert type(key_id) == bytes and len(key_id) == 16
        # FIXME: support more app payload serializers
        assert type(serializer) == str and serializer in ['cbor']
        assert type(ciphertext) == bytes

        channel_adr = bytes(self._channel['channel'])

        # if we don't have the key, buy it!
        if key_id in self._keys:
            self.log.info(
                'Key {key_id} already in key store (or currently being bought).',
                key_id=hl(uuid.UUID(bytes=key_id)))
        else:
            self.log.info('Key {key_id} not yet in key store - buying key ..',
                          key_id=hl(uuid.UUID(bytes=key_id)))

            # mark the key as currently being bought already (the location of code here is multi-entrant)
            self._keys[key_id] = False

            # get (current) price for key we want to buy
            quote = await self._session.call('xbr.marketmaker.get_quote',
                                             key_id)

            # set price we pay set to the (current) quoted price
            amount = unpack_uint256(quote['price'])

            self.log.info('Key {key_id} has current price quote {amount}',
                          key_id=hl(uuid.UUID(bytes=key_id)),
                          amount=hl(int(amount / 10**18)))

            if amount > self._max_price:
                raise ApplicationError(
                    'xbr.error.max_price_exceeded',
                    '{}.unwrap() - key {} needed cannot be bought: price {} exceeds maximum price of {}'
                    .format(self.__class__.__name__, uuid.UUID(bytes=key_id),
                            int(amount / 10**18),
                            int(self._max_price / 10**18)))

            # check (locally) we have enough balance left in the payment channel to buy the key
            balance = self._balance - amount
            if balance < 0:
                if self._auto_close_channel:
                    # FIXME: sign last transaction (from persisted local history)
                    last_tx = None
                    txns = self.past_transactions()
                    if txns:
                        last_tx = txns[0]

                    if last_tx:
                        # tx1 is the delegate portion, and tx2 is the market maker portion:
                        # tx1, tx2 = last_tx
                        # close_adr = tx1.channel
                        # close_seq = tx1.channel_seq
                        # close_balance = tx1.balance
                        # close_is_final = True

                        close_adr = channel_adr
                        close_seq = self._seq
                        close_balance = self._balance
                        close_is_final = True

                        signature = sign_eip712_data(self._pkey_raw, close_adr,
                                                     close_seq, close_balance,
                                                     close_is_final)

                        self.log.info(
                            'auto-closing payment channel {close_adr} [close_seq={close_seq}, close_balance={close_balance}, close_is_final={close_is_final}]',
                            close_adr=binascii.b2a_hex(close_adr).decode(),
                            close_seq=close_seq,
                            close_balance=int(close_balance / 10**18),
                            close_is_final=close_is_final)

                        # call market maker to initiate closing of payment channel
                        await self._session.call(
                            'xbr.marketmaker.close_channel', close_adr,
                            close_seq, pack_uint256(close_balance),
                            close_is_final, signature)

                        # FIXME: wait for and acquire new payment channel instead of bailing out ..

                        raise ApplicationError(
                            'xbr.error.channel_closed',
                            '{}.unwrap() - key {} cannot be bought: payment channel 0x{} ran empty and we initiated close at remaining balance of {}'
                            .format(self.__class__.__name__,
                                    uuid.UUID(bytes=key_id),
                                    binascii.b2a_hex(close_adr).decode(),
                                    int(close_balance / 10**18)))
                raise ApplicationError(
                    'xbr.error.insufficient_balance',
                    '{}.unwrap() - key {} cannot be bought: insufficient balance {} in payment channel for amount {}'
                    .format(self.__class__.__name__, uuid.UUID(bytes=key_id),
                            int(self._balance / 10**18), int(amount / 10**18)))

            buyer_pubkey = self._receive_key.public_key.encode(
                encoder=nacl.encoding.RawEncoder)
            channel_seq = self._seq + 1
            is_final = False

            # XBRSIG[1/8]: compute EIP712 typed data signature
            signature = sign_eip712_data(self._pkey_raw,
                                         channel_adr,
                                         channel_seq,
                                         balance,
                                         is_final=is_final)

            # persist 1st phase of the transaction locally
            self._save_transaction_phase1(channel_adr, self._addr,
                                          buyer_pubkey, key_id, channel_seq,
                                          amount, balance, signature)

            # call the market maker to buy the key
            try:
                receipt = await self._session.call('xbr.marketmaker.buy_key',
                                                   self._addr, buyer_pubkey,
                                                   key_id, channel_adr,
                                                   channel_seq,
                                                   pack_uint256(amount),
                                                   pack_uint256(balance),
                                                   signature)
            except ApplicationError as e:
                if e.error == 'xbr.error.channel_closed':
                    self.stop()
                raise e
            except Exception as e:
                self.log.error(
                    'Encountered error while calling market maker to buy key!')
                self.log.failure()
                self._keys[key_id] = e
                raise e

            # XBRSIG[8/8]: check market maker signature
            marketmaker_signature = receipt['signature']
            marketmaker_channel_seq = receipt['channel_seq']
            marketmaker_amount_paid = unpack_uint256(receipt['amount_paid'])
            marketmaker_remaining = unpack_uint256(receipt['remaining'])
            marketmaker_inflight = unpack_uint256(receipt['inflight'])

            signer_address = recover_eip712_signer(channel_adr,
                                                   marketmaker_channel_seq,
                                                   marketmaker_remaining,
                                                   False,
                                                   marketmaker_signature)
            if signer_address != self._market_maker_adr:
                self.log.warn(
                    '{klass}.unwrap()::XBRSIG[8/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
                    klass=self.__class__.__name__,
                    signer_address=hl(
                        binascii.b2a_hex(signer_address).decode()),
                    delegate_adr=hl(
                        binascii.b2a_hex(self._market_maker_adr).decode()))
                raise ApplicationError(
                    'xbr.error.invalid_signature',
                    '{}.unwrap()::XBRSIG[8/8] - EIP712 signature invalid or not signed by market maker'
                    .format(self.__class__.__name__))

            if self._seq + 1 != marketmaker_channel_seq:
                raise ApplicationError(
                    'xbr.error.invalid_transaction',
                    '{}.buy_key(): invalid transaction (channel sequence number mismatch - expected {}, but got {})'
                    .format(self.__class__.__name__, self._seq,
                            receipt['channel_seq']))

            if self._balance - amount != marketmaker_remaining:
                raise ApplicationError(
                    'xbr.error.invalid_transaction',
                    '{}.buy_key(): invalid transaction (channel remaining amount mismatch - expected {}, but got {})'
                    .format(self.__class__.__name__, self._balance - amount,
                            receipt['remaining']))

            self._seq = marketmaker_channel_seq
            self._balance = marketmaker_remaining

            # persist 2nd phase of the transaction locally
            self._save_transaction_phase2(channel_adr, self._market_maker_adr,
                                          buyer_pubkey, key_id,
                                          marketmaker_channel_seq,
                                          marketmaker_amount_paid,
                                          marketmaker_remaining,
                                          marketmaker_signature)

            # unseal the data encryption key
            sealed_key = receipt['sealed_key']
            unseal_box = nacl.public.SealedBox(self._receive_key)
            try:
                key = unseal_box.decrypt(sealed_key)
            except nacl.exceptions.CryptoError as e:
                self._keys[key_id] = e
                raise ApplicationError(
                    'xbr.error.decryption_failed',
                    '{}.unwrap() - could not unseal data encryption key: {}'.
                    format(self.__class__.__name__, e))

            # remember the key, so we can use it to actually decrypt application payload data
            self._keys[key_id] = nacl.secret.SecretBox(key)

            transactions_count = self.count_transactions()
            self.log.info(
                '{klass}.unwrap() - {tx_type} key {key_id} bought for {amount_paid} [payment_channel={payment_channel}, remaining={remaining}, inflight={inflight}, buyer_pubkey={buyer_pubkey}, transactions={transactions}]',
                klass=self.__class__.__name__,
                tx_type=hl('XBR BUY   ', color='magenta'),
                key_id=hl(uuid.UUID(bytes=key_id)),
                amount_paid=hl(str(int(marketmaker_amount_paid / 10**18)) +
                               ' XBR',
                               color='magenta'),
                payment_channel=hl(
                    binascii.b2a_hex(receipt['payment_channel']).decode()),
                remaining=hl(int(marketmaker_remaining / 10**18)),
                inflight=hl(int(marketmaker_inflight / 10**18)),
                buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode()),
                transactions=transactions_count)

        # if the key is already being bought, wait until the one buying path of execution has succeeded and done
        log_counter = 0
        while self._keys[key_id] is False:
            if log_counter % 100:
                self.log.info(
                    '{klass}.unwrap() - waiting for key "{key_id}" currently being bought ..',
                    klass=self.__class__.__name__,
                    key_id=hl(uuid.UUID(bytes=key_id)))
                log_counter += 1
            await txaio.sleep(.2)

        # check if the key buying failed and fail the unwrapping in turn
        if isinstance(self._keys[key_id], Exception):
            e = self._keys[key_id]
            raise e

        # now that we have the data encryption key, decrypt the application payload
        # the decryption key here is an instance of nacl.secret.SecretBox
        try:
            message = self._keys[key_id].decrypt(ciphertext)
        except nacl.exceptions.CryptoError as e:
            # Decryption failed. Ciphertext failed verification
            raise ApplicationError(
                'xbr.error.decryption_failed',
                '{}.unwrap() - failed to unwrap encrypted data: {}'.format(
                    self.__class__.__name__, e))

        # deserialize the application payload
        # FIXME: support more app payload serializers
        try:
            payload = cbor2.loads(message)
        except cbor2.decoder.CBORDecodeError as e:
            # premature end of stream (expected to read 4187 bytes, got 27 instead)
            raise ApplicationError(
                'xbr.error.deserialization_failed',
                '{}.unwrap() - failed to deserialize application payload: {}'.
                format(self.__class__.__name__, e))

        return payload
Example #5
0
    def _start_native_worker(self, wtype, id, options=None, details=None):

        assert (wtype in ['router', 'container'])

        ## prohibit starting a worker twice
        ##
        if id in self._workers:
            emsg = "ERROR: could not start worker - a worker with ID '{}'' is already running (or starting)".format(
                id)
            log.msg(emsg)
            raise ApplicationError('crossbar.error.worker_already_running',
                                   emsg)

        ## check worker options
        ##
        options = options or {}
        try:
            if wtype == 'router':
                checkconfig.check_router_options(options)
            elif wtype == 'container':
                checkconfig.check_container_options(options)
            else:
                raise Exception("logic error")
        except Exception as e:
            emsg = "ERROR: could not start native worker - invalid configuration ({})".format(
                e)
            log.msg(emsg)
            raise ApplicationError('crossbar.error.invalid_configuration',
                                   emsg)

        ## allow override Python executable from options
        ##
        if 'python' in options:
            exe = options['python']

            ## the executable must be an absolute path, e.g. /home/oberstet/pypy-2.2.1-linux64/bin/pypy
            ##
            if not os.path.isabs(exe):
                emsg = "ERROR: python '{}' from worker options must be an absolute path".format(
                    exe)
                log.msg(emsg)
                raise ApplicationError('crossbar.error.invalid_configuration',
                                       emsg)

            ## of course the path must exist and actually be executable
            ##
            if not (os.path.isfile(exe) and os.access(exe, os.X_OK)):
                emsg = "ERROR: python '{}' from worker options does not exist or isn't an executable".format(
                    exe)
                log.msg(emsg)
                raise ApplicationError('crossbar.error.invalid_configuration',
                                       emsg)
        else:
            exe = sys.executable

        ## all native workers (routers and containers for now) start from the same script
        ##
        filename = pkg_resources.resource_filename('crossbar',
                                                   'worker/process.py')

        ## assemble command line for forking the worker
        ##
        args = [exe, "-u", filename]
        args.extend(["--cbdir", self._node._cbdir])
        args.extend(["--node", str(self._node_id)])
        args.extend(["--worker", str(id)])
        args.extend(["--realm", self._realm])
        args.extend(["--type", wtype])

        ## allow override worker process title from options
        ##
        if options.get('title', None):
            args.extend(['--title', options['title']])

        ## allow overriding debug flag from options
        ##
        if options.get('debug', self.debug):
            args.append('--debug')

        ## forward explicit reactor selection
        ##
        if 'reactor' in options and sys.platform in options['reactor']:
            args.extend(['--reactor', options['reactor'][sys.platform]])
        elif self._node.options.reactor:
            args.extend(['--reactor', self._node.options.reactor])

        ## create worker process environment
        ##
        worker_env = create_process_env(options)

        ## log name of worker
        ##
        worker_logname = {
            'router': 'Router',
            'container': 'Container'
        }.get(wtype, 'Worker')

        ## topic URIs used (later)
        ##
        if wtype == 'router':
            starting_topic = 'crossbar.node.{}.on_router_starting'.format(
                self._node_id)
            started_topic = 'crossbar.node.{}.on_router_started'.format(
                self._node_id)
        elif wtype == 'container':
            starting_topic = 'crossbar.node.{}.on_container_starting'.format(
                self._node_id)
            started_topic = 'crossbar.node.{}.on_container_started'.format(
                self._node_id)
        else:
            raise Exception("logic error")

        ## add worker tracking instance to the worker map ..
        ##
        if wtype == 'router':
            worker = RouterWorkerProcess(self,
                                         id,
                                         details.authid,
                                         keeplog=options.get(
                                             'traceback', None))
        elif wtype == 'container':
            worker = ContainerWorkerProcess(self,
                                            id,
                                            details.authid,
                                            keeplog=options.get(
                                                'traceback', None))
        else:
            raise Exception("logic error")

        self._workers[id] = worker

        ## create a (custom) process endpoint
        ##
        ep = WorkerProcessEndpoint(self._node._reactor,
                                   exe,
                                   args,
                                   env=worker_env,
                                   worker=worker)

        ## ready handling
        ##
        def on_ready_success(id):
            log.msg("{} with ID '{}' and PID {} started".format(
                worker_logname, worker.id, worker.pid))

            worker.status = 'started'
            worker.started = datetime.utcnow()

            started_info = {
                'id': worker.id,
                'status': worker.status,
                'started': utcstr(worker.started),
                'who': worker.who
            }

            self.publish(started_topic,
                         started_info,
                         options=PublishOptions(exclude=[details.caller]))

            return started_info

        def on_ready_error(err):
            del self._workers[worker.id]

            emsg = 'ERROR: failed to start native worker - {}'.format(
                err.value)
            log.msg(emsg)
            raise ApplicationError("crossbar.error.cannot_start", emsg,
                                   worker.getlog())

        worker.ready.addCallbacks(on_ready_success, on_ready_error)

        def on_exit_success(res):
            del self._workers[worker.id]

        def on_exit_error(err):
            del self._workers[worker.id]

        worker.exit.addCallbacks(on_exit_success, on_exit_error)

        ## create a transport factory for talking WAMP to the native worker
        ##
        transport_factory = create_native_worker_client_factory(
            self._node._router_session_factory, worker.ready, worker.exit)
        transport_factory.noisy = False
        self._workers[id].factory = transport_factory

        ## now (immediately before actually forking) signal the starting of the worker
        ##
        starting_info = {
            'id': id,
            'status': worker.status,
            'created': utcstr(worker.created),
            'who': worker.who
        }

        ## the caller gets a progressive result ..
        if details.progress:
            details.progress(starting_info)

        ## .. while all others get an event
        self.publish(starting_topic,
                     starting_info,
                     options=PublishOptions(exclude=[details.caller]))

        ## now actually fork the worker ..
        ##
        if self.debug:
            log.msg(
                "Starting {} with ID '{}' using command line '{}' ..".format(
                    worker_logname, id, ' '.join(args)))
        else:
            log.msg("Starting {} with ID '{}' ..".format(worker_logname, id))

        d = ep.connect(transport_factory)

        def on_connect_success(proto):

            ## this seems to be called immediately when the child process
            ## has been forked. even if it then immediately fails because
            ## e.g. the executable doesn't even exist. in other words,
            ## I'm not sure under what conditions the deferred will errback ..

            pid = proto.transport.pid
            if self.debug:
                log.msg(
                    "Native worker process connected with PID {}".format(pid))

            ## note the PID of the worker
            worker.pid = pid

            ## proto is an instance of NativeWorkerClientProtocol
            worker.proto = proto

            worker.status = 'connected'
            worker.connected = datetime.utcnow()

        def on_connect_error(err):

            ## not sure when this errback is triggered at all ..
            if self.debug:
                log.msg("ERROR: Connecting forked native worker failed - {}".
                        format(err))

            ## in any case, forward the error ..
            worker.ready.errback(err)

        d.addCallbacks(on_connect_success, on_connect_error)

        return worker.ready
Example #6
0
    def _start_native_worker(self,
                             worker_type,
                             worker_id,
                             worker_options=None,
                             details=None):

        # prohibit starting a worker twice
        #
        if worker_id in self._workers:
            emsg = "Could not start worker: a worker with ID '{}' is already running (or starting)".format(
                worker_id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.worker_already_running',
                                   emsg)

        # check worker options
        #
        options = worker_options or {}
        try:
            if worker_type in self._node._native_workers:
                if self._node._native_workers[worker_type][
                        'checkconfig_options']:
                    self._node._native_workers[worker_type][
                        'checkconfig_options'](options)
                else:
                    raise Exception(
                        'No checkconfig_options for worker type "{worker_type}" implemented!'
                        .format(worker_type=worker_type))
            else:
                raise Exception('invalid worker type "{}"'.format(worker_type))
        except Exception as e:
            emsg = "Could not start native worker: invalid configuration ({})".format(
                e)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.invalid_configuration',
                                   emsg)

        # the fully qualified worker class as a string
        worker_class = qual(
            self._node._native_workers[worker_type]['worker_class'])

        # allow override Python executable from options
        #
        if 'python' in options:
            exe = options['python']

            # the executable must be an absolute path, e.g. /home/oberstet/pypy-2.2.1-linux64/bin/pypy
            #
            if not os.path.isabs(exe):
                emsg = "Invalid worker configuration: python executable '{}' must be an absolute path".format(
                    exe)
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.invalid_configuration',
                                       emsg)

            # of course the path must exist and actually be executable
            #
            if not (os.path.isfile(exe) and os.access(exe, os.X_OK)):
                emsg = "Invalid worker configuration: python executable '{}' does not exist or isn't an executable".format(
                    exe)
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.invalid_configuration',
                                       emsg)
        else:
            exe = sys.executable

        # allow override default Python module search paths from options
        #
        if 'pythonpath' in options:
            pythonpaths_to_add = [
                os.path.abspath(os.path.join(self._node._cbdir, p))
                for p in options.get('pythonpath', [])
            ]
        else:
            pythonpaths_to_add = []

        # assemble command line for forking the worker
        #
        # all native workers (routers and containers for now) start
        # from the same script in crossbar/worker/process.py -- we're
        # invoking via "-m" so that .pyc files, __pycache__ etc work
        # properly.
        #
        args = [exe, "-u", "-m", "crossbar.worker.process"]
        args.extend(["--cbdir", self._node._cbdir])
        args.extend(["--worker", str(worker_id)])
        args.extend(["--realm", self._realm])
        args.extend(["--klass", worker_class])
        args.extend(["--loglevel", get_global_log_level()])
        if "shutdown" in options:
            args.extend(["--shutdown", options["shutdown"]])

        # Node-level callback to inject worker arguments
        #
        self._node._extend_worker_args(args, options)

        # allow override worker process title from options
        #
        if options.get('title', None):
            args.extend(['--title', options['title']])

        # forward explicit reactor selection
        #
        if 'reactor' in options and sys.platform in options['reactor']:
            args.extend(['--reactor', options['reactor'][sys.platform]])
        # FIXME
        # elif self._node.options.reactor:
        #    args.extend(['--reactor', self._node.options.reactor])

        # create worker process environment
        #
        worker_env = create_process_env(options)

        # We need to use the same PYTHONPATH we were started with, so we can
        # find the Crossbar we're working with -- it may not be the same as the
        # one on the default path
        worker_env["PYTHONPATH"] = os.pathsep.join(pythonpaths_to_add +
                                                   sys.path)

        # log name of worker
        #
        worker_logname = self._node._native_workers[worker_type]['logname']

        # each worker is run under its own dedicated WAMP auth role
        #
        worker_auth_role = u'crossbar.worker.{}'.format(worker_id)

        # topic URIs used (later)
        #
        starting_topic = self._node._native_workers[worker_type]['topics'][
            'starting']
        started_topic = self._node._native_workers[worker_type]['topics'][
            'started']

        # add worker tracking instance to the worker map ..
        #
        WORKER = self._node._native_workers[worker_type]['class']
        worker = WORKER(self,
                        worker_id,
                        details.caller,
                        keeplog=options.get('traceback', None))
        self._workers[worker_id] = worker

        # create a (custom) process endpoint.
        #
        if platform.isWindows():
            childFDs = None  # Use the default Twisted ones
        else:
            # The communication between controller and container workers is
            # using WAMP running over 2 pipes.
            # For controller->container traffic this runs over FD 0 (`stdin`)
            # and for the container->controller traffic, this runs over FD 3.
            #
            # Note: We use FD 3, not FD 1 (`stdout`) or FD 2 (`stderr`) for
            # container->controller traffic, so that components running in the
            # container which happen to write to `stdout` or `stderr` do not
            # interfere with the container-controller communication.
            childFDs = {0: "w", 1: "r", 2: "r", 3: "r"}

        ep = WorkerProcessEndpoint(self._node._reactor,
                                   exe,
                                   args,
                                   env=worker_env,
                                   worker=worker,
                                   childFDs=childFDs)

        # ready handling
        #
        def on_ready_success(worker_id):
            self.log.info(
                '{worker_type} worker "{worker_id}" process {pid} started',
                worker_type=worker_logname,
                worker_id=worker.id,
                pid=worker.pid)

            self._node._reactor.addSystemEventTrigger(
                'before',
                'shutdown',
                self._cleanup_worker,
                self._node._reactor,
                worker,
            )

            worker.on_worker_started()

            started_info = {
                u'id': worker.id,
                u'status': worker.status,
                u'started': utcstr(worker.started),
                u'who': worker.who,
            }

            # FIXME: make start of stats printer dependent on log level ..
            if False:
                worker.log_stats(5.)

            self.publish(started_topic,
                         started_info,
                         options=PublishOptions(exclude=details.caller))

            return started_info

        def on_ready_error(err):
            del self._workers[worker.id]
            emsg = 'Failed to start native worker: {}'.format(err.value)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.cannot_start", emsg,
                                   worker.getlog())

        worker.ready.addCallbacks(on_ready_success, on_ready_error)

        def on_exit_success(_):
            self.log.info("Node worker {worker.id} ended successfully",
                          worker=worker)

            # clear worker log
            worker.log_stats(0)

            # remove the dedicated node router authrole we dynamically
            # added for the worker
            self._node._drop_worker_role(worker_auth_role)

            # remove our metadata tracking for the worker
            del self._workers[worker.id]

            # indicate that the worker excited successfully
            return True

        def on_exit_error(err):
            self.log.info("Node worker {worker.id} ended with error ({err})",
                          worker=worker,
                          err=err)

            # clear worker log
            worker.log_stats(0)

            # remove the dedicated node router authrole we dynamically
            # added for the worker
            self._node._drop_worker_role(worker_auth_role)

            # remove our metadata tracking for the worker
            del self._workers[worker.id]

            # indicate that the worker excited with error
            return False

        def check_for_shutdown(was_successful):
            self.log.info(
                'Checking for node shutdown: worker_exit_success={worker_exit_success}, shutdown_requested={shutdown_requested}, node_shutdown_triggers={node_shutdown_triggers}',
                worker_exit_success=was_successful,
                shutdown_requested=self._shutdown_requested,
                node_shutdown_triggers=self._node._node_shutdown_triggers)

            shutdown = self._shutdown_requested

            # automatically shutdown node whenever a worker ended (successfully, or with error)
            #
            if checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT in self._node._node_shutdown_triggers:
                self.log.info(
                    "Node worker ended, and trigger '{trigger}' active",
                    trigger=checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT)
                shutdown = True

            # automatically shutdown node when worker ended with error
            #
            if not was_successful and checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT_WITH_ERROR in self._node._node_shutdown_triggers:
                self.log.info(
                    "Node worker ended with error, and trigger '{trigger}' active",
                    trigger=checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT_WITH_ERROR
                )
                shutdown = True

            # automatically shutdown node when no more workers are left
            #
            if len(
                    self._workers
            ) == 0 and checkconfig.NODE_SHUTDOWN_ON_LAST_WORKER_EXIT in self._node._node_shutdown_triggers:
                self.log.info(
                    "No more node workers running, and trigger '{trigger}' active",
                    trigger=checkconfig.NODE_SHUTDOWN_ON_LAST_WORKER_EXIT)
                shutdown = True

            # initiate shutdown (but only if we are not already shutting down)
            #
            if shutdown:
                self.shutdown()
            else:
                self.log.info('Node will continue to run!')

        d_on_exit = worker.exit.addCallbacks(on_exit_success, on_exit_error)
        d_on_exit.addBoth(check_for_shutdown)

        # create a transport factory for talking WAMP to the native worker
        #
        transport_factory = create_native_worker_client_factory(
            self._node._router_session_factory, worker_auth_role, worker.ready,
            worker.exit)
        transport_factory.noisy = False
        self._workers[worker_id].factory = transport_factory

        # now (immediately before actually forking) signal the starting of the worker
        #
        starting_info = {
            u'id': worker_id,
            u'status': worker.status,
            u'created': utcstr(worker.created),
            u'who': worker.who,
        }

        # the caller gets a progressive result ..
        if details.progress:
            details.progress(starting_info)

        # .. while all others get an event
        self.publish(starting_topic,
                     starting_info,
                     options=PublishOptions(exclude=details.caller))

        # now actually fork the worker ..
        #
        self.log.info('{worker_logname} worker "{worker_id}" starting ..',
                      worker_logname=worker_logname,
                      worker_id=worker_id)
        self.log.debug(
            '{worker_logname} "{worker_id}" command line is "{cmdline}"',
            worker_logname=worker_logname,
            worker_id=worker_id,
            cmdline=' '.join(args))

        d = ep.connect(transport_factory)

        def on_connect_success(proto):

            # this seems to be called immediately when the child process
            # has been forked. even if it then immediately fails because
            # e.g. the executable doesn't even exist. in other words,
            # I'm not sure under what conditions the deferred will errback ..

            self.log.debug('Native worker "{worker_id}" connected',
                           worker_id=worker_id)

            worker.on_worker_connected(proto)

            # dynamically add a dedicated authrole to the router
            # for the worker we've just started
            self._node._add_worker_role(worker_auth_role, options)

        def on_connect_error(err):

            # not sure when this errback is triggered at all ..
            self.log.error(
                "Interal error: connection to forked native worker failed ({err})",
                err=err)

            # in any case, forward the error ..
            worker.ready.errback(err)

        d.addCallbacks(on_connect_success, on_connect_error)

        return worker.ready
Example #7
0
    def _start_guest_worker(self, worker_id, worker_config, details=None):
        """
        Start a new guest process on this node.

        :param config: The guest process configuration.
        :type config: obj

        :returns: int -- The PID of the new process.
        """
        # prohibit starting a worker twice
        #
        if worker_id in self._workers:
            emsg = "Could not start worker: a worker with ID '{}' is already running (or starting)".format(
                worker_id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.worker_already_running',
                                   emsg)

        try:
            checkconfig.check_guest(worker_config)
        except Exception as e:
            raise ApplicationError(
                u'crossbar.error.invalid_configuration',
                'invalid guest worker configuration: {}'.format(e))

        options = worker_config.get('options', {})

        # guest process working directory
        #
        workdir = self._node._cbdir
        if 'workdir' in options:
            workdir = os.path.join(workdir, options['workdir'])
        workdir = os.path.abspath(workdir)

        # guest process executable and command line arguments
        #

        # first try to configure the fully qualified path for the guest
        # executable by joining workdir and configured exectuable ..
        exe = os.path.abspath(
            os.path.join(workdir, worker_config['executable']))

        if check_executable(exe):
            self.log.info(
                "Using guest worker executable '{exe}' (executable path taken from configuration)",
                exe=exe)
        else:
            # try to detect the fully qualified path for the guest
            # executable by doing a "which" on the configured executable name
            exe = which(worker_config['executable'])
            if exe is not None and check_executable(exe):
                self.log.info(
                    "Using guest worker executable '{exe}' (executable path detected from environment)",
                    exe=exe)
            else:
                emsg = "Could not start worker: could not find and executable for '{}'".format(
                    worker_config['executable'])
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.invalid_configuration',
                                       emsg)

        # guest process command line arguments
        #
        args = [exe]
        args.extend(worker_config.get('arguments', []))

        # guest process environment
        #
        worker_env = create_process_env(options)

        # log name of worker
        #
        worker_logname = 'Guest'

        # topic URIs used (later)
        #
        starting_topic = u'{}.on_guest_starting'.format(self._uri_prefix)
        started_topic = u'{}.on_guest_started'.format(self._uri_prefix)

        # add worker tracking instance to the worker map ..
        #
        worker = GuestWorkerProcess(self,
                                    worker_id,
                                    details.caller,
                                    keeplog=options.get('traceback', None))

        self._workers[worker_id] = worker

        # create a (custom) process endpoint
        #
        ep = WorkerProcessEndpoint(self._node._reactor,
                                   exe,
                                   args,
                                   path=workdir,
                                   env=worker_env,
                                   worker=worker)

        # ready handling
        #
        def on_ready_success(proto):

            self.log.info('{worker_logname} worker "{worker_id}" started',
                          worker_logname=worker_logname,
                          worker_id=worker.id)

            worker.on_worker_started(proto)

            self._node._reactor.addSystemEventTrigger(
                'before',
                'shutdown',
                self._cleanup_worker,
                self._node._reactor,
                worker,
            )

            # directory watcher
            #
            if 'watch' in options:

                if HAS_FS_WATCHER:

                    # assemble list of watched directories
                    watched_dirs = []
                    for d in options['watch'].get('directories', []):
                        watched_dirs.append(
                            os.path.abspath(os.path.join(self._node._cbdir,
                                                         d)))

                    worker.watch_timeout = options['watch'].get('timeout', 1)

                    # create a filesystem watcher
                    worker.watcher = FilesystemWatcher(
                        workdir, watched_dirs=watched_dirs)

                    # make sure to stop the watch upon Twisted being shut down
                    def on_shutdown():
                        worker.watcher.stop()

                    self._node._reactor.addSystemEventTrigger(
                        'before', 'shutdown', on_shutdown)

                    # this handler will get fired by the watcher upon detecting an FS event
                    def on_filesystem_change(fs_event):
                        worker.watcher.stop()
                        proto.signal('TERM')

                        if options['watch'].get('action', None) == 'restart':
                            self.log.info(
                                "Filesystem watcher detected change {fs_event} - restarting guest in {watch_timeout} seconds ..",
                                fs_event=fs_event,
                                watch_timeout=worker.watch_timeout)
                            # Add a timeout large enough (perhaps add a config option later)
                            self._node._reactor.callLater(
                                worker.watch_timeout, self.start_guest,
                                worker_id, worker_config, details)
                            # Shut the worker down, after the restart event is scheduled
                            # FIXME: all workers should have a stop() method ..
                            # -> 'GuestWorkerProcess' object has no attribute 'stop'
                            # worker.stop()
                        else:
                            self.log.info(
                                "Filesystem watcher detected change {fs_event} - no action taken!",
                                fs_event=fs_event)

                    # now start watching ..
                    worker.watcher.start(on_filesystem_change)

                else:
                    self.log.warn(
                        "Cannot watch directories for changes - feature not available"
                    )

            # assemble guest worker startup information
            #
            started_info = {
                u'id': worker.id,
                u'status': worker.status,
                u'started': utcstr(worker.started),
                u'who': worker.who,
            }

            self.publish(started_topic,
                         started_info,
                         options=PublishOptions(exclude=details.caller))

            return started_info

        def on_ready_error(err):
            del self._workers[worker.id]

            emsg = 'Failed to start guest worker: {}'.format(err.value)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.cannot_start", emsg,
                                   ep.getlog())

        worker.ready.addCallbacks(on_ready_success, on_ready_error)

        def on_exit_success(res):
            self.log.info("Guest {worker_id} exited with success",
                          worker_id=worker.id)
            del self._workers[worker.id]

        def on_exit_error(err):
            self.log.error("Guest {worker_id} exited with error {err.value}",
                           worker_id=worker.id,
                           err=err)
            del self._workers[worker.id]

        worker.exit.addCallbacks(on_exit_success, on_exit_error)

        # create a transport factory for talking WAMP to the native worker
        #
        transport_factory = create_guest_worker_client_factory(
            worker_config, worker.ready, worker.exit)
        transport_factory.noisy = False
        self._workers[worker_id].factory = transport_factory

        # now (immediately before actually forking) signal the starting of the worker
        #
        starting_info = {
            u'id': worker_id,
            u'status': worker.status,
            u'created': utcstr(worker.created),
            u'who': worker.who,
        }

        # the caller gets a progressive result ..
        if details.progress:
            details.progress(starting_info)

        # .. while all others get an event
        self.publish(starting_topic,
                     starting_info,
                     options=PublishOptions(exclude=details.caller))

        # now actually fork the worker ..
        #
        self.log.info('{worker_logname} "{worker_id}" process starting ..',
                      worker_logname=worker_logname,
                      worker_id=worker_id)
        self.log.debug(
            '{worker_logname} "{worker_id}" process using command line "{cli}" ..',
            worker_logname=worker_logname,
            worker_id=worker_id,
            cli=' '.join(args))

        d = ep.connect(transport_factory)

        def on_connect_success(proto):

            # this seems to be called immediately when the child process
            # has been forked. even if it then immediately fails because
            # e.g. the executable doesn't even exist. in other words,
            # I'm not sure under what conditions the deferred will
            # errback - probably only if the forking of a new process fails
            # at OS level due to out of memory conditions or such.
            self.log.debug('{worker_logname} "{worker_id}" connected',
                           worker_logname=worker_logname,
                           worker_id=worker_id)

            # do not comment this: it will lead to on_worker_started being called
            # _before_ on_worker_connected, and we don't need it!
            # worker.on_worker_connected(proto)

        def on_connect_error(err):

            # not sure when this errback is triggered at all .. see above.
            self.log.failure(
                "Internal error: connection to forked guest worker failed ({log_failure.value})",
            )

            # in any case, forward the error ..
            worker.ready.errback(err)

        d.addCallbacks(on_connect_success, on_connect_error)

        return worker.ready
Example #8
0
 def test_exception(self):
     raise ApplicationError(u'wamp.error.history_unavailable')
Example #9
0
 def fail(err):
     raise ApplicationError(
         "crossbar.error.cannot_stop",
         "Failed to stop transport: {}".format(str(err.value)))
Example #10
0
    def _start_native_worker(self, wtype, id, options=None, details=None):

        assert (wtype in ['router', 'container', 'websocket-testee'])

        # prohibit starting a worker twice
        #
        if id in self._workers:
            emsg = "Could not start worker: a worker with ID '{}' is already running (or starting)".format(
                id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.worker_already_running',
                                   emsg)

        # check worker options
        #
        options = options or {}
        try:
            if wtype == 'router':
                checkconfig.check_router_options(options)
            elif wtype == 'container':
                checkconfig.check_container_options(options)
            elif wtype == 'websocket-testee':
                checkconfig.check_websocket_testee_options(options)
            else:
                raise Exception("logic error")
        except Exception as e:
            emsg = "Could not start native worker: invalid configuration ({})".format(
                e)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.invalid_configuration',
                                   emsg)

        # allow override Python executable from options
        #
        if 'python' in options:
            exe = options['python']

            # the executable must be an absolute path, e.g. /home/oberstet/pypy-2.2.1-linux64/bin/pypy
            #
            if not os.path.isabs(exe):
                emsg = "Invalid worker configuration: python executable '{}' must be an absolute path".format(
                    exe)
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.invalid_configuration',
                                       emsg)

            # of course the path must exist and actually be executable
            #
            if not (os.path.isfile(exe) and os.access(exe, os.X_OK)):
                emsg = "Invalid worker configuration: python executable '{}' does not exist or isn't an executable".format(
                    exe)
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.invalid_configuration',
                                       emsg)
        else:
            exe = sys.executable

        # all native workers (routers and containers for now) start from the same script
        #
        filename = FilePath(crossbar.__file__).parent().child("worker").child(
            "process.py").path

        # assemble command line for forking the worker
        #
        args = [exe, "-u", filename]
        args.extend(["--cbdir", self._node._cbdir])
        args.extend(["--node", str(self._node_id)])
        args.extend(["--worker", str(id)])
        args.extend(["--realm", self._realm])
        args.extend(["--type", wtype])
        args.extend(["--loglevel", _loglevel])

        # allow override worker process title from options
        #
        if options.get('title', None):
            args.extend(['--title', options['title']])

        # forward explicit reactor selection
        #
        if 'reactor' in options and sys.platform in options['reactor']:
            args.extend(['--reactor', options['reactor'][sys.platform]])
        # FIXME
        # elif self._node.options.reactor:
        #    args.extend(['--reactor', self._node.options.reactor])

        # create worker process environment
        #
        worker_env = create_process_env(options)

        # We need to use the same PYTHONPATH we were started with, so we can
        # find the Crossbar we're working with -- it may not be the same as the
        # one on the default path
        worker_env["PYTHONPATH"] = os.pathsep.join(sys.path)

        # log name of worker
        #
        worker_logname = {
            'router': 'Router',
            'container': 'Container',
            'websocket-testee': 'WebSocketTestee'
        }.get(wtype, 'Worker')

        # topic URIs used (later)
        #
        if wtype == 'router':
            starting_topic = 'crossbar.node.{}.on_router_starting'.format(
                self._node_id)
            started_topic = 'crossbar.node.{}.on_router_started'.format(
                self._node_id)
        elif wtype == 'container':
            starting_topic = 'crossbar.node.{}.on_container_starting'.format(
                self._node_id)
            started_topic = 'crossbar.node.{}.on_container_started'.format(
                self._node_id)
        elif wtype == 'websocket-testee':
            starting_topic = 'crossbar.node.{}.on_websocket_testee_starting'.format(
                self._node_id)
            started_topic = 'crossbar.node.{}.on_websocket_testee_started'.format(
                self._node_id)
        else:
            raise Exception("logic error")

        # add worker tracking instance to the worker map ..
        #
        if wtype == 'router':
            worker = RouterWorkerProcess(self,
                                         id,
                                         details.caller,
                                         keeplog=options.get(
                                             'traceback', None))
        elif wtype == 'container':
            worker = ContainerWorkerProcess(self,
                                            id,
                                            details.caller,
                                            keeplog=options.get(
                                                'traceback', None))
        elif wtype == 'websocket-testee':
            worker = WebSocketTesteeWorkerProcess(self,
                                                  id,
                                                  details.caller,
                                                  keeplog=options.get(
                                                      'traceback', None))
        else:
            raise Exception("logic error")

        self._workers[id] = worker

        # create a (custom) process endpoint.
        #
        if platform.isWindows():
            childFDs = None  # Use the default Twisted ones
        else:
            # The communication between controller and container workers is
            # using WAMP running over 2 pipes.
            # For controller->container traffic this runs over FD 0 (`stdin`)
            # and for the container->controller traffic, this runs over FD 3.
            #
            # Note: We use FD 3, not FD 1 (`stdout`) or FD 2 (`stderr`) for
            # container->controller traffic, so that components running in the
            # container which happen to write to `stdout` or `stderr` do not
            # interfere with the container-controller communication.
            childFDs = {0: "w", 1: "r", 2: "r", 3: "r"}

        ep = WorkerProcessEndpoint(self._node._reactor,
                                   exe,
                                   args,
                                   env=worker_env,
                                   worker=worker,
                                   childFDs=childFDs)

        # ready handling
        #
        def on_ready_success(id):
            self.log.info("{worker} with ID '{id}' and PID {pid} started",
                          worker=worker_logname,
                          id=worker.id,
                          pid=worker.pid)

            self._node._reactor.addSystemEventTrigger(
                'before',
                'shutdown',
                self._cleanup_worker,
                self._node._reactor,
                worker,
            )

            worker.status = 'started'
            worker.started = datetime.utcnow()

            started_info = {
                'id': worker.id,
                'status': worker.status,
                'started': utcstr(worker.started),
                'who': worker.who
            }

            # FIXME: make start of stats printer dependent on log level ..
            worker.log_stats(5.)

            self.publish(started_topic,
                         started_info,
                         options=PublishOptions(exclude=[details.caller]))

            return started_info

        def on_ready_error(err):
            del self._workers[worker.id]
            emsg = 'Failed to start native worker: {}'.format(err.value)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.cannot_start", emsg,
                                   worker.getlog())

        worker.ready.addCallbacks(on_ready_success, on_ready_error)

        def on_exit_success(_):
            self.log.info("Node worker {} ended successfully".format(
                worker.id))
            worker.log_stats(0)
            del self._workers[worker.id]
            return True

        def on_exit_error(err):
            self.log.info("Node worker {} ended with error ({})".format(
                worker.id, err))
            worker.log_stats(0)
            del self._workers[worker.id]
            return False

        def check_for_shutdown(was_successful):
            shutdown = False

            # automatically shutdown node whenever a worker ended (successfully, or with error)
            #
            if checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT in self._node._node_shutdown_triggers:
                self.log.info(
                    "Node worker ended, and trigger '{}' active".format(
                        checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT))
                shutdown = True

            # automatically shutdown node when worker ended with error
            #
            if not was_successful and checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT_WITH_ERROR in self._node._node_shutdown_triggers:
                self.log.info(
                    "Node worker ended with error, and trigger '{}' active".
                    format(
                        checkconfig.NODE_SHUTDOWN_ON_WORKER_EXIT_WITH_ERROR))
                shutdown = True

            # automatically shutdown node when no more workers are left
            #
            if len(
                    self._workers
            ) == 0 and checkconfig.NODE_SHUTDOWN_ON_LAST_WORKER_EXIT in self._node._node_shutdown_triggers:
                self.log.info(
                    "No more node workers running, and trigger '{}' active".
                    format(checkconfig.NODE_SHUTDOWN_ON_LAST_WORKER_EXIT))
                shutdown = True

            # initiate shutdown (but only if we are not already shutting down)
            #
            if shutdown:
                if not self._shutdown_requested:
                    self.log.info("Node shutting down ..")
                    self.shutdown()
                else:
                    # ignore: shutdown already initiated ..
                    self.log.info("Node is already shutting down.")
            else:
                self.log.info(
                    "Node will continue to run (node shutdown triggers active: {})"
                    .format(self._node._node_shutdown_triggers))

        d_on_exit = worker.exit.addCallbacks(on_exit_success, on_exit_error)
        d_on_exit.addBoth(check_for_shutdown)

        # create a transport factory for talking WAMP to the native worker
        #
        transport_factory = create_native_worker_client_factory(
            self._node._router_session_factory, worker.ready, worker.exit)
        transport_factory.noisy = False
        self._workers[id].factory = transport_factory

        # now (immediately before actually forking) signal the starting of the worker
        #
        starting_info = {
            'id': id,
            'status': worker.status,
            'created': utcstr(worker.created),
            'who': worker.who
        }

        # the caller gets a progressive result ..
        if details.progress:
            details.progress(starting_info)

        # .. while all others get an event
        self.publish(starting_topic,
                     starting_info,
                     options=PublishOptions(exclude=[details.caller]))

        # now actually fork the worker ..
        #
        self.log.info("Starting {worker} with ID '{id}'...",
                      worker=worker_logname,
                      id=id)
        self.log.debug("{worker} '{id}' command line is '{cmdline}'",
                       worker=worker_logname,
                       id=id,
                       cmdline=' '.join(args))

        d = ep.connect(transport_factory)

        def on_connect_success(proto):

            # this seems to be called immediately when the child process
            # has been forked. even if it then immediately fails because
            # e.g. the executable doesn't even exist. in other words,
            # I'm not sure under what conditions the deferred will errback ..

            pid = proto.transport.pid
            self.log.debug("Native worker process connected with PID {pid}",
                           pid=pid)

            # note the PID of the worker
            worker.pid = pid

            # proto is an instance of NativeWorkerClientProtocol
            worker.proto = proto

            worker.status = 'connected'
            worker.connected = datetime.utcnow()

        def on_connect_error(err):

            # not sure when this errback is triggered at all ..
            self.log.error(
                "Interal error: connection to forked native worker failed ({err})",
                err=err)

            # in any case, forward the error ..
            worker.ready.errback(err)

        d.addCallbacks(on_connect_success, on_connect_error)

        return worker.ready
Example #11
0
    def add_pythonpath(self, paths, prepend=True, details=None):
        """
        Add paths to Python module search paths.

        This procedure is registered under WAMP URI
        ``crossbar.worker.<worker_id>.add_pythonpath``.

        :param paths: List of paths. Relative paths will be resolved relative
                      to the node directory.
        :type paths: list of unicode
        :param prepend: If `True`, prepend the given paths to the current paths.
                        Otherwise append.
        :type prepend: bool
        """
        self.log.debug("{klass}.add_pythonpath", klass=self.__class__.__name__)

        paths_added = []
        for p in paths:
            # transform all paths (relative to cbdir) into absolute paths
            #
            path_to_add = os.path.abspath(
                os.path.join(self.config.extra.cbdir, p))
            if os.path.isdir(path_to_add):
                paths_added.append({'requested': p, 'resolved': path_to_add})
            else:
                emsg = "Cannot add Python search path '{}': resolved path '{}' is not a directory".format(
                    p, path_to_add)
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.invalid_argument',
                                       emsg,
                                       requested=p,
                                       resolved=path_to_add)

        # now extend python module search path
        #
        paths_added_resolved = [p['resolved'] for p in paths_added]
        if prepend:
            sys.path = paths_added_resolved + sys.path
        else:
            sys.path.extend(paths_added_resolved)

        # "It is important to note that the global working_set object is initialized from
        # sys.path when pkg_resources is first imported, but is only updated if you do all
        # future sys.path manipulation via pkg_resources APIs. If you manually modify sys.path,
        # you must invoke the appropriate methods on the working_set instance to keep it in sync."
        #
        # @see: https://pythonhosted.org/setuptools/pkg_resources.html#workingset-objects
        #
        for p in paths_added_resolved:
            pkg_resources.working_set.add_entry(p)

        # publish event "on_pythonpath_add" to all but the caller
        #
        topic = u'{}.on_pythonpath_add'.format(self._uri_prefix)
        res = {
            u'paths': sys.path,
            u'paths_added': paths_added,
            u'prepend': prepend,
            u'who': details.caller
        }
        self.publish(topic,
                     res,
                     options=PublishOptions(exclude=details.caller))

        return res
Example #12
0
    def create_resource(self, path_config):
        """
        Creates child resource to be added to the parent.

        :param path_config: Configuration for the new child resource.
        :type path_config: dict

        :returns: Resource -- the new child resource
        """
        # WAMP-WebSocket resource
        #
        if path_config['type'] == 'websocket':

            ws_factory = WampWebSocketServerFactory(self._router_session_factory, self.config.extra.cbdir, path_config, self._templates)

            # FIXME: Site.start/stopFactory should start/stop factories wrapped as Resources
            ws_factory.startFactory()

            return WebSocketResource(ws_factory)

        # Static file hierarchy resource
        #
        elif path_config['type'] == 'static':

            static_options = path_config.get('options', {})

            if 'directory' in path_config:

                static_dir = os.path.abspath(os.path.join(self.config.extra.cbdir, path_config['directory']))

            elif 'package' in path_config:

                if 'resource' not in path_config:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "missing resource")

                try:
                    mod = importlib.import_module(path_config['package'])
                except ImportError as e:
                    emsg = "Could not import resource {} from package {}: {}".format(path_config['resource'], path_config['package'], e)
                    self.log.error(emsg)
                    raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)
                else:
                    try:
                        static_dir = os.path.abspath(pkg_resources.resource_filename(path_config['package'], path_config['resource']))
                    except Exception as e:
                        emsg = "Could not import resource {} from package {}: {}".format(path_config['resource'], path_config['package'], e)
                        self.log.error(emsg)
                        raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)

            else:

                raise ApplicationError(u"crossbar.error.invalid_configuration", "missing web spec")

            static_dir = static_dir.encode('ascii', 'ignore')  # http://stackoverflow.com/a/20433918/884770

            # create resource for file system hierarchy
            #
            if static_options.get('enable_directory_listing', False):
                static_resource_class = StaticResource
            else:
                static_resource_class = StaticResourceNoListing

            cache_timeout = static_options.get('cache_timeout', DEFAULT_CACHE_TIMEOUT)

            static_resource = static_resource_class(static_dir, cache_timeout=cache_timeout)

            # set extra MIME types
            #
            static_resource.contentTypes.update(EXTRA_MIME_TYPES)
            if 'mime_types' in static_options:
                static_resource.contentTypes.update(static_options['mime_types'])
            patchFileContentTypes(static_resource)

            # render 404 page on any concrete path not found
            #
            static_resource.childNotFound = Resource404(self._templates, static_dir)

            return static_resource

        # WSGI resource
        #
        elif path_config['type'] == 'wsgi':

            if not _HAS_WSGI:
                raise ApplicationError(u"crossbar.error.invalid_configuration", "WSGI unsupported")

            # wsgi_options = path_config.get('options', {})

            if 'module' not in path_config:
                raise ApplicationError(u"crossbar.error.invalid_configuration", "missing WSGI app module")

            if 'object' not in path_config:
                raise ApplicationError(u"crossbar.error.invalid_configuration", "missing WSGI app object")

            # import WSGI app module and object
            mod_name = path_config['module']
            try:
                mod = importlib.import_module(mod_name)
            except ImportError as e:
                raise ApplicationError(u"crossbar.error.invalid_configuration", "WSGI app module '{}' import failed: {} - Python search path was {}".format(mod_name, e, sys.path))
            else:
                obj_name = path_config['object']
                if obj_name not in mod.__dict__:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "WSGI app object '{}' not in module '{}'".format(obj_name, mod_name))
                else:
                    app = getattr(mod, obj_name)

            # create a Twisted Web WSGI resource from the user's WSGI application object
            try:
                wsgi_resource = WSGIResource(self._reactor, self._reactor.getThreadPool(), app)
            except Exception as e:
                raise ApplicationError(u"crossbar.error.invalid_configuration", "could not instantiate WSGI resource: {}".format(e))
            else:
                return wsgi_resource

        # Redirecting resource
        #
        elif path_config['type'] == 'redirect':
            redirect_url = path_config['url'].encode('ascii', 'ignore')
            return RedirectResource(redirect_url)

        # JSON value resource
        #
        elif path_config['type'] == 'json':
            value = path_config['value']

            return JsonResource(value)

        # CGI script resource
        #
        elif path_config['type'] == 'cgi':

            cgi_processor = path_config['processor']
            cgi_directory = os.path.abspath(os.path.join(self.config.extra.cbdir, path_config['directory']))
            cgi_directory = cgi_directory.encode('ascii', 'ignore')  # http://stackoverflow.com/a/20433918/884770

            return CgiDirectory(cgi_directory, cgi_processor, Resource404(self._templates, cgi_directory))

        # WAMP-Longpoll transport resource
        #
        elif path_config['type'] == 'longpoll':

            path_options = path_config.get('options', {})

            lp_resource = WampLongPollResource(self._router_session_factory,
                                               timeout=path_options.get('request_timeout', 10),
                                               killAfter=path_options.get('session_timeout', 30),
                                               queueLimitBytes=path_options.get('queue_limit_bytes', 128 * 1024),
                                               queueLimitMessages=path_options.get('queue_limit_messages', 100),
                                               debug=path_options.get('debug', False),
                                               debug_transport_id=path_options.get('debug_transport_id', None)
                                               )
            lp_resource._templates = self._templates

            return lp_resource

        # Publisher resource (part of REST-bridge)
        #
        elif path_config['type'] == 'publisher':

            # create a vanilla session: the publisher will use this to inject events
            #
            publisher_session_config = ComponentConfig(realm=path_config['realm'], extra=None)
            publisher_session = ApplicationSession(publisher_session_config)

            # add the publisher session to the router
            #
            self._router_session_factory.add(publisher_session, authrole=path_config.get('role', 'anonymous'))

            # now create the publisher Twisted Web resource
            #
            return PublisherResource(path_config.get('options', {}), publisher_session)

        # Webhook resource (part of REST-bridge)
        #
        elif path_config['type'] == 'webhook':

            # create a vanilla session: the webhook will use this to inject events
            #
            webhook_session_config = ComponentConfig(realm=path_config['realm'], extra=None)
            webhook_session = ApplicationSession(webhook_session_config)

            # add the webhook session to the router
            #
            self._router_session_factory.add(webhook_session, authrole=path_config.get('role', 'anonymous'))

            # now create the webhook Twisted Web resource
            #
            return WebhookResource(path_config.get('options', {}), webhook_session)

        # Caller resource (part of REST-bridge)
        #
        elif path_config['type'] == 'caller':

            # create a vanilla session: the caller will use this to inject calls
            #
            caller_session_config = ComponentConfig(realm=path_config['realm'], extra=None)
            caller_session = ApplicationSession(caller_session_config)

            # add the calling session to the router
            #
            self._router_session_factory.add(caller_session, authrole=path_config.get('role', 'anonymous'))

            # now create the caller Twisted Web resource
            #
            return CallerResource(path_config.get('options', {}), caller_session)

        # File Upload resource
        #
        elif path_config['type'] == 'upload':

            upload_directory = os.path.abspath(os.path.join(self.config.extra.cbdir, path_config['directory']))
            upload_directory = upload_directory.encode('ascii', 'ignore')  # http://stackoverflow.com/a/20433918/884770
            if not os.path.isdir(upload_directory):
                emsg = "configured upload directory '{}' in file upload resource isn't a directory".format(upload_directory)
                self.log.error(emsg)
                raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)

            if 'temp_directory' in path_config:
                temp_directory = os.path.abspath(os.path.join(self.config.extra.cbdir, path_config['temp_directory']))
                temp_directory = temp_directory.encode('ascii', 'ignore')  # http://stackoverflow.com/a/20433918/884770
            else:
                temp_directory = os.path.abspath(tempfile.gettempdir())
                temp_directory = os.path.join(temp_directory, 'crossbar-uploads')
                if not os.path.exists(temp_directory):
                    os.makedirs(temp_directory)

            if not os.path.isdir(temp_directory):
                emsg = "configured temp directory '{}' in file upload resource isn't a directory".format(temp_directory)
                self.log.error(emsg)
                raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)

            # file upload progress and finish events are published via this session
            #
            upload_session_config = ComponentConfig(realm=path_config['realm'], extra=None)
            upload_session = ApplicationSession(upload_session_config)

            self._router_session_factory.add(upload_session, authrole=path_config.get('role', 'anonymous'))

            self.log.info("File upload resource started. Uploads to {upl} using temp folder {tmp}.", upl=upload_directory, tmp=temp_directory)

            return FileUploadResource(upload_directory, temp_directory, path_config['form_fields'], upload_session, path_config.get('options', {}))

        # Generic Twisted Web resource
        #
        elif path_config['type'] == 'resource':

            try:
                klassname = path_config['classname']

                self.log.debug("Starting class '{}'".format(klassname))

                c = klassname.split('.')
                module_name, klass_name = '.'.join(c[:-1]), c[-1]
                module = importlib.import_module(module_name)
                make = getattr(module, klass_name)

                return make(path_config.get('extra', {}))

            except Exception as e:
                emsg = "Failed to import class '{}' - {}".format(klassname, e)
                self.log.error(emsg)
                self.log.error("PYTHONPATH: {pythonpath}", pythonpath=sys.path)
                raise ApplicationError(u"crossbar.error.class_import_failed", emsg)

        # Schema Docs resource
        #
        elif path_config['type'] == 'schemadoc':

            realm = path_config['realm']

            if realm not in self.realm_to_id:
                raise ApplicationError(u"crossbar.error.no_such_object", "No realm with URI '{}' configured".format(realm))

            realm_id = self.realm_to_id[realm]

            realm_schemas = self.realms[realm_id].session._schemas

            return SchemaDocResource(self._templates, realm, realm_schemas)

        # Nested subpath resource
        #
        elif path_config['type'] == 'path':

            nested_paths = path_config.get('paths', {})

            if '/' in nested_paths:
                nested_resource = self.create_resource(nested_paths['/'])
            else:
                nested_resource = Resource()

            # nest subpaths under the current entry
            #
            self.add_paths(nested_resource, nested_paths)

            return nested_resource

        else:
            raise ApplicationError(u"crossbar.error.invalid_configuration", "invalid Web path type '{}'".format(path_config['type']))
Example #13
0
 def fail(err):
     emsg = "Cannot listen on transport endpoint: {}".format(err.value)
     self.log.error(emsg)
     raise ApplicationError(u"crossbar.error.cannot_listen", emsg)
Example #14
0
    def start_router_transport(self, id, config, details=None):
        """
        Start a transport on this router and return when the transport has started.

        **Usage:**

        This procedure is registered under

        * ``crossbar.node.<node_id>.worker.<worker_id>.start_router_transport``

        The procedure takes a WAMP transport configuration with a listening endpoint, e.g.

        .. code-block:: javascript

            {
                "type": "websocket",
                "endpoint": {
                    "type": "tcp",
                    "port": 8080
                }
            }

        **Errors:**

        The procedure may raise the following errors:

        * ``crossbar.error.invalid_configuration`` - the provided transport configuration is invalid
        * ``crossbar.error.already_running`` - a transport with the given ID is already running (or starting)
        * ``crossbar.error.cannot_listen`` - could not listen on the configured listening endpoint of the transport
        * ``crossbar.error.class_import_failed`` - a side-by-side component could not be instantiated

        **Events:**

        The procedure will publish an event when the transport **is starting** to

        * ``crossbar.node.<node_id>.worker.<worker_id>.on_router_transport_starting``

        and publish an event when the transport **has started** to

        * ``crossbar.node.<node_id>.worker.<worker_id>.on_router_transport_started``

        :param id: The ID of the transport to start.
        :type id: unicode
        :param config: The transport configuration.
        :type config: dict
        """
        self.log.debug("{}.start_router_transport".format(self.__class__.__name__),
                       id=id, config=config)

        # prohibit starting a transport twice
        #
        if id in self.transports:
            emsg = "Could not start transport: a transport with ID '{}' is already running (or starting)".format(id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.already_running', emsg)

        # check configuration
        #
        try:
            checkconfig.check_router_transport(config)
        except Exception as e:
            emsg = "Invalid router transport configuration: {}".format(e)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)
        else:
            self.log.debug("Starting {}-transport on router.".format(config['type']))

        # standalone WAMP-RawSocket transport
        #
        if config['type'] == 'rawsocket':

            transport_factory = WampRawSocketServerFactory(self._router_session_factory, config)
            transport_factory.noisy = False

        # standalone WAMP-WebSocket transport
        #
        elif config['type'] == 'websocket':

            transport_factory = WampWebSocketServerFactory(self._router_session_factory, self.config.extra.cbdir, config, self._templates)
            transport_factory.noisy = False

        # Flash-policy file server pseudo transport
        #
        elif config['type'] == 'flashpolicy':

            transport_factory = FlashPolicyFactory(config.get('allowed_domain', None), config.get('allowed_ports', None))

        # WebSocket testee pseudo transport
        #
        elif config['type'] == 'websocket.testee':

            transport_factory = WebSocketTesteeServerFactory(config, self._templates)

        # Stream testee pseudo transport
        #
        elif config['type'] == 'stream.testee':

            transport_factory = StreamTesteeServerFactory()

        # Twisted Web based transport
        #
        elif config['type'] == 'web':

            options = config.get('options', {})

            # create Twisted Web root resource
            #
            root_config = config['paths']['/']

            root_type = root_config['type']
            root_options = root_config.get('options', {})

            # Static file hierarchy root resource
            #
            if root_type == 'static':

                if 'directory' in root_config:

                    root_dir = os.path.abspath(os.path.join(self.config.extra.cbdir, root_config['directory']))

                elif 'package' in root_config:

                    if 'resource' not in root_config:
                        raise ApplicationError(u"crossbar.error.invalid_configuration", "missing resource")

                    try:
                        mod = importlib.import_module(root_config['package'])
                    except ImportError as e:
                        emsg = "Could not import resource {} from package {}: {}".format(root_config['resource'], root_config['package'], e)
                        self.log.error(emsg)
                        raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)
                    else:
                        try:
                            root_dir = os.path.abspath(pkg_resources.resource_filename(root_config['package'], root_config['resource']))
                        except Exception as e:
                            emsg = "Could not import resource {} from package {}: {}".format(root_config['resource'], root_config['package'], e)
                            self.log.error(emsg)
                            raise ApplicationError(u"crossbar.error.invalid_configuration", emsg)
                        else:
                            mod_version = getattr(mod, '__version__', '?.?.?')
                            self.log.info("Loaded static Web resource '{}' from package '{} {}' (filesystem path {})".format(root_config['resource'], root_config['package'], mod_version, root_dir))

                else:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "missing web spec")

                root_dir = root_dir.encode('ascii', 'ignore')  # http://stackoverflow.com/a/20433918/884770
                self.log.debug("Starting Web service at root directory {}".format(root_dir))

                # create resource for file system hierarchy
                #
                if root_options.get('enable_directory_listing', False):
                    static_resource_class = StaticResource
                else:
                    static_resource_class = StaticResourceNoListing

                cache_timeout = root_options.get('cache_timeout', DEFAULT_CACHE_TIMEOUT)

                root = static_resource_class(root_dir, cache_timeout=cache_timeout)

                # set extra MIME types
                #
                root.contentTypes.update(EXTRA_MIME_TYPES)
                if 'mime_types' in root_options:
                    root.contentTypes.update(root_options['mime_types'])
                patchFileContentTypes(root)

                # render 404 page on any concrete path not found
                #
                root.childNotFound = Resource404(self._templates, root_dir)

            # WSGI root resource
            #
            elif root_type == 'wsgi':

                if not _HAS_WSGI:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "WSGI unsupported")

                # wsgi_options = root_config.get('options', {})

                if 'module' not in root_config:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "missing WSGI app module")

                if 'object' not in root_config:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "missing WSGI app object")

                # import WSGI app module and object
                mod_name = root_config['module']
                try:
                    mod = importlib.import_module(mod_name)
                except ImportError as e:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "WSGI app module '{}' import failed: {} - Python search path was {}".format(mod_name, e, sys.path))
                else:
                    obj_name = root_config['object']
                    if obj_name not in mod.__dict__:
                        raise ApplicationError(u"crossbar.error.invalid_configuration", "WSGI app object '{}' not in module '{}'".format(obj_name, mod_name))
                    else:
                        app = getattr(mod, obj_name)

                # create a Twisted Web WSGI resource from the user's WSGI application object
                try:
                    wsgi_resource = WSGIResource(self._reactor, self._reactor.getThreadPool(), app)
                except Exception as e:
                    raise ApplicationError(u"crossbar.error.invalid_configuration", "could not instantiate WSGI resource: {}".format(e))
                else:
                    # create a root resource serving everything via WSGI
                    root = WSGIRootResource(wsgi_resource, {})

            # Redirecting root resource
            #
            elif root_type == 'redirect':

                redirect_url = root_config['url'].encode('ascii', 'ignore')
                root = RedirectResource(redirect_url)

            # Publisher resource (part of REST-bridge)
            #
            elif root_type == 'publisher':

                # create a vanilla session: the publisher will use this to inject events
                #
                publisher_session_config = ComponentConfig(realm=root_config['realm'], extra=None)
                publisher_session = ApplicationSession(publisher_session_config)

                # add the publishing session to the router
                #
                self._router_session_factory.add(publisher_session, authrole=root_config.get('role', 'anonymous'))

                # now create the publisher Twisted Web resource and add it to resource tree
                #
                root = PublisherResource(root_config.get('options', {}), publisher_session)

            # Webhook resource (part of REST-bridge)
            #
            elif root_type == 'webhook':

                # create a vanilla session: the webhook will use this to inject events
                #
                webhook_session_config = ComponentConfig(realm=root_config['realm'], extra=None)
                webhook_session = ApplicationSession(webhook_session_config)

                # add the publishing session to the router
                #
                self._router_session_factory.add(webhook_session, authrole=root_config.get('role', 'anonymous'))

                # now create the webhook Twisted Web resource and add it to resource tree
                #
                root = WebhookResource(root_config.get('options', {}), webhook_session)

            # Caller resource (part of REST-bridge)
            #
            elif root_type == 'caller':

                # create a vanilla session: the caller will use this to inject calls
                #
                caller_session_config = ComponentConfig(realm=root_config['realm'], extra=None)
                caller_session = ApplicationSession(caller_session_config)

                # add the calling session to the router
                #
                self._router_session_factory.add(caller_session, authrole=root_config.get('role', 'anonymous'))

                # now create the caller Twisted Web resource and add it to resource tree
                #
                root = CallerResource(root_config.get('options', {}), caller_session)

            # Generic Twisted Web resource
            #
            elif root_type == 'resource':

                try:
                    klassname = root_config['classname']

                    self.log.debug("Starting class '{}'".format(klassname))

                    c = klassname.split('.')
                    module_name, klass_name = '.'.join(c[:-1]), c[-1]
                    module = importlib.import_module(module_name)
                    make = getattr(module, klass_name)
                    root = make(root_config.get('extra', {}))

                except Exception as e:
                    emsg = "Failed to import class '{}' - {}".format(klassname, e)
                    self.log.error(emsg)
                    self.log.error("PYTHONPATH: {pythonpath}",
                                   pythonpath=sys.path)
                    raise ApplicationError(u"crossbar.error.class_import_failed", emsg)

            # Invalid root resource
            #
            else:
                raise ApplicationError(u"crossbar.error.invalid_configuration", "invalid Web root path type '{}'".format(root_type))

            # create Twisted Web resources on all non-root paths configured
            #
            self.add_paths(root, config.get('paths', {}))

            # create the actual transport factory
            #
            transport_factory = Site(root)
            transport_factory.noisy = False

            # Web access logging
            #
            if not options.get('access_log', False):
                transport_factory.log = lambda _: None

            # Traceback rendering
            #
            transport_factory.displayTracebacks = options.get('display_tracebacks', False)

            # HSTS
            #
            if options.get('hsts', False):
                if 'tls' in config['endpoint']:
                    hsts_max_age = int(options.get('hsts_max_age', 31536000))
                    transport_factory.requestFactory = createHSTSRequestFactory(transport_factory.requestFactory, hsts_max_age)
                else:
                    self.log.warn("Warning: HSTS requested, but running on non-TLS - skipping HSTS")

        # Unknown transport type
        #
        else:
            # should not arrive here, since we did check_transport() in the beginning
            raise Exception("logic error")

        # create transport endpoint / listening port from transport factory
        #
        d = create_listening_port_from_config(config['endpoint'], transport_factory, self.config.extra.cbdir, self._reactor)

        def ok(port):
            self.transports[id] = RouterTransport(id, config, transport_factory, port)
            self.log.debug("Router transport '{}'' started and listening".format(id))
            return

        def fail(err):
            emsg = "Cannot listen on transport endpoint: {}".format(err.value)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.cannot_listen", emsg)

        d.addCallbacks(ok, fail)
        return d
Example #15
0
        def fail(err):
            emsg = "Cannot stop listening on transport endpoint: {log_failure}"
            self.log.error(emsg, log_failure=err)

            raise ApplicationError("crossbar.error.cannot_stop", emsg)
Example #16
0
    def start_router_component(self, id, config, details=None):
        """
      Dynamically start an application component to run next to the router in "embedded mode".

      :param id: The ID of the component to start.
      :type id: str
      :param config: The component configuration.
      :type config: obj
      """
        if self.debug:
            log.msg(
                "{}.start_router_component".format(self.__class__.__name__),
                id, config)

        ## prohibit starting a component twice
        ##
        if id in self.components:
            emsg = "ERROR: could not start component - a component with ID '{}'' is already running (or starting)".format(
                id)
            log.msg(emsg)
            raise ApplicationError('crossbar.error.already_running', emsg)

        ## check configuration
        ##
        try:
            checkconfig.check_router_component(config)
        except Exception as e:
            emsg = "ERROR: invalid router component configuration ({})".format(
                e)
            log.msg(emsg)
            raise ApplicationError("crossbar.error.invalid_configuration",
                                   emsg)
        else:
            if self.debug:
                log.msg("Starting {}-component on router.".format(
                    config['type']))

        realm = config['realm']
        cfg = ComponentConfig(realm=realm, extra=config.get('extra', None))

        if config['type'] == 'class':

            try:
                klassname = config['classname']

                if self.debug:
                    log.msg("Starting class '{}'".format(klassname))

                c = klassname.split('.')
                module_name, klass_name = '.'.join(c[:-1]), c[-1]
                module = importlib.import_module(module_name)
                make = getattr(module, klass_name)

            except Exception as e:
                emsg = "Failed to import class '{}' - {}".format(klassname, e)
                log.msg(emsg)
                raise ApplicationError("crossbar.error.class_import_failed",
                                       emsg)

        elif config['type'] == 'wamplet':

            try:
                dist = config['package']
                name = config['entrypoint']

                if self.debug:
                    log.msg("Starting WAMPlet '{}/{}'".format(dist, name))

                ## make is supposed to make instances of ApplicationSession
                make = pkg_resources.load_entry_point(
                    dist, 'autobahn.twisted.wamplet', name)

            except Exception as e:
                emsg = "Failed to import wamplet '{}/{}' - {}".format(
                    dist, name, e)
                log.msg(emsg)
                raise ApplicationError("crossbar.error.class_import_failed",
                                       emsg)

        else:
            raise ApplicationError(
                "crossbar.error.invalid_configuration",
                "invalid component type '{}'".format(config['type']))

        ## .. and create and add an WAMP application session to
        ## run the component next to the router
        ##
        try:
            session = make(cfg)
        except Exception as e:
            raise ApplicationError("crossbar.error.class_import_failed",
                                   str(e))

        if not isinstance(session, ApplicationSession):
            raise ApplicationError(
                "crossbar.error.class_import_failed",
                "session not derived of ApplicationSession")

        self.components[id] = RouterComponent(id, config, session)
        self.session_factory.add(session,
                                 authrole=config.get('role', 'anonymous'))
Example #17
0
    def start_web_transport_service(self, transport_id, path, config, details=None):
        """
        Start a service on a Web transport.

        :param transport_id: The ID of the transport to start the Web transport service on.
        :type transport_id: str

        :param path: The path (absolute URL, eg "/myservice1") on which to start the service.
        :type path: str

        :param config: The Web service configuration.
        :type config: dict

        :param details: Call details.
        :type details: :class:`autobahn.wamp.types.CallDetails`
        """
        if not isinstance(config, dict) or 'type' not in config:
            raise ApplicationError('crossbar.invalid_argument', 'config parameter must be dict with type attribute')

        self.log.info('Starting "{service_type}" Web service on path "{path}" of transport "{transport_id}" {method}',
                      service_type=config.get('type', None),
                      path=path,
                      transport_id=transport_id,
                      method=hltype(self.start_web_transport_service))

        transport = self.transports.get(transport_id, None)
        if not transport:
            emsg = 'Cannot start service on transport: no transport with ID "{}"'.format(transport_id)
            self.log.error(emsg)
            raise ApplicationError('crossbar.error.not_running', emsg)

        if not isinstance(transport, self.personality.RouterWebTransport):
            emsg = 'Cannot start service on transport: transport is not a Web transport (transport_type={})'.format(hltype(transport.__class__))
            self.log.error(emsg)
            raise ApplicationError('crossbar.error.not_running', emsg)

        if transport.state != self.personality.RouterTransport.STATE_STARTED:
            emsg = 'Cannot start service on Web transport service: transport is not running (transport_state={})'.format(
                transport_id, self.personality.RouterWebTransport.STATES.get(transport.state, None))
            self.log.error(emsg)
            raise ApplicationError('crossbar.error.not_running', emsg)

        if path in transport.root:
            emsg = 'Cannot start service on Web transport "{}": a service is already running on path "{}"'.format(transport_id, path)
            self.log.error(emsg)
            raise ApplicationError('crossbar.error.already_running', emsg)

        caller = details.caller if details else None
        self.publish(self._uri_prefix + '.on_web_transport_service_starting',
                     transport_id,
                     path,
                     options=PublishOptions(exclude=caller))

        # now actually add the web service ..
        # note: currently this is NOT async, but direct/sync.
        webservice_factory = self.personality.WEB_SERVICE_FACTORIES[config['type']]

        webservice = yield maybeDeferred(webservice_factory.create, transport, path, config)
        transport.root[path] = webservice

        on_web_transport_service_started = {
            'transport_id': transport_id,
            'path': path,
            'config': config
        }
        caller = details.caller if details else None
        self.publish(self._uri_prefix + '.on_web_transport_service_started',
                     transport_id,
                     path,
                     on_web_transport_service_started,
                     options=PublishOptions(exclude=caller))

        returnValue(on_web_transport_service_started)
Example #18
0
    def start_router_transport(self, id, config, details=None):
        """
      Start a transport on this router.

      :param id: The ID of the transport to start.
      :type id: str
      :param config: The transport configuration.
      :type config: dict
      """
        if self.debug:
            log.msg(
                "{}.start_router_transport".format(self.__class__.__name__),
                id, config)

        ## prohibit starting a transport twice
        ##
        if id in self.transports:
            emsg = "ERROR: could not start transport - a transport with ID '{}'' is already running (or starting)".format(
                id)
            log.msg(emsg)
            raise ApplicationError('crossbar.error.already_running', emsg)

        ## check configuration
        ##
        try:
            checkconfig.check_router_transport(config)
        except Exception as e:
            emsg = "ERROR: invalid router transport configuration ({})".format(
                e)
            log.msg(emsg)
            raise ApplicationError("crossbar.error.invalid_configuration",
                                   emsg)
        else:
            if self.debug:
                log.msg("Starting {}-transport on router.".format(
                    config['type']))

        ## standalone WAMP-RawSocket transport
        ##
        if config['type'] == 'rawsocket':

            transport_factory = CrossbarWampRawSocketServerFactory(
                self.session_factory, config)
            transport_factory.noisy = False

        ## standalone WAMP-WebSocket transport
        ##
        elif config['type'] == 'websocket':

            transport_factory = CrossbarWampWebSocketServerFactory(
                self.session_factory, self.config.extra.cbdir, config,
                self._templates)
            transport_factory.noisy = False

        ## Flash-policy file server pseudo transport
        ##
        elif config['type'] == 'flashpolicy':

            transport_factory = FlashPolicyFactory(
                config.get('allowed_domain', None),
                config.get('allowed_ports', None))

        ## WebSocket testee pseudo transport
        ##
        elif config['type'] == 'websocket.testee':

            transport_factory = WebSocketTesteeServerFactory(
                config, self._templates)

        ## Stream testee pseudo transport
        ##
        elif config['type'] == 'stream.testee':

            transport_factory = StreamTesteeServerFactory()

        ## Twisted Web based transport
        ##
        elif config['type'] == 'web':

            options = config.get('options', {})

            ## create Twisted Web root resource
            ##
            root_config = config['paths']['/']

            root_type = root_config['type']
            root_options = root_config.get('options', {})

            ## Static file hierarchy root resource
            ##
            if root_type == 'static':

                if 'directory' in root_config:

                    root_dir = os.path.abspath(
                        os.path.join(self.config.extra.cbdir,
                                     root_config['directory']))

                elif 'package' in root_config:

                    if not 'resource' in root_config:
                        raise ApplicationError(
                            "crossbar.error.invalid_configuration",
                            "missing resource")

                    try:
                        mod = importlib.import_module(root_config['package'])
                    except ImportError as e:
                        emsg = "ERROR: could not import resource '{}' from package '{}' - {}".format(
                            root_config['resource'], root_config['package'], e)
                        log.msg(emsg)
                        raise ApplicationError(
                            "crossbar.error.invalid_configuration", emsg)
                    else:
                        try:
                            root_dir = os.path.abspath(
                                pkg_resources.resource_filename(
                                    root_config['package'],
                                    root_config['resource']))
                        except Exception as e:
                            emsg = "ERROR: could not import resource '{}' from package '{}' - {}".format(
                                root_config['resource'],
                                root_config['package'], e)
                            log.msg(emsg)
                            raise ApplicationError(
                                "crossbar.error.invalid_configuration", emsg)
                        else:
                            mod_version = getattr(mod, '__version__', '?.?.?')
                            log.msg(
                                "Loaded static Web resource '{}' from package '{} {}' (filesystem path {})"
                                .format(root_config['resource'],
                                        root_config['package'], mod_version,
                                        root_dir))

                else:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "missing web spec")

                root_dir = root_dir.encode(
                    'ascii',
                    'ignore')  # http://stackoverflow.com/a/20433918/884770
                if self.debug:
                    log.msg("Starting Web service at root directory {}".format(
                        root_dir))

                ## create resource for file system hierarchy
                ##
                if root_options.get('enable_directory_listing', False):
                    root = File(root_dir)
                else:
                    root = FileNoListing(root_dir)

                ## set extra MIME types
                ##
                root.contentTypes.update(EXTRA_MIME_TYPES)
                if 'mime_types' in root_options:
                    root.contentTypes.update(root_options['mime_types'])
                patchFileContentTypes(root)

                ## render 404 page on any concrete path not found
                ##
                root.childNotFound = Resource404(self._templates, root_dir)

            ## WSGI root resource
            ##
            elif root_type == 'wsgi':

                if not _HAS_WSGI:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "WSGI unsupported")

                wsgi_options = root_config.get('options', {})

                if not 'module' in root_config:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "missing module")

                if not 'object' in root_config:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "missing object")

                try:
                    mod = importlib.import_module(root_config['module'])
                except ImportError:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "module import failed")
                else:
                    if not root_config['object'] in mod.__dict__:
                        raise ApplicationError(
                            "crossbar.error.invalid_configuration",
                            "object not in module")
                    else:
                        app = getattr(mod, root_config['object'])

                ## create a Twisted Web WSGI resource from the user's WSGI application object
                try:
                    wsgi_resource = WSGIResource(reactor,
                                                 reactor.getThreadPool(), app)
                except Exception as e:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "could not instantiate WSGI resource: {}".format(e))
                else:
                    ## create a root resource serving everything via WSGI
                    root = WSGIRootResource(wsgi_resource, {})

            ## Redirecting root resource
            ##
            elif root_type == 'redirect':

                redirect_url = root_config['url'].encode('ascii', 'ignore')
                root = RedirectResource(redirect_url)

            ## Pusher resource
            ##
            elif root_type == 'pusher':

                ## create a vanilla session: the pusher will use this to inject events
                ##
                pusher_session_config = ComponentConfig(
                    realm=root_config['realm'], extra=None)
                pusher_session = ApplicationSession(pusher_session_config)

                ## add the pushing session to the router
                ##
                self.session_factory.add(pusher_session,
                                         authrole=root_config.get(
                                             'role', 'anonymous'))

                ## now create the pusher Twisted Web resource and add it to resource tree
                ##
                root = PusherResource(root_config.get('options', {}),
                                      pusher_session)

            ## Invalid root resource
            ##
            else:
                raise ApplicationError(
                    "crossbar.error.invalid_configuration",
                    "invalid Web root path type '{}'".format(root_type))

            ## create Twisted Web resources on all non-root paths configured
            ##
            self.add_paths(root, config.get('paths', {}))

            ## create the actual transport factory
            ##
            transport_factory = Site(root)
            transport_factory.noisy = False

            ## Web access logging
            ##
            if not options.get('access_log', False):
                transport_factory.log = lambda _: None

            ## Traceback rendering
            ##
            transport_factory.displayTracebacks = options.get(
                'display_tracebacks', False)

            ## HSTS
            ##
            if options.get('hsts', False):
                if 'tls' in config['endpoint']:
                    hsts_max_age = int(options.get('hsts_max_age', 31536000))
                    transport_factory.requestFactory = createHSTSRequestFactory(
                        transport_factory.requestFactory, hsts_max_age)
                else:
                    log.msg(
                        "Warning: HSTS requested, but running on non-TLS - skipping HSTS"
                    )

            ## enable Hixie-76 on Twisted Web
            ##
            if options.get('hixie76_aware', False):
                transport_factory.protocol = HTTPChannelHixie76Aware  # needed if Hixie76 is to be supported

        ## Unknown transport type
        ##
        else:
            ## should not arrive here, since we did check_transport() in the beginning
            raise Exception("logic error")

        ## create transport endpoint / listening port from transport factory
        ##
        d = create_listening_port_from_config(config['endpoint'],
                                              transport_factory,
                                              self.config.extra.cbdir, reactor)

        def ok(port):
            self.transports[id] = RouterTransport(id, config,
                                                  transport_factory, port)
            if self.debug:
                log.msg(
                    "Router transport '{}'' started and listening".format(id))
            return

        def fail(err):
            emsg = "ERROR: cannot listen on transport endpoint ({})".format(
                err.value)
            log.msg(emsg)
            raise ApplicationError("crossbar.error.cannot_listen", emsg)

        d.addCallbacks(ok, fail)
        return d
Example #19
0
 def on_ready_error(err):
     del self._workers[worker.id]
     emsg = 'Failed to start native worker: {}'.format(err.value)
     self.log.error(emsg)
     raise ApplicationError(u"crossbar.error.cannot_start", emsg,
                            worker.getlog())
Example #20
0
 def fail(err):
     emsg = "ERROR: cannot listen on transport endpoint ({})".format(
         err.value)
     log.msg(emsg)
     raise ApplicationError("crossbar.error.cannot_listen", emsg)
Example #21
0
    def start_container_component(self,
                                  id,
                                  config,
                                  reload_modules=False,
                                  details=None):
        """
        Starts a Class or WAMPlet in this component container.

        :param config: Component configuration.
        :type config: dict
        :param reload_modules: If `True`, enforce reloading of modules (user code)
                               that were modified (see: TrackingModuleReloader).
        :type reload_modules: bool
        :param details: Caller details.
        :type details: instance of :class:`autobahn.wamp.types.CallDetails`

        :returns dict -- A dict with combined info from component starting.
        """
        self.log.debug("{klass}.start_container_component({id}, {config})",
                       klass=self.__class__.__name__,
                       id=id,
                       config=config)

        # prohibit starting a component twice
        #
        if id in self.components:
            emsg = "Could not start component - a component with ID '{}'' is already running (or starting)".format(
                id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.already_running', emsg)

        # check configuration
        #
        try:
            checkconfig.check_container_component(config)
        except Exception as e:
            emsg = "Invalid container component configuration ({})".format(e)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.invalid_configuration",
                                   emsg)
        else:
            self.log.debug("Starting {type}-component in container.",
                           type=config['type'])

        # 1) create WAMP application component factory
        #
        realm = config['realm']
        extra = config.get('extra', None)
        component_config = ComponentConfig(realm=realm, extra=extra)

        try:
            create_component = _appsession_loader(config)
        except ApplicationError as e:
            self.log.error("Component loading failed", log_failure=Failure())
            if 'No module named' in str(e):
                self.log.error("  Python module search paths:")
                for path in e.kwargs['pythonpath']:
                    self.log.error("    {path}", path=path)
            raise

        # force reload of modules (user code)
        #
        if reload_modules:
            self._module_tracker.reload()

        # WAMP application session factory
        # ultimately, this gets called once the connection is
        # establised, from onOpen in autobahn/wamp/websocket.py:59
        def create_session():
            try:
                session = create_component(component_config)

                # any exception spilling out from user code in onXXX handlers is fatal!
                def panic(fail, msg):
                    self.log.error(
                        "Fatal error in component: {msg} - {log_failure.value}",
                        msg=msg,
                        log_failure=fail,
                    )
                    session.disconnect()

                session._swallow_error = panic
                return session
            except Exception:
                self.log.failure(
                    "Component instantiation failed: {log_failure.value}")
                raise

        # 2) create WAMP transport factory
        #
        transport_config = config['transport']

        # WAMP-over-WebSocket transport
        #
        if transport_config['type'] == 'websocket':

            # create a WAMP-over-WebSocket transport client factory
            #
            transport_factory = WampWebSocketClientFactory(
                create_session, transport_config['url'])
            transport_factory.noisy = False

        # WAMP-over-RawSocket transport
        #
        elif transport_config['type'] == 'rawsocket':

            transport_factory = WampRawSocketClientFactory(
                create_session, transport_config)
            transport_factory.noisy = False

        else:
            # should not arrive here, since we did `check_container_component()`
            raise Exception("logic error")

        # 3) create and connect client endpoint
        #
        endpoint = create_connecting_endpoint_from_config(
            transport_config['endpoint'], self.config.extra.cbdir,
            self._reactor, self.log)

        # now connect the client
        #
        d = endpoint.connect(transport_factory)

        def success(proto):
            component = ContainerComponent(id, config, proto, None)
            self.components[id] = component

            # FIXME: this is a total hack.
            #
            def close_wrapper(orig, was_clean, code, reason):
                """
                Wrap our protocol's onClose so we can tell when the component
                exits.
                """
                r = orig(was_clean, code, reason)
                if component.id not in self.components:
                    self.log.warn("Component '{id}' closed, but not in set.",
                                  id=component.id)
                    return r

                if was_clean:
                    self.log.info(
                        "Closed connection to '{id}' with code '{code}'",
                        id=component.id,
                        code=code)
                else:
                    self.log.error(
                        "Lost connection to component '{id}' with code '{code}'.",
                        id=component.id,
                        code=code)

                if reason:
                    self.log.warn(str(reason))

                del self.components[component.id]
                self._publish_component_stop(component)
                component._stopped.callback(component.marshal())

                if not self.components:
                    self.log.info(
                        "Container is hosting no more components: shutting down."
                    )
                    self.stop_container()

                return r

            # FIXME: due to history, the following is currently the case:
            # ITransportHandler.onClose is implemented directly on WampWebSocketClientProtocol,
            # while with WampRawSocketClientProtocol, the ITransportHandler is implemented
            # by the object living on proto._session
            #
            if isinstance(proto, WampWebSocketClientProtocol):
                proto.onClose = partial(close_wrapper, proto.onClose)

            elif isinstance(proto, WampRawSocketClientProtocol):
                # FIXME: doesn't work without guard, since proto_.session is not yet there when
                # proto comes into existance ..
                if proto._session:
                    proto._session.onClose = partial(close_wrapper,
                                                     proto._session.onClose)
            else:
                raise Exception("logic error")

            # publish event "on_component_start" to all but the caller
            #
            topic = self._uri_prefix + '.container.on_component_start'
            event = {u'id': id}
            self.publish(topic,
                         event,
                         options=PublishOptions(exclude=details.caller))
            return event

        def error(err):
            # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html
            if isinstance(err.value, internet.error.ConnectError):
                emsg = "Could not connect container component to router - transport establishment failed ({})".format(
                    err.value)
                self.log.error(emsg)
                raise ApplicationError(u'crossbar.error.cannot_connect', emsg)
            else:
                # should not arrive here (since all errors arriving here should be subclasses of ConnectError)
                raise err

        d.addCallbacks(success, error)

        return d
Example #22
0
    def create_resource(self, path_config):
        """
      Creates child resource to be added to the parent.

      :param path_config: Configuration for the new child resource.
      :type path_config: dict

      :returns: Resource -- the new child resource
      """
        ## WAMP-WebSocket resource
        ##
        if path_config['type'] == 'websocket':

            ws_factory = CrossbarWampWebSocketServerFactory(
                self.session_factory, self.config.extra.cbdir, path_config,
                self._templates)

            ## FIXME: Site.start/stopFactory should start/stop factories wrapped as Resources
            ws_factory.startFactory()

            return WebSocketResource(ws_factory)

        ## Static file hierarchy resource
        ##
        elif path_config['type'] == 'static':

            static_options = path_config.get('options', {})

            if 'directory' in path_config:

                static_dir = os.path.abspath(
                    os.path.join(self.config.extra.cbdir,
                                 path_config['directory']))

            elif 'package' in path_config:

                if not 'resource' in path_config:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "missing resource")

                try:
                    mod = importlib.import_module(path_config['package'])
                except ImportError as e:
                    emsg = "ERROR: could not import resource '{}' from package '{}' - {}".format(
                        path_config['resource'], path_config['package'], e)
                    log.msg(emsg)
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration", emsg)
                else:
                    try:
                        static_dir = os.path.abspath(
                            pkg_resources.resource_filename(
                                path_config['package'],
                                path_config['resource']))
                    except Exception as e:
                        emsg = "ERROR: could not import resource '{}' from package '{}' - {}".format(
                            path_config['resource'], path_config['package'], e)
                        log.msg(emsg)
                        raise ApplicationError(
                            "crossbar.error.invalid_configuration", emsg)

            else:

                raise ApplicationError("crossbar.error.invalid_configuration",
                                       "missing web spec")

            static_dir = static_dir.encode(
                'ascii',
                'ignore')  # http://stackoverflow.com/a/20433918/884770

            ## create resource for file system hierarchy
            ##
            if static_options.get('enable_directory_listing', False):
                static_resource = File(static_dir)
            else:
                static_resource = FileNoListing(static_dir)

            ## set extra MIME types
            ##
            static_resource.contentTypes.update(EXTRA_MIME_TYPES)
            if 'mime_types' in static_options:
                static_resource.contentTypes.update(
                    static_options['mime_types'])
            patchFileContentTypes(static_resource)

            ## render 404 page on any concrete path not found
            ##
            static_resource.childNotFound = Resource404(
                self._templates, static_dir)

            return static_resource

        ## WSGI resource
        ##
        elif path_config['type'] == 'wsgi':

            if not _HAS_WSGI:
                raise ApplicationError("crossbar.error.invalid_configuration",
                                       "WSGI unsupported")

            wsgi_options = path_config.get('options', {})

            if not 'module' in path_config:
                raise ApplicationError("crossbar.error.invalid_configuration",
                                       "missing module")

            if not 'object' in path_config:
                raise ApplicationError("crossbar.error.invalid_configuration",
                                       "missing object")

            try:
                mod = importlib.import_module(path_config['module'])
            except ImportError as e:
                raise ApplicationError("crossbar.error.invalid_configuration",
                                       "module import failed - {}".format(e))
            else:
                if not path_config['object'] in mod.__dict__:
                    raise ApplicationError(
                        "crossbar.error.invalid_configuration",
                        "object not in module")
                else:
                    app = getattr(mod, path_config['object'])

            ## create a Twisted Web WSGI resource from the user's WSGI application object
            try:
                wsgi_resource = WSGIResource(reactor, reactor.getThreadPool(),
                                             app)
            except Exception as e:
                raise ApplicationError(
                    "crossbar.error.invalid_configuration",
                    "could not instantiate WSGI resource: {}".format(e))
            else:
                return wsgi_resource

        ## Redirecting resource
        ##
        elif path_config['type'] == 'redirect':
            redirect_url = path_config['url'].encode('ascii', 'ignore')
            return RedirectResource(redirect_url)

        ## JSON value resource
        ##
        elif path_config['type'] == 'json':
            value = path_config['value']

            return JsonResource(value)

        ## CGI script resource
        ##
        elif path_config['type'] == 'cgi':

            cgi_processor = path_config['processor']
            cgi_directory = os.path.abspath(
                os.path.join(self.config.extra.cbdir,
                             path_config['directory']))
            cgi_directory = cgi_directory.encode(
                'ascii',
                'ignore')  # http://stackoverflow.com/a/20433918/884770

            return CgiDirectory(cgi_directory, cgi_processor,
                                Resource404(self._templates, cgi_directory))

        ## WAMP-Longpoll transport resource
        ##
        elif path_config['type'] == 'longpoll':

            path_options = path_config.get('options', {})

            lp_resource = WampLongPollResource(
                self.session_factory,
                timeout=path_options.get('request_timeout', 10),
                killAfter=path_options.get('session_timeout', 30),
                queueLimitBytes=path_options.get('queue_limit_bytes',
                                                 128 * 1024),
                queueLimitMessages=path_options.get('queue_limit_messages',
                                                    100),
                debug=path_options.get('debug', False),
                debug_transport_id=path_options.get('debug_transport_id',
                                                    None))
            lp_resource._templates = self._templates

            return lp_resource

        ## Pusher resource
        ##
        elif path_config['type'] == 'pusher':

            ## create a vanilla session: the pusher will use this to inject events
            ##
            pusher_session_config = ComponentConfig(realm=path_config['realm'],
                                                    extra=None)
            pusher_session = ApplicationSession(pusher_session_config)

            ## add the pushing session to the router
            ##
            self.session_factory.add(pusher_session,
                                     authrole=path_config.get(
                                         'role', 'anonymous'))

            ## now create the pusher Twisted Web resource
            ##
            return PusherResource(path_config.get('options', {}),
                                  pusher_session)

        ## Schema Docs resource
        ##
        elif path_config['type'] == 'schemadoc':

            realm = path_config['realm']

            if not realm in self.realm_to_id:
                raise ApplicationError(
                    "crossbar.error.no_such_object",
                    "No realm with URI '{}' configured".format(realm))

            realm_id = self.realm_to_id[realm]

            realm_schemas = self.realms[realm_id].session._schemas

            return SchemaDocResource(self._templates, realm, realm_schemas)

        ## Nested subpath resource
        ##
        elif path_config['type'] == 'path':

            nested_paths = path_config.get('paths', {})

            if '/' in nested_paths:
                nested_resource = self.create_resource(nested_paths['/'])
            else:
                nested_resource = Resource()

            ## nest subpaths under the current entry
            ##
            self.add_paths(nested_resource, nested_paths)

            return nested_resource

        else:
            raise ApplicationError(
                "crossbar.error.invalid_configuration",
                "invalid Web path type '{}'".format(path_config['type']))
Example #23
0
    def start_management_transport(self, config, details=None):
        """
      Start transport for local management router.

      :param config: Transport configuration.
      :type config: obj
      """
        if self.debug:
            log.msg(
                "{}.start_management_transport".format(
                    self.__class__.__name__), config)

        if self._management_transport:
            emsg = "ERROR: could not start management transport - already running (or starting)"
            log.msg(emsg)
            raise ApplicationError("crossbar.error.already_started", emsg)

        try:
            checkconfig.check_listening_transport_websocket(config)
        except Exception as e:
            emsg = "ERROR: could not start management transport - invalid configuration ({})".format(
                e)
            log.msg(emsg)
            raise ApplicationError('crossbar.error.invalid_configuration',
                                   emsg)

        self._management_transport = ManagementTransport(
            config, details.authid)

        factory = WampWebSocketServerFactory(
            self._node._router_session_factory, debug=False)
        factory.setProtocolOptions(failByDrop=False)
        factory.noisy = False

        starting_topic = '{}.on_management_transport_starting'.format(
            self._uri_prefix)
        starting_info = self._management_transport.marshal()

        ## the caller gets a progressive result ..
        if details.progress:
            details.progress(starting_info)

        ## .. while all others get an event
        self.publish(starting_topic,
                     starting_info,
                     options=PublishOptions(exclude=[details.caller]))

        try:
            self._management_transport.port = yield create_listening_port_from_config(
                config['endpoint'], factory, self.cbdir, reactor)
        except Exception as e:
            self._management_transport = None
            emsg = "ERROR: local management service endpoint cannot listen - {}".format(
                e)
            log.msg(emsg)
            raise ApplicationError("crossbar.error.cannot_listen", emsg)

        ## alright, manhole has started
        self._management_transport.started = datetime.utcnow()
        self._management_transport.status = 'started'

        started_topic = '{}.on_management_transport_started'.format(
            self._uri_prefix)
        started_info = self._management_transport.marshal()
        self.publish(started_topic,
                     started_info,
                     options=PublishOptions(exclude=[details.caller]))

        returnValue(started_info)
Example #24
0
    def start_router_realm_link(self, realm_id, link_id, link_config, details=None):
        """
        Start a new router link to a remote router on a (local) realm.

        The link configuration (``link_config``) must include the transport definition
        to the remote router. Here is an example:

        .. code-block:: json

            {
                "realm": "realm1",
                "transport": {
                    "type": "websocket",
                    "endpoint": {
                        "type": "tcp",
                        "host": "localhost",
                        "port": 8002
                    },
                    "url": "ws://localhost:8002/ws"
                }
            }

        :param realm_id: The ID of the (local) realm on which to start the link.
        :type realm_id: str

        :param link_id: The ID of the router link to start.
        :type link_id: str

        :param link_config: The router link configuration.
        :type link_config: dict

        :returns: The new link detail information.
        :rtype: dict
        """
        assert type(realm_id) == str
        assert type(link_id) == str
        assert type(link_config) == dict
        assert isinstance(details, CallDetails)

        self.log.info(
            'Router link {link_id} starting on realm {realm_id} {method}',
            link_id=hlid(link_id),
            realm_id=hlid(realm_id),
            method=hltype(RouterController.start_router_realm_link))

        if realm_id not in self.realms:
            raise ApplicationError('crossbar.error.no_such_object', 'no realm with ID {}'.format(realm_id))

        rlink_manager = self.realms[realm_id].rlink_manager

        if link_id in rlink_manager:
            raise ApplicationError('crossbar.error.already_running',
                                   'router link {} already running'.format(link_id))

        link_config = RLinkConfig.parse(self.personality, link_config, id=link_id)

        caller = SessionIdent.from_calldetails(details)

        rlink = yield rlink_manager.start_link(link_id, link_config, caller)

        started = rlink.marshal()

        self.publish('{}.on_router_realm_link_started'.format(self._uri_prefix), started)

        self.log.info('Router link {link_id} started', link_id=hlid(link_id))

        returnValue(started)
Example #25
0
    def start_guest(self, id, config, details=None):
        """
      Start a new guest process on this node.

      :param config: The guest process configuration.
      :type config: obj

      :returns: int -- The PID of the new process.
      """
        ## prohibit starting a worker twice
        ##
        if id in self._workers:
            emsg = "ERROR: could not start worker - a worker with ID '{}' is already running (or starting)".format(
                id)
            log.msg(emsg)
            raise ApplicationError('crossbar.error.worker_already_running',
                                   emsg)

        try:
            checkconfig.check_guest(config)
        except Exception as e:
            raise ApplicationError(
                'crossbar.error.invalid_configuration',
                'invalid guest worker configuration: {}'.format(e))

        options = config.get('options', {})

        ## guest process working directory
        ##
        workdir = self._node._cbdir
        if 'workdir' in options:
            workdir = os.path.join(workdir, options['workdir'])
        workdir = os.path.abspath(workdir)

        ## guest process executable and command line arguments
        ##

        ## first try to configure the fully qualified path for the guest
        ## executable by joining workdir and configured exectuable ..
        exe = os.path.abspath(os.path.join(workdir, config['executable']))

        if check_executable(exe):
            log.msg(
                "Using guest worker executable '{}' (executable path taken from configuration)"
                .format(exe))
        else:
            ## try to detect the fully qualified path for the guest
            ## executable by doing a "which" on the configured executable name
            exe = shutil.which(config['executable'])
            if exe is not None and check_executable(exe):
                log.msg(
                    "Using guest worker executable '{}' (executable path detected from environment)"
                    .format(exe))
            else:
                emsg = "ERROR: could not start worker - could not find and executable for '{}'".format(
                    config['executable'])
                log.msg(emsg)
                raise ApplicationError('crossbar.error.invalid_configuration',
                                       emsg)

        ## guest process command line arguments
        ##
        args = [exe]
        args.extend(config.get('arguments', []))

        ## guest process environment
        ##
        worker_env = create_process_env(options)

        ## log name of worker
        ##
        worker_logname = 'Guest'

        ## topic URIs used (later)
        ##
        starting_topic = 'crossbar.node.{}.on_guest_starting'.format(
            self._node_id)
        started_topic = 'crossbar.node.{}.on_guest_started'.format(
            self._node_id)

        ## add worker tracking instance to the worker map ..
        ##
        worker = GuestWorkerProcess(self,
                                    id,
                                    details.authid,
                                    keeplog=options.get('traceback', None))

        self._workers[id] = worker

        ## create a (custom) process endpoint
        ##
        ep = WorkerProcessEndpoint(self._node._reactor,
                                   exe,
                                   args,
                                   path=workdir,
                                   env=worker_env,
                                   worker=worker)

        ## ready handling
        ##
        def on_ready_success(proto):

            worker.pid = proto.transport.pid
            worker.status = 'started'
            worker.started = datetime.utcnow()

            log.msg("{} with ID '{}' and PID {} started".format(
                worker_logname, worker.id, worker.pid))

            ## directory watcher
            ##
            if 'watch' in options:

                if HAS_FSNOTIFY:

                    ## assemble list of watched directories
                    watched_dirs = []
                    for d in options['watch'].get('directories', []):
                        watched_dirs.append(
                            os.path.abspath(os.path.join(self._node._cbdir,
                                                         d)))

                    ## create a directory watcher
                    worker.watcher = DirWatcher(dirs=watched_dirs,
                                                notify_once=True)

                    ## make sure to stop the background thread running inside the
                    ## watcher upon Twisted being shut down
                    def on_shutdown():
                        worker.watcher.stop()

                    reactor.addSystemEventTrigger('before', 'shutdown',
                                                  on_shutdown)

                    ## this handler will get fired by the watcher upon detecting an FS event
                    def on_fsevent(evt):
                        worker.watcher.stop()
                        proto.signal('TERM')

                        if options['watch'].get('action', None) == 'restart':
                            log.msg("Restarting guest ..")
                            reactor.callLater(0.1, self.start_guest, id,
                                              config, details)

                    ## now run the watcher on a background thread
                    deferToThread(worker.watcher.loop, on_fsevent)

                else:
                    log.msg(
                        "Warning: cannot watch directory for changes - feature DirWatcher unavailable"
                    )

            ## assemble guest worker startup information
            ##
            started_info = {
                'id': worker.id,
                'status': worker.status,
                'started': utcstr(worker.started),
                'who': worker.who
            }

            self.publish(started_topic,
                         started_info,
                         options=PublishOptions(exclude=[details.caller]))

            return started_info

        def on_ready_error(err):
            del self._workers[worker.id]

            emsg = 'ERROR: failed to start guest worker - {}'.format(err.value)
            log.msg(emsg)
            raise ApplicationError("crossbar.error.cannot_start", emsg,
                                   ep.getlog())

        worker.ready.addCallbacks(on_ready_success, on_ready_error)

        def on_exit_success(res):
            log.msg("Guest excited with success")
            del self._workers[worker.id]

        def on_exit_error(err):
            log.msg("Guest excited with error", err)
            del self._workers[worker.id]

        worker.exit.addCallbacks(on_exit_success, on_exit_error)

        ## create a transport factory for talking WAMP to the native worker
        ##
        transport_factory = create_guest_worker_client_factory(
            config, worker.ready, worker.exit)
        transport_factory.noisy = False
        self._workers[id].factory = transport_factory

        ## now (immediately before actually forking) signal the starting of the worker
        ##
        starting_info = {
            'id': id,
            'status': worker.status,
            'created': utcstr(worker.created),
            'who': worker.who
        }

        ## the caller gets a progressive result ..
        if details.progress:
            details.progress(starting_info)

        ## .. while all others get an event
        self.publish(starting_topic,
                     starting_info,
                     options=PublishOptions(exclude=[details.caller]))

        ## now actually fork the worker ..
        ##
        if self.debug:
            log.msg(
                "Starting {} with ID '{}' using command line '{}' ..".format(
                    worker_logname, id, ' '.join(args)))
        else:
            log.msg("Starting {} with ID '{}' ..".format(worker_logname, id))

        d = ep.connect(transport_factory)

        def on_connect_success(proto):

            ## this seems to be called immediately when the child process
            ## has been forked. even if it then immediately fails because
            ## e.g. the executable doesn't even exist. in other words,
            ## I'm not sure under what conditions the deferred will
            ## errback - probably only if the forking of a new process fails
            ## at OS level due to out of memory conditions or such.

            pid = proto.transport.pid
            if self.debug:
                log.msg(
                    "Guest worker process connected with PID {}".format(pid))

            worker.pid = pid

            ## proto is an instance of GuestWorkerClientProtocol
            worker.proto = proto

            worker.status = 'connected'
            worker.connected = datetime.utcnow()

        def on_connect_error(err):

            ## not sure when this errback is triggered at all .. see above.
            if self.debug:
                log.msg(
                    "ERROR: Connecting forked guest worker failed - {}".format(
                        err))

            ## in any case, forward the error ..
            worker.ready.errback(err)

        d.addCallbacks(on_connect_success, on_connect_error)

        return worker.ready
Example #26
0
    def start_router_realm(self, realm_id, realm_config, details=None):
        """
        Starts a realm on this router worker.

        :param realm_id: The ID of the realm to start.
        :type realm_id: str

        :param realm_config: The realm configuration.
        :type realm_config: dict

        :param details: Call details.
        :type details: :class:`autobahn.wamp.types.CallDetails`
        """
        self.log.info('Starting router realm {realm_id} {method}',
                      realm_id=hlid(realm_id), method=hltype(RouterController.start_router_realm))

        # prohibit starting a realm twice
        #
        if realm_id in self.realms:
            emsg = "Could not start realm: a realm with ID '{}' is already running (or starting)".format(realm_id)
            self.log.error(emsg)
            raise ApplicationError('crossbar.error.already_running', emsg)

        # check configuration
        #
        try:
            self.personality.check_router_realm(self.personality, realm_config)
        except Exception as e:
            emsg = "Invalid router realm configuration: {}".format(e)
            self.log.error(emsg)
            raise ApplicationError("crossbar.error.invalid_configuration", emsg)

        # URI of the realm to start
        realm_name = realm_config['name']

        # router/realm wide options
        options = realm_config.get('options', {})

        enable_meta_api = options.get('enable_meta_api', True)

        # expose router/realm service API additionally on local node management router
        bridge_meta_api = options.get('bridge_meta_api', False)
        if bridge_meta_api:
            # FIXME
            bridge_meta_api_prefix = 'crossbar.worker.{worker_id}.realm.{realm_id}.root.'.format(worker_id=self._worker_id, realm_id=realm_id)
        else:
            bridge_meta_api_prefix = None

        # track realm
        rlm = self.router_realm_class(self, realm_id, realm_config)
        self.realms[realm_id] = rlm
        self.realm_to_id[realm_name] = realm_id

        # create a new router for the realm
        rlm.router = self._router_factory.start_realm(rlm)

        if rlm.router._store and hasattr(rlm.router._store, 'start'):
            yield rlm.router._store.start()

        # add a router/realm service session
        extra = {
            # the RouterServiceAgent will fire this when it is ready
            'onready': Deferred(),

            # if True, forward the WAMP meta API (implemented by RouterServiceAgent)
            # that is normally only exposed on the app router/realm _additionally_
            # to the local node management router.
            'enable_meta_api': enable_meta_api,
            'bridge_meta_api': bridge_meta_api,
            'bridge_meta_api_prefix': bridge_meta_api_prefix,

            # the management session on the local node management router to which
            # the WAMP meta API is exposed to additionally, when the bridge_meta_api option is set
            'management_session': self,
        }
        cfg = ComponentConfig(realm_name, extra)
        rlm.session = RouterServiceAgent(cfg, rlm.router)
        self._router_session_factory.add(rlm.session,
                                         rlm.router,
                                         authid='routerworker-{}-realm-{}-serviceagent'.format(self._worker_id, realm_id),
                                         authrole='trusted')

        yield extra['onready']
        self.log.info('RouterServiceAgent started on realm "{realm_name}"', realm_name=realm_name)

        self.publish('{}.on_realm_started'.format(self._uri_prefix), realm_id)

        topic = '{}.on_realm_started'.format(self._uri_prefix)
        event = rlm.marshal()
        caller = details.caller if details else None
        self.publish(topic, event, options=PublishOptions(exclude=caller))

        self.log.info('Realm "{realm_id}" (name="{realm_name}") started', realm_id=realm_id, realm_name=rlm.session._realm)
        return event
Example #27
0
    def close_channel(self,
                      market_maker_adr,
                      channel_adr,
                      channel_seq,
                      channel_balance,
                      channel_is_final,
                      marketmaker_signature,
                      details=None):
        """
        Called by a XBR Market Maker to close a paying channel.
        """
        assert type(market_maker_adr) == bytes and len(
            market_maker_adr
        ) == 20, 'market_maker_adr must be bytes[20], but was {}'.format(
            type(market_maker_adr))
        assert type(channel_adr) == bytes and len(
            channel_adr
        ) == 20, 'channel_adr must be bytes[20], but was {}'.format(
            type(channel_adr))
        assert type(
            channel_seq) == int, 'channel_seq must be int, but was {}'.format(
                type(channel_seq))
        assert type(channel_balance) == bytes and len(
            channel_balance
        ) == 32, 'channel_balance must be bytes[32], but was {}'.format(
            type(channel_balance))
        assert type(
            channel_is_final
        ) == bool, 'channel_is_final must be bool, but was {}'.format(
            type(channel_is_final))
        assert type(marketmaker_signature) == bytes and len(
            marketmaker_signature) == (
                32 + 32 + 1
            ), 'marketmaker_signature must be bytes[65], but was {}'.format(
                type(marketmaker_signature))
        assert details is None or isinstance(
            details,
            CallDetails), 'details must be autobahn.wamp.types.CallDetails'

        # check that the delegate_adr fits what we expect for the market maker
        if market_maker_adr != self._market_maker_adr:
            raise ApplicationError(
                'xbr.error.unexpected_delegate_adr',
                '{}.sell() - unexpected market maker (delegate) address: expected 0x{}, but got 0x{}'
                .format(self.__class__.__name__,
                        binascii.b2a_hex(self._market_maker_adr).decode(),
                        binascii.b2a_hex(market_maker_adr).decode()))

        # FIXME: must be the currently active channel .. and we need to track all of these
        if channel_adr != self._channel['channel']:
            self._session.leave()
            raise ApplicationError(
                'xbr.error.unexpected_channel_adr',
                '{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'
                .format(self.__class__.__name__,
                        binascii.b2a_hex(self._channel['channel']).decode(),
                        binascii.b2a_hex(channel_adr).decode()))

        # channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker)
        if channel_seq != self._seq:
            raise ApplicationError(
                'xbr.error.unexpected_channel_seq',
                '{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'
                .format(self.__class__.__name__, self._seq + 1, channel_seq))

        # channel balance: check we have consensus on off-chain channel state with peer (which is the market maker)
        channel_balance = unpack_uint256(channel_balance)
        if channel_balance != self._balance:
            raise ApplicationError(
                'xbr.error.unexpected_channel_balance',
                '{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'
                .format(self.__class__.__name__, self._balance,
                        channel_balance))

        # XBRSIG: check the signature (over all input data for the buying of the key)
        signer_address = recover_eip712_signer(channel_adr, channel_seq,
                                               channel_balance,
                                               channel_is_final,
                                               marketmaker_signature)
        if signer_address != market_maker_adr:
            self.log.warn(
                '{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}',
                klass=self.__class__.__name__,
                signer_address=hl(binascii.b2a_hex(signer_address).decode()),
                delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode()))
            raise ApplicationError(
                'xbr.error.invalid_signature',
                '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'
                .format(self.__class__.__name__))

        # XBRSIG: compute EIP712 typed data signature
        seller_signature = sign_eip712_data(self._pkey_raw, channel_adr,
                                            channel_seq, channel_balance,
                                            channel_is_final)

        receipt = {
            'delegate': self._addr,
            'seq': channel_seq,
            'balance': pack_uint256(channel_balance),
            'is_final': channel_is_final,
            'signature': seller_signature,
        }

        self.log.info(
            '{klass}.close_channel() - {tx_type} closing channel {channel_adr}, closing balance {channel_balance}, closing sequence {channel_seq} [caller={caller}, caller_authid="{caller_authid}"]',
            klass=self.__class__.__name__,
            tx_type=hl('XBR CLOSE  ', color='magenta'),
            channel_balance=hl(str(int(channel_balance / 10**18)) + ' XBR',
                               color='magenta'),
            channel_seq=hl(channel_seq),
            channel_adr=hl(binascii.b2a_hex(channel_adr).decode()),
            caller=hl(details.caller),
            caller_authid=hl(details.caller_authid))

        return receipt
Example #28
0
    def start_router_component(self, id, config, details=None):
        """
        Start an app component in this router worker.

        :param id: The ID of the component to start.
        :type id: str

        :param config: The component configuration.
        :type config: dict

        :param details: Call details.
        :type details: :class:`autobahn.wamp.types.CallDetails`
        """
        self.log.debug("{name}.start_router_component", name=self.__class__.__name__)

        # prohibit starting a component twice
        #
        if id in self.components:
            emsg = "Could not start component: a component with ID '{}'' is already running (or starting)".format(id)
            self.log.error(emsg)
            raise ApplicationError('crossbar.error.already_running', emsg)

        started_d = Deferred()

        # check configuration
        #
        try:
            self.personality.check_router_component(self.personality, config)
        except Exception as e:
            emsg = "Invalid router component configuration: {}".format(e)
            self.log.error(emsg)
            raise ApplicationError("crossbar.error.invalid_configuration", emsg)
        else:
            self.log.debug("Starting {type}-component on router.",
                           type=config['type'])

        # resolve references to other entities
        #
        references = {}
        for ref in config.get('references', []):
            ref_type, ref_id = ref.split(':')
            if ref_type == 'connection':
                if ref_id in self._connections:
                    references[ref] = self._connections[ref_id]
                else:
                    emsg = "cannot resolve reference '{}' - no '{}' with ID '{}'".format(ref, ref_type, ref_id)
                    self.log.error(emsg)
                    raise ApplicationError("crossbar.error.invalid_configuration", emsg)
            else:
                emsg = "cannot resolve reference '{}' - invalid reference type '{}'".format(ref, ref_type)
                self.log.error(emsg)
                raise ApplicationError("crossbar.error.invalid_configuration", emsg)

        # create component config
        #
        realm = config.get('realm', None)
        assert isinstance(realm, str)

        extra = config.get('extra', {})
        assert isinstance(extra, dict)

        # forward crossbar node base directory
        extra['cbdir'] = self.config.extra.cbdir

        # allow access to controller session
        controller = self if self.config.extra.expose_controller else None

        # expose an object shared between components
        shared = self.components_shared if self.config.extra.expose_shared else None

        # this is the component configuration provided to the components ApplicationSession
        component_config = ComponentConfig(realm=realm,
                                           extra=extra,
                                           keyring=None,
                                           controller=controller,
                                           shared=shared)

        # define component ctor function
        try:
            create_component = _appsession_loader(config)
        except ApplicationError as e:
            # for convenience, also log failed component loading
            self.log.error('component loading failed', log_failure=Failure())
            if 'No module named' in str(e):
                self.log.error('  Python module search paths:')
                for path in e.kwargs['pythonpath']:
                    self.log.error('    {path}', path=path)
            raise

        # .. and create and add an WAMP application session to
        # run the component next to the router
        try:
            session = create_component(component_config)

            # any exception spilling out from user code in onXXX handlers is fatal!
            def panic(fail, msg):
                self.log.error(
                    "Fatal error in component: {msg} - {log_failure.value}",
                    msg=msg, log_failure=fail
                )
                session.disconnect()
            session._swallow_error = panic
        except Exception:
            self.log.error(
                "Component instantiation failed",
                log_failure=Failure(),
            )
            raise

        # Note that 'join' is fired to listeners *before* onJoin runs,
        # so if you do 'yield self.leave()' in onJoin we'll still
        # publish "started" before "stopped".

        def publish_stopped(session, stop_details):
            self.log.info(
                "stopped component: {session} id={session_id}",
                session=class_name(session),
                session_id=session._session_id,
            )
            topic = self._uri_prefix + '.on_component_stop'
            event = {'id': id}
            caller = details.caller if details else None
            self.publish(topic, event, options=PublishOptions(exclude=caller))
            if not started_d.called:
                started_d.errback(Exception("Session left before being ready"))
            return event

        def publish_ready(session):
            """
            when our component is ready, we publish .on_component_ready
            """
            self.log.info(
                "component ready: {session} id={session_id}",
                session=class_name(session),
                session_id=session._session_id,
            )
            topic = self._uri_prefix + '.on_component_ready'
            event = {'id': id}
            self.publish(topic, event)
            started_d.callback(event)
            return event

        def publish_started(session, start_details):
            """
            when our component starts, we publish .on_component_start
            """

            # hook up handlers for "session is ready"
            session.on('ready', publish_ready)

            # publish .on_component_start
            self.log.info(
                "started component: {session} id={session_id}",
                session=class_name(session),
                session_id=session._session_id,
            )
            topic = self._uri_prefix + '.on_component_start'
            event = {'id': id}
            caller = details.caller if details else None
            self.publish(topic, event, options=PublishOptions(exclude=caller))
            return event

        session.on('leave', publish_stopped)
        session.on('join', publish_started)

        self.components[id] = RouterComponent(id, config, session)
        router = self._router_factory.get(realm)
        self._router_session_factory.add(session, router, authrole=config.get('role', 'anonymous'))
        self.log.debug(
            "Added component {id} (type '{name}')",
            id=id,
            name=class_name(session),
        )
        return started_d
Example #29
0
    def start_component(self, component_id, config, reload_modules=False, details=None):
        """
        Starts a component in this container worker.

        :param component_id: The ID under which to start the component.
        :type component_id: str

        :param config: Component configuration.
        :type config: dict

        :param reload_modules: If `True`, enforce reloading of modules (user code)
           that were modified (see: TrackingModuleReloader).
        :type reload_modules: bool

        :param details: Caller details.
        :type details: instance of :class:`autobahn.wamp.types.CallDetails`

        :returns: Component startup information.
        :rtype: dict
        """
        self.log.debug(u'{klass}.start_component({component_id}, {config})',
                       klass=self.__class__.__name__,
                       component_id=component_id,
                       config=config)

        # prohibit starting a component twice
        #
        if component_id in self.components:
            emsg = u'duplicate component "{}" - a component with this ID is already running (or starting)'.format(component_id)
            self.log.debug(emsg)
            raise ApplicationError(u'crossbar.error.already_running', emsg)

        # check component configuration
        #
        try:
            self.personality.check_container_component(self.personality, config)
        except Exception as e:
            emsg = u'invalid container component configuration: {}'.format(e)
            self.log.debug(emsg)
            raise ApplicationError(u'crossbar.error.invalid_configuration', emsg)
        else:
            self.log.debug(u'starting component "{component_id}" ..', component_id=component_id)

        # WAMP application component factory
        #
        realm = config.get(u'realm', None)
        extra = config.get(u'extra', None)
        controller = self if self.config.extra.expose_controller else None
        shared = self.components_shared if self.config.extra.expose_shared else None
        component_config = ComponentConfig(realm=realm,
                                           extra=extra,
                                           keyring=None,
                                           controller=controller,
                                           shared=shared)
        try:
            create_component = _appsession_loader(config)
        except ApplicationError as e:
            # for convenience, also log failed component loading
            self.log.error(u'component loading failed', log_failure=Failure())
            if u'No module named' in str(e):
                self.log.error(u'  Python module search paths:')
                for path in e.kwargs['pythonpath']:
                    self.log.error(u'    {path}', path=path)
            raise

        # force reload of modules (user code)
        #
        if reload_modules:
            self._module_tracker.reload()

        # prepare some cleanup code this connection goes away
        def _closed(session, was_clean):
            """
            This is moderate hack around the fact that we don't have any way
            to "listen" for a close event on websocket or rawsocket
            objects. Also, the rawsocket implementation doesn't have
            "a" function we can wrap anyway (they are asyncio vs
            Twisted specific), so for both WebSocket and rawsocket
            cases, we actually listen on the WAMP session for
            transport close notifications.

            Ideally we'd listen for "close" on the transport but this
            works fine for cleaning up the components.
            """
            if component_id not in self.components:
                self.log.warn(
                    "Component '{id}' closed, but not in set.",
                    id=component_id,
                )
                return

            if was_clean:
                self.log.info(
                    "Closed connection to '{id}'",
                    id=component_id,
                )
            else:
                self.log.error(
                    "Lost connection to component '{id}' uncleanly",
                    id=component_id,
                )

            component = self.components[component_id]
            del self.components[component_id]
            self._publish_component_stop(component)
            component._stopped.callback(component.marshal())
            del component

            # figure out if we need to shut down the container itself or not
            if not self.components:
                if self._exit_mode == self.SHUTDOWN_ON_LAST_COMPONENT_STOPPED:
                    self.log.info(
                        "Container is hosting no more components: stopping container in exit mode <{exit_mode}> ...",
                        exit_mode=self._exit_mode,
                    )
                    self.shutdown()
                else:
                    self.log.info(
                        "Container is hosting no more components: continue running in exit mode <{exit_mode}>",
                        exit_mode=self._exit_mode,
                    )
            else:
                self.log.info(
                    "Container is still hosting {component_count} components: continue running in exit mode <{exit_mode}>",
                    exit_mode=self._exit_mode,
                    component_count=len(self.components),
                )

        # WAMP application session factory
        #
        def create_session():
            try:
                session = create_component(component_config)

                # any exception spilling out from user code in onXXX handlers is fatal!
                def panic(fail, msg):
                    self.log.error(
                        "Fatal error in component: {msg} - {log_failure.value}",
                        msg=msg, log_failure=fail,
                    )
                    session.disconnect()
                session._swallow_error = panic

                # see note above, for _closed -- we should be
                # listening for "the transport was closed", but
                # "session disconnect" is close enough (since there
                # are no "proper events" from websocket/rawsocket
                # implementations).
                session.on('disconnect', _closed)

                return session

            except Exception:
                self.log.failure(u'component instantiation failed: {log_failure.value}')
                raise

        # WAMP transport factory
        #
        transport_config = config[u'transport']

        if transport_config[u'type'] == u'websocket':

            # create a WAMP-over-WebSocket transport client factory
            transport_factory = WampWebSocketClientFactory(create_session, transport_config[u'url'])
            transport_factory.noisy = False

        elif transport_config[u'type'] == u'rawsocket':

            transport_factory = WampRawSocketClientFactory(create_session,
                                                           transport_config)
            transport_factory.noisy = False

        else:
            # should not arrive here, since we did check the config before
            raise Exception(u'logic error')

        # create and connect client endpoint
        #
        endpoint = create_connecting_endpoint_from_config(transport_config[u'endpoint'],
                                                          self.config.extra.cbdir,
                                                          self._reactor,
                                                          self.log)

        # now, actually connect the client
        #
        d = endpoint.connect(transport_factory)

        def on_connect_success(proto):
            component = ContainerComponent(component_id, config, proto, None)
            self.components[component_id] = component

            # publish event "on_component_start" to all but the caller
            #
            uri = self._uri_prefix + u'.on_component_started'

            component_started = {
                u'id': component_id,
                u'config': config
            }

            self.publish(uri, component_started, options=PublishOptions(exclude=details.caller))

            return component_started

        def on_connect_error(err):
            # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html
            if isinstance(err.value, internet.error.ConnectError):
                emsg = u'could not connect container component to router - transport establishment failed ({})'.format(err.value)
                self.log.warn(emsg)
                raise ApplicationError(u'crossbar.error.cannot_connect', emsg)
            else:
                # should not arrive here (since all errors arriving here
                # should be subclasses of ConnectError)
                raise err

        d.addCallbacks(on_connect_success, on_connect_error)

        return d
Example #30
0
    def start_router_transport(self, id, config, details=None):
        """
        Start a transport on this router worker.

        :param id: The ID of the transport to start.
        :type id: str
        :param config: The transport configuration.
        :type config: dict
        """
        self.log.debug("{}.start_router_transport".format(
            self.__class__.__name__),
                       id=id,
                       config=config)

        # prohibit starting a transport twice
        #
        if id in self.transports:
            emsg = "Could not start transport: a transport with ID '{}' is already running (or starting)".format(
                id)
            self.log.error(emsg)
            raise ApplicationError(u'crossbar.error.already_running', emsg)

        # check configuration
        #
        try:
            checkconfig.check_router_transport(config)
        except Exception as e:
            emsg = "Invalid router transport configuration: {}".format(e)
            self.log.error(emsg)
            raise ApplicationError(u"crossbar.error.invalid_configuration",
                                   emsg)
        else:
            self.log.debug("Starting {}-transport on router.".format(
                config['type']))

        # standalone WAMP-RawSocket transport
        #
        if config['type'] == 'rawsocket':

            transport_factory = WampRawSocketServerFactory(
                self._router_session_factory, config)
            transport_factory.noisy = False

        # standalone WAMP-WebSocket transport
        #
        elif config['type'] == 'websocket':

            transport_factory = WampWebSocketServerFactory(
                self._router_session_factory, self.config.extra.cbdir, config,
                self._templates)
            transport_factory.noisy = False

        # Flash-policy file server pseudo transport
        #
        elif config['type'] == 'flashpolicy':

            transport_factory = FlashPolicyFactory(
                config.get('allowed_domain', None),
                config.get('allowed_ports', None))

        # WebSocket testee pseudo transport
        #
        elif config['type'] == 'websocket.testee':

            transport_factory = WebSocketTesteeServerFactory(
                config, self._templates)

        # Stream testee pseudo transport
        #
        elif config['type'] == 'stream.testee':

            transport_factory = StreamTesteeServerFactory()

        # Twisted Web based transport
        #
        elif config['type'] == 'web':

            options = config.get('options', {})

            # create Twisted Web root resource
            #
            if '/' in config['paths']:
                root_config = config['paths']['/']
                root = self._create_resource(root_config, nested=False)
            else:
                root = Resource404(self._templates, b'')

            # create Twisted Web resources on all non-root paths configured
            #
            self._add_paths(root, config.get('paths', {}))

            # create the actual transport factory
            #
            transport_factory = Site(root)
            transport_factory.noisy = False

            # Web access logging
            #
            if not options.get('access_log', False):
                transport_factory.log = lambda _: None

            # Traceback rendering
            #
            transport_factory.displayTracebacks = options.get(
                'display_tracebacks', False)

            # HSTS
            #
            if options.get('hsts', False):
                if 'tls' in config['endpoint']:
                    hsts_max_age = int(options.get('hsts_max_age', 31536000))
                    transport_factory.requestFactory = createHSTSRequestFactory(
                        transport_factory.requestFactory, hsts_max_age)
                else:
                    self.log.warn(
                        "Warning: HSTS requested, but running on non-TLS - skipping HSTS"
                    )

        # Unknown transport type
        #
        else:
            # should not arrive here, since we did check_transport() in the beginning
            raise Exception("logic error")

        # create transport endpoint / listening port from transport factory
        #
        d = create_listening_port_from_config(config['endpoint'],
                                              self.config.extra.cbdir,
                                              transport_factory, self._reactor,
                                              self.log)

        def ok(port):
            self.transports[id] = RouterTransport(id, config,
                                                  transport_factory, port)
            self.log.debug(
                "Router transport '{}'' started and listening".format(id))
            return

        def fail(err):
            emsg = "Cannot listen on transport endpoint: {log_failure}"
            self.log.error(emsg, log_failure=err)
            raise ApplicationError(u"crossbar.error.cannot_listen", emsg)

        d.addCallbacks(ok, fail)
        return d