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.
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.
33 raise endpoints.UnauthorizedException("Please log in as an admin user")
37 - - - - app.yaml - - - -
40 # Path to your API backend.
42 # For the legacy python runtime this would be "script: services.py"
45 - - - - services.py - - - -
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):
57 @endpoints.method(GetNotesRequest, Notes, name='notes.list', path='notes',
59 def list(self, request):
60 raise endpoints.UnauthorizedException("Please log in as an admin user")
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'
84 'EndpointsErrorMessage',
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.
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.
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.
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.
142 environ: A dictionary with a key CURRENT_VERSION_ID that maps to a version
143 string of the format <major>.<minor>.
146 The app revision (minor version) of the current app, or None if one couldn't
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).
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.
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 '
244 self
.restricted
= kwargs
.pop('restricted', True)
245 self
.service_app
= wsgi_service
.service_mappings(protorpc_services
,
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.
258 environ: WSGI environment dictionary.
261 True if access should be denied, else False.
263 if not self
.restricted
:
265 server
= environ
.get(self
.__SERVER
_SOFTWARE
, '')
266 if (server
.startswith(self
.__DEV
_APPSERVER
_PREFIX
) or
267 server
.startswith(self
.__TEST
_APPSERVER
_PREFIX
)):
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.
276 status: HTTP status code.
277 headers: Dictionary of (lowercase) header name to value.
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.
291 status_code: HTTP status code to be returned.
292 error_message: Error message to be returned.
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.
314 status: HTTP status of the response from the backend
315 body: JSON-encoded error in format expected by Endpoints frontend.
318 Tuple of (http status, body)
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
)
330 status
, body
= self
.__write
_error
(error_class
.http_status
,
331 rpc_error
.error_message
)
334 def __call__(self
, environ
, start_response
):
335 """Wrapper for Swarm server app.
338 environ: WSGI request environment.
339 start_response: WSGI start response function.
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)
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
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')]
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()
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
)
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).
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.
421 A new WSGIApplication that serves the API backend and config registry.
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
)