Beispiel #1
0
    def handle(self, _channel=CHANNEL.IBM_MQ, ts_format='YYYYMMDDHHmmssSS'):
        request = loads(self.request.raw_request)
        msg = request['msg']
        service_name = request['service_name']

        # Make MQ-level attributes easier to handle
        correlation_id = unhexlify(
            msg['correlation_id']) if msg['correlation_id'] else None
        expiration = datetime_from_ms(
            msg['expiration']) if msg['expiration'] else None

        timestamp = '{}{}'.format(msg['put_date'], msg['put_time'])
        timestamp = arrow_get(timestamp,
                              ts_format).replace(tzinfo='UTC').datetime

        # Extract MQMD
        mqmd = msg['mqmd']
        mqmd = b64decode(mqmd)
        mqmd = pickle_loads(mqmd)

        # Find the message's CCSID
        request_ccsid = mqmd.CodedCharSetId

        # Try to find an encoding matching the CCSID,
        # if not found, use the default one.
        try:
            encoding = CCSIDConfig.encoding_map[request_ccsid]
        except KeyError:
            encoding = CCSIDConfig.default_encoding

        # Encode the input Unicode data into bytes
        msg['text'] = msg['text'].encode(encoding)

        # Extract the business payload
        data = payload_from_request(self.server.json_parser, self.cid,
                                    msg['text'], request['data_format'], None)

        # Invoke the target service
        self.invoke(service_name,
                    data,
                    _channel,
                    wmq_ctx={
                        'msg_id': unhexlify(msg['msg_id']),
                        'correlation_id': correlation_id,
                        'timestamp': timestamp,
                        'put_time': msg['put_time'],
                        'put_date': msg['put_date'],
                        'expiration': expiration,
                        'reply_to': msg['reply_to'],
                        'data': data,
                        'mqmd': mqmd
                    })
Beispiel #2
0
    def handle(self,
               _channel=CHANNEL.WEBSPHERE_MQ,
               ts_format='YYYYMMDDHHmmssSS'):
        request = loads(self.request.raw_request)
        msg = request['msg']
        service_name = request['service_name']

        # Make MQ-level attributes easier to handle
        correlation_id = unhexlify(
            msg['correlation_id']) if msg['correlation_id'] else None
        expiration = datetime_from_ms(
            msg['expiration']) if msg['expiration'] else None

        timestamp = '{}{}'.format(msg['put_date'], msg['put_time'])
        timestamp = arrow_get(timestamp,
                              ts_format).replace(tzinfo='UTC').datetime

        data = payload_from_request(self.cid, msg['text'],
                                    request['data_format'], None)

        msg['mqmd'] = b64decode(msg['mqmd'])

        self.invoke(service_name,
                    data,
                    _channel,
                    wmq_ctx={
                        'msg_id': unhexlify(msg['msg_id']),
                        'correlation_id': correlation_id,
                        'timestamp': timestamp,
                        'put_time': msg['put_time'],
                        'put_date': msg['put_date'],
                        'expiration': expiration,
                        'reply_to': msg['reply_to'],
                        'data': data,
                        'mqmd': pickle_loads(msg['mqmd'])
                    })
Beispiel #3
0
    def handle(self):
        payload = self.request.input.get('payload')
        if payload:
            payload = b64decode(payload)
            payload = payload_from_request(self.cid, payload,
                                           self.request.input.data_format,
                                           self.request.input.transport)

        id = self.request.input.get('id')
        name = self.request.input.get('name')
        pid = self.request.input.get('pid') or 0
        all_pids = self.request.input.get('all_pids')
        timeout = self.request.input.get('timeout') or None

        channel = self.request.input.get('channel')
        data_format = self.request.input.get('data_format')
        transport = self.request.input.get('transport')
        expiration = self.request.input.get(
            'expiration') or BROKER.DEFAULT_EXPIRATION

        if name and id:
            raise ZatoException(
                'Cannot accept both id:`{}` and name:`{}`'.format(id, name))

        if self.request.input.get('is_async'):

            if id:
                impl_name = self.server.service_store.id_to_impl_name[id]
                name = self.server.service_store.service_data(
                    impl_name)['name']

            # If PID is given on input it means we must invoke this particular server process by it ID
            if pid and pid != self.server.pid:
                response = self.server.invoke_by_pid(name, payload, pid)
            else:
                response = self.invoke_async(name, payload, channel,
                                             data_format, transport,
                                             expiration)

        else:
            # This branch the same as above in is_async branch, except in is_async there was no all_pids

            # It is possible that we were given the all_pids flag on input but we know
            # ourselves that there is only one process, the current one, so we can just
            # invoke it directly instead of going through IPC.
            if all_pids and self.server.fs_server_config.main.gunicorn_workers > 1:
                use_all_pids = True
            else:
                use_all_pids = False

            if use_all_pids:
                args = (name, payload, timeout) if timeout else (name, payload)
                response = dumps(self.server.invoke_all_pids(*args))
            else:
                if pid and pid != self.server.pid:
                    response = self.server.invoke(name,
                                                  payload,
                                                  pid=pid,
                                                  data_format=data_format)
                else:
                    func, id_ = (self.invoke,
                                 name) if name else (self.invoke_by_id, id)
                    response = func(id_,
                                    payload,
                                    channel,
                                    data_format,
                                    transport,
                                    serialize=True)

        if isinstance(response, basestring):
            if response:
                response = response if isinstance(
                    response, bytes) else response.encode('utf8')
                self.response.payload.response = b64encode(response).decode(
                    'utf8') if response else ''
Beispiel #4
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,
                 _accept_any_http=accept_any_http,
                 _accept_any_internal=accept_any_internal,
                 _rate_limit_type_http=RATE_LIMIT.OBJECT_TYPE.HTTP_SOAP,
                 _rate_limit_type_sso_user=RATE_LIMIT.OBJECT_TYPE.SSO_USER,
                 _stack_format=stack_format,
                 _exc_sep='*' * 80,
                 _jwt=_jwt,
                 _sso_ext_auth=_sso_ext_auth):

        # Needed as one of the first steps
        http_method = wsgi_environ['REQUEST_METHOD']
        http_method = http_method if isinstance(
            http_method, unicode) else http_method.decode('utf8')

        http_accept = wsgi_environ.get('HTTP_ACCEPT') or _accept_any_http
        http_accept = http_accept.replace('*', _accept_any_internal).replace(
            '/', 'HTTP_SEP')
        http_accept = http_accept if isinstance(
            http_accept, unicode) else http_accept.decode('utf8')

        # Needed in later steps
        path_info = wsgi_environ['PATH_INFO'] if PY3 else wsgi_environ[
            'PATH_INFO'].decode('utf8')

        # Immediately reject the request if it is not a support HTTP method, no matter what channel
        # it would have otherwise matched.
        if http_method not in self.http_methods_allowed:
            wsgi_environ[
                'zato.http.response.status'] = _status_method_not_allowed
            return client_json_error(cid, 'Unsupported HTTP method')

        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,
                                                      http_method, http_accept,
                                                      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

        # Read the raw data
        payload = wsgi_environ['wsgi.input'].read()

        # Store for later use prior to any kind of parsing
        wsgi_environ['zato.http.raw_request'] = payload

        # 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')

                # We 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 = {}

                match_target = channel_item['match_target']
                sec = self.url_data.url_sec[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
                    auth_result = self.url_data.check_security(
                        sec, cid, channel_item, path_info, payload,
                        wsgi_environ, post_data, worker_store)

                # Check rate limiting now - this could not have been done earlier because we wanted
                # for security checks to be made first. Otherwise, someone would be able to invoke
                # our endpoint without credentials as many times as it is needed to exhaust the rate limit
                # denying in this manner access to genuine users.
                if channel_item.get('is_rate_limit_active'):
                    self.server.rate_limiting.check_limit(
                        cid, _rate_limit_type_http, channel_item['name'],
                        wsgi_environ['zato.http.remote_addr'])

                # Security definition-based checks went fine but it is still possible
                # that this sec_def is linked to an SSO user whose rate limits we need to check.

                # Check SSO-related limits only if SSO is enabled
                if self._sso_api_user:

                    # Not all sec_def types may have associated SSO users
                    if sec.sec_def != ZATO_NONE:

                        if sec.sec_def.sec_type in _sso_ext_auth:

                            # JWT comes with external sessions whereas Basic Auth does not
                            if sec.sec_def.sec_type == _jwt:
                                ext_session_id = auth_result.raw_token
                            else:
                                ext_session_id = None

                            # Try to log in the user to SSO by that account's external credentials.
                            self.server.sso_tool.on_external_auth(
                                sec.sec_def.sec_type, sec.sec_def.id,
                                sec.sec_def.username, cid, wsgi_environ,
                                ext_session_id)
                        else:
                            raise Exception('Unexpected sec_type `{}`'.format(
                                sec.sec_def.sec_type))

                # 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 as e:
                _format_exc = format_exc()
                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:

                    # JSON Schema validation
                    if isinstance(e, JSONSchemaValidationException):
                        status_code = _status_bad_request
                        needs_prefix = False if e.needs_err_details else True
                        response = JSONSchemaDictError(
                            cid,
                            e.needs_err_details,
                            e.error_msg,
                            needs_prefix=needs_prefix).serialize()

                    # Rate limiting and whitelisting
                    elif isinstance(e, RateLimitingException):
                        response, status_code, status = self._on_rate_limiting_exception(
                            cid, e, channel_item)

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

                _exc = _stack_format(
                    e,
                    style='color',
                    show_vals='like_source',
                    truncate_vals=5000,
                    add_summary=True,
                    source_lines=20) if _stack_format else _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`, `%s`',
                    cid, status_code, _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

        # This is 404, no such URL path and SOAP action is not known either.
        else:
            response = _response_404.format(path_info,
                                            wsgi_environ.get('REQUEST_METHOD'),
                                            wsgi_environ.get('HTTP_ACCEPT'),
                                            cid)
            wsgi_environ['zato.http.response.status'] = _status_not_found

            logger.error(response)
            return response