Adrià Vilanova MartÃnez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # Copyright 2016 Google Inc. All Rights Reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | """Helper utilities for the endpoints package.""" |
| 16 | |
| 17 | # pylint: disable=g-bad-name |
| 18 | from __future__ import absolute_import |
| 19 | |
| 20 | from six.moves import cStringIO |
| 21 | import json |
| 22 | import os |
| 23 | import wsgiref.headers |
| 24 | |
| 25 | from google.appengine.api import app_identity |
| 26 | from google.appengine.api.modules import modules |
| 27 | |
| 28 | |
| 29 | class StartResponseProxy(object): |
| 30 | """Proxy for the typical WSGI start_response object.""" |
| 31 | |
| 32 | def __init__(self): |
| 33 | self.call_context = {} |
| 34 | self.body_buffer = cStringIO.StringIO() |
| 35 | |
| 36 | def __enter__(self): |
| 37 | return self |
| 38 | |
| 39 | def __exit__(self, exc_type, exc_value, traceback): |
| 40 | # Close out the cStringIO.StringIO buffer to prevent memory leakage. |
| 41 | if self.body_buffer: |
| 42 | self.body_buffer.close() |
| 43 | |
| 44 | def Proxy(self, status, headers, exc_info=None): |
| 45 | """Save args, defer start_response until response body is parsed. |
| 46 | |
| 47 | Create output buffer for body to be written into. |
| 48 | Note: this is not quite WSGI compliant: The body should come back as an |
| 49 | iterator returned from calling service_app() but instead, StartResponse |
| 50 | returns a writer that will be later called to output the body. |
| 51 | See google/appengine/ext/webapp/__init__.py::Response.wsgi_write() |
| 52 | write = start_response('%d %s' % self.__status, self.__wsgi_headers) |
| 53 | write(body) |
| 54 | |
| 55 | Args: |
| 56 | status: Http status to be sent with this response |
| 57 | headers: Http headers to be sent with this response |
| 58 | exc_info: Exception info to be displayed for this response |
| 59 | Returns: |
| 60 | callable that takes as an argument the body content |
| 61 | """ |
| 62 | self.call_context['status'] = status |
| 63 | self.call_context['headers'] = headers |
| 64 | self.call_context['exc_info'] = exc_info |
| 65 | |
| 66 | return self.body_buffer.write |
| 67 | |
| 68 | @property |
| 69 | def response_body(self): |
| 70 | return self.body_buffer.getvalue() |
| 71 | |
| 72 | @property |
| 73 | def response_headers(self): |
| 74 | return self.call_context.get('headers') |
| 75 | |
| 76 | @property |
| 77 | def response_status(self): |
| 78 | return self.call_context.get('status') |
| 79 | |
| 80 | @property |
| 81 | def response_exc_info(self): |
| 82 | return self.call_context.get('exc_info') |
| 83 | |
| 84 | |
| 85 | def send_wsgi_not_found_response(start_response, cors_handler=None): |
| 86 | return send_wsgi_response('404 Not Found', [('Content-Type', 'text/plain')], |
| 87 | 'Not Found', start_response, |
| 88 | cors_handler=cors_handler) |
| 89 | |
| 90 | |
| 91 | def send_wsgi_error_response(message, start_response, cors_handler=None): |
| 92 | body = json.dumps({'error': {'message': message}}) |
| 93 | return send_wsgi_response('500', [('Content-Type', 'application/json')], body, |
| 94 | start_response, cors_handler=cors_handler) |
| 95 | |
| 96 | |
| 97 | def send_wsgi_rejected_response(rejection_error, start_response, |
| 98 | cors_handler=None): |
| 99 | body = rejection_error.to_json() |
| 100 | return send_wsgi_response('400', [('Content-Type', 'application/json')], body, |
| 101 | start_response, cors_handler=cors_handler) |
| 102 | |
| 103 | |
| 104 | def send_wsgi_redirect_response(redirect_location, start_response, |
| 105 | cors_handler=None): |
| 106 | return send_wsgi_response('302', [('Location', redirect_location)], '', |
| 107 | start_response, cors_handler=cors_handler) |
| 108 | |
| 109 | |
| 110 | def send_wsgi_no_content_response(start_response, cors_handler=None): |
| 111 | return send_wsgi_response('204 No Content', [], '', start_response, |
| 112 | cors_handler) |
| 113 | |
| 114 | |
| 115 | def send_wsgi_response(status, headers, content, start_response, |
| 116 | cors_handler=None): |
| 117 | """Dump reformatted response to CGI start_response. |
| 118 | |
| 119 | This calls start_response and returns the response body. |
| 120 | |
| 121 | Args: |
| 122 | status: A string containing the HTTP status code to send. |
| 123 | headers: A list of (header, value) tuples, the headers to send in the |
| 124 | response. |
| 125 | content: A string containing the body content to write. |
| 126 | start_response: A function with semantics defined in PEP-333. |
| 127 | cors_handler: A handler to process CORS request headers and update the |
| 128 | headers in the response. Or this can be None, to bypass CORS checks. |
| 129 | |
| 130 | Returns: |
| 131 | A string containing the response body. |
| 132 | """ |
| 133 | if cors_handler: |
| 134 | cors_handler.update_headers(headers) |
| 135 | |
| 136 | # Update content length. |
| 137 | content_len = len(content) if content else 0 |
| 138 | headers = [(header, value) for header, value in headers |
| 139 | if header.lower() != 'content-length'] |
| 140 | headers.append(('Content-Length', '%s' % content_len)) |
| 141 | |
| 142 | start_response(status, headers) |
| 143 | return content |
| 144 | |
| 145 | |
| 146 | def get_headers_from_environ(environ): |
| 147 | """Get a wsgiref.headers.Headers object with headers from the environment. |
| 148 | |
| 149 | Headers in environ are prefixed with 'HTTP_', are all uppercase, and have |
| 150 | had dashes replaced with underscores. This strips the HTTP_ prefix and |
| 151 | changes underscores back to dashes before adding them to the returned set |
| 152 | of headers. |
| 153 | |
| 154 | Args: |
| 155 | environ: An environ dict for the request as defined in PEP-333. |
| 156 | |
| 157 | Returns: |
| 158 | A wsgiref.headers.Headers object that's been filled in with any HTTP |
| 159 | headers found in environ. |
| 160 | """ |
| 161 | headers = wsgiref.headers.Headers([]) |
| 162 | for header, value in environ.items(): |
| 163 | if header.startswith('HTTP_'): |
| 164 | headers[header[5:].replace('_', '-')] = value |
| 165 | # Content-Type is special; it does not start with 'HTTP_'. |
| 166 | if 'CONTENT_TYPE' in environ: |
| 167 | headers['CONTENT-TYPE'] = environ['CONTENT_TYPE'] |
| 168 | return headers |
| 169 | |
| 170 | |
| 171 | def put_headers_in_environ(headers, environ): |
| 172 | """Given a list of headers, put them into environ based on PEP-333. |
| 173 | |
| 174 | This converts headers to uppercase, prefixes them with 'HTTP_', and |
| 175 | converts dashes to underscores before adding them to the environ dict. |
| 176 | |
| 177 | Args: |
| 178 | headers: A list of (header, value) tuples. The HTTP headers to add to the |
| 179 | environment. |
| 180 | environ: An environ dict for the request as defined in PEP-333. |
| 181 | """ |
| 182 | for key, value in headers: |
| 183 | environ['HTTP_%s' % key.upper().replace('-', '_')] = value |
| 184 | |
| 185 | |
| 186 | def is_running_on_app_engine(): |
| 187 | return os.environ.get('GAE_MODULE_NAME') is not None |
| 188 | |
| 189 | |
| 190 | def is_running_on_devserver(): |
| 191 | server_software = os.environ.get('SERVER_SOFTWARE', '') |
| 192 | return (server_software.startswith('Development/') and |
| 193 | server_software != 'Development/1.0 (testbed)') |
| 194 | |
| 195 | |
| 196 | def is_running_on_localhost(): |
| 197 | return os.environ.get('SERVER_NAME') == 'localhost' |
| 198 | |
| 199 | |
| 200 | def get_hostname_prefix(): |
| 201 | """Returns the hostname prefix of a running Endpoints service. |
| 202 | |
| 203 | The prefix is the portion of the hostname that comes before the API name. |
| 204 | For example, if a non-default version and a non-default service are in use, |
| 205 | the returned result would be '{VERSION}-dot-{SERVICE}-'. |
| 206 | |
| 207 | Returns: |
| 208 | str, the hostname prefix. |
| 209 | """ |
| 210 | parts = [] |
| 211 | |
| 212 | # Check if this is the default version |
| 213 | version = modules.get_current_version_name() |
| 214 | default_version = modules.get_default_version() |
| 215 | if version != default_version: |
| 216 | parts.append(version) |
| 217 | |
| 218 | # Check if this is the default module |
| 219 | module = modules.get_current_module_name() |
| 220 | if module != 'default': |
| 221 | parts.append(module) |
| 222 | |
| 223 | # If there is anything to prepend, add an extra blank entry for the trailing |
| 224 | # -dot- |
| 225 | if parts: |
| 226 | parts.append('') |
| 227 | |
| 228 | return '-dot-'.join(parts) |
| 229 | |
| 230 | |
| 231 | def get_app_hostname(): |
| 232 | """Return hostname of a running Endpoints service. |
| 233 | |
| 234 | Returns hostname of an running Endpoints API. It can be 1) "localhost:PORT" |
| 235 | if running on development server, or 2) "app_id.appspot.com" if running on |
| 236 | external app engine prod, or "app_id.googleplex.com" if running as Google |
| 237 | first-party Endpoints API, or 4) None if not running on App Engine |
| 238 | (e.g. Tornado Endpoints API). |
| 239 | |
| 240 | Returns: |
| 241 | A string representing the hostname of the service. |
| 242 | """ |
| 243 | if not is_running_on_app_engine() or is_running_on_localhost(): |
| 244 | return None |
| 245 | |
| 246 | app_id = app_identity.get_application_id() |
| 247 | |
| 248 | prefix = get_hostname_prefix() |
| 249 | suffix = 'appspot.com' |
| 250 | |
| 251 | if ':' in app_id: |
| 252 | tokens = app_id.split(':') |
| 253 | api_name = tokens[1] |
| 254 | if tokens[0] == 'google.com': |
| 255 | suffix = 'googleplex.com' |
| 256 | else: |
| 257 | api_name = app_id |
| 258 | |
| 259 | return '{0}{1}.{2}'.format(prefix, api_name, suffix) |
| 260 | |
| 261 | |
| 262 | def check_list_type(objects, allowed_type, name, allow_none=True): |
| 263 | """Verify that objects in list are of the allowed type or raise TypeError. |
| 264 | |
| 265 | Args: |
| 266 | objects: The list of objects to check. |
| 267 | allowed_type: The allowed type of items in 'settings'. |
| 268 | name: Name of the list of objects, added to the exception. |
| 269 | allow_none: If set, None is also allowed. |
| 270 | |
| 271 | Raises: |
| 272 | TypeError: if object is not of the allowed type. |
| 273 | |
| 274 | Returns: |
| 275 | The list of objects, for convenient use in assignment. |
| 276 | """ |
| 277 | if objects is None: |
| 278 | if not allow_none: |
| 279 | raise TypeError('%s is None, which is not allowed.' % name) |
| 280 | return objects |
| 281 | if not isinstance(objects, (tuple, list)): |
| 282 | raise TypeError('%s is not a list.' % name) |
| 283 | if not all(isinstance(i, allowed_type) for i in objects): |
| 284 | type_list = sorted(list(set(type(obj) for obj in objects))) |
| 285 | raise TypeError('%s contains types that don\'t match %s: %s' % |
| 286 | (name, allowed_type.__name__, type_list)) |
| 287 | return objects |
| 288 | |
| 289 | |
| 290 | def snake_case_to_headless_camel_case(snake_string): |
| 291 | """Convert snake_case to headlessCamelCase. |
| 292 | |
| 293 | Args: |
| 294 | snake_string: The string to be converted. |
| 295 | Returns: |
| 296 | The input string converted to headlessCamelCase. |
| 297 | """ |
| 298 | return ''.join([snake_string.split('_')[0]] + |
| 299 | list(sub_string.capitalize() |
| 300 | for sub_string in snake_string.split('_')[1:])) |