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 delAuth(self, cbtid: str) -> bool: """ Delete an existing cookie (if any), including any authentication info and the cookie itself. :param cbtid: Cookie value (ID) of cookie to delete. :return: Flag indicating an existing cookie was deleted. """ was_existing = False with self._db.begin(write=True) as txn: cookie_oid = self._schema.idx_cookies_by_value[txn, cbtid] if cookie_oid: del self._schema.cookies[txn, cookie_oid] was_existing = True if was_existing: self.log.info( '{func} cookie with cbtid="{cbtid}" did exist and was deleted', func=hltype(self.delAuth), cbtid=hlid(cbtid)) else: self.log.info('{func} no cookie with cbtid="{cbtid}" exists', func=hltype(self.delAuth), cbtid=hlid(cbtid)) return was_existing
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
async def authenticate(realm, authid, details): """ this is our dynamic authenticator procedure that will be called by Crossbar.io when a session is authenticating """ log.info( 'authenticate(realm="{realm}", authid="{authid}", details={details}) {func}', realm=hl(realm), authid=hl(authid), details=details, func=hltype(create_rlink_authenticator), ) assert ('authmethod' in details) assert (details['authmethod'] == 'cryptosign') assert ('authextra' in details) assert ('pubkey' in details['authextra']) pubkey = details['authextra']['pubkey'] log.info( 'authenticating session using realm="{realm}", pubkey={pubkey} .. {func}', realm=hl(realm), pubkey=hl(pubkey), func=hltype(create_rlink_authenticator), ) if pubkey in pubkey_to_principals: principal = pubkey_to_principals[pubkey] auth = { 'pubkey': pubkey, 'realm': principal['realm'], 'authid': principal['authid'], 'role': principal['role'], 'extra': principal['extra'], 'cache': True } # Note: with WAMP-cryptosign, even though a client may or may not request a `realm`, but in any case, the # effective realm the client is authenticated will be returned in the principal `auth['role']` (!) effective_realm = auth['realm'] log.info( 'found valid principal authid="{authid}", authrole="{authrole}", realm="{realm}" matching given client public key {func}', func=hltype(create_rlink_authenticator), authid=hl(auth['authid']), authrole=hl(auth['role']), realm=hl(effective_realm), ) # only now that we know the effective realm a client is to be joined to (see above), maybe active (start) # the desired application realm to let the client join to subsequently # await _maybe_activate_realm(controller, effective_realm) return auth else: msg = 'no principal with matching public key 0x{}'.format(pubkey) log.warn(msg) raise ApplicationError('com.example.no_such_user', msg)
def onConnect(self): self.log.info('{func}() ...', func=hltype(self.onConnect)) authid = self.config.extra.get('authid', None) authrole = self.config.extra.get('authrole', None) authextra = self.config.extra.get('authextra', {}) # FIXME: use cryptosign-proxy authmethods = ['cryptosign'] # use WorkerController.get_public_key to call node controller # FIXME: the following does _not_ work with onConnect (?!) # _public_key = await self._router_controller.get_public_key() 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) res = self._rlink_manager._controller.get_public_key() res.addCallback(actually_join) self.log.info('{func}() done (res={res}).', func=hltype(self.onConnect), res=res) return res
def start(self): """ Implements :meth:`crossbar._interfaces.IInventory.start` """ if self._running: raise RuntimeError('inventory is already running') else: self.log.info('{func} starting realm inventory', func=hltype(self.start)) self._running = True self.log.info('{func} realm inventory ready!', func=hltype(self.start))
def get_public_key(self): """ Call into node controller (over secure controller-worker pipe) to get the node's public key. :return: """ self.log.info('{func}() ...', func=hltype(self.get_public_key)) result = yield self.call("crossbar.get_public_key") self.log.info('{func}(): {result}', func=hltype(self.get_public_key), result=result) return result
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 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 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 dropProto(self, cbtid: str, proto: ISession): """ Remove given WebSocket connection from the set of connections associated with the cookie having the given ID. Return the new count of connections associated with the cookie. """ self.log.debug('{func} removing proto {proto} from cookie "{cbtid}"', func=hltype(self.dropProto), proto=proto, cbtid=hlid(cbtid)) if self.exists(cbtid): if cbtid in self._connections: # remove this WebSocket connection from the set of connections # associated with the same cookie self._connections[cbtid].discard(proto) remaining = len(self._connections[cbtid]) if remaining: return remaining else: del self._connections[cbtid] else: if cbtid in self._connections: del self._connections[cbtid] return 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
def parse(self, headers): """ Parse HTTP request header for cookie. If cookie is found, return cookie ID, else return None. """ self.log.debug('{func} parsing cookie from {headers}', func=hltype(self.parse), headers=headers) # see if there already is a cookie set in the HTTP request if 'cookie' in headers: try: cookie = http_cookies.SimpleCookie() header_cookie = str(headers['cookie']) cookie.load(header_cookie) if self._cookie_id_field not in cookie and self._cookie_id_field in header_cookie: # Sometimes Python can't parse cookie. So let's parse it manually. header_cookies_as_array = header_cookie.split(";") if len(header_cookies_as_array) != 0: header_cookie_id_indexes = [] for cookie_raw in header_cookies_as_array: if (self._cookie_id_field + "=") in cookie_raw: header_cookie_id_indexes.append( header_cookies_as_array.index(cookie_raw)) if len(header_cookie_id_indexes) > 0: cookie.load(header_cookies_as_array[ header_cookie_id_indexes[0]]) except http_cookies.CookieError: pass else: if self._cookie_id_field in cookie: cbtid = cookie[self._cookie_id_field].value return cbtid return None
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 onChallenge(self, challenge): self.log.debug('{func}(challenge={challenge})', func=hltype(self.onChallenge), challenge=challenge) if challenge.method == 'cryptosign': # alright, we've got a challenge from the router. # sign the challenge with our private key. channel_id_type = 'tls-unique' channel_id_map = self._router_controller._transport.transport_details.channel_id if channel_id_type in channel_id_map: channel_id = channel_id_map[channel_id_type] else: channel_id = None channel_id_type = None # use WorkerController.get_public_key to call node controller # FIXME: await? signed_challenge = self._router_controller.sign_challenge( challenge, channel_id, channel_id_type) # send back the signed challenge for verification return signed_challenge else: raise Exception( 'internal error: we asked to authenticate using wamp-cryptosign, but now received a challenge for {}' .format(challenge.method))
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 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 onJoin(self, details): assert self.config.extra and 'on_ready' in self.config.extra assert self.config.extra and 'other' in self.config.extra remote = self.config.extra['other'] assert isinstance(remote, RLinkRemoteSession) self._exclude_authid = self.config.extra.get('exclude_authid', None) self._exclude_authrole = self.config.extra.get('exclude_authrole', None) # setup local->remote event forwarding forward_events = self.config.extra.get('forward_events', False) if forward_events: yield self._setup_event_forwarding(remote) # setup local->remote invocation forwarding forward_invocations = self.config.extra.get('forward_invocations', False) if forward_invocations: yield self._setup_invocation_forwarding(remote) self.log.debug( 'Router link local session ready (forward_events={forward_events}, forward_invocations={forward_invocations}, realm={realm}, authid={authid}, authrole={authrole}, session={session}) {method}', method=hltype(RLinkLocalSession.onJoin), forward_events=hluserid(forward_events), forward_invocations=hluserid(forward_invocations), realm=hluserid(details.realm), authid=hluserid(details.authid), authrole=hluserid(details.authrole), session=hlid(details.session)) on_ready = self.config.extra.get('on_ready', None) if on_ready and not on_ready.called: self.config.extra['on_ready'].callback(self)
def store_session_joined(self, session: ISession, details: SessionDetails): """ Implements :meth:`crossbar._interfaces.IRealmStore.store_session_joined` """ self.log.info('{func} new session joined session={session}, details={details}', func=hltype(self.store_session_joined), session=session, details=details)
def store_session_left(self, session: ISession, details: CloseDetails): """ Implements :meth:`crossbar._interfaces.IRealmStore.store_session_left` """ self.log.info('{func} session left session={session}, details={details}', func=hltype(self.store_session_left), session=session, details=details)
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 __init__(self, config: Dict[str, Any]): CookieStore.__init__(self, config) # transient cookie database self._cookies: Dict[str, Dict[str, Any]] = {} self.log.info('{func} cookie stored created with config=\n{config}', func=hltype(CookieStoreMemoryBacked.__init__), config=pformat(config))
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 __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 sign_challenge(self, challenge: Challenge, channel_id: Optional[bytes], channel_id_type=Optional[str]): """ Call into node controller (over secure controller-worker pipe) to sign challenge with node key. :param challenge: :param channel_id: :param channel_id_type: :return: """ self.log.info('{func}() ...', func=hltype(self.sign_challenge)) result = yield self.call("crossbar.sign_challenge", challenge.method, challenge.extra, channel_id, channel_id_type) self.log.info('{func}(): {result}', func=hltype(self.sign_challenge), result=result) return result
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 stop(self): """ Implements :meth:`crossbar._interfaces.IRealmStore.stop` """ if not self._running: raise RuntimeError('store is not running') else: self.log.info('{func} stopping realm store', func=hltype(self.start)) # currently nothing to do in stores of type "memory" self._running = False
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 onLeave(self, details): self.log.warn( 'Router link local session down! (realm={realm}, authid={authid}, authrole={authrole}, session={session}, details={details}) {method}', method=hltype(RLinkLocalSession.onLeave), realm=hluserid(self.config.realm), authid=hluserid(self._authid), authrole=hluserid(self._authrole), details=details, session=hlid(self._session_id)) BridgeSession.onLeave(self, details)
def stop(self): """ Implements :meth:`crossbar._interfaces.IInventory.stop` """ if not self._running: raise RuntimeError('inventory is not running') else: self.log.info('{func} stopping realm inventory', func=hltype(self.start)) self._running = False