def onHello(self, realm, details): try: # check if the realm the session wants to join actually exists # if realm not in self._router_factory: return types.Deny( ApplicationError.NO_SUCH_REALM, message="no realm '{}' exists on this router".format( realm)) authmethods = details.authmethods or ["anonymous"] # perform authentication # if self._transport._authid is not None and ( self._transport._authmethod == u'trusted' or self._transport._authprovider in authmethods): # already authenticated .. e.g. via HTTP Cookie or TLS client-certificate # check if role still exists on realm # allow = self._router_factory[realm].has_role( self._transport._authrole) if allow: return types.Accept( authid=self._transport._authid, authrole=self._transport._authrole, authmethod=self._transport._authmethod, authprovider=self._transport._authprovider) else: return types.Deny( ApplicationError.NO_SUCH_ROLE, message= "session was previously authenticated (via transport), but role '{}' no longer exists on realm '{}'" .format(self._transport._authrole, realm)) else: # if authentication is enabled on the transport .. # if "auth" in self._transport_config: # iterate over authentication methods announced by client .. # for authmethod in authmethods: # .. and if the configuration has an entry for the authmethod # announced, process .. if authmethod in self._transport_config["auth"]: # "WAMP-Challenge-Response" authentication # if authmethod == u"wampcra": cfg = self._transport_config['auth']['wampcra'] if cfg['type'] == 'static': if details.authid in cfg.get('users', {}): user = cfg['users'][details.authid] # the authid the session will be authenticated as is from the user data, or when # the user data doesn't contain an authid, from the HELLO message the client sent # authid = user.get( "authid", details.authid) # construct a pending WAMP-CRA authentication # self._pending_auth = PendingAuthWampCra( details.pending_session, authid, user['role'], u'static', user['secret'].encode('utf8')) # send challenge to client # extra = { u'challenge': self._pending_auth.challenge } # when using salted passwords, provide the client with # the salt and then PBKDF2 parameters used # if 'salt' in user: extra[u'salt'] = user['salt'] extra[u'iterations'] = user.get( 'iterations', 1000) extra[u'keylen'] = user.get( 'keylen', 32) return types.Challenge( u'wampcra', extra) else: return types.Deny( message= "no user with authid '{}' in user database" .format(details.authid)) elif cfg['type'] == 'dynamic': # call the configured dynamic authenticator procedure # via the router's service session # service_session = self._router_factory.get( realm)._realm.session session_details = { # forward transport level details of the WAMP session that # wishes to authenticate 'transport': self._transport._transport_info, # the following WAMP session ID will be assigned to the session # if (and only if) the subsequent authentication succeeds. 'session': self._pending_session_id } d = service_session.call( cfg['authenticator'], realm, details.authid, session_details) def on_authenticate_ok(user): # the authid the session will be authenticated as is from the dynamic # authenticator response, or when the response doesn't contain an authid, # from the HELLO message the client sent # authid = user.get( "authid", details.authid) # construct a pending WAMP-CRA authentication # self._pending_auth = PendingAuthWampCra( details.pending_session, authid, user['role'], u'dynamic', user['secret'].encode('utf8')) # send challenge to client # extra = { u'challenge': self._pending_auth.challenge } # when using salted passwords, provide the client with # the salt and the PBKDF2 parameters used # if 'salt' in user: extra[u'salt'] = user['salt'] extra[u'iterations'] = user.get( 'iterations', 1000) extra[u'keylen'] = user.get( 'keylen', 32) return types.Challenge( u'wampcra', extra) def on_authenticate_error(err): error = None message = "dynamic WAMP-CRA credential getter failed: {}".format( err) if isinstance(err.value, ApplicationError): error = err.value.error if err.value.args and len( err.value.args): message = str( err.value.args[0] ) # exception does not need to contain a string return types.Deny(error, message) d.addCallbacks(on_authenticate_ok, on_authenticate_error) return d else: return types.Deny( message= "illegal WAMP-CRA authentication config (type '{0}' is unknown)" .format(cfg['type'])) # WAMP-Ticket authentication # elif authmethod == u"ticket": cfg = self._transport_config['auth']['ticket'] # use static principal database from configuration # if cfg['type'] == 'static': if details.authid in cfg.get( 'principals', {}): principal = cfg['principals'][ details.authid] # the authid the session will be authenticated as is from the principal data, or when # the principal data doesn't contain an authid, from the HELLO message the client sent # authid = principal.get( "authid", details.authid) self._pending_auth = PendingAuthTicket( realm, authid, principal['role'], u'static', principal['ticket'].encode('utf8')) return types.Challenge(u'ticket') else: return types.Deny( message= "no principal with authid '{}' in principal database" .format(details.authid)) # use configured procedure to dynamically get a ticket for the principal # elif cfg['type'] == 'dynamic': self._pending_auth = PendingAuthTicket( realm, details.authid, None, cfg['authenticator'], None) return types.Challenge(u'ticket') else: return types.Deny( message= "illegal WAMP-Ticket authentication config (type '{0}' is unknown)" .format(cfg['type'])) # "Mozilla Persona" authentication # elif authmethod == u"mozilla_persona": cfg = self._transport_config['auth'][ 'mozilla_persona'] audience = cfg.get('audience', self._transport._origin) provider = cfg.get( 'provider', "https://verifier.login.persona.org/verify" ) # authrole mapping # authrole = cfg.get('role', 'anonymous') # check if role exists on realm anyway # if not self._router_factory[realm].has_role( authrole): return types.Deny( ApplicationError.NO_SUCH_ROLE, message= "authentication failed - realm '{}' has no role '{}'" .format(realm, authrole)) # ok, now challenge the client for doing Mozilla Persona auth. # self._pending_auth = PendingAuthPersona( provider, audience, authrole) return types.Challenge("mozilla-persona") # "Anonymous" authentication # elif authmethod == u"anonymous": cfg = self._transport_config['auth'][ 'anonymous'] # authrole mapping # authrole = cfg.get('role', 'anonymous') # check if role exists on realm anyway # if not self._router_factory[realm].has_role( authrole): return types.Deny( ApplicationError.NO_SUCH_ROLE, message= "authentication failed - realm '{}' has no role '{}'" .format(realm, authrole)) # authid generation if self._transport._cbtid: # if cookie tracking is enabled, set authid to cookie value authid = self._transport._cbtid else: # if no cookie tracking, generate a random value for authid authid = util.newid(24) self._transport._authid = authid self._transport._authrole = authrole self._transport._authmethod = authmethod return types.Accept( authid=authid, authrole=authrole, authmethod=self._transport._authmethod) # "Cookie" authentication # elif authmethod == u"cookie": # the client requested cookie authentication, but there is 1) no cookie set, # or 2) a cookie set, but that cookie wasn't authenticated before using # a different auth method (if it had been, we would never have entered here, since then # auth info would already have been extracted from the transport) # consequently, we skip this auth method and move on to next auth method. pass # Unknown authentication method # else: self.log.info("unknown authmethod '{}'".format( authmethod)) return types.Deny( message="unknown authentication method {}". format(authmethod)) # if authentication is configured, by default, deny. # return types.Deny( message= "authentication using method '{}' denied by configuration" .format(authmethod)) else: # if authentication is _not_ configured, by default, allow anyone. # # authid generation if self._transport._cbtid: # if cookie tracking is enabled, set authid to cookie value authid = self._transport._cbtid else: # if no cookie tracking, generate a random value for authid authid = util.newid(24) return types.Accept(authid=authid, authrole="anonymous", authmethod="anonymous") except Exception as e: traceback.print_exc() return types.Deny(message="internal error: {}".format(e))
class _CommonResource(Resource): """ Shared components between PublisherResource and CallerResource. """ isLeaf = True decode_as_json = True def __init__(self, options, session, auth_config=None): """ Ctor. :param options: Options for path service from configuration. :type options: dict :param session: Instance of `ApplicationSession` to be used for forwarding events. :type session: obj """ Resource.__init__(self) self._options = options self._session = session self.log = make_logger() self._key = None if 'key' in options: self._key = options['key'].encode('utf8') self._secret = None if 'secret' in options: self._secret = options['secret'].encode('utf8') self._post_body_limit = int(options.get('post_body_limit', 0)) self._timestamp_delta_limit = int(options.get('timestamp_delta_limit', 300)) self._require_ip = None if 'require_ip' in options: self._require_ip = [ip_network(net) for net in options['require_ip']] self._require_tls = options.get('require_tls', None) self._auth_config = auth_config or {} self._pending_auth = None def _deny_request(self, request, code, **kwargs): """ Called when client request is denied. """ if "log_category" not in kwargs.keys(): kwargs["log_category"] = "AR" + str(code) self.log.debug(code=code, **kwargs) error_str = log_categories[kwargs['log_category']].format(**kwargs) body = dump_json({"error": error_str, "args": [], "kwargs": {}}, True).encode('utf8') request.setResponseCode(code) return body def _fail_request(self, request, **kwargs): """ Called when client request fails. """ res = {} err = kwargs["failure"] if isinstance(err.value, ApplicationError): res['error'] = err.value.error if err.value.args: res['args'] = err.value.args else: res['args'] = [] if err.value.kwargs: res['kwargs'] = err.value.kwargs else: res['kwargs'] = {} # This is a user-level error, not a CB error, so return 200 code = 200 else: # This is a "CB" error, so return 500 and a generic error res['error'] = u'wamp.error.runtime_error' res['args'] = ["Sorry, Crossbar.io has encountered a problem."] res['kwargs'] = {} # CB-level error, return 500 code = 500 self.log.failure(None, failure=err, log_category="AR500") body = json.dumps(res).encode('utf8') if "log_category" not in kwargs.keys(): kwargs["log_category"] = "AR" + str(code) self.log.debug(code=code, **kwargs) request.setResponseCode(code) request.write(body) request.finish() def _complete_request(self, request, code, body, **kwargs): """ Called when client request is complete. """ if "log_category" not in kwargs.keys(): kwargs["log_category"] = "AR" + str(code) self.log.debug(code=code, **kwargs) request.setResponseCode(code) request.write(body) def _set_common_headers(self, request): """ Set common HTTP response headers. """ origin = request.getHeader(b'origin') if origin is None or origin == b'null': origin = b'*' request.setHeader(b'access-control-allow-origin', origin) request.setHeader(b'access-control-allow-credentials', b'true') request.setHeader(b'cache-control', b'no-store,no-cache,must-revalidate,max-age=0') request.setHeader(b'content-type', b'application/json; charset=UTF-8') headers = request.getHeader(b'access-control-request-headers') if headers is not None: request.setHeader(b'access-control-allow-headers', headers) def render(self, request): """ Handle the request. All requests start here. """ self.log.debug(log_category="AR100", method=request.method, path=request.path) self._set_common_headers(request) try: if request.method not in (b"POST", b"PUT", b"OPTIONS"): return self._deny_request(request, 405, method=request.method, allowed="POST, PUT") else: if request.method == b"OPTIONS": # http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.7 request.setHeader(b'allow', b'POST,PUT,OPTIONS') # https://www.w3.org/TR/cors/#access-control-allow-methods-response-header request.setHeader(b'access-control-allow-methods', b'POST,PUT,OPTIONS') request.setResponseCode(200) return b'' else: return self._render_request(request) except Exception as e: self.log.failure(log_category="CB501", exc=e) return self._deny_request(request, 500, log_category="CB500") def _render_request(self, request): """ Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller processor. """ # read HTTP/POST|PUT body body = request.content.read() args = {native_string(x): y[0] for x, y in request.args.items()} headers = request.requestHeaders # check content type + charset encoding # content_type_header = headers.getRawHeaders(b"content-type", []) if len(content_type_header) > 0: content_type_elements = [ x.strip().lower() for x in content_type_header[0].split(b";") ] else: content_type_elements = [] if self.decode_as_json: # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES # (but we allow missing content type .. will catch later during JSON # parsing anyway) if len(content_type_elements) > 0 and \ content_type_elements[0] not in _ALLOWED_CONTENT_TYPES: return self._deny_request( request, 400, accepted=list(_ALLOWED_CONTENT_TYPES), given=content_type_elements[0], log_category="AR452" ) encoding_parts = {} if len(content_type_elements) > 1: try: for item in content_type_elements: if b"=" not in item: # Don't bother looking at things "like application/json" continue # Parsing things like: # charset=utf-8 _ = native_string(item).split("=") assert len(_) == 2 # We don't want duplicates key = _[0].strip().lower() assert key not in encoding_parts encoding_parts[key] = _[1].strip().lower() except: return self._deny_request(request, 400, log_category="AR450") charset_encoding = encoding_parts.get("charset", "utf-8") if charset_encoding not in ["utf-8", 'utf8']: return self._deny_request( request, 400, log_category="AR450") # enforce "post_body_limit" # body_length = len(body) content_length_header = headers.getRawHeaders(b"content-length", []) if len(content_length_header) == 1: content_length = int(content_length_header[0]) elif len(content_length_header) > 1: return self._deny_request( request, 400, log_category="AR463") else: content_length = body_length if body_length != content_length: # Prevent the body length from being different to the given # Content-Length. This is so that clients can't lie and bypass # length restrictions by giving an incorrect header with a large # body. return self._deny_request(request, 400, bodylen=body_length, conlen=content_length, log_category="AR465") if self._post_body_limit and content_length > self._post_body_limit: return self._deny_request( request, 413, length=content_length, accepted=self._post_body_limit ) # # parse/check HTTP/POST|PUT query parameters # # key # if 'key' in args: key_str = args["key"] else: if self._secret: return self._deny_request( request, 400, reason=u"'key' field missing", log_category="AR461") # timestamp # if 'timestamp' in args: timestamp_str = args["timestamp"] try: ts = datetime.datetime.strptime(native_string(timestamp_str), "%Y-%m-%dT%H:%M:%S.%fZ") delta = abs((ts - datetime.datetime.utcnow()).total_seconds()) if self._timestamp_delta_limit and delta > self._timestamp_delta_limit: return self._deny_request( request, 400, log_category="AR464") except ValueError: return self._deny_request( request, 400, reason=u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')".format(native_string(timestamp_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason=u"signed request required, but mandatory 'timestamp' field missing", log_category="AR461") # seq # if 'seq' in args: seq_str = args["seq"] try: # FIXME: check sequence seq = int(seq_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid sequence number '{0}' (must be an integer)".format(native_string(seq_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason=u"'seq' field missing", log_category="AR461") # nonce # if 'nonce' in args: nonce_str = args["nonce"] try: # FIXME: check nonce nonce = int(nonce_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid nonce '{0}' (must be an integer)".format(native_string(nonce_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason=u"'nonce' field missing", log_category="AR461") # signature # if 'signature' in args: signature_str = args["signature"] else: if self._secret: return self._deny_request( request, 400, reason=u"'signature' field missing", log_category="AR461") # do more checks if signed requests are required # if self._secret: if key_str != self._key: return self._deny_request( request, 401, reason=u"unknown key '{0}' in signed request".format(native_string(key_str)), log_category="AR460") # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature hm = hmac.new(self._secret, None, hashlib.sha256) hm.update(key_str) hm.update(timestamp_str) hm.update(seq_str) hm.update(nonce_str) hm.update(body) signature_recomputed = base64.urlsafe_b64encode(hm.digest()) if signature_str != signature_recomputed: return self._deny_request(request, 401, log_category="AR459") else: self.log.debug("REST request signature valid.", log_category="AR203") # user_agent = headers.get("user-agent", "unknown") client_ip = request.getClientIP() is_secure = request.isSecure() # enforce client IP address # if self._require_ip: ip = ip_address(client_ip) allowed = False for net in self._require_ip: if ip in net: allowed = True break if not allowed: return self._deny_request(request, 400, log_category="AR466") # enforce TLS # if self._require_tls: if not is_secure: return self._deny_request(request, 400, reason=u"request denied because not using TLS") # authenticate request # # TODO: also support HTTP Basic AUTH for ticket def on_auth_ok(value): if value is True: # treat like original behavior and just accept the request_id pass elif isinstance(value, types.Accept): self._session._authid = value.authid self._session._authrole = value.authrole # realm? else: # FIXME: not returning deny request... probably not ideal request.write(self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return _validator.reset() validation_result = _validator.validate(body) # validate() returns a 4-tuple, of which item 0 is whether it # is valid if not validation_result[0]: request.write(self._deny_request( request, 400, log_category="AR451")) request.finish() return event = body.decode('utf8') if self.decode_as_json: try: event = json.loads(event) except Exception as e: request.write(self._deny_request( request, 400, exc=e, log_category="AR453")) request.finish() return if not isinstance(event, dict): request.write(self._deny_request( request, 400, log_category="AR454")) request.finish() return d = maybeDeferred(self._process, request, event) def finish(value): if isinstance(value, bytes): request.write(value) request.finish() d.addCallback(finish) def on_auth_error(err): # XXX: is it ideal to write to the request? request.write(self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return authmethod = None authid = None signature = None authorization_header = headers.getRawHeaders(b"authorization", []) if len(authorization_header) == 1: # HTTP Basic Authorization will be processed as ticket authentication authorization = authorization_header[0] auth_scheme, auth_details = authorization.split(b" ", 1) if auth_scheme.lower() == b"basic": try: credentials = binascii.a2b_base64(auth_details + b'===') credentials = credentials.split(b":", 1) if len(credentials) == 2: authmethod = "ticket" authid = credentials[0].decode("utf-8") signature = credentials[1].decode("utf-8") else: return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") except binascii.Error: # authentication failed return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") elif 'authmethod' in args and args['authmethod'].decode("utf-8") == 'ticket': if "ticket" not in args or "authid" not in args: # AR401 - fail if the ticket or authid are not in the args on_auth_ok(False) else: authmethod = "ticket" authid = args['authid'].decode("utf-8") signature = args['ticket'].decode("utf-8") if authmethod and authid and signature: hdetails = types.HelloDetails( authid=authid, authmethods=[authmethod] ) # wire up some variables for the authenticators to work, this is hackish # a custom header based authentication scheme can be implemented # without adding alternate authenticators by forwarding all headers. self._session._transport._transport_info = { "http_headers_received": { native_string(x).lower(): native_string(y[0]) for x, y in request.requestHeaders.getAllRawHeaders() } } self._session._pending_session_id = None self._session._router_factory = self._session._transport._routerFactory if authmethod == "ticket": self._pending_auth = PendingAuthTicket(self._session, self._auth_config['ticket']) self._pending_auth.hello(self._session._realm, hdetails) auth_d = maybeDeferred(self._pending_auth.authenticate, signature) auth_d.addCallbacks(on_auth_ok, on_auth_error) else: # don't return the value or it will be written to the request on_auth_ok(True) return server.NOT_DONE_YET def _process(self, request, event): raise NotImplementedError()
def _render_request(self, request): """ Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller processor. """ # read HTTP/POST|PUT body body = request.content.read() args = {native_string(x): y[0] for x, y in request.args.items()} headers = request.requestHeaders # check content type + charset encoding # content_type_header = headers.getRawHeaders(b"content-type", []) if len(content_type_header) > 0: content_type_elements = [ x.strip().lower() for x in content_type_header[0].split(b";") ] else: content_type_elements = [] if self.decode_as_json: # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES # (but we allow missing content type .. will catch later during JSON # parsing anyway) if len(content_type_elements) > 0 and \ content_type_elements[0] not in _ALLOWED_CONTENT_TYPES: return self._deny_request( request, 400, accepted=list(_ALLOWED_CONTENT_TYPES), given=content_type_elements[0], log_category="AR452" ) encoding_parts = {} if len(content_type_elements) > 1: try: for item in content_type_elements: if b"=" not in item: # Don't bother looking at things "like application/json" continue # Parsing things like: # charset=utf-8 _ = native_string(item).split("=") assert len(_) == 2 # We don't want duplicates key = _[0].strip().lower() assert key not in encoding_parts encoding_parts[key] = _[1].strip().lower() except: return self._deny_request(request, 400, log_category="AR450") charset_encoding = encoding_parts.get("charset", "utf-8") if charset_encoding not in ["utf-8", 'utf8']: return self._deny_request( request, 400, log_category="AR450") # enforce "post_body_limit" # body_length = len(body) content_length_header = headers.getRawHeaders(b"content-length", []) if len(content_length_header) == 1: content_length = int(content_length_header[0]) elif len(content_length_header) > 1: return self._deny_request( request, 400, log_category="AR463") else: content_length = body_length if body_length != content_length: # Prevent the body length from being different to the given # Content-Length. This is so that clients can't lie and bypass # length restrictions by giving an incorrect header with a large # body. return self._deny_request(request, 400, bodylen=body_length, conlen=content_length, log_category="AR465") if self._post_body_limit and content_length > self._post_body_limit: return self._deny_request( request, 413, length=content_length, accepted=self._post_body_limit ) # # parse/check HTTP/POST|PUT query parameters # # key # if 'key' in args: key_str = args["key"] else: if self._secret: return self._deny_request( request, 400, reason=u"'key' field missing", log_category="AR461") # timestamp # if 'timestamp' in args: timestamp_str = args["timestamp"] try: ts = datetime.datetime.strptime(native_string(timestamp_str), "%Y-%m-%dT%H:%M:%S.%fZ") delta = abs((ts - datetime.datetime.utcnow()).total_seconds()) if self._timestamp_delta_limit and delta > self._timestamp_delta_limit: return self._deny_request( request, 400, log_category="AR464") except ValueError: return self._deny_request( request, 400, reason=u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')".format(native_string(timestamp_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason=u"signed request required, but mandatory 'timestamp' field missing", log_category="AR461") # seq # if 'seq' in args: seq_str = args["seq"] try: # FIXME: check sequence seq = int(seq_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid sequence number '{0}' (must be an integer)".format(native_string(seq_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason=u"'seq' field missing", log_category="AR461") # nonce # if 'nonce' in args: nonce_str = args["nonce"] try: # FIXME: check nonce nonce = int(nonce_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid nonce '{0}' (must be an integer)".format(native_string(nonce_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason=u"'nonce' field missing", log_category="AR461") # signature # if 'signature' in args: signature_str = args["signature"] else: if self._secret: return self._deny_request( request, 400, reason=u"'signature' field missing", log_category="AR461") # do more checks if signed requests are required # if self._secret: if key_str != self._key: return self._deny_request( request, 401, reason=u"unknown key '{0}' in signed request".format(native_string(key_str)), log_category="AR460") # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature hm = hmac.new(self._secret, None, hashlib.sha256) hm.update(key_str) hm.update(timestamp_str) hm.update(seq_str) hm.update(nonce_str) hm.update(body) signature_recomputed = base64.urlsafe_b64encode(hm.digest()) if signature_str != signature_recomputed: return self._deny_request(request, 401, log_category="AR459") else: self.log.debug("REST request signature valid.", log_category="AR203") # user_agent = headers.get("user-agent", "unknown") client_ip = request.getClientIP() is_secure = request.isSecure() # enforce client IP address # if self._require_ip: ip = ip_address(client_ip) allowed = False for net in self._require_ip: if ip in net: allowed = True break if not allowed: return self._deny_request(request, 400, log_category="AR466") # enforce TLS # if self._require_tls: if not is_secure: return self._deny_request(request, 400, reason=u"request denied because not using TLS") # authenticate request # # TODO: also support HTTP Basic AUTH for ticket def on_auth_ok(value): if value is True: # treat like original behavior and just accept the request_id pass elif isinstance(value, types.Accept): self._session._authid = value.authid self._session._authrole = value.authrole # realm? else: # FIXME: not returning deny request... probably not ideal request.write(self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return _validator.reset() validation_result = _validator.validate(body) # validate() returns a 4-tuple, of which item 0 is whether it # is valid if not validation_result[0]: request.write(self._deny_request( request, 400, log_category="AR451")) request.finish() return event = body.decode('utf8') if self.decode_as_json: try: event = json.loads(event) except Exception as e: request.write(self._deny_request( request, 400, exc=e, log_category="AR453")) request.finish() return if not isinstance(event, dict): request.write(self._deny_request( request, 400, log_category="AR454")) request.finish() return d = maybeDeferred(self._process, request, event) def finish(value): if isinstance(value, bytes): request.write(value) request.finish() d.addCallback(finish) def on_auth_error(err): # XXX: is it ideal to write to the request? request.write(self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return authmethod = None authid = None signature = None authorization_header = headers.getRawHeaders(b"authorization", []) if len(authorization_header) == 1: # HTTP Basic Authorization will be processed as ticket authentication authorization = authorization_header[0] auth_scheme, auth_details = authorization.split(b" ", 1) if auth_scheme.lower() == b"basic": try: credentials = binascii.a2b_base64(auth_details + b'===') credentials = credentials.split(b":", 1) if len(credentials) == 2: authmethod = "ticket" authid = credentials[0].decode("utf-8") signature = credentials[1].decode("utf-8") else: return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") except binascii.Error: # authentication failed return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") elif 'authmethod' in args and args['authmethod'].decode("utf-8") == 'ticket': if "ticket" not in args or "authid" not in args: # AR401 - fail if the ticket or authid are not in the args on_auth_ok(False) else: authmethod = "ticket" authid = args['authid'].decode("utf-8") signature = args['ticket'].decode("utf-8") if authmethod and authid and signature: hdetails = types.HelloDetails( authid=authid, authmethods=[authmethod] ) # wire up some variables for the authenticators to work, this is hackish # a custom header based authentication scheme can be implemented # without adding alternate authenticators by forwarding all headers. self._session._transport._transport_info = { "http_headers_received": { native_string(x).lower(): native_string(y[0]) for x, y in request.requestHeaders.getAllRawHeaders() } } self._session._pending_session_id = None self._session._router_factory = self._session._transport._routerFactory if authmethod == "ticket": self._pending_auth = PendingAuthTicket(self._session, self._auth_config['ticket']) self._pending_auth.hello(self._session._realm, hdetails) auth_d = maybeDeferred(self._pending_auth.authenticate, signature) auth_d.addCallbacks(on_auth_ok, on_auth_error) else: # don't return the value or it will be written to the request on_auth_ok(True) return server.NOT_DONE_YET
class _CommonResource(Resource): """ Shared components between PublisherResource and CallerResource. """ isLeaf = True decode_as_json = True def __init__(self, options, session, auth_config=None): """ Ctor. :param options: Options for path service from configuration. :type options: dict :param session: Instance of `ApplicationSession` to be used for forwarding events. :type session: obj """ Resource.__init__(self) self._options = options self._session = session self.log = make_logger() self._key = None if 'key' in options: self._key = options['key'].encode('utf8') self._secret = None if 'secret' in options: self._secret = options['secret'].encode('utf8') self._post_body_limit = int(options.get('post_body_limit', 0)) self._timestamp_delta_limit = int( options.get('timestamp_delta_limit', 300)) self._require_ip = None if 'require_ip' in options: self._require_ip = [ ip_network(net) for net in options['require_ip'] ] self._require_tls = options.get('require_tls', None) self._auth_config = auth_config or {} self._pending_auth = None def _deny_request(self, request, code, **kwargs): """ Called when client request is denied. """ if "log_category" not in kwargs.keys(): kwargs["log_category"] = "AR" + str(code) self.log.debug(code=code, **kwargs) error_str = log_categories[kwargs['log_category']].format(**kwargs) body = dump_json({ "error": error_str, "args": [], "kwargs": {} }, True).encode('utf8') request.setResponseCode(code) return body def _fail_request(self, request, **kwargs): """ Called when client request fails. """ res = {} err = kwargs["failure"] if isinstance(err.value, ApplicationError): res['error'] = err.value.error if err.value.args: res['args'] = err.value.args else: res['args'] = [] if err.value.kwargs: res['kwargs'] = err.value.kwargs else: res['kwargs'] = {} # This is a user-level error, not a CB error, so return 200 code = 200 else: # This is a "CB" error, so return 500 and a generic error res['error'] = u'wamp.error.runtime_error' res['args'] = ["Sorry, Crossbar.io has encountered a problem."] res['kwargs'] = {} # CB-level error, return 500 code = 500 self.log.failure(None, failure=err, log_category="AR500") body = json.dumps(res).encode('utf8') if "log_category" not in kwargs.keys(): kwargs["log_category"] = "AR" + str(code) self.log.debug(code=code, **kwargs) request.setResponseCode(code) request.write(body) request.finish() def _complete_request(self, request, code, body, **kwargs): """ Called when client request is complete. """ if "log_category" not in kwargs.keys(): kwargs["log_category"] = "AR" + str(code) self.log.debug(code=code, **kwargs) request.setResponseCode(code) request.write(body) def _set_common_headers(self, request): """ Set common HTTP response headers. """ origin = request.getHeader(b'origin') if origin is None or origin == b'null': origin = b'*' request.setHeader(b'access-control-allow-origin', origin) request.setHeader(b'access-control-allow-credentials', b'true') request.setHeader(b'cache-control', b'no-store,no-cache,must-revalidate,max-age=0') request.setHeader(b'content-type', b'application/json; charset=UTF-8') headers = request.getHeader(b'access-control-request-headers') if headers is not None: request.setHeader(b'access-control-allow-headers', headers) def render(self, request): """ Handle the request. All requests start here. """ self.log.debug(log_category="AR100", method=request.method, path=request.path) self._set_common_headers(request) try: if request.method not in (b"POST", b"PUT", b"OPTIONS"): return self._deny_request(request, 405, method=request.method, allowed="POST, PUT") else: if request.method == b"OPTIONS": # http://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.14.7 request.setHeader(b'allow', b'POST,PUT,OPTIONS') # https://www.w3.org/TR/cors/#access-control-allow-methods-response-header request.setHeader(b'access-control-allow-methods', b'POST,PUT,OPTIONS') request.setResponseCode(200) return b'' else: return self._render_request(request) except Exception as e: self.log.failure(log_category="CB501", exc=e) return self._deny_request(request, 500, log_category="CB500") def _render_request(self, request): """ Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller processor. """ # read HTTP/POST|PUT body body = request.content.read() args = {native_string(x): y[0] for x, y in request.args.items()} headers = request.requestHeaders # check content type + charset encoding # content_type_header = headers.getRawHeaders(b"content-type", []) if len(content_type_header) > 0: content_type_elements = [ x.strip().lower() for x in content_type_header[0].split(b";") ] else: content_type_elements = [] if self.decode_as_json: # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES # (but we allow missing content type .. will catch later during JSON # parsing anyway) if len(content_type_elements) > 0 and \ content_type_elements[0] not in _ALLOWED_CONTENT_TYPES: return self._deny_request( request, 400, accepted=list(_ALLOWED_CONTENT_TYPES), given=content_type_elements[0], log_category="AR452") encoding_parts = {} if len(content_type_elements) > 1: try: for item in content_type_elements: if b"=" not in item: # Don't bother looking at things "like application/json" continue # Parsing things like: # charset=utf-8 _ = native_string(item).split("=") assert len(_) == 2 # We don't want duplicates key = _[0].strip().lower() assert key not in encoding_parts encoding_parts[key] = _[1].strip().lower() except: return self._deny_request(request, 400, log_category="AR450") charset_encoding = encoding_parts.get("charset", "utf-8") if charset_encoding not in ["utf-8", 'utf8']: return self._deny_request(request, 400, log_category="AR450") # enforce "post_body_limit" # body_length = len(body) content_length_header = headers.getRawHeaders(b"content-length", []) if len(content_length_header) == 1: content_length = int(content_length_header[0]) elif len(content_length_header) > 1: return self._deny_request(request, 400, log_category="AR463") else: content_length = body_length if body_length != content_length: # Prevent the body length from being different to the given # Content-Length. This is so that clients can't lie and bypass # length restrictions by giving an incorrect header with a large # body. return self._deny_request(request, 400, bodylen=body_length, conlen=content_length, log_category="AR465") if self._post_body_limit and content_length > self._post_body_limit: return self._deny_request(request, 413, length=content_length, accepted=self._post_body_limit) # # parse/check HTTP/POST|PUT query parameters # # key # if 'key' in args: key_str = args["key"] else: if self._secret: return self._deny_request(request, 400, reason=u"'key' field missing", log_category="AR461") # timestamp # if 'timestamp' in args: timestamp_str = args["timestamp"] try: ts = datetime.datetime.strptime(native_string(timestamp_str), "%Y-%m-%dT%H:%M:%S.%fZ") delta = abs((ts - datetime.datetime.utcnow()).total_seconds()) if self._timestamp_delta_limit and delta > self._timestamp_delta_limit: return self._deny_request(request, 400, log_category="AR464") except ValueError: return self._deny_request( request, 400, reason= u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')" .format(native_string(timestamp_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason= u"signed request required, but mandatory 'timestamp' field missing", log_category="AR461") # seq # if 'seq' in args: seq_str = args["seq"] try: # FIXME: check sequence seq = int(seq_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid sequence number '{0}' (must be an integer)" .format(native_string(seq_str)), log_category="AR462") else: if self._secret: return self._deny_request(request, 400, reason=u"'seq' field missing", log_category="AR461") # nonce # if 'nonce' in args: nonce_str = args["nonce"] try: # FIXME: check nonce nonce = int(nonce_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid nonce '{0}' (must be an integer)".format( native_string(nonce_str)), log_category="AR462") else: if self._secret: return self._deny_request(request, 400, reason=u"'nonce' field missing", log_category="AR461") # signature # if 'signature' in args: signature_str = args["signature"] else: if self._secret: return self._deny_request(request, 400, reason=u"'signature' field missing", log_category="AR461") # do more checks if signed requests are required # if self._secret: if key_str != self._key: return self._deny_request( request, 401, reason=u"unknown key '{0}' in signed request".format( native_string(key_str)), log_category="AR460") # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature hm = hmac.new(self._secret, None, hashlib.sha256) hm.update(key_str) hm.update(timestamp_str) hm.update(seq_str) hm.update(nonce_str) hm.update(body) signature_recomputed = base64.urlsafe_b64encode(hm.digest()) if signature_str != signature_recomputed: return self._deny_request(request, 401, log_category="AR459") else: self.log.debug("REST request signature valid.", log_category="AR203") # user_agent = headers.get("user-agent", "unknown") client_ip = request.getClientIP() is_secure = request.isSecure() # enforce client IP address # if self._require_ip: ip = ip_address(client_ip) allowed = False for net in self._require_ip: if ip in net: allowed = True break if not allowed: return self._deny_request(request, 400, log_category="AR466") # enforce TLS # if self._require_tls: if not is_secure: return self._deny_request( request, 400, reason=u"request denied because not using TLS") # authenticate request # # TODO: also support HTTP Basic AUTH for ticket def on_auth_ok(value): if value is True: # treat like original behavior and just accept the request_id pass elif isinstance(value, types.Accept): self._session._authid = value.authid self._session._authrole = value.authrole # realm? else: # FIXME: not returning deny request... probably not ideal request.write( self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return _validator.reset() validation_result = _validator.validate(body) # validate() returns a 4-tuple, of which item 0 is whether it # is valid if not validation_result[0]: request.write( self._deny_request(request, 400, log_category="AR451")) request.finish() return event = body.decode('utf8') if self.decode_as_json: try: event = json.loads(event) except Exception as e: request.write( self._deny_request(request, 400, exc=e, log_category="AR453")) request.finish() return if not isinstance(event, dict): request.write( self._deny_request(request, 400, log_category="AR454")) request.finish() return d = maybeDeferred(self._process, request, event) def finish(value): if isinstance(value, bytes): request.write(value) request.finish() d.addCallback(finish) def on_auth_error(err): # XXX: is it ideal to write to the request? request.write( self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return authmethod = None authid = None signature = None authorization_header = headers.getRawHeaders(b"authorization", []) if len(authorization_header) == 1: # HTTP Basic Authorization will be processed as ticket authentication authorization = authorization_header[0] auth_scheme, auth_details = authorization.split(b" ", 1) if auth_scheme.lower() == b"basic": try: credentials = binascii.a2b_base64(auth_details + b'===') credentials = credentials.split(b":", 1) if len(credentials) == 2: authmethod = "ticket" authid = credentials[0].decode("utf-8") signature = credentials[1].decode("utf-8") else: return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") except binascii.Error: # authentication failed return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") elif 'authmethod' in args and args['authmethod'].decode( "utf-8") == 'ticket': if "ticket" not in args or "authid" not in args: # AR401 - fail if the ticket or authid are not in the args on_auth_ok(False) else: authmethod = "ticket" authid = args['authid'].decode("utf-8") signature = args['ticket'].decode("utf-8") if authmethod and authid and signature: hdetails = types.HelloDetails(authid=authid, authmethods=[authmethod]) # wire up some variables for the authenticators to work, this is hackish # a custom header based authentication scheme can be implemented # without adding alternate authenticators by forwarding all headers. self._session._transport._transport_info = { "http_headers_received": { native_string(x).lower(): native_string(y[0]) for x, y in request.requestHeaders.getAllRawHeaders() } } self._session._pending_session_id = None self._session._router_factory = self._session._transport._routerFactory if authmethod == "ticket": self._pending_auth = PendingAuthTicket( self._session, self._auth_config['ticket']) self._pending_auth.hello(self._session._realm, hdetails) auth_d = maybeDeferred(self._pending_auth.authenticate, signature) auth_d.addCallbacks(on_auth_ok, on_auth_error) else: # don't return the value or it will be written to the request on_auth_ok(True) return server.NOT_DONE_YET def _process(self, request, event): raise NotImplementedError()
def _render_request(self, request): """ Receives an HTTP/POST|PUT request, and then calls the Publisher/Caller processor. """ # read HTTP/POST|PUT body body = request.content.read() args = {native_string(x): y[0] for x, y in request.args.items()} headers = request.requestHeaders # check content type + charset encoding # content_type_header = headers.getRawHeaders(b"content-type", []) if len(content_type_header) > 0: content_type_elements = [ x.strip().lower() for x in content_type_header[0].split(b";") ] else: content_type_elements = [] if self.decode_as_json: # if the client sent a content type, it MUST be one of _ALLOWED_CONTENT_TYPES # (but we allow missing content type .. will catch later during JSON # parsing anyway) if len(content_type_elements) > 0 and \ content_type_elements[0] not in _ALLOWED_CONTENT_TYPES: return self._deny_request( request, 400, accepted=list(_ALLOWED_CONTENT_TYPES), given=content_type_elements[0], log_category="AR452") encoding_parts = {} if len(content_type_elements) > 1: try: for item in content_type_elements: if b"=" not in item: # Don't bother looking at things "like application/json" continue # Parsing things like: # charset=utf-8 _ = native_string(item).split("=") assert len(_) == 2 # We don't want duplicates key = _[0].strip().lower() assert key not in encoding_parts encoding_parts[key] = _[1].strip().lower() except: return self._deny_request(request, 400, log_category="AR450") charset_encoding = encoding_parts.get("charset", "utf-8") if charset_encoding not in ["utf-8", 'utf8']: return self._deny_request(request, 400, log_category="AR450") # enforce "post_body_limit" # body_length = len(body) content_length_header = headers.getRawHeaders(b"content-length", []) if len(content_length_header) == 1: content_length = int(content_length_header[0]) elif len(content_length_header) > 1: return self._deny_request(request, 400, log_category="AR463") else: content_length = body_length if body_length != content_length: # Prevent the body length from being different to the given # Content-Length. This is so that clients can't lie and bypass # length restrictions by giving an incorrect header with a large # body. return self._deny_request(request, 400, bodylen=body_length, conlen=content_length, log_category="AR465") if self._post_body_limit and content_length > self._post_body_limit: return self._deny_request(request, 413, length=content_length, accepted=self._post_body_limit) # # parse/check HTTP/POST|PUT query parameters # # key # if 'key' in args: key_str = args["key"] else: if self._secret: return self._deny_request(request, 400, reason=u"'key' field missing", log_category="AR461") # timestamp # if 'timestamp' in args: timestamp_str = args["timestamp"] try: ts = datetime.datetime.strptime(native_string(timestamp_str), "%Y-%m-%dT%H:%M:%S.%fZ") delta = abs((ts - datetime.datetime.utcnow()).total_seconds()) if self._timestamp_delta_limit and delta > self._timestamp_delta_limit: return self._deny_request(request, 400, log_category="AR464") except ValueError: return self._deny_request( request, 400, reason= u"invalid timestamp '{0}' (must be UTC/ISO-8601, e.g. '2011-10-14T16:59:51.123Z')" .format(native_string(timestamp_str)), log_category="AR462") else: if self._secret: return self._deny_request( request, 400, reason= u"signed request required, but mandatory 'timestamp' field missing", log_category="AR461") # seq # if 'seq' in args: seq_str = args["seq"] try: # FIXME: check sequence seq = int(seq_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid sequence number '{0}' (must be an integer)" .format(native_string(seq_str)), log_category="AR462") else: if self._secret: return self._deny_request(request, 400, reason=u"'seq' field missing", log_category="AR461") # nonce # if 'nonce' in args: nonce_str = args["nonce"] try: # FIXME: check nonce nonce = int(nonce_str) # noqa except: return self._deny_request( request, 400, reason=u"invalid nonce '{0}' (must be an integer)".format( native_string(nonce_str)), log_category="AR462") else: if self._secret: return self._deny_request(request, 400, reason=u"'nonce' field missing", log_category="AR461") # signature # if 'signature' in args: signature_str = args["signature"] else: if self._secret: return self._deny_request(request, 400, reason=u"'signature' field missing", log_category="AR461") # do more checks if signed requests are required # if self._secret: if key_str != self._key: return self._deny_request( request, 401, reason=u"unknown key '{0}' in signed request".format( native_string(key_str)), log_category="AR460") # Compute signature: HMAC[SHA256]_{secret} (key | timestamp | seq | nonce | body) => signature hm = hmac.new(self._secret, None, hashlib.sha256) hm.update(key_str) hm.update(timestamp_str) hm.update(seq_str) hm.update(nonce_str) hm.update(body) signature_recomputed = base64.urlsafe_b64encode(hm.digest()) if signature_str != signature_recomputed: return self._deny_request(request, 401, log_category="AR459") else: self.log.debug("REST request signature valid.", log_category="AR203") # user_agent = headers.get("user-agent", "unknown") client_ip = request.getClientIP() is_secure = request.isSecure() # enforce client IP address # if self._require_ip: ip = ip_address(client_ip) allowed = False for net in self._require_ip: if ip in net: allowed = True break if not allowed: return self._deny_request(request, 400, log_category="AR466") # enforce TLS # if self._require_tls: if not is_secure: return self._deny_request( request, 400, reason=u"request denied because not using TLS") # authenticate request # # TODO: also support HTTP Basic AUTH for ticket def on_auth_ok(value): if value is True: # treat like original behavior and just accept the request_id pass elif isinstance(value, types.Accept): self._session._authid = value.authid self._session._authrole = value.authrole # realm? else: # FIXME: not returning deny request... probably not ideal request.write( self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return _validator.reset() validation_result = _validator.validate(body) # validate() returns a 4-tuple, of which item 0 is whether it # is valid if not validation_result[0]: request.write( self._deny_request(request, 400, log_category="AR451")) request.finish() return event = body.decode('utf8') if self.decode_as_json: try: event = json.loads(event) except Exception as e: request.write( self._deny_request(request, 400, exc=e, log_category="AR453")) request.finish() return if not isinstance(event, dict): request.write( self._deny_request(request, 400, log_category="AR454")) request.finish() return d = maybeDeferred(self._process, request, event) def finish(value): if isinstance(value, bytes): request.write(value) request.finish() d.addCallback(finish) def on_auth_error(err): # XXX: is it ideal to write to the request? request.write( self._deny_request(request, 401, reason=u"not authorized", log_category="AR401")) request.finish() return authmethod = None authid = None signature = None authorization_header = headers.getRawHeaders(b"authorization", []) if len(authorization_header) == 1: # HTTP Basic Authorization will be processed as ticket authentication authorization = authorization_header[0] auth_scheme, auth_details = authorization.split(b" ", 1) if auth_scheme.lower() == b"basic": try: credentials = binascii.a2b_base64(auth_details + b'===') credentials = credentials.split(b":", 1) if len(credentials) == 2: authmethod = "ticket" authid = credentials[0].decode("utf-8") signature = credentials[1].decode("utf-8") else: return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") except binascii.Error: # authentication failed return self._deny_request(request, 401, reason=u"not authorized", log_category="AR401") elif 'authmethod' in args and args['authmethod'].decode( "utf-8") == 'ticket': if "ticket" not in args or "authid" not in args: # AR401 - fail if the ticket or authid are not in the args on_auth_ok(False) else: authmethod = "ticket" authid = args['authid'].decode("utf-8") signature = args['ticket'].decode("utf-8") if authmethod and authid and signature: hdetails = types.HelloDetails(authid=authid, authmethods=[authmethod]) # wire up some variables for the authenticators to work, this is hackish # a custom header based authentication scheme can be implemented # without adding alternate authenticators by forwarding all headers. self._session._transport._transport_info = { "http_headers_received": { native_string(x).lower(): native_string(y[0]) for x, y in request.requestHeaders.getAllRawHeaders() } } self._session._pending_session_id = None self._session._router_factory = self._session._transport._routerFactory if authmethod == "ticket": self._pending_auth = PendingAuthTicket( self._session, self._auth_config['ticket']) self._pending_auth.hello(self._session._realm, hdetails) auth_d = maybeDeferred(self._pending_auth.authenticate, signature) auth_d.addCallbacks(on_auth_ok, on_auth_error) else: # don't return the value or it will be written to the request on_auth_ok(True) return server.NOT_DONE_YET
def onHello(self, realm, details): try: ## check if the realm the session wants to join actually exists ## if realm not in self._router_factory: return types.Deny(ApplicationError.NO_SUCH_REALM, message = "no realm '{}' exists on this router".format(realm)) ## perform authentication ## if self._transport._authid is not None: ## already authenticated .. e.g. via cookie ## check if role still exists on realm ## allow = self._router_factory[realm].has_role(self._transport._authrole) if allow: return types.Accept(authid = self._transport._authid, authrole = self._transport._authrole, authmethod = self._transport._authmethod, authprovider = 'transport') else: return types.Deny(ApplicationError.NO_SUCH_ROLE, message = "session was previously authenticated (via transport), but role '{}' no longer exists on realm '{}'".format(self._transport._authrole, realm)) else: ## if authentication is enabled on the transport .. ## if "auth" in self._transport_config: ## iterate over authentication methods announced by client .. ## for authmethod in details.authmethods or ["anonymous"]: ## .. and if the configuration has an entry for the authmethod ## announced, process .. if authmethod in self._transport_config["auth"]: ## "WAMP-Challenge-Response" authentication ## if authmethod == u"wampcra": cfg = self._transport_config['auth']['wampcra'] if cfg['type'] == 'static': if details.authid in cfg.get('users', {}): user = cfg['users'][details.authid] self._pending_auth = PendingAuthWampCra(details.pending_session, details.authid, user['role'], u'static', user['secret'].encode('utf8')) ## send challenge to client ## extra = { u'challenge': self._pending_auth.challenge } ## when using salted passwords, provide the client with ## the salt and then PBKDF2 parameters used if 'salt' in user: extra[u'salt'] = user['salt'] extra[u'iterations'] = user.get('iterations', 1000) extra[u'keylen'] = user.get('keylen', 32) return types.Challenge(u'wampcra', extra) else: return types.Deny(message = "no user with authid '{}' in user database".format(details.authid)) elif cfg['type'] == 'dynamic': ## call the configured dynamic authenticator procedure ## via the router's service session ## service_session = self._router_factory.get(realm)._realm.session d = service_session.call(cfg['authenticator'], realm, details.authid) def on_authenticate_ok(user): ## construct a pending WAMP-CRA authentication ## self._pending_auth = PendingAuthWampCra(details.pending_session, details.authid, user['role'], u'dynamic', user['secret'].encode('utf8')) ## send challenge to client ## extra = { u'challenge': self._pending_auth.challenge } ## when using salted passwords, provide the client with ## the salt and the PBKDF2 parameters used ## if 'salt' in user: extra[u'salt'] = user['salt'] extra[u'iterations'] = user.get('iterations', 1000) extra[u'keylen'] = user.get('keylen', 32) return types.Challenge(u'wampcra', extra) def on_authenticate_error(err): error = None message = "dynamic WAMP-CRA credential getter failed: {}".format(err) if isinstance(err.value, ApplicationError): error = err.value.error if err.value.args and len(err.value.args): message = err.value.args[0] return types.Deny(error, message) d.addCallbacks(on_authenticate_ok, on_authenticate_error) return d else: return types.Deny(message = "illegal WAMP-CRA authentication config (type '{0}' is unknown)".format(cfg['type'])) ## WAMP-Ticket authentication ## elif authmethod == u"ticket": cfg = self._transport_config['auth']['ticket'] ## use static principal database from configuration ## if cfg['type'] == 'static': if details.authid in cfg.get('principals', {}): user = cfg['principals'][details.authid] self._pending_auth = PendingAuthTicket(realm, details.authid, user['role'], u'static', user['ticket'].encode('utf8')) return types.Challenge(u'ticket') else: return types.Deny(message = "no principal with authid '{}' in principal database".format(details.authid)) ## use configured procedure to dynamically get a ticket elif cfg['type'] == 'dynamic': self._pending_auth = PendingAuthTicket(realm, details.authid, None, cfg['authenticator'], None) return types.Challenge(u'ticket') else: return types.Deny(message = "illegal WAMP-Ticket authentication config (type '{0}' is unknown)".format(cfg['type'])) ## "Mozilla Persona" authentication ## elif authmethod == u"mozilla_persona": cfg = self._transport_config['auth']['mozilla_persona'] audience = cfg.get('audience', self._transport._origin) provider = cfg.get('provider', "https://verifier.login.persona.org/verify") ## authrole mapping ## authrole = cfg.get('role', 'anonymous') ## check if role exists on realm anyway ## if not self._router_factory[realm].has_role(authrole): return types.Deny(ApplicationError.NO_SUCH_ROLE, message = "authentication failed - realm '{}' has no role '{}'".format(realm, authrole)) ## ok, now challenge the client for doing Mozilla Persona auth. ## self._pending_auth = PendingAuthPersona(provider, audience, authrole) return types.Challenge("mozilla-persona") ## "Anonymous" authentication ## elif authmethod == u"anonymous": cfg = self._transport_config['auth']['anonymous'] ## authrole mapping ## authrole = cfg.get('role', 'anonymous') ## check if role exists on realm anyway ## if not self._router_factory[realm].has_role(authrole): return types.Deny(ApplicationError.NO_SUCH_ROLE, message = "authentication failed - realm '{}' has no role '{}'".format(realm, authrole)) ## authid generation ## if self._transport._cbtid: ## if cookie tracking is enabled, set authid to cookie value ## authid = self._transport._cbtid else: ## if no cookie tracking, generate a random value for authid ## authid = util.newid(24) self._transport._authid = authid self._transport._authrole = authrole self._transport._authmethod = authmethod return types.Accept(authid = authid, authrole = authrole, authmethod = self._transport._authmethod) ## "Cookie" authentication ## elif authmethod == u"cookie": pass # if self._transport._cbtid: # cookie = self._transport.factory._cookies[self._transport._cbtid] # authid = cookie['authid'] # authrole = cookie['authrole'] # authmethod = "cookie.{}".format(cookie['authmethod']) # return types.Accept(authid = authid, authrole = authrole, authmethod = authmethod) # else: # return types.Deny() else: log.msg("unknown authmethod '{}'".format(authmethod)) return types.Deny(message = "unknown authentication method {}".format(authmethod)) ## if authentication is configured, by default, deny. ## return types.Deny(message = "authentication using method '{}' denied by configuration".format(authmethod)) else: ## if authentication is _not_ configured, by default, allow anyone. ## ## authid generation ## if self._transport._cbtid: ## if cookie tracking is enabled, set authid to cookie value ## authid = self._transport._cbtid else: ## if no cookie tracking, generate a random value for authid ## authid = util.newid(24) return types.Accept(authid = authid, authrole = "anonymous", authmethod = "anonymous") except Exception as e: traceback.print_exc() return types.Deny(message = "internal error: {}".format(e))