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)
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
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
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)
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')
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)
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
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))
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
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
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))
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)
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
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)
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)
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))
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__))
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 }
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))
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))
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
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)
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()
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'))
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
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
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