blob: fe883d0864528bf672e8d127a42f5a87d7f964af [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# 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
18from __future__ import absolute_import
19
20from six.moves import cStringIO
21import json
22import os
23import wsgiref.headers
24
25from google.appengine.api import app_identity
26from google.appengine.api.modules import modules
27
28
29class 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
85def 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
91def 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
97def 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
104def 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
110def 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
115def 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
146def 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
171def 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
186def is_running_on_app_engine():
187 return os.environ.get('GAE_MODULE_NAME') is not None
188
189
190def 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
196def is_running_on_localhost():
197 return os.environ.get('SERVER_NAME') == 'localhost'
198
199
200def 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
231def 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
262def 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
290def 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:]))