| # 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. |
| |
| """Helper utilities for the endpoints package.""" |
| |
| # pylint: disable=g-bad-name |
| from __future__ import absolute_import |
| |
| from six.moves import cStringIO |
| import json |
| import os |
| import wsgiref.headers |
| |
| from google.appengine.api import app_identity |
| from google.appengine.api.modules import modules |
| |
| |
| class StartResponseProxy(object): |
| """Proxy for the typical WSGI start_response object.""" |
| |
| def __init__(self): |
| self.call_context = {} |
| self.body_buffer = cStringIO.StringIO() |
| |
| def __enter__(self): |
| return self |
| |
| def __exit__(self, exc_type, exc_value, traceback): |
| # Close out the cStringIO.StringIO buffer to prevent memory leakage. |
| if self.body_buffer: |
| self.body_buffer.close() |
| |
| def Proxy(self, status, headers, exc_info=None): |
| """Save args, defer start_response until response body is parsed. |
| |
| Create output buffer for body to be written into. |
| Note: this is not quite WSGI compliant: The body should come back as an |
| iterator returned from calling service_app() but instead, StartResponse |
| returns a writer that will be later called to output the body. |
| See google/appengine/ext/webapp/__init__.py::Response.wsgi_write() |
| write = start_response('%d %s' % self.__status, self.__wsgi_headers) |
| write(body) |
| |
| Args: |
| status: Http status to be sent with this response |
| headers: Http headers to be sent with this response |
| exc_info: Exception info to be displayed for this response |
| Returns: |
| callable that takes as an argument the body content |
| """ |
| self.call_context['status'] = status |
| self.call_context['headers'] = headers |
| self.call_context['exc_info'] = exc_info |
| |
| return self.body_buffer.write |
| |
| @property |
| def response_body(self): |
| return self.body_buffer.getvalue() |
| |
| @property |
| def response_headers(self): |
| return self.call_context.get('headers') |
| |
| @property |
| def response_status(self): |
| return self.call_context.get('status') |
| |
| @property |
| def response_exc_info(self): |
| return self.call_context.get('exc_info') |
| |
| |
| def send_wsgi_not_found_response(start_response, cors_handler=None): |
| return send_wsgi_response('404 Not Found', [('Content-Type', 'text/plain')], |
| 'Not Found', start_response, |
| cors_handler=cors_handler) |
| |
| |
| def send_wsgi_error_response(message, start_response, cors_handler=None): |
| body = json.dumps({'error': {'message': message}}) |
| return send_wsgi_response('500', [('Content-Type', 'application/json')], body, |
| start_response, cors_handler=cors_handler) |
| |
| |
| def send_wsgi_rejected_response(rejection_error, start_response, |
| cors_handler=None): |
| body = rejection_error.to_json() |
| return send_wsgi_response('400', [('Content-Type', 'application/json')], body, |
| start_response, cors_handler=cors_handler) |
| |
| |
| def send_wsgi_redirect_response(redirect_location, start_response, |
| cors_handler=None): |
| return send_wsgi_response('302', [('Location', redirect_location)], '', |
| start_response, cors_handler=cors_handler) |
| |
| |
| def send_wsgi_no_content_response(start_response, cors_handler=None): |
| return send_wsgi_response('204 No Content', [], '', start_response, |
| cors_handler) |
| |
| |
| def send_wsgi_response(status, headers, content, start_response, |
| cors_handler=None): |
| """Dump reformatted response to CGI start_response. |
| |
| This calls start_response and returns the response body. |
| |
| Args: |
| status: A string containing the HTTP status code to send. |
| headers: A list of (header, value) tuples, the headers to send in the |
| response. |
| content: A string containing the body content to write. |
| start_response: A function with semantics defined in PEP-333. |
| cors_handler: A handler to process CORS request headers and update the |
| headers in the response. Or this can be None, to bypass CORS checks. |
| |
| Returns: |
| A string containing the response body. |
| """ |
| if cors_handler: |
| cors_handler.update_headers(headers) |
| |
| # Update content length. |
| content_len = len(content) if content else 0 |
| headers = [(header, value) for header, value in headers |
| if header.lower() != 'content-length'] |
| headers.append(('Content-Length', '%s' % content_len)) |
| |
| start_response(status, headers) |
| return content |
| |
| |
| def get_headers_from_environ(environ): |
| """Get a wsgiref.headers.Headers object with headers from the environment. |
| |
| Headers in environ are prefixed with 'HTTP_', are all uppercase, and have |
| had dashes replaced with underscores. This strips the HTTP_ prefix and |
| changes underscores back to dashes before adding them to the returned set |
| of headers. |
| |
| Args: |
| environ: An environ dict for the request as defined in PEP-333. |
| |
| Returns: |
| A wsgiref.headers.Headers object that's been filled in with any HTTP |
| headers found in environ. |
| """ |
| headers = wsgiref.headers.Headers([]) |
| for header, value in environ.items(): |
| if header.startswith('HTTP_'): |
| headers[header[5:].replace('_', '-')] = value |
| # Content-Type is special; it does not start with 'HTTP_'. |
| if 'CONTENT_TYPE' in environ: |
| headers['CONTENT-TYPE'] = environ['CONTENT_TYPE'] |
| return headers |
| |
| |
| def put_headers_in_environ(headers, environ): |
| """Given a list of headers, put them into environ based on PEP-333. |
| |
| This converts headers to uppercase, prefixes them with 'HTTP_', and |
| converts dashes to underscores before adding them to the environ dict. |
| |
| Args: |
| headers: A list of (header, value) tuples. The HTTP headers to add to the |
| environment. |
| environ: An environ dict for the request as defined in PEP-333. |
| """ |
| for key, value in headers: |
| environ['HTTP_%s' % key.upper().replace('-', '_')] = value |
| |
| |
| def is_running_on_app_engine(): |
| return os.environ.get('GAE_MODULE_NAME') is not None |
| |
| |
| def is_running_on_devserver(): |
| server_software = os.environ.get('SERVER_SOFTWARE', '') |
| return (server_software.startswith('Development/') and |
| server_software != 'Development/1.0 (testbed)') |
| |
| |
| def is_running_on_localhost(): |
| return os.environ.get('SERVER_NAME') == 'localhost' |
| |
| |
| def get_hostname_prefix(): |
| """Returns the hostname prefix of a running Endpoints service. |
| |
| The prefix is the portion of the hostname that comes before the API name. |
| For example, if a non-default version and a non-default service are in use, |
| the returned result would be '{VERSION}-dot-{SERVICE}-'. |
| |
| Returns: |
| str, the hostname prefix. |
| """ |
| parts = [] |
| |
| # Check if this is the default version |
| version = modules.get_current_version_name() |
| default_version = modules.get_default_version() |
| if version != default_version: |
| parts.append(version) |
| |
| # Check if this is the default module |
| module = modules.get_current_module_name() |
| if module != 'default': |
| parts.append(module) |
| |
| # If there is anything to prepend, add an extra blank entry for the trailing |
| # -dot- |
| if parts: |
| parts.append('') |
| |
| return '-dot-'.join(parts) |
| |
| |
| def get_app_hostname(): |
| """Return hostname of a running Endpoints service. |
| |
| Returns hostname of an running Endpoints API. It can be 1) "localhost:PORT" |
| if running on development server, or 2) "app_id.appspot.com" if running on |
| external app engine prod, or "app_id.googleplex.com" if running as Google |
| first-party Endpoints API, or 4) None if not running on App Engine |
| (e.g. Tornado Endpoints API). |
| |
| Returns: |
| A string representing the hostname of the service. |
| """ |
| if not is_running_on_app_engine() or is_running_on_localhost(): |
| return None |
| |
| app_id = app_identity.get_application_id() |
| |
| prefix = get_hostname_prefix() |
| suffix = 'appspot.com' |
| |
| if ':' in app_id: |
| tokens = app_id.split(':') |
| api_name = tokens[1] |
| if tokens[0] == 'google.com': |
| suffix = 'googleplex.com' |
| else: |
| api_name = app_id |
| |
| return '{0}{1}.{2}'.format(prefix, api_name, suffix) |
| |
| |
| def check_list_type(objects, allowed_type, name, allow_none=True): |
| """Verify that objects in list are of the allowed type or raise TypeError. |
| |
| Args: |
| objects: The list of objects to check. |
| allowed_type: The allowed type of items in 'settings'. |
| name: Name of the list of objects, added to the exception. |
| allow_none: If set, None is also allowed. |
| |
| Raises: |
| TypeError: if object is not of the allowed type. |
| |
| Returns: |
| The list of objects, for convenient use in assignment. |
| """ |
| if objects is None: |
| if not allow_none: |
| raise TypeError('%s is None, which is not allowed.' % name) |
| return objects |
| if not isinstance(objects, (tuple, list)): |
| raise TypeError('%s is not a list.' % name) |
| if not all(isinstance(i, allowed_type) for i in objects): |
| type_list = sorted(list(set(type(obj) for obj in objects))) |
| raise TypeError('%s contains types that don\'t match %s: %s' % |
| (name, allowed_type.__name__, type_list)) |
| return objects |
| |
| |
| def snake_case_to_headless_camel_case(snake_string): |
| """Convert snake_case to headlessCamelCase. |
| |
| Args: |
| snake_string: The string to be converted. |
| Returns: |
| The input string converted to headlessCamelCase. |
| """ |
| return ''.join([snake_string.split('_')[0]] + |
| list(sub_string.capitalize() |
| for sub_string in snake_string.split('_')[1:])) |