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.
17 """Helper for Cloud Endpoints API server in the development app server.
19 This is a fake apiserver proxy that does simple transforms on requests that
20 come in to /_ah/api and then re-dispatches them to /_ah/spi. It does not do
21 any authentication, quota checking, DoS checking, etc.
23 In addition, the proxy loads api configs from
24 /_ah/spi/BackendService.getApiConfigs prior to each call, in case the
25 configuration has changed.
36 from google
.appengine
.tools
.devappserver2
.endpoints
import api_config_manager
37 from google
.appengine
.tools
.devappserver2
.endpoints
import api_request
38 from google
.appengine
.tools
.devappserver2
.endpoints
import discovery_api_proxy
39 from google
.appengine
.tools
.devappserver2
.endpoints
import discovery_service
40 from google
.appengine
.tools
.devappserver2
.endpoints
import errors
41 from google
.appengine
.tools
.devappserver2
.endpoints
import util
44 __all__
= ['API_SERVING_PATTERN',
45 'EndpointsDispatcher']
48 # Pattern for paths handled by this module.
49 API_SERVING_PATTERN
= '_ah/api/.*'
51 _SPI_ROOT_FORMAT
= '/_ah/spi/%s'
52 _SERVER_SOURCE_IP
= '0.2.0.3'
55 _CORS_HEADER_ORIGIN
= 'Origin'
56 _CORS_HEADER_REQUEST_METHOD
= 'Access-Control-Request-Method'
57 _CORS_HEADER_REQUEST_HEADERS
= 'Access-Control-Request-Headers'
58 _CORS_HEADER_ALLOW_ORIGIN
= 'Access-Control-Allow-Origin'
59 _CORS_HEADER_ALLOW_METHODS
= 'Access-Control-Allow-Methods'
60 _CORS_HEADER_ALLOW_HEADERS
= 'Access-Control-Allow-Headers'
61 _CORS_ALLOWED_METHODS
= frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT'))
64 class EndpointsDispatcher(object):
65 """Dispatcher that handles requests to the built-in apiserver handlers."""
67 _API_EXPLORER_URL
= 'https://developers.google.com/apis-explorer/?base='
69 def __init__(self
, dispatcher
, config_manager
=None):
70 """Constructor for EndpointsDispatcher.
73 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
74 config_manager: An ApiConfigManager instance that allows a caller to
75 set up an existing configuration for testing.
77 self
._dispatcher
= dispatcher
78 if config_manager
is None:
79 config_manager
= api_config_manager
.ApiConfigManager()
80 self
.config_manager
= config_manager
81 self
._dispatchers
= []
82 self
._add
_dispatcher
('/_ah/api/explorer/?$',
83 self
.handle_api_explorer_request
)
84 self
._add
_dispatcher
('/_ah/api/static/.*$',
85 self
.handle_api_static_request
)
87 def _add_dispatcher(self
, path_regex
, dispatch_function
):
88 """Add a request path and dispatch handler.
91 path_regex: A string regex, the path to match against incoming requests.
92 dispatch_function: The function to call for these requests. The function
93 should take (request, start_response) as arguments and
94 return the contents of the response body.
96 self
._dispatchers
.append((re
.compile(path_regex
), dispatch_function
))
98 def __call__(self
, environ
, start_response
):
99 """Handle an incoming request.
102 environ: An environ dict for the request as defined in PEP-333.
103 start_response: A function used to begin the response to the caller.
104 This follows the semantics defined in PEP-333. In particular, it's
105 called with (status, response_headers, exc_info=None), and it returns
106 an object with a write(body_data) function that can be used to write
107 the body of the response.
110 An iterable over strings containing the body of the HTTP response.
112 request
= api_request
.ApiRequest(environ
)
114 # PEP-333 requires that we return an iterator that iterates over the
115 # response body. Yielding the returned body accomplishes this.
116 yield self
.dispatch(request
, start_response
)
118 def dispatch(self
, request
, start_response
):
119 """Handles dispatch to apiserver handlers.
121 This typically ends up calling start_response and returning the entire
122 body of the response.
125 request: An ApiRequest, the request from the user.
126 start_response: A function with semantics defined in PEP-333.
129 A string, the body of the response.
131 # Check if this matches any of our special handlers.
132 dispatched_response
= self
.dispatch_non_api_requests(request
,
134 if dispatched_response
is not None:
135 return dispatched_response
137 # Get API configuration first. We need this so we know how to
139 api_config_response
= self
.get_api_configs()
140 if not self
.handle_get_api_configs_response(api_config_response
):
141 return self
.fail_request(request
, 'BackendService.getApiConfigs Error',
146 return self
.call_spi(request
, start_response
)
147 except errors
.RequestError
as error
:
148 return self
._handle
_request
_error
(request
, error
, start_response
)
150 def dispatch_non_api_requests(self
, request
, start_response
):
151 """Dispatch this request if this is a request to a reserved URL.
153 If the request matches one of our reserved URLs, this calls
154 start_response and returns the response body.
157 request: An ApiRequest, the request from the user.
158 start_response: A function with semantics defined in PEP-333.
161 None if the request doesn't match one of the reserved URLs this
162 handles. Otherwise, returns the response body.
164 for path_regex
, dispatch_function
in self
._dispatchers
:
165 if path_regex
.match(request
.relative_url
):
166 return dispatch_function(request
, start_response
)
169 def handle_api_explorer_request(self
, request
, start_response
):
170 """Handler for requests to _ah/api/explorer.
172 This calls start_response and returns the response body.
175 request: An ApiRequest, the request from the user.
176 start_response: A function with semantics defined in PEP-333.
179 A string containing the response body (which is empty, in this case).
181 base_url
= 'http://%s:%s/_ah/api' % (request
.server
, request
.port
)
182 redirect_url
= self
._API
_EXPLORER
_URL
+ base_url
183 return util
.send_wsgi_redirect_response(redirect_url
, start_response
)
185 def handle_api_static_request(self
, request
, start_response
):
186 """Handler for requests to _ah/api/static/.*.
188 This calls start_response and returns the response body.
191 request: An ApiRequest, the request from the user.
192 start_response: A function with semantics defined in PEP-333.
195 A string containing the response body.
197 discovery_api
= discovery_api_proxy
.DiscoveryApiProxy()
198 response
, body
= discovery_api
.get_static_file(request
.relative_url
)
199 status_string
= '%d %s' % (response
.status
, response
.reason
)
200 if response
.status
== 200:
201 # Some of the headers that come back from the server can't be passed
202 # along in our response. Specifically, the response from the server has
203 # transfer-encoding: chunked, which doesn't apply to the response that
204 # we're forwarding. There may be other problematic headers, so we strip
205 # off everything but Content-Type.
206 return util
.send_wsgi_response(status_string
,
208 response
.getheader('Content-Type'))],
209 body
, start_response
)
211 logging
.error('Discovery API proxy failed on %s with %d. Details: %s',
212 request
.relative_url
, response
.status
, body
)
213 return util
.send_wsgi_response(status_string
, response
.getheaders(), body
,
216 def get_api_configs(self
):
217 """Makes a call to the BackendService.getApiConfigs endpoint.
220 A ResponseTuple containing the response information from the HTTP
223 headers
= [('Content-Type', 'application/json')]
225 response
= self
._dispatcher
.add_request(
226 'POST', '/_ah/spi/BackendService.getApiConfigs',
227 headers
, request_body
, _SERVER_SOURCE_IP
)
231 def verify_response(response
, status_code
, content_type
=None):
232 """Verifies that a response has the expected status and content type.
235 response: The ResponseTuple to be checked.
236 status_code: An int, the HTTP status code to be compared with response
238 content_type: A string with the acceptable Content-Type header value.
239 None allows any content type.
242 True if both status_code and content_type match, else False.
244 status
= int(response
.status
.split(' ', 1)[0])
245 if status
!= status_code
:
247 if content_type
is None:
249 for header
, value
in response
.headers
:
250 if header
.lower() == 'content-type':
251 return value
== content_type
255 def handle_get_api_configs_response(self
, api_config_response
):
256 """Parses the result of GetApiConfigs and stores its information.
259 api_config_response: The ResponseTuple from the GetApiConfigs call.
262 True on success, False on failure
264 if self
.verify_response(api_config_response
, 200, 'application/json'):
265 self
.config_manager
.parse_api_config_response(
266 api_config_response
.content
)
271 def call_spi(self
, orig_request
, start_response
):
272 """Generate SPI call (from earlier-saved request).
274 This calls start_response and returns the response body.
277 orig_request: An ApiRequest, the original request from the user.
278 start_response: A function with semantics defined in PEP-333.
281 A string containing the response body.
283 if orig_request
.is_rpc():
284 method_config
= self
.lookup_rpc_method(orig_request
)
287 method_config
, params
= self
.lookup_rest_method(orig_request
)
288 if not method_config
:
289 cors_handler
= EndpointsDispatcher
.__CheckCorsHeaders
(orig_request
)
290 return util
.send_wsgi_not_found_response(start_response
,
291 cors_handler
=cors_handler
)
293 # Prepare the request for the back end.
294 spi_request
= self
.transform_request(orig_request
, params
, method_config
)
296 # Check if this SPI call is for the Discovery service. If so, route
297 # it to our Discovery handler.
298 discovery
= discovery_service
.DiscoveryService(self
.config_manager
)
299 discovery_response
= discovery
.handle_discovery_request(
300 spi_request
.path
, spi_request
, start_response
)
301 if discovery_response
:
302 return discovery_response
304 # Send the request to the user's SPI handlers.
305 url
= _SPI_ROOT_FORMAT
% spi_request
.path
306 spi_request
.headers
['Content-Type'] = 'application/json'
307 response
= self
._dispatcher
.add_request('POST', url
,
308 spi_request
.headers
.items(),
310 spi_request
.source_ip
)
311 return self
.handle_spi_response(orig_request
, spi_request
, response
,
314 class __CheckCorsHeaders(object):
315 """Track information about CORS headers and our response to them."""
317 def __init__(self
, request
):
318 self
.allow_cors_request
= False
320 self
.cors_request_method
= None
321 self
.cors_request_headers
= None
323 self
.__check
_cors
_request
(request
)
325 def __check_cors_request(self
, request
):
326 """Check for a CORS request, and see if it gets a CORS response."""
327 # Check for incoming CORS headers.
328 self
.origin
= request
.headers
[_CORS_HEADER_ORIGIN
]
329 self
.cors_request_method
= request
.headers
[_CORS_HEADER_REQUEST_METHOD
]
330 self
.cors_request_headers
= request
.headers
[
331 _CORS_HEADER_REQUEST_HEADERS
]
333 # Check if the request should get a CORS response.
335 ((self
.cors_request_method
is None) or
336 (self
.cors_request_method
.upper() in _CORS_ALLOWED_METHODS
))):
337 self
.allow_cors_request
= True
339 def update_headers(self
, headers_in
):
340 """Add CORS headers to the response, if needed."""
341 if not self
.allow_cors_request
:
345 headers
= wsgiref
.headers
.Headers(headers_in
)
346 headers
[_CORS_HEADER_ALLOW_ORIGIN
] = self
.origin
347 headers
[_CORS_HEADER_ALLOW_METHODS
] = ','.join(tuple(
348 _CORS_ALLOWED_METHODS
))
349 if self
.cors_request_headers
is not None:
350 headers
[_CORS_HEADER_ALLOW_HEADERS
] = self
.cors_request_headers
352 def handle_spi_response(self
, orig_request
, spi_request
, response
,
354 """Handle SPI response, transforming output as needed.
356 This calls start_response and returns the response body.
359 orig_request: An ApiRequest, the original request from the user.
360 spi_request: An ApiRequest, the transformed request that was sent to the
362 response: A ResponseTuple, the response from the SPI handler.
363 start_response: A function with semantics defined in PEP-333.
366 A string containing the response body.
368 # Verify that the response is json. If it isn't treat, the body as an
369 # error message and wrap it in a json error response.
370 for header
, value
in response
.headers
:
371 if (header
.lower() == 'content-type' and
372 not value
.lower().startswith('application/json')):
373 return self
.fail_request(orig_request
,
374 'Non-JSON reply: %s' % response
.content
,
377 self
.check_error_response(response
)
379 # Need to check is_rpc() against the original request, because the
380 # incoming request here has had its path modified.
381 if orig_request
.is_rpc():
382 body
= self
.transform_jsonrpc_response(spi_request
, response
.content
)
384 body
= self
.transform_rest_response(response
.content
)
386 cors_handler
= EndpointsDispatcher
.__CheckCorsHeaders
(orig_request
)
387 return util
.send_wsgi_response(response
.status
, response
.headers
, body
,
388 start_response
, cors_handler
=cors_handler
)
390 def fail_request(self
, orig_request
, message
, start_response
):
391 """Write an immediate failure response to outfile, no redirect.
393 This calls start_response and returns the error body.
396 orig_request: An ApiRequest, the original request from the user.
397 message: A string containing the error message to be displayed to user.
398 start_response: A function with semantics defined in PEP-333.
401 A string containing the body of the error response.
403 cors_handler
= EndpointsDispatcher
.__CheckCorsHeaders
(orig_request
)
404 return util
.send_wsgi_error_response(message
, start_response
,
405 cors_handler
=cors_handler
)
407 def lookup_rest_method(self
, orig_request
):
408 """Looks up and returns rest method for the currently-pending request.
411 orig_request: An ApiRequest, the original request from the user.
414 A tuple of (method descriptor, parameters), or (None, None) if no method
415 was found for the current request.
417 method_name
, method
, params
= self
.config_manager
.lookup_rest_method(
418 orig_request
.path
, orig_request
.http_method
)
419 orig_request
.method_name
= method_name
420 return method
, params
422 def lookup_rpc_method(self
, orig_request
):
423 """Looks up and returns RPC method for the currently-pending request.
426 orig_request: An ApiRequest, the original request from the user.
429 The RPC method descriptor that was found for the current request, or None
432 if not orig_request
.body_json
:
434 method_name
= orig_request
.body_json
.get('method', '')
435 version
= orig_request
.body_json
.get('apiVersion', '')
436 orig_request
.method_name
= method_name
437 return self
.config_manager
.lookup_rpc_method(method_name
, version
)
439 def transform_request(self
, orig_request
, params
, method_config
):
440 """Transforms orig_request to apiserving request.
442 This method uses orig_request to determine the currently-pending request
443 and returns a new transformed request ready to send to the SPI. This
444 method accepts a rest-style or RPC-style request.
447 orig_request: An ApiRequest, the original request from the user.
448 params: A dictionary containing path parameters for rest requests, or
449 None for an RPC request.
450 method_config: A dict, the API config of the method to be called.
453 An ApiRequest that's a copy of the current request, modified so it can
454 be sent to the SPI. The path is updated and parts of the body or other
455 properties may also be changed.
457 if orig_request
.is_rpc():
458 request
= self
.transform_jsonrpc_request(orig_request
)
460 method_params
= method_config
.get('request', {}).get('parameters', {})
461 request
= self
.transform_rest_request(orig_request
, params
, method_params
)
462 request
.path
= method_config
.get('rosyMethod', '')
465 def _check_enum(self
, parameter_name
, value
, field_parameter
):
466 """Checks if the parameter value is valid if an enum.
468 If the parameter is not an enum, does nothing. If it is, verifies that
472 parameter_name: A string containing the name of the parameter, which is
473 either just a variable name or the name with the index appended. For
474 example 'var' or 'var[2]'.
475 value: A string or list of strings containing the value(s) to be used as
476 enum(s) for the parameter.
477 field_parameter: The dictionary containing information specific to the
478 field in question. This is retrieved from request.parameters in the
482 EnumRejectionError: If the given value is not among the accepted
483 enum values in the field parameter.
485 if 'enum' not in field_parameter
:
488 enum_values
= [enum
['backendValue']
489 for enum
in field_parameter
['enum'].values()
490 if 'backendValue' in enum
]
491 if value
not in enum_values
:
492 raise errors
.EnumRejectionError(parameter_name
, value
, enum_values
)
494 def _check_parameter(self
, parameter_name
, value
, field_parameter
):
495 """Checks if the parameter value is valid against all parameter rules.
497 If the value is a list this will recursively call _check_parameter
498 on the values in the list. Otherwise, it checks all parameter rules for the
501 In the list case, '[index-of-value]' is appended to the parameter name for
502 error reporting purposes.
504 Currently only checks if value adheres to enum rule, but more checks may be
508 parameter_name: A string containing the name of the parameter, which is
509 either just a variable name or the name with the index appended, in the
510 recursive case. For example 'var' or 'var[2]'.
511 value: A string or list of strings containing the value(s) to be used for
513 field_parameter: The dictionary containing information specific to the
514 field in question. This is retrieved from request.parameters in the
517 if isinstance(value
, list):
518 for index
, element
in enumerate(value
):
519 parameter_name_index
= '%s[%d]' % (parameter_name
, index
)
520 self
._check
_parameter
(parameter_name_index
, element
, field_parameter
)
523 self
._check
_enum
(parameter_name
, value
, field_parameter
)
525 def _add_message_field(self
, field_name
, value
, params
):
526 """Converts a . delimitied field name to a message field in parameters.
528 This adds the field to the params dict, broken out so that message
529 parameters appear as sub-dicts within the outer param.
534 {'a': {'b': {'c': ['foo']}}}
537 field_name: A string containing the '.' delimitied name to be converted
539 value: The value to be set.
540 params: The dictionary holding all the parameters, where the value is
543 if '.' not in field_name
:
544 params
[field_name
] = value
547 root
, remaining
= field_name
.split('.', 1)
548 sub_params
= params
.setdefault(root
, {})
549 self
._add
_message
_field
(remaining
, value
, sub_params
)
551 def _update_from_body(self
, destination
, source
):
552 """Updates the dictionary for an API payload with the request body.
554 The values from the body should override those already in the payload, but
555 for nested fields (message objects) the values can be combined
559 destination: A dictionary containing an API payload parsed from the
560 path and query parameters in a request.
561 source: A dictionary parsed from the body of the request.
563 for key
, value
in source
.iteritems():
564 destination_value
= destination
.get(key
)
565 if isinstance(value
, dict) and isinstance(destination_value
, dict):
566 self
._update
_from
_body
(destination_value
, value
)
568 destination
[key
] = value
570 def transform_rest_request(self
, orig_request
, params
, method_parameters
):
571 """Translates a Rest request into an apiserving request.
573 This makes a copy of orig_request and transforms it to apiserving
574 format (moving request parameters to the body).
576 The request can receive values from the path, query and body and combine
577 them before sending them along to the SPI server. In cases of collision,
578 objects from the body take precedence over those from the query, which in
579 turn take precedence over those from the path.
581 In the case that a repeated value occurs in both the query and the path,
582 those values can be combined, but if that value also occurred in the body,
583 it would override any other values.
585 In the case of nested values from message fields, non-colliding values
586 from subfields can be combined. For example, if '?a.c=10' occurs in the
587 query string and "{'a': {'b': 11}}" occurs in the body, then they will be
597 before being sent to the SPI server.
600 orig_request: An ApiRequest, the original request from the user.
601 params: A dict with URL path parameters extracted by the config_manager
603 method_parameters: A dictionary containing the API configuration for the
604 parameters for the request.
607 A copy of the current request that's been modified so it can be sent
608 to the SPI. The body is updated to include parameters from the
611 request
= orig_request
.copy()
614 # Handle parameters from the URL path.
615 for key
, value
in params
.iteritems():
616 # Values need to be in a list to interact with query parameter values
617 # and to account for case of repeated parameters
618 body_json
[key
] = [value
]
620 # Add in parameters from the query string.
621 if request
.parameters
:
622 # For repeated elements, query and path work together
623 for key
, value
in request
.parameters
.iteritems():
625 body_json
[key
] = value
+ body_json
[key
]
627 body_json
[key
] = value
629 # Validate all parameters we've merged so far and convert any '.' delimited
630 # parameters to nested parameters. We don't use iteritems since we may
631 # modify body_json within the loop. For instance, 'a.b' is not a valid key
632 # and would be replaced with 'a'.
633 for key
, value
in body_json
.items():
634 current_parameter
= method_parameters
.get(key
, {})
635 repeated
= current_parameter
.get('repeated', False)
638 body_json
[key
] = body_json
[key
][0]
640 # Order is important here. Parameter names are dot-delimited in
641 # parameters instead of nested in dictionaries as a message field is, so
642 # we need to call _check_parameter on them before calling
643 # _add_message_field.
645 self
._check
_parameter
(key
, body_json
[key
], current_parameter
)
646 # Remove the old key and try to convert to nested message value
647 message_value
= body_json
.pop(key
)
648 self
._add
_message
_field
(key
, message_value
, body_json
)
650 # Add in values from the body of the request.
651 if request
.body_json
:
652 self
._update
_from
_body
(body_json
, request
.body_json
)
654 request
.body_json
= body_json
655 request
.body
= json
.dumps(request
.body_json
)
658 def transform_jsonrpc_request(self
, orig_request
):
659 """Translates a JsonRpc request/response into apiserving request/response.
662 orig_request: An ApiRequest, the original request from the user.
665 A new request with the request_id updated and params moved to the body.
667 request
= orig_request
.copy()
668 request
.request_id
= request
.body_json
.get('id')
669 request
.body_json
= request
.body_json
.get('params', {})
670 request
.body
= json
.dumps(request
.body_json
)
673 def check_error_response(self
, response
):
674 """Raise an exception if the response from the SPI was an error.
677 response: A ResponseTuple containing the backend response.
680 BackendError if the response is an error.
682 status_code
= int(response
.status
.split(' ', 1)[0])
683 if status_code
>= 300:
684 raise errors
.BackendError(response
)
686 def transform_rest_response(self
, response_body
):
687 """Translates an apiserving REST response so it's ready to return.
689 Currently, the only thing that needs to be fixed here is indentation,
690 so it's consistent with what the live app will return.
693 response_body: A string containing the backend response.
696 A reformatted version of the response JSON.
698 body_json
= json
.loads(response_body
)
699 return json
.dumps(body_json
, indent
=1, sort_keys
=True)
701 def transform_jsonrpc_response(self
, spi_request
, response_body
):
702 """Translates an apiserving response to a JsonRpc response.
705 spi_request: An ApiRequest, the transformed request that was sent to the
707 response_body: A string containing the backend response to transform
711 A string with the updated, JsonRPC-formatted request body.
713 body_json
= {'result': json
.loads(response_body
)}
714 return self
._finish
_rpc
_response
(spi_request
.request_id
,
715 spi_request
.is_batch(), body_json
)
717 def _finish_rpc_response(self
, request_id
, is_batch
, body_json
):
718 """Finish adding information to a JSON RPC response.
721 request_id: None if the request didn't have a request ID. Otherwise, this
722 is a string containing the request ID for the request.
723 is_batch: A boolean indicating whether the request is a batch request.
724 body_json: A dict containing the JSON body of the response.
727 A string with the updated, JsonRPC-formatted request body.
729 if request_id
is not None:
730 body_json
['id'] = request_id
732 body_json
= [body_json
]
733 return json
.dumps(body_json
, indent
=1, sort_keys
=True)
735 def _handle_request_error(self
, orig_request
, error
, start_response
):
736 """Handle a request error, converting it to a WSGI response.
739 orig_request: An ApiRequest, the original request from the user.
740 error: A RequestError containing information about the error.
741 start_response: A function with semantics defined in PEP-333.
744 A string containing the response body.
746 headers
= [('Content-Type', 'application/json')]
747 if orig_request
.is_rpc():
748 # JSON RPC errors are returned with status 200 OK and the
749 # error details in the body.
751 body
= self
._finish
_rpc
_response
(orig_request
.body_json
.get('id'),
752 orig_request
.is_batch(),
755 status_code
= error
.status_code()
756 body
= error
.rest_error()
758 response_status
= '%d %s' % (status_code
,
759 httplib
.responses
.get(status_code
,
761 cors_handler
= EndpointsDispatcher
.__CheckCorsHeaders
(orig_request
)
762 return util
.send_wsgi_response(response_status
, headers
, body
,
763 start_response
, cors_handler
=cors_handler
)