Index: third_party/google-endpoints/endpoints/endpoints_dispatcher.py |
diff --git a/third_party/google-endpoints/endpoints/endpoints_dispatcher.py b/third_party/google-endpoints/endpoints/endpoints_dispatcher.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c9433e94e719f70e4bc54b50688f8ff02f7ead87 |
--- /dev/null |
+++ b/third_party/google-endpoints/endpoints/endpoints_dispatcher.py |
@@ -0,0 +1,793 @@ |
+# Copyright 2016 Google Inc. All Rights Reserved. |
+# |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
+# you may not use this file except in compliance with the License. |
+# You may obtain a copy of the License at |
+# |
+# http://www.apache.org/licenses/LICENSE-2.0 |
+# |
+# Unless required by applicable law or agreed to in writing, software |
+# distributed under the License is distributed on an "AS IS" BASIS, |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+# See the License for the specific language governing permissions and |
+# limitations under the License. |
+ |
+"""Dispatcher middleware for Cloud Endpoints API server. |
+ |
+This middleware does simple transforms on requests that come into the base path |
+and then re-dispatches them to the main backend. It does not do any |
+authentication, quota checking, DoS checking, etc. |
+ |
+In addition, the middleware loads API configs prior to each call, in case the |
+configuration has changed. |
+""" |
+ |
+# pylint: disable=g-bad-name |
+import cStringIO |
+import httplib |
+import json |
+import logging |
+import re |
+import urlparse |
+import wsgiref |
+ |
+import api_config_manager |
+import api_request |
+import discovery_api_proxy |
+import discovery_service |
+import errors |
+import parameter_converter |
+import util |
+ |
+ |
+__all__ = ['EndpointsDispatcherMiddleware'] |
+ |
+_SERVER_SOURCE_IP = '0.2.0.3' |
+ |
+# Internal constants |
+_CORS_HEADER_ORIGIN = 'Origin' |
+_CORS_HEADER_REQUEST_METHOD = 'Access-Control-Request-Method' |
+_CORS_HEADER_REQUEST_HEADERS = 'Access-Control-Request-Headers' |
+_CORS_HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' |
+_CORS_HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods' |
+_CORS_HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers' |
+_CORS_HEADER_ALLOW_CREDS = 'Access-Control-Allow-Credentials' |
+_CORS_HEADER_EXPOSE_HEADERS = 'Access-Control-Expose-Headers' |
+_CORS_ALLOWED_METHODS = frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT')) |
+_CORS_EXPOSED_HEADERS = frozenset( |
+ ('Content-Encoding', 'Content-Length', 'Date', 'ETag', 'Server') |
+) |
+ |
+ |
+class EndpointsDispatcherMiddleware(object): |
+ """Dispatcher that handles requests to the built-in apiserver handlers.""" |
+ |
+ _API_EXPLORER_URL = 'https://apis-explorer.appspot.com/apis-explorer/?base=' |
+ |
+ def __init__(self, backend_wsgi_app, config_manager=None): |
+ """Constructor for EndpointsDispatcherMiddleware. |
+ |
+ Args: |
+ backend_wsgi_app: A WSGI server that serves the app's endpoints. |
+ config_manager: An ApiConfigManager instance that allows a caller to |
+ set up an existing configuration for testing. |
+ """ |
+ if config_manager is None: |
+ config_manager = api_config_manager.ApiConfigManager() |
+ self.config_manager = config_manager |
+ |
+ self._backend = backend_wsgi_app |
+ self._dispatchers = [] |
+ for base_path in self._backend.base_paths: |
+ self._add_dispatcher('%sexplorer/?$' % base_path, |
+ self.handle_api_explorer_request) |
+ self._add_dispatcher('%sstatic/.*$' % base_path, |
+ self.handle_api_static_request) |
+ |
+ def _add_dispatcher(self, path_regex, dispatch_function): |
+ """Add a request path and dispatch handler. |
+ |
+ Args: |
+ path_regex: A string regex, the path to match against incoming requests. |
+ dispatch_function: The function to call for these requests. The function |
+ should take (request, start_response) as arguments and |
+ return the contents of the response body. |
+ """ |
+ self._dispatchers.append((re.compile(path_regex), dispatch_function)) |
+ |
+ def __call__(self, environ, start_response): |
+ """Handle an incoming request. |
+ |
+ Args: |
+ environ: An environ dict for the request as defined in PEP-333. |
+ start_response: A function used to begin the response to the caller. |
+ This follows the semantics defined in PEP-333. In particular, it's |
+ called with (status, response_headers, exc_info=None), and it returns |
+ an object with a write(body_data) function that can be used to write |
+ the body of the response. |
+ |
+ Yields: |
+ An iterable over strings containing the body of the HTTP response. |
+ """ |
+ request = api_request.ApiRequest(environ, |
+ base_paths=self._backend.base_paths) |
+ |
+ # PEP-333 requires that we return an iterator that iterates over the |
+ # response body. Yielding the returned body accomplishes this. |
+ yield self.dispatch(request, start_response) |
+ |
+ def dispatch(self, request, start_response): |
+ """Handles dispatch to apiserver handlers. |
+ |
+ This typically ends up calling start_response and returning the entire |
+ body of the response. |
+ |
+ Args: |
+ request: An ApiRequest, the request from the user. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string, the body of the response. |
+ """ |
+ # Check if this matches any of our special handlers. |
+ dispatched_response = self.dispatch_non_api_requests(request, |
+ start_response) |
+ if dispatched_response is not None: |
+ return dispatched_response |
+ |
+ # Get API configuration first. We need this so we know how to |
+ # call the back end. |
+ api_config_response = self.get_api_configs() |
+ if api_config_response: |
+ self.config_manager.process_api_config_response(api_config_response) |
+ else: |
+ return self.fail_request(request, 'get_api_configs Error', |
+ start_response) |
+ |
+ # Call the service. |
+ try: |
+ return self.call_backend(request, start_response) |
+ except errors.RequestError as error: |
+ return self._handle_request_error(request, error, start_response) |
+ |
+ def dispatch_non_api_requests(self, request, start_response): |
+ """Dispatch this request if this is a request to a reserved URL. |
+ |
+ If the request matches one of our reserved URLs, this calls |
+ start_response and returns the response body. This also handles OPTIONS |
+ CORS requests. |
+ |
+ Args: |
+ request: An ApiRequest, the request from the user. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ None if the request doesn't match one of the reserved URLs this |
+ handles. Otherwise, returns the response body. |
+ """ |
+ for path_regex, dispatch_function in self._dispatchers: |
+ if path_regex.match(request.relative_url): |
+ return dispatch_function(request, start_response) |
+ |
+ if request.http_method == 'OPTIONS': |
+ cors_handler = self._create_cors_handler(request) |
+ if cors_handler.allow_cors_request: |
+ # The server returns 200 rather than 204, for some reason. |
+ return util.send_wsgi_response('200', [], '', start_response, |
+ cors_handler) |
+ |
+ return None |
+ |
+ def handle_api_explorer_request(self, request, start_response): |
+ """Handler for requests to {base_path}/explorer. |
+ |
+ This calls start_response and returns the response body. |
+ |
+ Args: |
+ request: An ApiRequest, the request from the user. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string containing the response body (which is empty, in this case). |
+ """ |
+ protocol = 'http' if 'localhost' in request.server else 'https' |
+ base_path = request.base_path.strip('/') |
+ base_url = '{0}://{1}:{2}/{3}'.format( |
+ protocol, request.server, request.port, base_path) |
+ redirect_url = self._API_EXPLORER_URL + base_url |
+ return util.send_wsgi_redirect_response(redirect_url, start_response) |
+ |
+ def handle_api_static_request(self, request, start_response): |
+ """Handler for requests to {base_path}/static/.*. |
+ |
+ This calls start_response and returns the response body. |
+ |
+ Args: |
+ request: An ApiRequest, the request from the user. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string containing the response body. |
+ """ |
+ discovery_api = discovery_api_proxy.DiscoveryApiProxy() |
+ response, body = discovery_api.get_static_file(request.relative_url) |
+ status_string = '%d %s' % (response.status, response.reason) |
+ if response.status == 200: |
+ # Some of the headers that come back from the server can't be passed |
+ # along in our response. Specifically, the response from the server has |
+ # transfer-encoding: chunked, which doesn't apply to the response that |
+ # we're forwarding. There may be other problematic headers, so we strip |
+ # off everything but Content-Type. |
+ return util.send_wsgi_response(status_string, |
+ [('Content-Type', |
+ response.getheader('Content-Type'))], |
+ body, start_response) |
+ else: |
+ logging.error('Discovery API proxy failed on %s with %d. Details: %s', |
+ request.relative_url, response.status, body) |
+ return util.send_wsgi_response(status_string, response.getheaders(), body, |
+ start_response) |
+ |
+ def get_api_configs(self): |
+ return self._backend.get_api_configs() |
+ |
+ @staticmethod |
+ def verify_response(response, status_code, content_type=None): |
+ """Verifies that a response has the expected status and content type. |
+ |
+ Args: |
+ response: The ResponseTuple to be checked. |
+ status_code: An int, the HTTP status code to be compared with response |
+ status. |
+ content_type: A string with the acceptable Content-Type header value. |
+ None allows any content type. |
+ |
+ Returns: |
+ True if both status_code and content_type match, else False. |
+ """ |
+ status = int(response.status.split(' ', 1)[0]) |
+ if status != status_code: |
+ return False |
+ |
+ if content_type is None: |
+ return True |
+ |
+ for header, value in response.headers: |
+ if header.lower() == 'content-type': |
+ return value == content_type |
+ |
+ # If we fall through to here, the verification has failed, so return False. |
+ return False |
+ |
+ def prepare_backend_environ(self, host, method, relative_url, headers, body, |
+ source_ip, port): |
+ """Build an environ object for the backend to consume. |
+ |
+ Args: |
+ host: A string containing the host serving the request. |
+ method: A string containing the HTTP method of the request. |
+ relative_url: A string containing path and query string of the request. |
+ headers: A list of (key, value) tuples where key and value are both |
+ strings. |
+ body: A string containing the request body. |
+ source_ip: The source IP address for the request. |
+ port: The port to which to direct the request. |
+ |
+ Returns: |
+ An environ object with all the information necessary for the backend to |
+ process the request. |
+ """ |
+ if isinstance(body, unicode): |
+ body = body.encode('ascii') |
+ |
+ url = urlparse.urlsplit(relative_url) |
+ if port != 80: |
+ host = '%s:%s' % (host, port) |
+ else: |
+ host = host |
+ environ = {'CONTENT_LENGTH': str(len(body)), |
+ 'PATH_INFO': url.path, |
+ 'QUERY_STRING': url.query, |
+ 'REQUEST_METHOD': method, |
+ 'REMOTE_ADDR': source_ip, |
+ 'SERVER_NAME': host, |
+ 'SERVER_PORT': str(port), |
+ 'SERVER_PROTOCOL': 'HTTP/1.1', |
+ 'wsgi.version': (1, 0), |
+ 'wsgi.url_scheme': 'http', |
+ 'wsgi.errors': cStringIO.StringIO(), |
+ 'wsgi.multithread': True, |
+ 'wsgi.multiprocess': True, |
+ 'wsgi.input': cStringIO.StringIO(body)} |
+ util.put_headers_in_environ(headers, environ) |
+ environ['HTTP_HOST'] = host |
+ return environ |
+ |
+ def call_backend(self, orig_request, start_response): |
+ """Generate API call (from earlier-saved request). |
+ |
+ This calls start_response and returns the response body. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string containing the response body. |
+ """ |
+ if orig_request.is_rpc(): |
+ method_config = self.lookup_rpc_method(orig_request) |
+ params = None |
+ else: |
+ method_config, params = self.lookup_rest_method(orig_request) |
+ if not method_config: |
+ cors_handler = self._create_cors_handler(orig_request) |
+ return util.send_wsgi_not_found_response(start_response, |
+ cors_handler=cors_handler) |
+ |
+ # Prepare the request for the back end. |
+ transformed_request = self.transform_request( |
+ orig_request, params, method_config) |
+ |
+ # Check if this call is for the Discovery service. If so, route |
+ # it to our Discovery handler. |
+ discovery = discovery_service.DiscoveryService( |
+ self.config_manager, self._backend) |
+ discovery_response = discovery.handle_discovery_request( |
+ transformed_request.path, transformed_request, start_response) |
+ if discovery_response: |
+ return discovery_response |
+ |
+ url = transformed_request.base_path + transformed_request.path |
+ transformed_request.headers['Content-Type'] = 'application/json' |
+ transformed_environ = self.prepare_backend_environ( |
+ orig_request.server, 'POST', url, transformed_request.headers.items(), |
+ transformed_request.body, transformed_request.source_ip, |
+ orig_request.port) |
+ |
+ # Send the transformed request to the backend app and capture the response. |
+ with util.StartResponseProxy() as start_response_proxy: |
+ body_iter = self._backend(transformed_environ, start_response_proxy.Proxy) |
+ status = start_response_proxy.response_status |
+ headers = start_response_proxy.response_headers |
+ |
+ # Get response body |
+ body = start_response_proxy.response_body |
+ # In case standard WSGI behavior is implemented later... |
+ if not body: |
+ body = ''.join(body_iter) |
+ |
+ return self.handle_backend_response(orig_request, transformed_request, |
+ status, headers, body, method_config, |
+ start_response) |
+ |
+ class __CheckCorsHeaders(object): |
+ """Track information about CORS headers and our response to them.""" |
+ |
+ def __init__(self, request): |
+ self.allow_cors_request = False |
+ self.origin = None |
+ self.cors_request_method = None |
+ self.cors_request_headers = None |
+ |
+ self.__check_cors_request(request) |
+ |
+ def __check_cors_request(self, request): |
+ """Check for a CORS request, and see if it gets a CORS response.""" |
+ # Check for incoming CORS headers. |
+ self.origin = request.headers[_CORS_HEADER_ORIGIN] |
+ self.cors_request_method = request.headers[_CORS_HEADER_REQUEST_METHOD] |
+ self.cors_request_headers = request.headers[ |
+ _CORS_HEADER_REQUEST_HEADERS] |
+ |
+ # Check if the request should get a CORS response. |
+ if (self.origin and |
+ ((self.cors_request_method is None) or |
+ (self.cors_request_method.upper() in _CORS_ALLOWED_METHODS))): |
+ self.allow_cors_request = True |
+ |
+ def update_headers(self, headers_in): |
+ """Add CORS headers to the response, if needed.""" |
+ if not self.allow_cors_request: |
+ return |
+ |
+ # Add CORS headers. |
+ headers = wsgiref.headers.Headers(headers_in) |
+ headers[_CORS_HEADER_ALLOW_CREDS] = 'true' |
+ headers[_CORS_HEADER_ALLOW_ORIGIN] = self.origin |
+ headers[_CORS_HEADER_ALLOW_METHODS] = ','.join(tuple( |
+ _CORS_ALLOWED_METHODS)) |
+ headers[_CORS_HEADER_EXPOSE_HEADERS] = ','.join(tuple( |
+ _CORS_EXPOSED_HEADERS)) |
+ if self.cors_request_headers is not None: |
+ headers[_CORS_HEADER_ALLOW_HEADERS] = self.cors_request_headers |
+ |
+ def _create_cors_handler(self, request): |
+ return EndpointsDispatcherMiddleware.__CheckCorsHeaders(request) |
+ |
+ def handle_backend_response(self, orig_request, backend_request, |
+ response_status, response_headers, |
+ response_body, method_config, start_response): |
+ """Handle backend response, transforming output as needed. |
+ |
+ This calls start_response and returns the response body. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ backend_request: An ApiRequest, the transformed request that was |
+ sent to the backend handler. |
+ response_status: A string, the status from the response. |
+ response_headers: A dict, the headers from the response. |
+ response_body: A string, the body of the response. |
+ method_config: A dict, the API config of the method to be called. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string containing the response body. |
+ """ |
+ # Verify that the response is json. If it isn't treat, the body as an |
+ # error message and wrap it in a json error response. |
+ for header, value in response_headers: |
+ if (header.lower() == 'content-type' and |
+ not value.lower().startswith('application/json')): |
+ return self.fail_request(orig_request, |
+ 'Non-JSON reply: %s' % response_body, |
+ start_response) |
+ |
+ self.check_error_response(response_body, response_status) |
+ |
+ # Need to check is_rpc() against the original request, because the |
+ # incoming request here has had its path modified. |
+ if orig_request.is_rpc(): |
+ body = self.transform_jsonrpc_response(backend_request, response_body) |
+ else: |
+ # Check if the response from the API was empty. Empty REST responses |
+ # generate a HTTP 204. |
+ empty_response = self.check_empty_response(orig_request, method_config, |
+ start_response) |
+ if empty_response is not None: |
+ return empty_response |
+ |
+ body = self.transform_rest_response(response_body) |
+ |
+ cors_handler = self._create_cors_handler(orig_request) |
+ return util.send_wsgi_response(response_status, response_headers, body, |
+ start_response, cors_handler=cors_handler) |
+ |
+ def fail_request(self, orig_request, message, start_response): |
+ """Write an immediate failure response to outfile, no redirect. |
+ |
+ This calls start_response and returns the error body. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ message: A string containing the error message to be displayed to user. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string containing the body of the error response. |
+ """ |
+ cors_handler = self._create_cors_handler(orig_request) |
+ return util.send_wsgi_error_response( |
+ message, start_response, cors_handler=cors_handler) |
+ |
+ def lookup_rest_method(self, orig_request): |
+ """Looks up and returns rest method for the currently-pending request. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ |
+ Returns: |
+ A tuple of (method descriptor, parameters), or (None, None) if no method |
+ was found for the current request. |
+ """ |
+ method_name, method, params = self.config_manager.lookup_rest_method( |
+ orig_request.path, orig_request.http_method) |
+ orig_request.method_name = method_name |
+ return method, params |
+ |
+ def lookup_rpc_method(self, orig_request): |
+ """Looks up and returns RPC method for the currently-pending request. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ |
+ Returns: |
+ The RPC method descriptor that was found for the current request, or None |
+ if none was found. |
+ """ |
+ if not orig_request.body_json: |
+ return None |
+ method_name = orig_request.body_json.get('method', '') |
+ version = orig_request.body_json.get('apiVersion', '') |
+ orig_request.method_name = method_name |
+ return self.config_manager.lookup_rpc_method(method_name, version) |
+ |
+ def transform_request(self, orig_request, params, method_config): |
+ """Transforms orig_request to apiserving request. |
+ |
+ This method uses orig_request to determine the currently-pending request |
+ and returns a new transformed request ready to send to the backend. This |
+ method accepts a rest-style or RPC-style request. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ params: A dictionary containing path parameters for rest requests, or |
+ None for an RPC request. |
+ method_config: A dict, the API config of the method to be called. |
+ |
+ Returns: |
+ An ApiRequest that's a copy of the current request, modified so it can |
+ be sent to the backend. The path is updated and parts of the body or |
+ other properties may also be changed. |
+ """ |
+ if orig_request.is_rpc(): |
+ request = self.transform_jsonrpc_request(orig_request) |
+ else: |
+ method_params = method_config.get('request', {}).get('parameters', {}) |
+ request = self.transform_rest_request(orig_request, params, method_params) |
+ request.path = method_config.get('rosyMethod', '') |
+ return request |
+ |
+ def _add_message_field(self, field_name, value, params): |
+ """Converts a . delimitied field name to a message field in parameters. |
+ |
+ This adds the field to the params dict, broken out so that message |
+ parameters appear as sub-dicts within the outer param. |
+ |
+ For example: |
+ {'a.b.c': ['foo']} |
+ becomes: |
+ {'a': {'b': {'c': ['foo']}}} |
+ |
+ Args: |
+ field_name: A string containing the '.' delimitied name to be converted |
+ into a dictionary. |
+ value: The value to be set. |
+ params: The dictionary holding all the parameters, where the value is |
+ eventually set. |
+ """ |
+ if '.' not in field_name: |
+ params[field_name] = value |
+ return |
+ |
+ root, remaining = field_name.split('.', 1) |
+ sub_params = params.setdefault(root, {}) |
+ self._add_message_field(remaining, value, sub_params) |
+ |
+ def _update_from_body(self, destination, source): |
+ """Updates the dictionary for an API payload with the request body. |
+ |
+ The values from the body should override those already in the payload, but |
+ for nested fields (message objects) the values can be combined |
+ recursively. |
+ |
+ Args: |
+ destination: A dictionary containing an API payload parsed from the |
+ path and query parameters in a request. |
+ source: A dictionary parsed from the body of the request. |
+ """ |
+ for key, value in source.iteritems(): |
+ destination_value = destination.get(key) |
+ if isinstance(value, dict) and isinstance(destination_value, dict): |
+ self._update_from_body(destination_value, value) |
+ else: |
+ destination[key] = value |
+ |
+ def transform_rest_request(self, orig_request, params, method_parameters): |
+ """Translates a Rest request into an apiserving request. |
+ |
+ This makes a copy of orig_request and transforms it to apiserving |
+ format (moving request parameters to the body). |
+ |
+ The request can receive values from the path, query and body and combine |
+ them before sending them along to the backend. In cases of collision, |
+ objects from the body take precedence over those from the query, which in |
+ turn take precedence over those from the path. |
+ |
+ In the case that a repeated value occurs in both the query and the path, |
+ those values can be combined, but if that value also occurred in the body, |
+ it would override any other values. |
+ |
+ In the case of nested values from message fields, non-colliding values |
+ from subfields can be combined. For example, if '?a.c=10' occurs in the |
+ query string and "{'a': {'b': 11}}" occurs in the body, then they will be |
+ combined as |
+ |
+ { |
+ 'a': { |
+ 'b': 11, |
+ 'c': 10, |
+ } |
+ } |
+ |
+ before being sent to the backend. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ params: A dict with URL path parameters extracted by the config_manager |
+ lookup. |
+ method_parameters: A dictionary containing the API configuration for the |
+ parameters for the request. |
+ |
+ Returns: |
+ A copy of the current request that's been modified so it can be sent |
+ to the backend. The body is updated to include parameters from the |
+ URL. |
+ """ |
+ request = orig_request.copy() |
+ body_json = {} |
+ |
+ # Handle parameters from the URL path. |
+ for key, value in params.iteritems(): |
+ # Values need to be in a list to interact with query parameter values |
+ # and to account for case of repeated parameters |
+ body_json[key] = [value] |
+ |
+ # Add in parameters from the query string. |
+ if request.parameters: |
+ # For repeated elements, query and path work together |
+ for key, value in request.parameters.iteritems(): |
+ if key in body_json: |
+ body_json[key] = value + body_json[key] |
+ else: |
+ body_json[key] = value |
+ |
+ # Validate all parameters we've merged so far and convert any '.' delimited |
+ # parameters to nested parameters. We don't use iteritems since we may |
+ # modify body_json within the loop. For instance, 'a.b' is not a valid key |
+ # and would be replaced with 'a'. |
+ for key, value in body_json.items(): |
+ current_parameter = method_parameters.get(key, {}) |
+ repeated = current_parameter.get('repeated', False) |
+ |
+ if not repeated: |
+ body_json[key] = body_json[key][0] |
+ |
+ # Order is important here. Parameter names are dot-delimited in |
+ # parameters instead of nested in dictionaries as a message field is, so |
+ # we need to call transform_parameter_value on them before calling |
+ # _add_message_field. |
+ body_json[key] = parameter_converter.transform_parameter_value( |
+ key, body_json[key], current_parameter) |
+ # Remove the old key and try to convert to nested message value |
+ message_value = body_json.pop(key) |
+ self._add_message_field(key, message_value, body_json) |
+ |
+ # Add in values from the body of the request. |
+ if request.body_json: |
+ self._update_from_body(body_json, request.body_json) |
+ |
+ request.body_json = body_json |
+ request.body = json.dumps(request.body_json) |
+ return request |
+ |
+ def transform_jsonrpc_request(self, orig_request): |
+ """Translates a JsonRpc request/response into apiserving request/response. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ |
+ Returns: |
+ A new request with the request_id updated and params moved to the body. |
+ """ |
+ request = orig_request.copy() |
+ request.request_id = request.body_json.get('id') |
+ request.body_json = request.body_json.get('params', {}) |
+ request.body = json.dumps(request.body_json) |
+ return request |
+ |
+ def check_error_response(self, body, status): |
+ """Raise an exception if the response from the backend was an error. |
+ |
+ Args: |
+ body: A string containing the backend response body. |
+ status: A string containing the backend response status. |
+ |
+ Raises: |
+ BackendError if the response is an error. |
+ """ |
+ status_code = int(status.split(' ', 1)[0]) |
+ if status_code >= 300: |
+ raise errors.BackendError(body, status) |
+ |
+ def check_empty_response(self, orig_request, method_config, start_response): |
+ """If the response from the backend is empty, return a HTTP 204 No Content. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ method_config: A dict, the API config of the method to be called. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ If the backend response was empty, this returns a string containing the |
+ response body that should be returned to the user. If the backend |
+ response wasn't empty, this returns None, indicating that we should not |
+ exit early with a 204. |
+ """ |
+ response_config = method_config.get('response', {}).get('body') |
+ if response_config == 'empty': |
+ # The response to this function should be empty. We should return a 204. |
+ # Note that it's possible that the backend returned something, but we'll |
+ # ignore it. This matches the behavior in the Endpoints server. |
+ cors_handler = self._create_cors_handler(orig_request) |
+ return util.send_wsgi_no_content_response(start_response, cors_handler) |
+ |
+ def transform_rest_response(self, response_body): |
+ """Translates an apiserving REST response so it's ready to return. |
+ |
+ Currently, the only thing that needs to be fixed here is indentation, |
+ so it's consistent with what the live app will return. |
+ |
+ Args: |
+ response_body: A string containing the backend response. |
+ |
+ Returns: |
+ A reformatted version of the response JSON. |
+ """ |
+ body_json = json.loads(response_body) |
+ return json.dumps(body_json, indent=1, sort_keys=True) |
+ |
+ def transform_jsonrpc_response(self, backend_request, response_body): |
+ """Translates an apiserving response to a JsonRpc response. |
+ |
+ Args: |
+ backend_request: An ApiRequest, the transformed request that was sent to |
+ the backend handler. |
+ response_body: A string containing the backend response to transform |
+ back to JsonRPC. |
+ |
+ Returns: |
+ A string with the updated, JsonRPC-formatted request body. |
+ """ |
+ body_json = {'result': json.loads(response_body)} |
+ return self._finish_rpc_response(backend_request.request_id, |
+ backend_request.is_batch(), body_json) |
+ |
+ def _finish_rpc_response(self, request_id, is_batch, body_json): |
+ """Finish adding information to a JSON RPC response. |
+ |
+ Args: |
+ request_id: None if the request didn't have a request ID. Otherwise, this |
+ is a string containing the request ID for the request. |
+ is_batch: A boolean indicating whether the request is a batch request. |
+ body_json: A dict containing the JSON body of the response. |
+ |
+ Returns: |
+ A string with the updated, JsonRPC-formatted request body. |
+ """ |
+ if request_id is not None: |
+ body_json['id'] = request_id |
+ if is_batch: |
+ body_json = [body_json] |
+ return json.dumps(body_json, indent=1, sort_keys=True) |
+ |
+ def _handle_request_error(self, orig_request, error, start_response): |
+ """Handle a request error, converting it to a WSGI response. |
+ |
+ Args: |
+ orig_request: An ApiRequest, the original request from the user. |
+ error: A RequestError containing information about the error. |
+ start_response: A function with semantics defined in PEP-333. |
+ |
+ Returns: |
+ A string containing the response body. |
+ """ |
+ headers = [('Content-Type', 'application/json')] |
+ if orig_request.is_rpc(): |
+ # JSON RPC errors are returned with status 200 OK and the |
+ # error details in the body. |
+ status_code = 200 |
+ body = self._finish_rpc_response(orig_request.body_json.get('id'), |
+ orig_request.is_batch(), |
+ error.rpc_error()) |
+ else: |
+ status_code = error.status_code() |
+ body = error.rest_error() |
+ |
+ response_status = '%d %s' % (status_code, |
+ httplib.responses.get(status_code, |
+ 'Unknown Error')) |
+ cors_handler = self._create_cors_handler(orig_request) |
+ return util.send_wsgi_response(response_status, headers, body, |
+ start_response, cors_handler=cors_handler) |