Exemple #1
0
    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))
Exemple #2
0
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()
Exemple #3
0
    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
Exemple #4
0
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()
Exemple #5
0
    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
Exemple #6
0
   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))