Ejemplo n.º 1
0
        def actually_join(_public_key):
            authextra.update({
                # forward the client pubkey: this allows us to omit authid as
                # the router can identify us with the pubkey already
                'pubkey': _public_key,

                # not yet implemented. a public key the router should provide
                # a trustchain for its public key. the trustroot can eg be
                # hard-coded in the client, or come from a command line option.
                'trustroot': None,

                # not yet implemented. for authenticating the router, this
                # challenge will need to be signed by the router and send back
                # in AUTHENTICATE for client to verify. A string with a hex
                # encoded 32 bytes random value.
                'challenge': None,

                # https://tools.ietf.org/html/rfc5929
                'channel_binding': 'tls-unique'
            })

            self.log.info(
                '{func} joining with realm="{realm}", authmethods={authmethods}, authid="{authid}", authrole="{authrole}", authextra={authextra}',
                func=hltype(self.onConnect),
                realm=hlval(self.config.realm),
                authmethods=hlval(authmethods),
                authid=hlval(authid),
                authrole=hlval(authrole),
                authextra=authextra)

            self.join(self.config.realm,
                      authmethods=authmethods,
                      authid=authid,
                      authrole=authrole,
                      authextra=authextra)
Ejemplo n.º 2
0
    def detach(self, session=None) -> List[int]:
        self.log.debug('{func}(session={session})',
                       func=hltype(self.detach),
                       session=session)

        detached_session_ids = []
        if session is None:
            # detach all sessions from router
            for session in list(self._session_id_to_session.values()):
                self._detach(session)
                detached_session_ids.append(session._session_id)
        else:
            # detach single session from router
            self._detach(session)
            detached_session_ids.append(session._session_id)

        self.log.info(
            '{func} router session detached from realm "{realm}" (session={session}, '
            'detached_session_ids={detached_session_ids}, authid="{authid}", authrole="{authrole}", '
            'authmethod="{authmethod}", authprovider="{authprovider}")',
            func=hltype(self.detach),
            session=hlid(session._session_id) if session else '',
            authid=hlid(session._authid),
            authrole=hlid(session._authrole),
            authmethod=hlval(session._authmethod),
            authprovider=hlval(session._authprovider),
            detached_session_ids=hlval(len(detached_session_ids)),
            realm=hlid(session._realm))

        return detached_session_ids
Ejemplo n.º 3
0
    def drop_role(self, realm: str, role: str) -> RouterRole:
        """
        Drop a role from a realm.

        :param realm: The name of the realm to drop.
        :param role: The URI of the role (on the realm) to drop.
        :return: The dropped role object.
        """
        self.log.info('{func}: realm="{realm}", role="{role}"',
                      func=hltype(self.drop_role),
                      realm=hlval(realm),
                      role=hlval(role))

        if realm not in self._routers:
            raise RuntimeError(
                'no router started for realm "{}"'.format(realm))

        router = self._routers[realm]

        if role not in router._roles:
            raise RuntimeError(
                'no role "{}" started on router for realm "{}"'.format(
                    role, realm))

        role_obj = router._roles[role]
        router.drop_role(role_obj)
        return role_obj
Ejemplo n.º 4
0
    def dataReceived(self, data: bytes):
        self.log.debug('{func} received {data_len} bytes for peer="{peer}"',
                       func=hltype(self.dataReceived),
                       peer=hlval(self.peer),
                       data_len=hlval(len(data)))

        # bytes received from Twisted, forward to the networking framework independent code for websocket
        self._dataReceived(data)
Ejemplo n.º 5
0
    def authenticate(self, signature: str) -> Union[Accept, Deny]:

        if signature == self._signature:
            # signature was valid: accept the client
            return self._accept()
        else:
            # signature was invalid: deny the client
            self.log.warn(
                '{func}: WAMP-CRA client signature is invalid (expected {expected} but got {signature})',
                func=hltype(self.authenticate),
                expected=hlval(self._signature),
                signature=hlval(signature, color='red'))
            return Deny(message='WAMP-CRA client signature is invalid')
Ejemplo n.º 6
0
    def connectionLost(self, reason: Failure = connectionDone):
        # Twisted networking framework entry point, called by Twisted
        # when the connection is lost (either a client or a server)

        was_clean = False
        if isinstance(reason.value, ConnectionDone):
            self.log.debug("Connection to/from {peer} was closed cleanly",
                           peer=self.peer)
            was_clean = True

        elif _is_tls_error(reason.value):
            self.log.error(_maybe_tls_reason(reason.value))

        elif isinstance(reason.value, ConnectionAborted):
            self.log.debug("Connection to/from {peer} was aborted locally",
                           peer=self.peer)

        elif isinstance(reason.value, ConnectionLost):
            message = str(reason.value)
            if hasattr(reason.value, 'message'):
                message = reason.value.message
            self.log.debug(
                "Connection to/from {peer} was lost in a non-clean fashion: {message}",
                peer=self.peer,
                message=message,
            )

        # at least: FileDescriptorOverrun, ConnectionFdescWentAway - but maybe others as well?
        else:
            self.log.debug(
                "Connection to/from {peer} lost ({error_type}): {error})",
                peer=self.peer,
                error_type=type(reason.value),
                error=reason.value)

        # ok, now forward to the networking framework independent code for websocket
        self._connectionLost(reason)

        # ok, done!
        if was_clean:
            self.log.debug(
                '{func} connection lost for peer="{peer}", closed cleanly',
                func=hltype(self.connectionLost),
                peer=hlval(self.peer))
        else:
            self.log.debug(
                '{func} connection lost for peer="{peer}", closed with error {reason}',
                func=hltype(self.connectionLost),
                peer=hlval(self.peer),
                reason=reason)
Ejemplo n.º 7
0
        def got_authorization(authorization):
            # backward compatibility
            if isinstance(authorization, bool):
                authorization = {'allow': authorization, 'cache': False}
                if action in ['call', 'publish']:
                    authorization['disclose'] = False

            auto_disclose_trusted = True
            if auto_disclose_trusted and authrole == 'trusted' and action in [
                    'call', 'publish'
            ]:
                authorization['disclose'] = True

            if not cached_authorization and authorization.get('cache', False):
                self._authorization_cache[cache_key] = authorization
                self.log.debug(
                    '{func} add authorization cache entry for key {cache_key}:\n{authorization}',
                    func=hltype(got_authorization),
                    cache_key=hlval(cache_key),
                    authorization=pformat(authorization))

            self.log.debug(
                "Authorized action '{action}' for URI '{uri}' by session {session_id} with authid '{authid}' and "
                "authrole '{authrole}' -> authorization: {authorization}",
                session_id=session._session_id,
                uri=uri,
                action=action,
                authid=session._authid,
                authrole=session._authrole,
                authorization=authorization)

            return authorization
Ejemplo n.º 8
0
    def __init__(self, dbpath: str, config: Dict[str, Any]):
        """
        Initialize a database-backed cookiestore. Example configuration:

        .. code-block:: json

            {
                "type": "database",
                "path": ".cookies",
                "purge_on_startup": false,
                "maxsize": 1048576,
                "readonly": false,
                "sync": true
            }

        :param dbpath: Filesystem path to database.
        :param config: Database cookie store configuration.
        """
        self.log.info(
            '{func}: initializing database-backed cookiestore with config=\n{config}',
            func=hltype(CookieStoreDatabaseBacked.__init__),
            config=pformat(config))
        CookieStore.__init__(self, config)

        maxsize = config['store'].get('maxsize', 1024 * 2**20)
        assert type(maxsize) == int, "maxsize must be an int, was {}".format(
            type(maxsize))
        # allow maxsize 128kiB to 128GiB
        assert maxsize >= 128 * 1024 and maxsize <= 128 * 2**30, "maxsize must be >=128kiB and <=128GiB, was {}".format(
            maxsize)

        readonly = config['store'].get('readonly', False)
        assert type(
            readonly) == bool, "readonly must be a bool, was {}".format(
                type(readonly))

        sync = config['store'].get('sync', True)
        assert type(sync) == bool, "sync must be a bool, was {}".format(
            type(sync))

        if config['store'].get('purge_on_startup', False):
            zlmdb.Database.scratch(dbpath)
            self.log.warn(
                '{func}: scratched embedded database (purge_on_startup is enabled)!',
                func=hltype(CookieStoreDatabaseBacked.__init__))

        self._db = zlmdb.Database(dbpath=dbpath,
                                  maxsize=maxsize,
                                  readonly=readonly,
                                  sync=sync)
        # self._db.__enter__()
        self._schema = cookiestore.CookieStoreSchema.attach(self._db)

        dbstats = self._db.stats(include_slots=True)

        self.log.info(
            '{func}: database-backed cookiestore opened from dbpath="{dbpath}" - dbstats=\n{dbstats}',
            func=hltype(CookieStoreDatabaseBacked.__init__),
            dbpath=hlval(dbpath),
            dbstats=pformat(dbstats))
Ejemplo n.º 9
0
    def add_role(self, realm: str, config: Dict[str, Any]) -> RouterRole:
        """
        Add a role to a realm.

        :param realm: The name of the realm to add the role to.
        :param config: The role configuration.
        :return: The new role object.
        """
        self.log.info('{func}: realm="{realm}", config=\n{config}',
                      func=hltype(self.add_role),
                      realm=hlval(realm),
                      config=pformat(config))

        if realm not in self._routers:
            raise RuntimeError(
                'no router started for realm "{}"'.format(realm))

        router = self._routers[realm]
        uri = config['name']

        role: RouterRole
        if 'permissions' in config:
            role = RouterRoleStaticAuth(router, uri, config['permissions'])
        elif 'authorizer' in config:
            role = RouterRoleDynamicAuth(router, uri, config['authorizer'])
        else:
            allow_by_default = config.get('allow-by-default', False)
            role = RouterRole(router, uri, allow_by_default=allow_by_default)

        router.add_role(role)
        return role
Ejemplo n.º 10
0
    def exists(self, cbtid: str) -> bool:
        """
        Check if a cookie with given value currently exists in the cookie store.

        :param cbtid: Cookie value (ID) to check.
        :return: Flag indicating whether a cookie (authenticated or not) is stored in the database.
        """
        # check if a cookie with the given value exists
        with self._db.begin() as txn:
            cookie_exists = self._schema.idx_cookies_by_value[
                txn, cbtid] is not None
        self.log.debug('{func}(cbtid="{cbtid}") -> {cookie_exists}',
                       func=hltype(self.exists),
                       cbtid=hlval(cbtid),
                       cookie_exists=hlval(cookie_exists))
        return cookie_exists
Ejemplo n.º 11
0
    def connectionMade(self):
        # Twisted networking framework entry point, called by Twisted
        # when the connection is established (either a client or a server)

        # determine preliminary transport details (what is know at this point)
        self._transport_details = create_transport_details(
            self.transport, self.is_server)
        self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET

        # backward compatibility
        self.peer = self._transport_details.peer

        # try to set "Nagle" option for TCP sockets
        try:
            self.transport.setTcpNoDelay(self.tcpNoDelay)
        except:  # don't touch this! does not work: AttributeError, OSError
            # eg Unix Domain sockets throw Errno 22 on this
            pass

        # ok, now forward to the networking framework independent code for websocket
        self._connectionMade()

        # ok, done!
        self.log.debug('{func} connection established for peer="{peer}"',
                       func=hltype(self.connectionMade),
                       peer=hlval(self.peer))
Ejemplo n.º 12
0
    def __init__(self, personality, factory, config):
        """

        :param personality:
        :param factory:
        :param config: Realm store configuration item.
        """
        from twisted.internet import reactor

        self._reactor = reactor
        self._personality = personality
        self._factory = factory

        dbpath = config.get('path', None)
        assert type(dbpath) == str

        maxsize = config.get('maxsize', 128 * 2**20)
        assert type(maxsize) == int
        # allow maxsize 128kiB to 128GiB
        assert maxsize >= 128 * 1024 and maxsize <= 128 * 2**30

        readonly = config.get('readonly', False)
        assert type(readonly) == bool

        sync = config.get('sync', True)
        assert type(sync) == bool

        self._config = config

        self._type = self._config.get('type', None)
        assert self._type == self.STORE_TYPE

        self._db = zlmdb.Database(dbpath=dbpath, maxsize=maxsize, readonly=readonly, sync=sync)
        self._db.__enter__()
        self._schema = RealmStore.attach(self._db)

        self._running = False
        self._process_buffers_thread = None

        self._max_buffer = config.get('max-buffer', 10000)
        self._buffer_flush = config.get('buffer-flush', 200)
        self._buffer = []
        self._log_counter = 0

        # map: registration.id -> deque( (session, call, registration, authorization) )
        self._queued_calls = {}

        self.log.info(
            '{func} realm store initialized (type="{stype}", dbpath="{dbpath}", maxsize={maxsize}, '
            'readonly={readonly}, sync={sync})',
            func=hltype(self.__init__),
            stype=hlval(self._type),
            dbpath=dbpath,
            maxsize=maxsize,
            readonly=readonly,
            sync=sync)
Ejemplo n.º 13
0
    def has_role(self, uri):
        """
        Check if a role with given URI exists on this router.

        :returns: bool - `True` if a role under the given URI exists on this router.
        """
        self.log.info('{func}: uri="{uri}", exists={exists}',
                      func=hltype(self.has_role),
                      uri=hlval(uri),
                      exists=(uri in self._roles))
        return uri in self._roles
Ejemplo n.º 14
0
    def _store_session_joined(self, txn: zlmdb.Transaction, ses: cfxdb.realmstore.Session):

        # FIXME: use idx_sessions_by_session_id to check there is no session with (session_id, joined_at) yet

        self._schema.sessions[txn, ses.oid] = ses

        cnt = self._schema.sessions.count(txn)
        self.log.info('{func} database record inserted [total={total}] session={session}',
                      func=hltype(self._store_session_joined),
                      total=hlval(cnt),
                      session=ses)
Ejemplo n.º 15
0
    def get_member(self, ethadr_raw):
        if self.is_attached():
            is_member = yield self.call('xbr.network.is_member', ethadr_raw)
            if is_member:
                member_data = yield self.call('xbr.network.get_member_by_wallet', ethadr_raw)

                member_data['address'] = web3.Web3.toChecksumAddress(member_data['address'])
                member_data['oid'] = uuid.UUID(bytes=member_data['oid'])
                member_data['balance']['eth'] = web3.Web3.fromWei(unpack_uint256(member_data['balance']['eth']),
                                                                  'ether')
                member_data['balance']['xbr'] = web3.Web3.fromWei(unpack_uint256(member_data['balance']['xbr']),
                                                                  'ether')
                member_data['created'] = np.datetime64(member_data['created'], 'ns')

                member_level = member_data['level']
                member_data['level'] = {
                    # Member is active.
                    1: 'ACTIVE',
                    # Member is active and verified.
                    2: 'VERIFIED',
                    # Member is retired.
                    3: 'RETIRED',
                    # Member is subject to a temporary penalty.
                    4: 'PENALTY',
                    # Member is currently blocked and cannot current actively participate in the market.
                    5: 'BLOCKED',
                }.get(member_level, None)

                self.log.info(
                    'Member {member_oid} found for address 0x{member_adr} - current member level {member_level}',
                    member_level=hlval(member_data['level']),
                    member_oid=hlid(member_data['oid']),
                    member_adr=hlval(member_data['address']))

                return member_data
            else:
                self.log.warn('Address {output_ethadr} is not a member in the XBR network',
                              output_ethadr=ethadr_raw)
        else:
            self.log.warn('not connected: could not retrieve member data for address {output_ethadr}',
                          output_ethadr=ethadr_raw)
Ejemplo n.º 16
0
    def _store_session_left(self, txn: zlmdb.Transaction, session: ISession, details: CloseDetails):

        # FIXME: apparently, session ID is already erased at this point:(
        _session_id = session._session_id

        # FIXME: move left_at to autobahn.wamp.types.CloseDetails
        _left_at = np.datetime64(time_ns(), 'ns')

        # lookup session by WAMP session ID and find the most recent session
        # according to joined_at timestamp
        session_obj = None
        _from_key = (_session_id, np.datetime64(0, 'ns'))
        _to_key = (_session_id, np.datetime64(time_ns(), 'ns'))
        for session_oid in self._schema.idx_sessions_by_session_id.select(txn,
                                                                          from_key=_from_key,
                                                                          to_key=_to_key,
                                                                          reverse=True,
                                                                          return_keys=False,
                                                                          return_values=True):
            session_obj = self._schema.sessions[txn, session_oid]

            # if we have an index, that index must always resolve to an indexed record
            assert session_obj

            # we only want the most recent session
            break

        if session_obj:
            # FIXME: also store other CloseDetails attributes
            session_obj.left_at = _left_at

            self.log.info('{func} database record session={session} updated: left_at={left_at}',
                          func=hltype(self._store_session_left),
                          left_at=hlval(_left_at),
                          session=hlval(_session_id))
        else:
            self.log.warn('{func} could not update database record for session={session}: record not found!',
                          func=hltype(self._store_session_left),
                          session=hlval(_session_id))
Ejemplo n.º 17
0
    def __init__(self, personality, factory, config):
        """

        See the example here:

        https://github.com/crossbario/crossbar-examples/tree/master/scaling-microservices/queued

        .. code-block:: json

            "store": {
                "type": "memory",
                "limit": 1000,      // global default for limit on call queues
                "call-queue": [
                    {
                        "uri": "com.example.compute",
                        "match": "exact",
                        "limit": 10000  // procedure specific call queue limit
                    }
                ]
            }
        """
        from twisted.internet import reactor

        self._reactor = reactor
        self._personality = personality
        self._factory = factory
        self._config = config

        self._type = self._config.get('type', None)
        assert self._type == self.STORE_TYPE

        # limit to event history per subscription
        self._limit = self._config.get('limit', self.GLOBAL_HISTORY_LIMIT)

        # map of publication ID -> event dict
        self._event_store = {}

        # map of publication ID -> set of subscription IDs
        self._event_subscriptions = {}

        # map of subscription ID -> (limit, deque(of publication IDs))
        self._event_history = {}

        # map: registration.id -> deque( (session, call, registration, authorization) )
        self._queued_calls = {}

        self._running = False

        self.log.info('{func} realm store initialized (type="{stype}")',
                      stype=hlval(self._type),
                      func=hltype(self.__init__))
Ejemplo n.º 18
0
    def attach(self, session: ISession):
        """
        Implements :func:`autobahn.wamp.interfaces.IRouter.attach`
        """
        self.log.debug('{func}(session={session})',
                       func=hltype(self.attach),
                       session=session)

        if session._session_id not in self._session_id_to_session:
            self._session_id_to_session[session._session_id] = session
        else:
            raise Exception("session with ID {} already attached".format(
                session._session_id))

        self._broker.attach(session)
        self._dealer.attach(session)

        self._attached += 1

        self.log.info(
            '{func} new session attached for realm="{realm}", session={session}, authid="{authid}", '
            'authrole="{authrole}", authmethod="{authmethod}", authprovider="{authprovider}", authextra=\n{authextra}',
            func=hltype(self.attach),
            session=hlid(session._session_id) if session else '',
            authid=hlid(session._authid),
            authrole=hlid(session._authrole),
            authmethod=hlval(session._authmethod),
            authprovider=hlval(session._authprovider),
            authextra=pformat(session._authextra)
            if session._authextra else None,
            realm=hlid(session._realm))

        return {
            'broker': self._broker._role_features,
            'dealer': self._dealer._role_features
        }
Ejemplo n.º 19
0
    def start(self):
        """
        Implements :meth:`crossbar._interfaces.IRealmStore.start`
        """
        if self._running:
            raise RuntimeError('store is already running')
        else:
            self.log.info(
                '{func} starting realm store type="{stype}"',
                func=hltype(self.start),
                stype=hlval(self._type),
            )

        # currently nothing to do in stores of type "memory"
        self._running = True
        self.log.info('{func} realm store ready!', func=hltype(self.start))
Ejemplo n.º 20
0
    def start(self):
        """
        Implements :meth:`crossbar._interfaces.IRealmStore.start`
        """
        if self._running:
            raise RuntimeError('store is already running')
        else:
            self.log.info(
                '{func} starting realm store type="{stype}"',
                func=hltype(self.start),
                stype=hlval(self._type),
            )

        self._buffer = []
        self._log_counter = 0
        self._running = True
        self._process_buffers_thread = yield self._reactor.callInThread(self._process_buffers)
        self.log.info('{func} realm store ready!', func=hltype(self.start))
Ejemplo n.º 21
0
    def start_realm(self, realm: RouterRealm) -> Router:
        """
        Starts a realm on this router.

        :param realm: The realm to start.
        :returns: The router instance for the started realm.
        :rtype: instance of :class:`crossbar.router.session.CrossbarRouter`
        """
        # extract name (URI in general) of realm from realm configuration
        assert 'name' in realm.config
        uri = realm.config['name']
        assert type(uri) == str
        self.log.info('{func}: realm={realm} with URI "{uri}"',
                      func=hltype(self.start_realm),
                      realm=realm,
                      uri=hlval(uri))

        if realm in self._routers:
            raise RuntimeError(
                'router for realm "{}" already running'.format(uri))

        # setup optional store for realm persistence features
        store: Optional[IRealmStore] = None
        if 'store' in realm.config and realm.config['store']:
            # the worker's node personality
            psn = self._worker.personality
            store = psn.create_realm_store(psn, self, realm.config['store'])
            self.log.info(
                '{func}: initialized realm store {store_class} for realm "{realm}"',
                func=hltype(self.start_realm),
                store_class=hlval(store.__class__, color='green'),
                realm=hlval(uri))

        # setup optional inventory for realm API catalogs
        inventory: Optional[IInventory] = None
        if 'inventory' in realm.config and realm.config['inventory']:
            # the worker's node personality
            psn = self._worker.personality
            inventory = psn.create_realm_inventory(psn, self,
                                                   realm.config['inventory'])
            assert inventory
            self.log.info(
                '{func}: initialized realm inventory <{inventory_type}> for realm "{realm}", '
                'loaded {total_count} types, from config:\n{config}',
                func=hltype(self.start_realm),
                inventory_type=hlval(inventory.type, color='green'),
                total_count=hlval(inventory.repo.total_count),
                realm=hlval(uri),
                config=pformat(realm.config['inventory']))

        # setup realm options
        options = RouterOptions(
            uri_check=self._options.uri_check,
            event_dispatching_chunk_size=self._options.
            event_dispatching_chunk_size,
        )
        for arg in ['uri_check', 'event_dispatching_chunk_size']:
            if arg in realm.config.get('options', {}):
                setattr(options, arg, realm.config['options'][arg])

        # now create a router for the realm
        router = self.router(self,
                             realm,
                             options,
                             store=store,
                             inventory=inventory)
        self._routers[uri] = router

        return router
Ejemplo n.º 22
0
    def __init__(self, factory, cbdir, config, templates):
        """

        :param factory: WAMP session factory.
        :type factory: An instance of ..

        :param cbdir: The Crossbar.io node directory.
        :type cbdir: str

        :param config: Crossbar transport configuration.
        :type config: dict

        :param templates:
        :type templates:
        """
        self.debug_traffic = config.get('debug_traffic', False)

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

        # announce Crossbar.io server version
        #
        self.showServerVersion = options.get('show_server_version',
                                             self.showServerVersion)
        if self.showServerVersion:
            server = "Crossbar/{}".format(crossbar.__version__)
        else:
            # do not disclose crossbar version
            server = "Crossbar"

        # external (public) listening port (eg when running behind a reverse proxy)
        #
        externalPort = options.get('external_port', None)

        # explicit list of WAMP serializers
        #
        if 'serializers' in config:
            serializers = []
            sers = set(config['serializers'])

            if 'flatbuffers' in sers:
                # try FlatBuffers WAMP serializer
                try:
                    from autobahn.wamp.serializer import FlatBuffersSerializer
                    serializers.append(FlatBuffersSerializer(batched=True))
                    serializers.append(FlatBuffersSerializer())
                except ImportError('FlatBuffersSerializer'):
                    self.log.warn(
                        "Warning: could not load WAMP-FlatBuffers serializer")
                else:
                    sers.discard('flatbuffers')

            if 'cbor' in sers:
                # try CBOR WAMP serializer
                try:
                    from autobahn.wamp.serializer import CBORSerializer
                    serializers.append(CBORSerializer(batched=True))
                    serializers.append(CBORSerializer())
                except ImportError('CBORSerializer'):
                    self.log.warn(
                        "Warning: could not load WAMP-CBOR serializer")
                else:
                    sers.discard('cbor')

            if 'msgpack' in sers:
                # try MsgPack WAMP serializer
                try:
                    from autobahn.wamp.serializer import MsgPackSerializer
                    serializers.append(MsgPackSerializer(batched=True))
                    serializers.append(MsgPackSerializer())
                except ImportError('MsgPackSerializer'):
                    self.log.warn(
                        "Warning: could not load WAMP-MsgPack serializer")
                else:
                    sers.discard('msgpack')

            if 'ubjson' in sers:
                # try UBJSON WAMP serializer
                try:
                    from autobahn.wamp.serializer import UBJSONSerializer
                    serializers.append(UBJSONSerializer(batched=True))
                    serializers.append(UBJSONSerializer())
                except ImportError('UBJSONSerializer'):
                    self.log.warn(
                        "Warning: could not load WAMP-UBJSON serializer")
                else:
                    sers.discard('ubjson')

            if 'json' in sers:
                # try JSON WAMP serializer
                try:
                    from autobahn.wamp.serializer import JsonSerializer
                    serializers.append(JsonSerializer(batched=True))
                    serializers.append(JsonSerializer())
                except ImportError('JsonSerializer'):
                    self.log.warn(
                        "Warning: could not load WAMP-JSON serializer")
                else:
                    sers.discard('json')

            if not serializers:
                raise Exception("no valid WAMP serializers specified")

            if len(sers) > 0:
                raise Exception(
                    "invalid WAMP serializers specified (the following were unprocessed) {}"
                    .format(sers))

        else:
            serializers = None

        websocket.WampWebSocketServerFactory.__init__(
            self,
            factory,
            serializers=serializers,
            url=config.get('url', None),
            server=server,
            externalPort=externalPort)

        # Crossbar.io node directory
        self._cbdir = cbdir

        # transport configuration
        self._config = config

        # Jinja2 templates for 404 etc
        self._templates = templates

        # enable cookie tracking if a cookie store is configured
        if 'cookie' in config:
            # cookie store configuration item
            cookie_config = config['cookie']

            # cookie store
            cookie_store_config = cookie_config['store']
            cookie_store_type = cookie_store_config['type']

            # setup ephemeral, memory-backed cookie store
            if cookie_store_type == 'memory':
                self._cookiestore = CookieStoreMemoryBacked(cookie_config)
                self.log.info("Memory-backed cookie store active.")

            # setup persistent, file-backed cookie store
            elif cookie_store_type == 'file':
                cookie_store_file = os.path.abspath(
                    os.path.join(self._cbdir, cookie_store_config['filename']))
                self._cookiestore = CookieStoreFileBacked(
                    cookie_store_file, cookie_config)
                self.log.info(
                    "File-backed cookie store active {cookie_store_file}",
                    cookie_store_file=hlval(cookie_store_file))

            # setup persistent, database-backed cookie store
            elif cookie_store_type == 'database':
                cookie_dbpath = os.path.abspath(
                    os.path.join(self._cbdir, cookie_store_config['path']))
                self._cookiestore = CookieStoreDatabaseBacked(
                    cookie_dbpath, cookie_config)
                self.log.info(
                    "Database-backed cookie store active! [cookiestore={cookiestore}]",
                    cookiestore=hltype(CookieStoreDatabaseBacked))

            else:
                # should not arrive here as the config should have been checked before
                raise NotImplementedError(
                    '{}: implementation of cookiestore of type "{}" missing'.
                    format(self.__class__.__name__, cookie_store_type))
        else:
            # this disables cookie tracking (both with or without WAMP-cookie authentication)
            self._cookiestore = None

        # set WebSocket options
        set_websocket_options(self, options)
Ejemplo n.º 23
0
    def onConnect(self, request):

        self.log.debug('{func}(request={request})',
                       func=hltype(self.onConnect),
                       request=request)

        if self.factory.debug_traffic:
            from twisted.internet import reactor

            def print_traffic():
                self.log.info(
                    "Traffic {peer}: {wire_in} / {wire_out} in / out bytes - {ws_in} / {ws_out} in / out msgs",
                    peer=self.peer,
                    wire_in=self.trafficStats.incomingOctetsWireLevel,
                    wire_out=self.trafficStats.outgoingOctetsWireLevel,
                    ws_in=self.trafficStats.incomingWebSocketMessages,
                    ws_out=self.trafficStats.outgoingWebSocketMessages,
                )
                reactor.callLater(1, print_traffic)

            print_traffic()

        # if WebSocket client did not set WS subprotocol, assume "wamp.2.json"
        #
        self.STRICT_PROTOCOL_NEGOTIATION = self.factory._requireWebSocketSubprotocol

        # handle WebSocket opening handshake
        #
        protocol, headers = websocket.WampWebSocketServerProtocol.onConnect(
            self, request)

        self.log.debug(
            '{func}: proceed with WebSocket opening handshake for WebSocket subprotocol "{protocol}"',
            func=hltype(self.onConnect),
            protocol=hlval(protocol))

        try:

            self._origin = request.origin

            # transport-level WMAP authentication info
            #
            self._authid = None
            self._authrole = None
            self._authrealm = None
            self._authmethod = None
            self._authextra = None
            self._authprovider = None

            # cookie tracking and cookie-based authentication
            #
            self._cbtid = None

            if self.factory._cookiestore:

                # try to parse an already set cookie from HTTP request headers
                self._cbtid = self.factory._cookiestore.parse(request.headers)

                if self._cbtid:
                    self.log.info(
                        '{func}: parsed tracking/authentication cookie cbtid "{cbtid}" from HTTP request headers',
                        func=hltype(self.onConnect),
                        cbtid=hlval(self._cbtid))
                else:
                    self.log.info(
                        '{func}: no tracking/authentication cookie cbtid found in HTTP request headers!',
                        func=hltype(self.onConnect))

                # if no cookie is set, or it doesn't exist in our database, create a new cookie
                if self._cbtid is None or not self.factory._cookiestore.exists(
                        self._cbtid):

                    self._cbtid, headers[
                        'Set-Cookie'] = self.factory._cookiestore.create()

                    if 'cookie' in self.factory._config:
                        if 'secure' in self.factory._config[
                                'cookie'] and self.factory._config['cookie'][
                                    'secure'] is True:
                            headers['Set-Cookie'] += ';Secure'
                        if 'http_strict' in self.factory._config[
                                'cookie'] and self.factory._config['cookie'][
                                    'http_strict'] is True:
                            headers['Set-Cookie'] += ';HttpOnly'
                        if 'same_site' in self.factory._config['cookie']:
                            headers[
                                'Set-Cookie'] += ';SameSite=' + self.factory._config[
                                    'cookie']['same_site']

                    self.log.info('{func}: setting new cookie {cookie}',
                                  func=hltype(self.onConnect),
                                  cookie=hlval(headers['Set-Cookie'],
                                               color='yellow'))
                else:
                    self.log.info(
                        '{func}: tracking/authentication cookie cbtid "{cbtid}" already set and stored',
                        func=hltype(self.onConnect),
                        cbtid=hlval(self._cbtid))

                # add this WebSocket connection to the set of connections
                # associated with the same cookie
                self.factory._cookiestore.addProto(self._cbtid, self)

                self.log.debug(
                    "Cookie tracking enabled on WebSocket connection {ws}",
                    ws=self)

                # if cookie-based authentication is enabled, set auth info from cookie store
                #
                if 'auth' in self.factory._config and 'cookie' in self.factory._config[
                        'auth']:

                    self._authid, self._authrole, self._authmethod, self._authrealm, self._authextra = self.factory._cookiestore.getAuth(
                        self._cbtid)

                    if self._authid:
                        # there is a cookie set, and the cookie was previously successfully authenticated,
                        # so immediately authenticate the client using that information
                        self._authprovider = 'cookie'
                        self.log.info(
                            '{func} authenticated client via cookie {cookiename}={cbtid} as authid="{authid}", authrole="{authrole}", authmethod="{authmethod}", authprovider="{authprovider}", authrealm="{authrealm}"',
                            func=hltype(self.onConnect),
                            cookiename=self.factory._cookiestore.
                            _cookie_id_field,
                            cbtid=hlval(self._cbtid, color='green'),
                            authid=hlid(self._authid),
                            authrole=hlid(self._authrole),
                            authmethod=hlval(self._authmethod),
                            authprovider=hlval(self._authprovider),
                            authrealm=hlid(self._authrealm))
                    else:
                        # there is a cookie set, but the cookie wasn't authenticated yet using a different auth method
                        self.log.info(
                            '{func} cookie-based authentication enabled, but cookie {cbtid} is not authenticated yet',
                            cbtid=hlval(self._cbtid, color='blue'),
                            func=hltype(self.onConnect))
                else:
                    self.log.info(
                        '{func} cookie-based authentication disabled on connection',
                        func=hltype(self.onConnect))
            else:
                self.log.info(
                    '{func} cookie tracking disabled on WebSocket connection',
                    func=hltype(self.onConnect))

            # negotiated WebSocket subprotocol in use, e.g. "wamp.2.cbor.batched"
            self._transport_details.websocket_protocol = protocol

            # WebSocket extensions in use. will be filled in onOpen(), see below
            self._transport_details.websocket_extensions_in_use = None

            # Crossbar.io tracking ID (for cookie tracking)
            self._transport_details.http_cbtid = self._cbtid

            # all HTTP headers as received by the WebSocket client
            self._transport_details.http_headers_received = request.headers

            # only customer user headers (such as cookie)
            self._transport_details.http_headers_sent = headers

            # accept the WebSocket connection, speaking subprotocol `protocol`
            # and setting HTTP headers `headers`
            return protocol, headers

        except:
            self.log.failure()
Ejemplo n.º 24
0
    def validate(self,
                 payload_type: str,
                 uri: str,
                 args: Optional[List[Any]],
                 kwargs: Optional[Dict[str, Any]],
                 validate: Optional[Dict[str, Any]] = None):
        """
        Implements :func:`autobahn.wamp.interfaces.IRouter.validate`

        Called to validate application payloads sent in WAMP calls, call results and errors, as well
        as events from:

        * :class:`crossbar.router.dealer.Dealer`
        * :class:`crossbar.router.broker.Broker`
        """
        assert payload_type in [
            # meta arguments parsed from URI
            'meta',

            # rpc_service.RequestType ##############################################################

            # WAMP event published either using normal or router-acknowledged publications
            'event',

            # WAMP call, the (only or the initial) caller request
            'call',

            # WAMP call, any call updates sent by the caller subsequently and while the call is
            # still active
            'call_progress',

            # rpc_service.ResponseType #############################################################

            # WAMP event confirmation sent by subscribers for subscribed-confirmed publications
            'event_result',

            # WAMP call result, the (only or the initial) callee response
            'call_result',

            # WAMP call progressive result, any call result updates sent by the callee subsequently
            # and while the call is still active
            'call_result_progress',

            # WAMP call error result, the callee error response payload
            'call_error',
        ]

        if self._inventory:
            if validate:
                if payload_type in validate:
                    # type against which we validate the application payload args/kwargs
                    validation_type = validate[payload_type]

                    self.log.info(
                        '{func} validate "{payload_type}" on URI "{uri}" for payload with '
                        'len(args)={args}, len(kwargs)={kwargs} using validation_type="{validation_type}"',
                        func=hltype(self.validate),
                        payload_type=hlval(payload_type.upper(), color='blue'),
                        uri=hlval(uri, color='magenta'),
                        args=hlval(len(args) if args is not None else '-'),
                        kwargs=hlval(
                            len(kwargs) if kwargs is not None else '-'),
                        validation_type=hlval(validation_type, color='blue'),
                        cb_level="trace")

                    try:
                        self._inventory.repo.validate(validation_type, args,
                                                      kwargs)
                    except InvalidPayload as e:
                        self.log.warn('{func} {msg}',
                                      func=hltype(self.validate),
                                      msg=hlval(
                                          'validation error: {}'.format(e),
                                          color='red'))
                        raise
                    else:
                        self.log.info('{func} {msg}',
                                      func=hltype(self.validate),
                                      msg=hlval('validation success!',
                                                color='green'))
                else:
                    self.log.warn(
                        '{func} {msg} (type inventory active, but no payload configuration for payload_type "{payload_type}" in validate for URI "{uri}"',
                        func=hltype(self.validate),
                        payload_type=hlval(payload_type, color='yellow'),
                        uri=hlval(uri),
                        msg=hlval('validation skipped!', color='yellow'))
            else:
                self.log.warn(
                    '{func} {msg} (type inventory active, but missing configuration for payload_type "{payload_type}" on URI "{uri}"',
                    func=hltype(self.validate),
                    uri=hlval(uri),
                    payload_type=hlval(payload_type, color='yellow'),
                    msg=hlval('validation skipped!', color='yellow'))
Ejemplo n.º 25
0
    def getChild(self, path, request, retry=True):
        self.log.debug(
            'ZipFileResource.getChild(path={path}, request={request}, prepath={prepath}, postpath={postpath})',
            path=path,
            prepath=request.prepath,
            postpath=request.postpath,
            request=request)

        # complete request URI
        request_path = b'/'.join([path] + request.postpath).decode('utf8')

        # local search path
        search_path = request_path

        # possibly apply default object name
        if (search_path == ''
                or search_path.endswith('/')) and self._default_object:
            search_path += self._default_object

        # possibly apply local object (=archive) prefix
        if self._object_prefix:
            search_path = os.path.join(self._object_prefix, search_path)

        found = search_path in self._zipfiles
        cached = False
        default = False

        if found:
            # check cache
            data = self._zipfiles[search_path]

            # get data if not cached
            if not data:
                if self._archive:
                    # open file within ZIP archive
                    data = self._archive.open(search_path).read()
                    if self._cache:
                        self._zipfiles[search_path] = data
                        self.log.debug(
                            'contents for file {search_path} from archive {archive_file} cached in memory',
                            search_path=search_path,
                            archive_file=self._archive_file)
                    else:
                        self.log.debug(
                            'contents for file {search_path} from archive {archive_file} read from file',
                            search_path=search_path,
                            archive_file=self._archive_file)
                else:
                    self.log.debug('cache archive not loaded')
                    return resource.NoResource()
            else:
                cached = True
                self.log.debug(
                    'cache hit: contents for file {search_path} from archive {archive_file} cached in memory',
                    search_path=search_path,
                    archive_file=self._archive_file)
            # file size
            file_size = len(data)
            fd = io.BytesIO(data)

            # guess MIME type from file extension
            _, ext = os.path.splitext(search_path)
            content_type = self.contentTypes.get(ext, None)

            # create and return resource that returns the file contents
            res = ZipFileResource(fd, file_size, content_type)

        else:
            if self._default_file and retry:
                res = self.getChild(self._default_file, request, False)
                default = True
            else:
                res = resource.NoResource()

        self.log.info(
            'ZipArchiveResource processed HTTP/GET for request_path="{request_path}", search_path="{search_path}": found={found}, cached={cached}, default={default}',
            request_path=hlval(request_path),
            search_path=hlval(search_path),
            found=hlval(found),
            cached=hlval(cached),
            default=hlval(default))

        return res
Ejemplo n.º 26
0
    def authorize(self, session: ISession, uri: str, action: str,
                  options: Dict[str, Any]):
        """
        Authorizes a session for an action on an URI.

        Implements :func:`autobahn.wamp.interfaces.IRouter.authorize`
        """
        assert (action in ['call', 'register', 'publish', 'subscribe'])

        # the realm, authid and authrole under which the session that wishes to perform the
        # given action on the given URI was authenticated under
        realm = session._realm
        # authid = session._authid
        authrole = session._authrole

        # the permission of a WAMP client is always determined (only) from
        # WAMP realm, authrole, URI and action already
        cache_key = (realm, authrole, uri, action)

        # if we do have a cache entry, use the authorization cached
        cached_authorization = self._authorization_cache.get(cache_key, None)

        # normally, the role should exist on the router (and hence we should not arrive
        # here), but the role might have been dynamically removed - and anyway, safety first!
        if authrole in self._roles:
            if cached_authorization:
                self.log.debug(
                    '{func} authorization cache entry found key {cache_key}:\n{authorization}',
                    func=hltype(self.authorize),
                    cache_key=hlval(cache_key),
                    authorization=pformat(cached_authorization))
                d = txaio.create_future_success(cached_authorization)
            else:
                # the authorizer procedure of the role which we will call
                authorize = self._roles[authrole].authorize
                d = txaio.as_future(authorize, session, uri, action, options)
        else:
            # remove cache entry
            if cached_authorization:
                del self._authorization_cache[cache_key]

            # outright deny, since the role isn't active anymore
            d = txaio.create_future_success(False)

        # XXX would be nicer for dynamic-authorizer authors if we
        # sanity-checked the return-value ('authorization') here
        # (i.e. is it a dict? does it have 'allow' in it? does it have
        # disallowed keys in it?)

        def got_authorization(authorization):
            # backward compatibility
            if isinstance(authorization, bool):
                authorization = {'allow': authorization, 'cache': False}
                if action in ['call', 'publish']:
                    authorization['disclose'] = False

            auto_disclose_trusted = True
            if auto_disclose_trusted and authrole == 'trusted' and action in [
                    'call', 'publish'
            ]:
                authorization['disclose'] = True

            if not cached_authorization and authorization.get('cache', False):
                self._authorization_cache[cache_key] = authorization
                self.log.debug(
                    '{func} add authorization cache entry for key {cache_key}:\n{authorization}',
                    func=hltype(got_authorization),
                    cache_key=hlval(cache_key),
                    authorization=pformat(authorization))

            self.log.debug(
                "Authorized action '{action}' for URI '{uri}' by session {session_id} with authid '{authid}' and "
                "authrole '{authrole}' -> authorization: {authorization}",
                session_id=session._session_id,
                uri=uri,
                action=action,
                authid=session._authid,
                authrole=session._authrole,
                authorization=authorization)

            return authorization

        d.addCallback(got_authorization)
        return d
Ejemplo n.º 27
0
    def setAuth(self, cbtid: str, authid: Optional[str],
                authrole: Optional[str], authmethod: Optional[str],
                authextra: Optional[Dict[str, Any]],
                authrealm: Optional[str]) -> bool:
        """
        Update the authentication information associated and stored for an existing cookie (if any).

        :param cbtid: Cookie value (ID) to update authentication for.
        :param authid: The WAMP authid a cookie-authenticating session is to be assigned.
        :param authrole: The WAMP authrole a cookie-authenticating session is to join under.
        :param authmethod: The WAMP authentication method to be returned to the client performing
            this cookie-based authentication.
        :param authextra: The WAMP authentication extra data to be returned to the client performing
            this cookie-based authentication.
        :param authrealm: The WAMP realm a cookie-authenticating session is to join.
        :return: Flag indicating an existing cookie was modified.
        """

        was_existing = False
        was_modified = False
        with self._db.begin(write=True) as txn:
            cookie_oid = self._schema.idx_cookies_by_value[txn, cbtid]
            if cookie_oid:
                # read current cookie from database
                cookie = self._schema.cookies[txn, cookie_oid]
                assert cookie
                was_existing = True
                if (authid != cookie.authid or authrole != cookie.authrole
                        or authmethod != cookie.authmethod
                        or authrealm != cookie.authrealm
                        or authextra != cookie.authextra):
                    cookie.authid = authid
                    cookie.authrole = authrole
                    cookie.authmethod = authmethod
                    cookie.authrealm = authrealm
                    cookie.authextra = authextra
                    # write updated cookie to database
                    self._schema.cookies[txn, cookie.oid] = cookie
                    was_modified = True

        if was_existing:
            if was_modified:
                self.log.info(
                    '{func} cookie with cbtid="{cbtid}" exists, but was updated (authid="{authid}", authrole='
                    '"{authrole}", authmethod="{authmethod}", authrealm="{authrealm}", authextra={authextra})',
                    func=hltype(self.setAuth),
                    cbtid=hlid(cbtid),
                    authid=hlid(authid),
                    authrole=hlid(authrole),
                    authmethod=hlval(authmethod),
                    authrealm=hlval(authrealm),
                    authextra=pformat(authextra))
            else:
                self.log.info(
                    '{func} cookie with cbtid="{cbtid}" exists and needs no update',
                    func=hltype(self.setAuth),
                    cbtid=hlid(cbtid))
        else:
            self.log.info(
                '{func} no cookie to modify with cbtid="{cbtid}" exists',
                func=hltype(self.setAuth),
                cbtid=hlid(cbtid))

        return was_modified