OLD | NEW |
(Empty) | |
| 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 |
| 19 import cStringIO |
| 20 import json |
| 21 import os |
| 22 import wsgiref.headers |
| 23 |
| 24 from google.appengine.api import app_identity |
| 25 |
| 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.iteritems(): |
| 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 return os.environ.get('SERVER_SOFTWARE', '').startswith('Development/') |
| 192 |
| 193 |
| 194 def is_running_on_localhost(): |
| 195 return os.environ.get('SERVER_NAME') == 'localhost' |
| 196 |
| 197 |
| 198 def get_app_hostname(): |
| 199 """Return hostname of a running Endpoints service. |
| 200 |
| 201 Returns hostname of an running Endpoints API. It can be 1) "localhost:PORT" |
| 202 if running on development server, or 2) "app_id.appspot.com" if running on |
| 203 external app engine prod, or "app_id.googleplex.com" if running as Google |
| 204 first-party Endpoints API, or 4) None if not running on App Engine |
| 205 (e.g. Tornado Endpoints API). |
| 206 |
| 207 Returns: |
| 208 A string representing the hostname of the service. |
| 209 """ |
| 210 if not is_running_on_app_engine() or is_running_on_localhost(): |
| 211 return None |
| 212 |
| 213 version = modules.get_current_version_name() |
| 214 app_id = app_identity.get_application_id() |
| 215 |
| 216 suffix = 'appspot.com' |
| 217 |
| 218 if ':' in app_id: |
| 219 tokens = app_id.split(':') |
| 220 api_name = tokens[1] |
| 221 if tokens[0] == 'google.com': |
| 222 suffix = 'googleplex.com' |
| 223 else: |
| 224 api_name = app_id |
| 225 |
| 226 # Check if this is the default version |
| 227 default_version = modules.get_default_version() |
| 228 if version == default_version: |
| 229 return '{0}.{1}'.format(app_id, suffix) |
| 230 else: |
| 231 return '{0}-dot-{1}.{2}'.format(version, api_name, suffix) |
| 232 |
| 233 |
| 234 def check_list_type(objects, allowed_type, name, allow_none=True): |
| 235 """Verify that objects in list are of the allowed type or raise TypeError. |
| 236 |
| 237 Args: |
| 238 objects: The list of objects to check. |
| 239 allowed_type: The allowed type of items in 'settings'. |
| 240 name: Name of the list of objects, added to the exception. |
| 241 allow_none: If set, None is also allowed. |
| 242 |
| 243 Raises: |
| 244 TypeError: if object is not of the allowed type. |
| 245 |
| 246 Returns: |
| 247 The list of objects, for convenient use in assignment. |
| 248 """ |
| 249 if objects is None: |
| 250 if not allow_none: |
| 251 raise TypeError('%s is None, which is not allowed.' % name) |
| 252 return objects |
| 253 if not isinstance(objects, (tuple, list)): |
| 254 raise TypeError('%s is not a list.' % name) |
| 255 if not all(isinstance(i, allowed_type) for i in objects): |
| 256 type_list = sorted(list(set(type(obj) for obj in objects))) |
| 257 raise TypeError('%s contains types that don\'t match %s: %s' % |
| 258 (name, allowed_type.__name__, type_list)) |
| 259 return objects |
| 260 |
| 261 |
| 262 def snake_case_to_headless_camel_case(snake_string): |
| 263 """Convert snake_case to headlessCamelCase. |
| 264 |
| 265 Args: |
| 266 snake_string: The string to be converted. |
| 267 Returns: |
| 268 The input string converted to headlessCamelCase. |
| 269 """ |
| 270 return ''.join([snake_string.split('_')[0]] + |
| 271 list(sub_string.capitalize() |
| 272 for sub_string in snake_string.split('_')[1:])) |
OLD | NEW |