Пример #1
0
    def dispatch(self, cid, req_timestamp, wsgi_environ, worker_store, _status_response=status_response,
        no_url_match=(None, False), _response_404=response_404, _has_debug=_has_debug,
        _http_soap_action='HTTP_SOAPACTION', _stringio=StringIO, _gzipfile=GzipFile):
        """ Base method for dispatching incoming HTTP/SOAP messages. If the security
        configuration is one of the technical account or HTTP basic auth,
        the security validation is being performed. Otherwise, that step
        is postponed until a concrete transport-specific handler is invoked.
        """
        # Needed in later steps
        path_info = wsgi_environ['PATH_INFO'].decode('utf-8')

        if _http_soap_action in wsgi_environ:
            soap_action = self._handle_quotes_soap_action(wsgi_environ[_http_soap_action])
        else:
            soap_action = ''

        # Can we recognize this combination of URL path and SOAP action at all?
        # This gives us the URL info and security data - but note that here
        # we still haven't validated credentials, only matched the URL.
        # Credentials are checked in a call to self.url_data.check_security
        url_match, channel_item = self.url_data.match(path_info, soap_action, bool(soap_action))

        if _has_debug and channel_item:
            logger.debug('url_match:`%r`, channel_item:`%r`', url_match, sorted(channel_item.items()))

        # This is needed in parallel.py's on_wsgi_request
        wsgi_environ['zato.channel_item'] = channel_item

        payload = wsgi_environ['wsgi.input'].read()

        # OK, we can possibly handle it
        if url_match not in no_url_match:

            try:

                # Raise 404 if the channel is inactive
                if not channel_item['is_active']:
                    logger.warn('url_data:`%s` is not active, raising NotFound', sorted(url_match.items()))
                    raise NotFound(cid, 'Channel inactive')

                expected_method = channel_item['method']
                if expected_method:
                    actual_method = wsgi_environ['REQUEST_METHOD']
                    if expected_method != actual_method:
                        logger.warn(
                            'Expected `%s` instead of `%s` for `%s`', expected_method, actual_method, channel_item['url_path'])
                        raise MethodNotAllowed(cid, 'Method `{}` is not allowed here'.format(actual_method))

                # Need to read security info here so we know if POST needs to be
                # parsed. If so, we do it here and reuse it in other places
                # so it doesn't have to be parsed two or more times.
                post_data = {}
                sec = self.url_data.url_sec[channel_item['match_target']]

                if sec.sec_def != ZATO_NONE or sec.sec_use_rbac is True:

                    if sec.sec_def != ZATO_NONE:

                        if sec.sec_def.sec_type == SEC_DEF_TYPE.OAUTH:
                            post_data.update(QueryDict(payload, encoding='utf-8'))

                        # Eagerly parse the request but only if we expect XPath-based credentials. The request will be re-used
                        # in later steps, it won't be parsed twice or more.
                        elif sec.sec_def.sec_type == SEC_DEF_TYPE.XPATH_SEC:
                            wsgi_environ['zato.request.payload'] = payload_from_request(
                                cid, payload, channel_item.data_format, channel_item.transport)

                    # Will raise an exception on any security violation
                    self.url_data.check_security(
                        sec, cid, channel_item, path_info, payload, wsgi_environ, post_data, worker_store)

                # This is handy if someone invoked URLData's OAuth API manually
                wsgi_environ['zato.oauth.post_data'] = post_data

                # OK, no security exception at that point means we can finally invoke the service.
                response = self.request_handler.handle(cid, url_match, channel_item, wsgi_environ,
                    payload, worker_store, self.simple_io_config, post_data, path_info, soap_action)

                wsgi_environ['zato.http.response.headers']['Content-Type'] = response.content_type
                wsgi_environ['zato.http.response.headers'].update(response.headers)
                wsgi_environ['zato.http.response.status'] = _status_response[response.status_code]

                if channel_item['content_encoding'] == 'gzip':

                    s = _stringio()
                    with _gzipfile(fileobj=s, mode='w') as f:
                        f.write(response.payload)
                    response.payload = s.getvalue()
                    s.close()

                    wsgi_environ['zato.http.response.headers']['Content-Encoding'] = 'gzip'

                # Finally return payload to the client
                return response.payload

            except Exception, e:
                _format_exc = format_exc(e)
                status = _status_internal_server_error

                if isinstance(e, ClientHTTPError):

                    response = e.msg
                    status_code = e.status

                    # TODO: Refactor this series of if/else's into a lookup dict.

                    if isinstance(e, Unauthorized):
                        status = _status_unauthorized
                        wsgi_environ['zato.http.response.headers']['WWW-Authenticate'] = e.challenge

                    elif isinstance(e, BadRequest):
                        status = _status_bad_request

                    elif isinstance(e, NotFound):
                        status = _status_not_found

                    elif isinstance(e, MethodNotAllowed):
                        status = _status_method_not_allowed

                    elif isinstance(e, Forbidden):
                        status = _status_forbidden

                    elif isinstance(e, TooManyRequests):
                        status = _status_too_many_requests

                else:
                    status_code = INTERNAL_SERVER_ERROR
                    response = _format_exc if self.return_tracebacks else self.default_error_message

                # TODO: This should be configurable. Some people may want such
                # things to be on DEBUG whereas for others ERROR will make most sense
                # in given circumstances.
                logger.error('Caught an exception, cid:`%s`, status_code:`%s`, _format_exc:`%s`', cid, status_code, _format_exc)

                try:
                    error_wrapper = get_client_error_wrapper(channel_item['transport'], channel_item['data_format'])
                except KeyError:
                    # It's OK. Apparently it's neither 'soap' nor json'
                    if logger.isEnabledFor(TRACE1):
                        msg = 'No client error wrapper for transport:`{}`, data_format:`{}`'.format(
                            channel_item.get('transport'), channel_item.get('data_format'))
                        logger.log(TRACE1, msg)
                else:
                    response = error_wrapper(cid, response)

                wsgi_environ['zato.http.response.status'] = status

                return response
Пример #2
0
    def dispatch(self, cid, req_timestamp, wsgi_environ, worker_store):
        """ Base method for dispatching incoming HTTP/SOAP messages. If the security
        configuration is one of the technical account or HTTP basic auth,
        the security validation is being performed. Otherwise, that step
        is postponed until a concrete transport-specific handler is invoked.
        """

        # Needed in later steps
        path_info = wsgi_environ['PATH_INFO']
        soap_action = wsgi_environ.get('HTTP_SOAPACTION', '')

        # Fix up SOAP action - turns "my:soap:action" into my:soap:action,
        # that is, strips it out of surrounding quotes, if any.
        if soap_action:
            soap_action = self._handle_quotes_soap_action(soap_action)

        # Can we recognize this combination of URL path and SOAP action at all?
        # This gives us the URL info and security data - but note that here
        # we still haven't validated credentials, only matched the URL.
        # Credentials are checked in a call to self.url_data.check_security
        url_match, channel_item = self.url_data.match(path_info, soap_action)

        if channel_item:
            logger.debug('url_match:`%r`, channel_item:`%r`', url_match,
                         sorted(channel_item.items()))

        # This is needed in parallel.py's on_wsgi_request
        wsgi_environ['zato.http.channel_item'] = channel_item

        payload = wsgi_environ['wsgi.input'].read()

        # OK, we can possibly handle it
        if url_match not in (None, False):

            # This is a synchronous call so that whatever happens next we are always
            # able to have at least initial audit log of requests.
            if channel_item['audit_enabled']:
                self.url_data.audit_set_request(cid, channel_item, payload,
                                                wsgi_environ)

            try:

                # Raise 404 if the channel is inactive
                if not channel_item['is_active']:
                    logger.warn(
                        'url_data:`%s` is not active, raising NotFound',
                        sorted(url_match.items()))
                    raise NotFound(cid, 'Channel inactive')

                expected_method = channel_item['method']
                if expected_method:
                    actual_method = wsgi_environ['REQUEST_METHOD']
                    if expected_method != actual_method:
                        logger.warn('Expected `%s` instead of `%s` for `%s`',
                                    expected_method, actual_method,
                                    channel_item['url_path'])
                        raise MethodNotAllowed(
                            cid, 'Method `{}` is not allowed here'.format(
                                actual_method))

                # Need to read security info here so we know if POST needs to be
                # parsed. If so, we do it here and reuse it in other places
                # so it doesn't have to be parsed two or more times.
                sec = self.url_data.url_sec[channel_item['match_target']]
                if sec.sec_def != ZATO_NONE and sec.sec_def.sec_type == SEC_DEF_TYPE.OAUTH:
                    post_data = QueryDict(payload, encoding='utf-8')
                else:
                    post_data = {}

                # This is handy if someone invoked URLData's OAuth API manually
                wsgi_environ['zato.oauth.post_data'] = post_data

                # Eagerly parse the request but only if we expect XPath-based credentials. The request will be re-used
                # in later steps, it won't be parsed twice or more.
                if sec.sec_def != ZATO_NONE and sec.sec_def.sec_type == SEC_DEF_TYPE.XPATH_SEC:
                    wsgi_environ[
                        'zato.request.payload'] = payload_from_request(
                            cid, payload, channel_item.data_format,
                            channel_item.transport)

                # Will raise an exception on any security violation
                self.url_data.check_security(sec, cid, channel_item, path_info,
                                             payload, wsgi_environ, post_data,
                                             worker_store)

                # OK, no security exception at that point means we can finally
                # invoke the service.
                response = self.request_handler.handle(
                    cid, url_match, channel_item, wsgi_environ, payload,
                    worker_store, self.simple_io_config, post_data)

                # Got response from the service so we can construct response headers now
                self.add_response_headers(wsgi_environ, response)

                # Return the payload to the client
                return response.payload

            except Exception, e:
                _format_exc = format_exc(e)
                status = _status_internal_server_error

                if isinstance(e, ClientHTTPError):

                    response = e.msg
                    status_code = e.status

                    # TODO: Refactor this series of if/else's into a lookup dict.

                    if isinstance(e, Unauthorized):
                        status = _status_unauthorized
                        wsgi_environ['zato.http.response.headers'][
                            'WWW-Authenticate'] = e.challenge

                    elif isinstance(e, BadRequest):
                        status = _status_bad_request

                    elif isinstance(e, NotFound):
                        status = _status_not_found

                    elif isinstance(e, MethodNotAllowed):
                        status = _status_method_not_allowed

                    elif isinstance(e, Forbidden):
                        status = _status_forbidden

                    elif isinstance(e, TooManyRequests):
                        status = _status_too_many_requests

                else:
                    status_code = INTERNAL_SERVER_ERROR
                    response = _format_exc

                # TODO: This should be configurable. Some people may want such
                # things to be on DEBUG whereas for others ERROR will make most sense
                # in given circumstances.
                logger.error(
                    'Caught an exception, cid:`%s`, status_code:`%s`, _format_exc:`%s`',
                    cid, status_code, _format_exc)

                try:
                    error_wrapper = get_client_error_wrapper(
                        channel_item['transport'], channel_item['data_format'])
                except KeyError:
                    # It's OK. Apparently it's neither 'soap' nor json'
                    if logger.isEnabledFor(TRACE1):
                        msg = 'No client error wrapper for transport:`{}`, data_format:`{}`'.format(
                            channel_item.get('transport'),
                            channel_item.get('data_format'))
                        logger.log(TRACE1, msg)
                else:
                    response = error_wrapper(cid, response)

                wsgi_environ['zato.http.response.status'] = status

                return response