App Engine Python SDK version 1.7.7
[gae.git] / python / google / appengine / ext / endpoints / apiserving.py
blob540614cc0e9b141b72963dbc35adf3d311a7fbfe
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
19 """A library supporting use of the Google API Server.
21 This library helps you configure a set of ProtoRPC services to act as
22 Endpoints backends. In addition to translating ProtoRPC to Endpoints
23 compatible errors, it exposes a helper service that describes your services.
25 Usage:
26 1) Create an endpoints.api_server instead of a webapp.WSGIApplication.
27 2) Annotate your ProtoRPC Service class with @endpoints.api to give your
28 API a name, version, and short description
29 3) To return an error from Google API Server raise an endpoints.*Exception
30 The ServiceException classes specify the http status code returned.
32 For example:
33 raise endpoints.UnauthorizedException("Please log in as an admin user")
36 Sample usage:
37 - - - - app.yaml - - - -
39 handlers:
40 # Path to your API backend.
41 - url: /_ah/spi/.*
42 # For the legacy python runtime this would be "script: services.py"
43 script: services.app
45 - - - - services.py - - - -
47 import endpoints
48 import postservice
50 app = endpoints.api_server([postservice.PostService], debug=True)
52 - - - - postservice.py - - - -
54 @endpoints.api(name='guestbook', version='v0.2', description='Guestbook API')
55 class PostService(remote.Service):
56 ...
57 @endpoints.method(GetNotesRequest, Notes, name='notes.list', path='notes',
58 http_method='GET')
59 def list(self, request):
60 raise endpoints.UnauthorizedException("Please log in as an admin user")
61 """
64 import cgi
65 import cStringIO
66 import httplib
67 import logging
68 import os
70 from protorpc import messages
71 from protorpc import protojson
72 from protorpc import remote
73 from protorpc.wsgi import service as wsgi_service
75 from google.appengine.ext.endpoints import api_backend_service
76 from google.appengine.ext.endpoints import api_config
77 from google.appengine.ext.endpoints import api_exceptions
79 package = 'google.appengine.endpoints'
82 __all__ = [
83 'api_server',
84 'EndpointsErrorMessage',
85 'package',
89 _ERROR_NAME_MAP = dict((httplib.responses[c.http_status], c) for c in [
90 api_exceptions.BadRequestException,
91 api_exceptions.ForbiddenException,
92 api_exceptions.InternalServerErrorException,
93 api_exceptions.NotFoundException,
94 api_exceptions.UnauthorizedException,
97 _ALL_JSON_CONTENT_TYPES = frozenset([protojson.CONTENT_TYPE] +
98 protojson.ALTERNATIVE_CONTENT_TYPES)
104 class EndpointsErrorMessage(messages.Message):
105 """Message for returning error back to Google Endpoints frontend.
107 Fields:
108 state: State of RPC, should be 'APPLICATION_ERROR'.
109 error_message: Error message associated with status.
112 class State(messages.Enum):
113 """Enumeration of possible RPC states.
115 Values:
116 OK: Completed successfully.
117 RUNNING: Still running, not complete.
118 REQUEST_ERROR: Request was malformed or incomplete.
119 SERVER_ERROR: Server experienced an unexpected error.
120 NETWORK_ERROR: An error occured on the network.
121 APPLICATION_ERROR: The application is indicating an error.
122 When in this state, RPC should also set application_error.
124 OK = 0
125 RUNNING = 1
127 REQUEST_ERROR = 2
128 SERVER_ERROR = 3
129 NETWORK_ERROR = 4
130 APPLICATION_ERROR = 5
131 METHOD_NOT_FOUND_ERROR = 6
133 state = messages.EnumField(State, 1, required=True)
134 error_message = messages.StringField(2)
138 def _get_app_revision(environ=None):
139 """Gets the app revision (minor app version) of the current app.
141 Args:
142 environ: A dictionary with a key CURRENT_VERSION_ID that maps to a version
143 string of the format <major>.<minor>.
145 Returns:
146 The app revision (minor version) of the current app, or None if one couldn't
147 be found.
149 if environ is None:
150 environ = os.environ
151 if 'CURRENT_VERSION_ID' in environ:
152 return environ['CURRENT_VERSION_ID'].split('.')[1]
155 class _ApiServer(object):
156 """ProtoRPC wrapper, registers APIs and formats errors for Google API Server.
158 - - - - ProtoRPC error format - - - -
159 HTTP/1.0 400 Please log in as an admin user.
160 content-type: application/json
163 "state": "APPLICATION_ERROR",
164 "error_message": "Please log in as an admin user",
165 "error_name": "unauthorized",
168 - - - - Reformatted error format - - - -
169 HTTP/1.0 401 UNAUTHORIZED
170 content-type: application/json
173 "state": "APPLICATION_ERROR",
174 "error_message": "Please log in as an admin user"
179 __SPI_PREFIX = '/_ah/spi/'
180 __BACKEND_SERVICE_ROOT = '%sBackendService' % __SPI_PREFIX
181 __SERVER_SOFTWARE = 'SERVER_SOFTWARE'
182 __DEV_APPSERVER_PREFIX = 'Development/'
183 __TEST_APPSERVER_PREFIX = 'WSGIServer/'
184 __HEADER_NAME_PEER = 'HTTP_X_APPENGINE_PEER'
185 __GOOGLE_PEER = 'apiserving'
187 def __init__(self, api_services, **kwargs):
188 """Initialize an _ApiServer instance.
190 The primary function of this method is to set up the WSGIApplication
191 instance for the service handlers described by the services passed in.
192 Additionally, it registers each API in ApiConfigRegistry for later use
193 in the BackendService.getApiConfigs() (API config enumeration service).
195 Args:
196 api_services: List of protorpc.remote.Service classes implementing the API
197 **kwargs: Passed through to protorpc.wsgi.service.service_handlers except:
198 protocols - ProtoRPC protocols are not supported, and are disallowed.
199 restricted - If True or unset, the API will only be allowed to serve to
200 Google's API serving infrastructure once deployed. Set to False to
201 allow other clients. Under dev_appserver, all clients are accepted.
202 NOTE! Under experimental launch, this is not a secure restriction and
203 other authentication mechanisms *must* be used to control access to
204 the API. The restriction is only intended to notify developers of
205 a possible upcoming feature to securely restrict access to the API.
207 Raises:
208 TypeError: if protocols are configured (this feature is not supported).
209 ApiConfigurationError: if there's a problem with the API config.
211 protorpc_services = []
212 generator = api_config.ApiConfigGenerator()
213 self.api_config_registry = api_backend_service.ApiConfigRegistry()
214 api_name_version_map = {}
215 for service in api_services:
216 key = (service.api_info.name, service.api_info.version)
217 services = api_name_version_map.setdefault(key, [])
218 if service in services:
219 raise api_config.ApiConfigurationError(
220 'Can\'t add the same class to an API twice: %s' % service.__name__)
221 services.append(service)
223 for services in api_name_version_map.values():
224 config_file = generator.pretty_print_config_to_json(services)
228 self.api_config_registry.register_spi(config_file)
229 for api_service in services:
230 protorpc_class_name = api_service.__name__
231 root = self.__SPI_PREFIX + protorpc_class_name
232 if not any(service[0] == root or service[1] == api_service
233 for service in protorpc_services):
234 protorpc_services.append((root, api_service))
237 backend_service = api_backend_service.BackendServiceImpl.new_factory(
238 self.api_config_registry, _get_app_revision())
239 protorpc_services.insert(0, (self.__BACKEND_SERVICE_ROOT, backend_service))
241 if 'protocols' in kwargs:
242 raise TypeError('__init__() got an unexpected keyword argument '
243 "'protocols'")
244 self.restricted = kwargs.pop('restricted', True)
245 self.service_app = wsgi_service.service_mappings(protorpc_services,
246 **kwargs)
248 def __is_request_restricted(self, environ):
249 """Determine if access to SPI should be denied.
251 Access will always be allowed in dev_appserver and under unit tests, but
252 will only be allowed in production if the HTTP header HTTP_X_APPENGINE_PEER
253 is set to 'apiserving'. Google's Endpoints server sets this header by
254 default and App Engine may securely prevent outside callers from setting it
255 in the future to allow better protection of the API backend.
257 Args:
258 environ: WSGI environment dictionary.
260 Returns:
261 True if access should be denied, else False.
263 if not self.restricted:
264 return False
265 server = environ.get(self.__SERVER_SOFTWARE, '')
266 if (server.startswith(self.__DEV_APPSERVER_PREFIX) or
267 server.startswith(self.__TEST_APPSERVER_PREFIX)):
268 return False
269 peer_name = environ.get(self.__HEADER_NAME_PEER, '')
270 return peer_name.lower() != self.__GOOGLE_PEER
272 def __is_json_error(self, status, headers):
273 """Determine if response is an error.
275 Args:
276 status: HTTP status code.
277 headers: Dictionary of (lowercase) header name to value.
279 Returns:
280 True if the response was an error, else False.
282 content_header = headers.get('content-type', '')
283 content_type, unused_params = cgi.parse_header(content_header)
284 return (status.startswith('400') and
285 content_type.lower() in _ALL_JSON_CONTENT_TYPES)
287 def __write_error(self, status_code, error_message=None):
288 """Return the HTTP status line and body for a given error code and message.
290 Args:
291 status_code: HTTP status code to be returned.
292 error_message: Error message to be returned.
294 Returns:
295 Tuple (http_status, body):
296 http_status: HTTP status line, e.g. 200 OK.
297 body: Body of the HTTP request.
299 if error_message is None:
300 error_message = httplib.responses[status_code]
301 status = '%d %s' % (status_code, httplib.responses[status_code])
302 message = EndpointsErrorMessage(
303 state=EndpointsErrorMessage.State.APPLICATION_ERROR,
304 error_message=error_message)
305 return status, protojson.encode_message(message)
307 def protorpc_to_endpoints_error(self, status, body):
308 """Convert a ProtoRPC error to the format expected by Google Endpoints.
310 If the body does not contain an ProtoRPC message in state APPLICATION_ERROR
311 the status and body will be returned unchanged.
313 Args:
314 status: HTTP status of the response from the backend
315 body: JSON-encoded error in format expected by Endpoints frontend.
317 Returns:
318 Tuple of (http status, body)
320 try:
321 rpc_error = protojson.decode_message(remote.RpcStatus, body)
322 except (ValueError, messages.ValidationError):
323 rpc_error = remote.RpcStatus()
325 if rpc_error.state == remote.RpcStatus.State.APPLICATION_ERROR:
328 error_class = _ERROR_NAME_MAP.get(rpc_error.error_name)
329 if error_class:
330 status, body = self.__write_error(error_class.http_status,
331 rpc_error.error_message)
332 return status, body
334 def __call__(self, environ, start_response):
335 """Wrapper for Swarm server app.
337 Args:
338 environ: WSGI request environment.
339 start_response: WSGI start response function.
341 Returns:
342 Response from service_app or appropriately transformed error response.
345 def StartResponse(status, headers, exc_info=None):
346 """Save args, defer start_response until response body is parsed.
348 Create output buffer for body to be written into.
349 Note: this is not quite WSGI compliant: The body should come back as an
350 iterator returned from calling service_app() but instead, StartResponse
351 returns a writer that will be later called to output the body.
352 See google/appengine/ext/webapp/__init__.py::Response.wsgi_write()
353 write = start_response('%d %s' % self.__status, self.__wsgi_headers)
354 write(body)
356 Args:
357 status: Http status to be sent with this response
358 headers: Http headers to be sent with this response
359 exc_info: Exception info to be displayed for this response
360 Returns:
361 callable that takes as an argument the body content
363 call_context['status'] = status
364 call_context['headers'] = headers
365 call_context['exc_info'] = exc_info
367 return body_buffer.write
369 if self.__is_request_restricted(environ):
370 status, body = self.__write_error(httplib.NOT_FOUND)
371 headers = [('Content-Type', 'text/plain')]
372 exception = None
374 else:
376 call_context = {}
377 body_buffer = cStringIO.StringIO()
378 body_iter = self.service_app(environ, StartResponse)
379 status = call_context['status']
380 headers = call_context['headers']
381 exception = call_context['exc_info']
384 body = body_buffer.getvalue()
386 if not body:
387 body = ''.join(body_iter)
390 headers_dict = dict([(k.lower(), v) for k, v in headers])
391 if self.__is_json_error(status, headers_dict):
392 status, body = self.protorpc_to_endpoints_error(status, body)
394 start_response(status, headers, exception)
395 return [body]
400 def api_server(api_services, **kwargs):
401 """Create an api_server.
403 The primary function of this method is to set up the WSGIApplication
404 instance for the service handlers described by the services passed in.
405 Additionally, it registers each API in ApiConfigRegistry for later use
406 in the BackendService.getApiConfigs() (API config enumeration service).
408 Args:
409 api_services: List of protorpc.remote.Service classes implementing the API
410 **kwargs: Passed through to protorpc.wsgi.service.service_handlers except:
411 protocols - ProtoRPC protocols are not supported, and are disallowed.
412 restricted - If True or unset, the API will only be allowed to serve to
413 Google's API serving infrastructure once deployed. Set to False to
414 allow other clients. Under dev_appserver, all clients are accepted.
415 NOTE! Under experimental launch, this is not a secure restriction and
416 other authentication mechanisms *must* be used to control access to
417 the API. The restriction is only intended to notify developers of
418 a possible upcoming feature to securely restrict access to the API.
420 Returns:
421 A new WSGIApplication that serves the API backend and config registry.
423 Raises:
424 TypeError: if protocols are configured (this feature is not supported).
427 if 'protocols' in kwargs:
428 raise TypeError("__init__() got an unexpected keyword argument 'protocols'")
429 return _ApiServer(api_services, **kwargs)