"""Rest Protocol.""" import functools import pprint from urllib import parse from ddtrace import tracer from ddtrace.propagation import http as ddtrace_http from owsrequest import request as owsrequest from tornado import httpclient, httputil from tornado.escape import url_unescape from tornado.netutil import Resolver from grass import config from grass.logic.jwt_enabled_services import jwtEnabledServices ALLOWED_RESPONSE_HEADERS = ( 'Date', 'Cache-Control', 'Server', 'Content-Type', 'Location', 'Set-Cookie', ) def on_fetch(request_handler, response): """Handle the response. Args: handler (RequestHandler): the current request handler. response (Response): the tornado response object. """ if response.error: request_handler.logger.warning( f'Fetch on {response.effective_url}, resulted in an error: {str(response.error)}' # noqa: E501 ) request_handler.set_status(response.code) for header in ALLOWED_RESPONSE_HEADERS: header_values = response.headers.get_list(header) if header_values: for header_value in header_values: if header == 'Set-Cookie': request_handler.add_header(header, header_value) else: request_handler.set_header(header, header_value) if response.body: request_handler.write(response.body) request_handler.add_request_information() request_handler.finish() async def request( method, handler, service, path, params=None, body=None, headers=None, cookies=None, files=None, ): """Request handler. Args: method (str): name of the method. handler (RequestHandler): the current request handler. service (Service): the service object which allows the resolution of where the service is located. path (str): path of the service. params (dict): list of the params for the call. body (str): body of the request. headers (dict): additional headers to send (redirect) cookies (dict): additional cookies to add to the request. files (dict): file data objects (not available.) """ assert not files, 'Sending file throught Grass is not yet supported.' Resolver.configure( 'tornado.netutil.ThreadedResolver', num_threads=config.NUM_THREADS ) resolver = Resolver() httpclient.AsyncHTTPClient.configure( None, max_clients=config.MAX_CLIENTS, resolver=resolver ) uri = service.resolve(method, path) if params: uri = f'{uri}?{parse.urlencode(params, doseq=True)}' # Pass down the correlation id. correlation_id = f'{handler.get_correlation_id()}.{handler.get_unique_call_id()}' request = httpclient.HTTPRequest( uri, method=method, body=body, headers=headers or {}, follow_redirects=False, allow_nonstandard_methods=True, request_timeout=60, ) request.headers.update( {'Correlation-Id': correlation_id, 'Host': parse.urlparse(uri).netloc} ) jwtInstance = jwtEnabledServices.getInstance() jwtServicesCacheList = jwtInstance.getCache(config.JWT_SECRET_NAME) serviceName = handler.name if ( jwtServicesCacheList and ( (serviceName not in jwtServicesCacheList) or ( serviceName in jwtServicesCacheList and request.headers.get('Authorization') is None ) ) ) or config.environment == config.DEV_ENVIRONMENT: # remove any existing Authorization Headers # so owsrequest is not confused request.headers.pop('Authorization', None) if config.environment in (config.QA_ENVIRONMENT, config.PROD_ENVIRONMENT): request.headers.update( { 'Authorization': create_ows_authorization( handler, request, correlation_id ) } ) with tracer.trace('httpclient.AsyncHTTPClient') as span: propagator = ddtrace_http.HTTPPropagator() propagator.inject(span.context, request.headers) if isinstance(request.headers, httputil.HTTPHeaders): handler.logger.debug( f'Request Headers {pprint.pformat(request.headers.__dict__)}' ) result = await httpclient.AsyncHTTPClient().fetch(request, raise_error=False) on_fetch(handler, result) @tracer.wrap() def create_ows_authorization(handler, request, correlation_id): """Create an authorization. Args: handler (RequestHandler): the request handler. request (HTTPRequest): the http request. correlation_id (str): the correlation id. Return: str: the authorization code. """ # Unescape the path so hmac calculated in grass and other microservices # will be the same. # flask will automatically unescape the incoming request path and leave # query string as is. url_parts = parse.urlparse(request.url) request.path_url = url_unescape(url_parts.path) if url_parts.query: request.path_url = f'{request.path_url}?{url_parts.query}' return owsrequest.create_authorization( config.SERVICE_NAME, config.environment, handler.name, request, correlation_id ) delete = functools.partial(request, 'DELETE') get = functools.partial(request, 'GET') head = functools.partial(request, 'HEAD') post = functools.partial(request, 'POST') put = functools.partial(request, 'PUT') patch = functools.partial(request, 'PATCH')