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.
21 """Helper CGI for Apiserver in the development app server.
23 This is a fake apiserver proxy that does simple transforms on requests that
24 come in to /_ah/api and then re-dispatches them to /_ah/spi. It does not do
25 any authentication, quota checking, DoS checking, etc.
27 In addition, the proxy loads api configs from
28 /_ah/spi/BackendService.getApiConfigs prior to making the first call to the
29 backend at /_ah/spi and afterwards if app.yaml is changed.
32 from __future__
import with_statement
46 import simplejson
as json
53 API_SERVING_PATTERN
= '/_ah/api/.*'
59 SPI_ROOT_FORMAT
= 'http://127.0.0.1:%s/_ah/spi/%s'
62 _API_REST_PATH_FORMAT
= '{!name}/{!version}/%s'
63 _PATH_VARIABLE_PATTERN
= r
'[a-zA-Z_][a-zA-Z_.\d]*'
64 _RESERVED_PATH_VARIABLE_PATTERN
= r
'!' + _PATH_VARIABLE_PATTERN
65 _PATH_VALUE_PATTERN
= r
'[^:/?#\[\]{}]*'
66 _CORS_HEADER_ORIGIN
= 'Origin'.lower()
67 _CORS_HEADER_REQUEST_METHOD
= 'Access-Control-Request-Method'.lower()
68 _CORS_HEADER_REQUEST_HEADERS
= 'Access-Control-Request-Headers'.lower()
69 _CORS_HEADER_ALLOW_ORIGIN
= 'Access-Control-Allow-Origin'
70 _CORS_HEADER_ALLOW_METHODS
= 'Access-Control-Allow-Methods'
71 _CORS_HEADER_ALLOW_HEADERS
= 'Access-Control-Allow-Headers'
72 _CORS_ALLOWED_METHODS
= frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT'))
73 _INVALID_ENUM_TEMPLATE
= 'Invalid string value: %r. Allowed values: %r'
76 class RequestRejectionError(Exception):
77 """Base class for rejected requests.
79 To be raised when parsing the request values and comparing them against the
80 generated discovery document.
83 def Message(self
): raise NotImplementedError
84 def Errors(self
): raise NotImplementedError
87 """JSON string representing the rejected value.
89 Calling this will fail on the base class since it relies on Message and
90 Errors being implemented on the class. It is up to a subclass to implement
94 JSON string representing the rejected value.
98 'errors': self
.Errors(),
100 'message': self
.Message(),
105 class EnumRejectionError(RequestRejectionError
):
106 """Custom request rejection exception for enum values."""
109 def __init__(self
, parameter_name
, value
, allowed_values
):
110 """Constructor for EnumRejectionError.
113 parameter_name: String; the name of the enum parameter which had a value
115 value: The actual value passed in for the enum. Usually string.
116 allowed_values: List of strings allowed for the enum.
118 self
.parameter_name
= parameter_name
120 self
.allowed_values
= allowed_values
124 """A descriptive message describing the error."""
125 return _INVALID_ENUM_TEMPLATE
% (self
.value
, self
.allowed_values
)
130 """A list containing the errors associated with the rejection.
132 Intended to mimic those returned from an API in production in Google's API
136 A list with a single element that is a dictionary containing the error
142 'reason': 'invalidParameter',
143 'message': self
.Message(),
144 'locationType': 'parameter',
145 'location': self
.parameter_name
,
150 class ApiRequest(object):
151 """Simple data object representing an API request.
153 Takes an app_server CGI request and environment in the constructor.
154 Parses the request into convenient pieces and stores them as members.
156 API_PREFIX
= '/_ah/api/'
158 def __init__(self
, base_env_dict
, dev_appserver
, request
=None):
162 base_env_dict: Dictionary of CGI environment parameters.
163 dev_appserver: used to call standard SplitURL method.
164 request: AppServerRequest. Can be None.
166 self
.cgi_env
= base_env_dict
168 self
.http_method
= base_env_dict
['REQUEST_METHOD']
169 self
.port
= base_env_dict
['SERVER_PORT']
171 self
.path
, self
.query
= dev_appserver
.SplitURL(request
.relative_url
)
174 self
.body
= request
.infile
.read()
175 for header
in request
.headers
.headers
:
176 header_name
, header_value
= header
.split(':', 1)
177 self
.headers
[header_name
.strip()] = header_value
.strip()
180 self
.path
= self
.API_PREFIX
182 assert self
.path
.startswith(self
.API_PREFIX
)
183 self
.path
= self
.path
[len(self
.API_PREFIX
):]
184 self
.parameters
= cgi
.parse_qs(self
.query
, keep_blank_values
=True)
185 self
.body_obj
= json
.loads(self
.body
) if self
.body
else {}
186 self
.request_id
= None
197 return self
.path
== 'rpc'
200 class DiscoveryApiProxy(object):
201 """Proxies discovery service requests to a known cloud endpoint."""
205 _DISCOVERY_PROXY_HOST
= 'webapis-discovery.appspot.com'
206 _STATIC_PROXY_HOST
= 'webapis-discovery.appspot.com'
207 _DISCOVERY_API_PATH_PREFIX
= '/_ah/api/discovery/v1/'
209 def _DispatchRequest(self
, path
, body
):
210 """Proxies GET request to discovery service API.
213 path: URL path relative to discovery service.
214 body: HTTP POST request body.
217 HTTP response body or None if it failed.
219 full_path
= self
._DISCOVERY
_API
_PATH
_PREFIX
+ path
220 headers
= {'Content-type': 'application/json'}
221 connection
= httplib
.HTTPSConnection(self
._DISCOVERY
_PROXY
_HOST
)
223 connection
.request('POST', full_path
, body
, headers
)
224 response
= connection
.getresponse()
225 response_body
= response
.read()
226 if response
.status
!= 200:
227 logging
.error('Discovery API proxy failed on %s with %d.\r\n'
228 'Request: %s\r\nResponse: %s',
229 full_path
, response
.status
, body
, response_body
)
235 def GenerateDiscoveryDoc(self
, api_config
, api_format
):
236 """Generates a discovery document from an API file.
239 api_config: .api file contents as string.
240 api_format: 'rest' or 'rpc' depending on the which kind of discvoery doc.
243 Discovery doc as JSON string.
246 ValueError: When api_format is invalid.
248 if api_format
not in ['rest', 'rpc']:
249 raise ValueError('Invalid API format')
250 path
= 'apis/generate/' + api_format
251 request_dict
= {'config': json
.dumps(api_config
)}
252 request_body
= json
.dumps(request_dict
)
253 return self
._DispatchRequest
(path
, request_body
)
255 def GenerateDirectory(self
, api_configs
):
256 """Generates an API directory from a list of API files.
259 api_configs: list of strings which are the .api file contents.
262 API directory as JSON string.
264 request_dict
= {'configs': api_configs
}
265 request_body
= json
.dumps(request_dict
)
266 return self
._DispatchRequest
('apis/generate/directory', request_body
)
268 def GetStaticFile(self
, path
):
269 """Returns static content via a GET request.
272 path: URL path after the domain.
275 Tuple of (response, response_body):
276 response: HTTPResponse object.
277 response_body: Response body as string.
279 connection
= httplib
.HTTPSConnection(self
._STATIC
_PROXY
_HOST
)
281 connection
.request('GET', path
, None, {})
282 response
= connection
.getresponse()
283 response_body
= response
.read()
286 return response
, response_body
289 class DiscoveryService(object):
290 """Implements the local devserver discovery service.
292 This has a static minimal version of the discoverable part of the
294 It only handles returning the discovery doc and directory, and ignores
295 directory parameters to filter the results.
297 The discovery docs/directory are created by calling a cloud endpoint
298 discovery service to generate the discovery docs/directory from an .api
299 file/set of .api files.
302 _GET_REST_API
= 'apisdev.getRest'
303 _GET_RPC_API
= 'apisdev.getRpc'
304 _LIST_API
= 'apisdev.list'
309 'discovery.apis.getRest': {
310 'path': 'apis/{api}/{version}/rest',
312 'rosyMethod': _GET_REST_API
,
314 'discovery.apis.getRpc': {
315 'path': 'apis/{api}/{version}/rpc',
317 'rosyMethod': _GET_RPC_API
,
319 'discovery.apis.list': {
322 'rosyMethod': _LIST_API
,
327 def __init__(self
, config_manager
, api_request
, outfile
):
328 """Initializes an instance of the DiscoveryService.
331 config_manager: an instance of ApiConfigManager.
332 api_request: an instance of ApiRequest.
333 outfile: the CGI file object to write the response to.
335 self
._config
_manager
= config_manager
336 self
._params
= json
.loads(api_request
.body
or '{}')
337 self
._outfile
= outfile
338 self
._discovery
_proxy
= DiscoveryApiProxy()
340 def _SendSuccessResponse(self
, response
):
341 """Sends an HTTP 200 json success response.
344 response: Response body as string to return.
347 Sends back an HTTP 200 json success response.
349 headers
= {'Content-Type': 'application/json; charset=UTF-8'}
350 return SendCGIResponse('200', headers
, response
, self
._outfile
)
352 def _GetRpcOrRest(self
, api_format
):
353 """Sends back HTTP response with API directory.
356 api_format: Either 'rest' or 'rpc'. Sends CGI response containing
357 the discovery doc for the api/version.
362 api
= self
._params
['api']
363 version
= self
._params
['version']
364 lookup_key
= (api
, version
)
365 api_config
= self
._config
_manager
.configs
.get(lookup_key
)
367 logging
.warn('No discovery doc for version %s of api %s', version
, api
)
368 SendCGINotFoundResponse(self
._outfile
)
370 doc
= self
._discovery
_proxy
.GenerateDiscoveryDoc(api_config
, api_format
)
372 error_msg
= ('Failed to convert .api to discovery doc for '
373 'version %s of api %s') % (version
, api
)
374 logging
.error('%s', error_msg
)
375 SendCGIErrorResponse(error_msg
, self
._outfile
)
377 self
._SendSuccessResponse
(doc
)
380 return self
._GetRpcOrRest
('rest')
383 return self
._GetRpcOrRest
('rpc')
386 """Sends HTTP response containing the API directory."""
388 for api_config
in self
._config
_manager
.configs
.itervalues():
392 if not api_config
== self
.API_CONFIG
:
393 api_configs
.append(json
.dumps(api_config
))
394 directory
= self
._discovery
_proxy
.GenerateDirectory(api_configs
)
396 logging
.error('Failed to get API directory')
399 SendCGINotFoundResponse(self
._outfile
)
401 self
._SendSuccessResponse
(directory
)
403 def HandleDiscoveryRequest(self
, path
):
404 """Returns the result of a discovery service request.
407 path: the SPI API path
410 JSON string with result of discovery service API request.
412 if path
== self
._GET
_REST
_API
:
414 elif path
== self
._GET
_RPC
_API
:
416 elif path
== self
._LIST
_API
:
423 class ApiConfigManager(object):
424 """Manages loading api configs and method lookup."""
427 self
._rpc
_method
_dict
= {}
428 self
._rest
_methods
= []
432 def HasSpiEndpoint(config
):
433 """Checks if an SPI is registered with this App.
436 config: Parsed app.yaml as an appinfo proto.
439 True if any handler is registered for (/_ah/spi/.*).
441 return any(h
.url
.startswith('/_ah/spi/') for h
in config
.handlers
)
443 def _AddDiscoveryConfig(self
):
444 lookup_key
= (DiscoveryService
.API_CONFIG
['name'],
445 DiscoveryService
.API_CONFIG
['version'])
446 self
.configs
[lookup_key
] = DiscoveryService
.API_CONFIG
448 def ParseApiConfigResponse(self
, body
):
449 """Parses a json api config and registers methods for dispatch.
452 Parses method name, etc for all methods and updates the indexing
453 datastructures with the information.
456 body: body of getApiConfigs response
461 response_obj
= json
.loads(body
)
462 except ValueError, unused_err
:
463 logging
.error('Cannot parse BackendService.getApiConfigs response: %s',
466 self
._AddDiscoveryConfig
()
467 for api_config_json
in response_obj
.get('items', []):
469 config
= json
.loads(api_config_json
)
470 except ValueError, unused_err
:
471 logging
.error('Can not parse API config: %s',
474 lookup_key
= config
.get('name', ''), config
.get('version', '')
475 self
.configs
[lookup_key
] = config
477 for config
in self
.configs
.itervalues():
478 version
= config
.get('version', '')
484 sorted_methods
= self
._GetSortedMethods
(config
.get('methods', {}))
486 for method_name
, method
in sorted_methods
:
487 self
.SaveRpcMethod(method_name
, version
, method
)
488 self
.SaveRestMethod(method_name
, version
, method
)
490 def _GetSortedMethods(self
, methods
):
491 """Get a copy of 'methods' sorted the same way AppEngine sorts them.
494 methods: Json configuration of an API's methods.
497 The same configuration with the methods sorted based on what order
498 they'll be checked by the server.
504 def _SortMethodsComparison(method_info1
, method_info2
):
505 """Sort method info by path and http_method.
508 method_info1: Method name and info for the first method to compare.
509 method_info2: Method name and info for the method to compare to.
512 Negative if the first method should come first, positive if the
513 first method should come after the second. Zero if they're
517 def _ScorePath(path
):
518 """Calculate the score for this path, used for comparisons.
520 Higher scores have priority, and if scores are equal, the path text
521 is sorted alphabetically. Scores are based on the number and location
522 of the constant parts of the path. The server has some special handling
523 for variables with regexes, which we don't handle here.
526 path: The request path that we're calculating a score for.
529 The score for the given path.
537 parts
= path
.split('/')
540 if not part
or part
[0] != '{':
546 score
<<= 31 - len(parts
)
550 path_score1
= _ScorePath(method_info1
[1].get('path', ''))
551 path_score2
= _ScorePath(method_info2
[1].get('path', ''))
552 if path_score1
!= path_score2
:
553 return path_score2
- path_score1
556 path_result
= cmp(method_info1
[1].get('path', ''),
557 method_info2
[1].get('path', ''))
562 method_result
= cmp(method_info1
[1].get('httpMethod', ''),
563 method_info2
[1].get('httpMethod', ''))
566 return sorted(methods
.items(), _SortMethodsComparison
)
569 def _ToSafePathParamName(matched_parameter
):
570 """Creates a safe string to be used as a regex group name.
572 Only alphanumeric characters and underscore are allowed in variable name
573 tokens, and numeric are not allowed as the first character.
575 We cast the matched_parameter to base32 (since the alphabet is safe),
576 strip the padding (= not safe) and prepend with _, since we know a token
577 can begin with underscore.
580 matched_parameter: String; parameter matched from URL template.
583 String, safe to be used as a regex group name.
585 return '_' + base64
.b32encode(matched_parameter
).rstrip('=')
588 def _FromSafePathParamName(safe_parameter
):
589 """Takes a safe regex group name and converts it back to the original value.
591 Only alphanumeric characters and underscore are allowed in variable name
592 tokens, and numeric are not allowed as the first character.
594 The safe_parameter is a base32 representation of the actual value.
597 safe_parameter: String, safe regex group name.
600 String; parameter matched from URL template.
602 assert safe_parameter
.startswith('_')
603 safe_parameter_as_base32
= safe_parameter
[1:]
605 padding_length
= - len(safe_parameter_as_base32
) % 8
606 padding
= '=' * padding_length
607 return base64
.b32decode(safe_parameter_as_base32
+ padding
)
610 def CompilePathPattern(pattern
):
611 """Generates a compiled regex pattern for a path pattern.
613 e.g. '/{!name}/{!version}/notes/{id}'
614 returns re.compile(r'/([^:/?#\[\]{}]*)'
616 r'/notes/(?P<id>[^:/?#\[\]{}]*)')
617 Note in this example that !name and !version are reserved variable names
618 used to match the API name and version that should not be migrated into the
619 method argument namespace. As such they are not named in the regex, so
620 groupdict() excludes them.
623 pattern: parameterized path pattern to be checked
626 compiled regex to match this path pattern
629 def ReplaceReservedVariable(match
):
630 """Replaces a {!variable} with a regex to match it not by name.
633 match: The matching regex group as sent by re.sub()
636 Regex to match the variable by name, if the full pattern was matched.
638 if match
.lastindex
> 1:
639 return '%s(%s)' % (match
.group(1), _PATH_VALUE_PATTERN
)
640 return match
.group(0)
642 def ReplaceVariable(match
):
643 """Replaces a {variable} with a regex to match it by name.
645 Changes the string corresponding to the variable name to the base32
646 representation of the string, prepended by an underscore. This is
647 necessary because we can have message variable names in URL patterns
648 (e.g. via {x.y}) but the character '.' can't be in a regex group name.
651 match: The matching regex group as sent by re.sub()
654 Regex to match the variable by name, if the full pattern was matched.
656 if match
.lastindex
> 1:
657 var_name
= ApiConfigManager
._ToSafePathParamName
(match
.group(2))
658 return '%s(?P<%s>%s)' % (match
.group(1), var_name
,
660 return match
.group(0)
665 pattern
= re
.sub('(/|^){(%s)}(?=/|$)' % _RESERVED_PATH_VARIABLE_PATTERN
,
666 ReplaceReservedVariable
, pattern
, 2)
667 pattern
= re
.sub('(/|^){(%s)}(?=/|$)' % _PATH_VARIABLE_PATTERN
,
668 ReplaceVariable
, pattern
)
669 return re
.compile(pattern
+ '/?$')
671 def SaveRpcMethod(self
, method_name
, version
, method
):
672 """Store JsonRpc api methods in a map for lookup at call time.
674 (rpcMethodName, apiVersion) => method.
677 method_name: Name of the API method
678 version: Version of the API
679 method: method descriptor (as in the api config file).
681 self
._rpc
_method
_dict
[(method_name
, version
)] = method
683 def LookupRpcMethod(self
, method_name
, version
):
684 """Lookup the JsonRPC method at call time.
686 The method is looked up in self._rpc_method_dict, the dictionary that
687 it is saved in for SaveRpcMethod().
690 method_name: String name of the method
691 version: String version of the API
694 Method descriptor as specified in the API configuration.
696 method
= self
._rpc
_method
_dict
.get((method_name
, version
))
699 def SaveRestMethod(self
, method_name
, version
, method
):
700 """Store Rest api methods in a list for lookup at call time.
702 The list is self._rest_methods, a list of tuples:
703 [(<compiled_path>, <path_pattern>, <method_dict>), ...]
705 <compiled_path> is a compiled regex to match against the incoming URL
706 <path_pattern> is a string representing the original path pattern,
707 checked on insertion to prevent duplicates. -and-
708 <method_dict> is a dict (httpMethod, apiVersion) => (method_name, method)
710 This structure is a bit complex, it supports use in two contexts:
712 - SaveRestMethod is called repeatedly, each method will have a path,
713 which we want to be compiled for fast lookup at call time
714 - We want to prevent duplicate incoming path patterns, so store the
715 un-compiled path, not counting on a compiled regex being a stable
716 comparison as it is not documented as being stable for this use.
717 - Need to store the method that will be mapped at calltime.
718 - Different methods may have the same path but different http method.
721 - Quickly scan through the list attempting .match(path) on each
722 compiled regex to find the path that matches.
723 - When a path is matched, look up the API version and method from the
724 request and get the method name and method config for the matching
725 API method and method name.
728 method_name: Name of the API method
729 version: Version of the API
730 method: method descriptor (as in the api config file).
732 path_pattern
= _API_REST_PATH_FORMAT
% method
.get('path', '')
733 http_method
= method
.get('httpMethod', '').lower()
734 for _
, path
, methods
in self
._rest
_methods
:
735 if path
== path_pattern
:
736 methods
[(http_method
, version
)] = method_name
, method
739 self
._rest
_methods
.append(
740 (self
.CompilePathPattern(path_pattern
),
742 {(http_method
, version
): (method_name
, method
)}))
745 def _GetPathParams(match
):
746 """Gets path parameters from a regular expression match.
749 match: _sre.SRE_Match object for a path.
752 A dictionary containing the variable names converted from base64
755 for var_name
, value
in match
.groupdict().iteritems():
756 actual_var_name
= ApiConfigManager
._FromSafePathParamName
(var_name
)
757 result
[actual_var_name
] = value
760 def LookupRestMethod(self
, path
, http_method
):
761 """Look up the rest method at call time.
763 The method is looked up in self._rest_methods, the list it is saved
764 in for SaveRestMethod.
767 path: Path from the URL of the request.
768 http_method: HTTP method of the request.
771 Tuple of (<method name>, <method>, <params>)
773 <method name> is the string name of the method that was matched.
774 <method> is the descriptor as specified in the API configuration. -and-
775 <params> is a dict of path parameters matched in the rest request.
777 for compiled_path_pattern
, unused_path
, methods
in self
._rest
_methods
:
778 match
= compiled_path_pattern
.match(path
)
780 params
= self
._GetPathParams
(match
)
781 version
= match
.group(2)
782 method_key
= (http_method
.lower(), version
)
783 method_name
, method
= methods
.get(method_key
, (None, None))
784 if method
is not None:
787 logging
.warn('No endpoint found for path: %s', path
)
791 return method_name
, method
, params
794 def CreateApiserverDispatcher(config_manager
=None):
795 """Function to create Apiserver dispatcher.
798 config_manager: Allow setting of ApiConfigManager for testing.
801 New dispatcher capable of handling requests to the built-in apiserver
807 from google
.appengine
.tools
import dev_appserver
809 class ApiserverDispatcher(dev_appserver
.URLDispatcher
):
810 """Dispatcher that handles requests to the built-in apiserver handlers."""
812 _API_EXPLORER_URL
= 'https://developers.google.com/apis-explorer/?base='
814 class RequestState(object):
815 """Enum tracking request state."""
823 def __init__(self
, config_manager
=None, *args
, **kwargs
):
826 self
._request
_stage
= self
.RequestState
.INIT
827 self
._is
_batch
= False
828 if config_manager
is None:
829 config_manager
= ApiConfigManager()
830 self
.config_manager
= config_manager
831 self
._dispatchers
= []
832 self
._AddDispatcher
('/_ah/api/explorer/?$',
833 self
.HandleApiExplorerRequest
)
834 self
._AddDispatcher
('/_ah/api/static/.*$',
835 self
.HandleApiStaticRequest
)
836 dev_appserver
.URLDispatcher
.__init
__(self
, *args
, **kwargs
)
838 def _AddDispatcher(self
, path_regex
, dispatch_function
):
839 """Add a request path and dispatch handler.
842 path_regex: Regex path to match against incoming requests.
843 dispatch_function: Function to call for these requests. The function
844 should take (request, outfile, base_env_dict) as arguments and
847 self
._dispatchers
.append((re
.compile(path_regex
), dispatch_function
))
849 def _EndRequest(self
):
850 """End the request and clean up.
852 Sets the request state to END and cleans up any variables that
855 self
._request
_stage
= self
.RequestState
.END
856 self
._is
_batch
= False
859 """Check if the current request is an RPC request.
861 This should only be used after Dispatch, where this info is cached.
864 True if the current request is an RPC. False if not.
866 assert self
._is
_rpc
is not None
869 def DispatchNonApiRequests(self
, request
, outfile
, base_env_dict
):
870 """Dispatch this request if this is a request to a reserved URL.
873 request: AppServerRequest.
874 outfile: The response file.
875 base_env_dict: Dictionary of CGI environment parameters if available.
879 False if the request doesn't match one of the reserved URLs this
880 handles. True if it is handled.
882 for path_regex
, dispatch_function
in self
._dispatchers
:
883 if path_regex
.match(request
.relative_url
):
884 return dispatch_function(request
, outfile
, base_env_dict
)
891 """Handles dispatch to apiserver handlers.
893 base_env_dict should contain at least:
894 REQUEST_METHOD, REMOTE_ADDR, SERVER_SOFTWARE, SERVER_NAME,
895 SERVER_PROTOCOL, SERVER_PORT
898 request: AppServerRequest.
899 outfile: The response file.
900 base_env_dict: Dictionary of CGI environment parameters if available.
904 AppServerRequest internal redirect for normal API calls or
905 None for error conditions (e.g. method not found -> 404) and
906 other calls not requiring the GetApiConfigs redirect.
908 if self
._request
_stage
!= self
.RequestState
.INIT
:
909 return self
.FailRequest('Dispatch in unexpected state', outfile
)
911 if not base_env_dict
:
912 return self
.FailRequest('CGI Environment Not Available', outfile
)
914 if self
.DispatchNonApiRequests(request
, outfile
, base_env_dict
):
918 self
.request
= ApiRequest(base_env_dict
, dev_appserver
, request
)
924 self
._is
_rpc
= self
.request
._IsRpc
()
927 self
._request
_stage
= self
.RequestState
.GET_API_CONFIGS
928 return self
.GetApiConfigs(base_env_dict
, dev_appserver
)
930 def HandleApiExplorerRequest(self
, unused_request
, outfile
, base_env_dict
):
931 """Handler for requests to _ah/api/explorer.
934 unused_request: AppServerRequest.
935 outfile: The response file.
936 base_env_dict: Dictionary of CGI environment parameters
937 if available. Defaults to None.
941 We will redirect these requests to the google apis explorer.
943 base_url
= 'http://%s:%s/_ah/api' % (base_env_dict
['SERVER_NAME'],
944 base_env_dict
['SERVER_PORT'])
945 redirect_url
= self
._API
_EXPLORER
_URL
+ base_url
946 SendCGIRedirectResponse(redirect_url
, outfile
)
949 def HandleApiStaticRequest(self
, request
, outfile
, unused_base_env_dict
):
950 """Handler for requests to _ah/api/static/.*.
953 request: AppServerRequest.
954 outfile: The response file.
955 unused_base_env_dict: Dictionary of CGI environment parameters
956 if available. Defaults to None.
960 We will redirect these requests to an endpoint proxy.
962 discovery_api_proxy
= DiscoveryApiProxy()
963 response
, body
= discovery_api_proxy
.GetStaticFile(request
.relative_url
)
964 if response
.status
== 200:
965 SendCGIResponse('200',
966 {'Content-Type': response
.getheader('Content-Type')},
969 logging
.error('Discovery API proxy failed on %s with %d. Details: %s',
970 request
.relative_url
, response
.status
, body
)
971 SendCGIResponse(response
.status
, dict(response
.getheaders()), body
,
975 def EndRedirect(self
, dispatched_output
, outfile
):
976 """Handle the end of getApiConfigs and SPI complete notification.
978 This EndRedirect is called twice.
980 The first time is upon completion of the BackendService.getApiConfigs()
981 call. After this call, the set of all available methods and their
982 parameters / paths / config is contained in dispatched_output. This is
983 parsed and used to dispatch the request to the SPI backend itself.
985 In order to cause a second dispatch and EndRedirect, this EndRedirect
986 will return an AppServerRequest filled out with the SPI backend request.
988 The second time it is called is upon completion of the call to the SPI
989 backend. After this call, if the initial request (sent in Dispatch, prior
990 to getApiConfigs) is used to reformat the response as needed. This
991 currently only results in changes for JsonRPC requests, where the response
992 body is moved into {'result': response_body_goes_here} and the request id
993 is copied back into the response.
996 dispatched_output: resulting output from the SPI
997 outfile: final output file for this handler
1000 An AppServerRequest for redirect or None for an immediate response.
1002 if self
._request
_stage
== self
.RequestState
.GET_API_CONFIGS
:
1003 if self
.HandleGetApiConfigsResponse(dispatched_output
, outfile
):
1004 return self
.CallSpi(outfile
)
1005 elif self
._request
_stage
== self
.RequestState
.SPI_CALL
:
1006 return self
.HandleSpiResponse(dispatched_output
, outfile
)
1008 return self
.FailRequest('EndRedirect in unexpected state', outfile
)
1010 def GetApiConfigs(self
, cgi_env
, dev_appserver
):
1011 """Makes a call to BackendService.getApiConfigs and parses result.
1014 cgi_env: CGI environment dictionary as passed in by the framework
1015 dev_appserver: dev_appserver instance used to generate AppServerRequest.
1018 AppServerRequest to be returned as an internal redirect to getApiConfigs
1020 request
= ApiRequest(cgi_env
, dev_appserver
)
1021 request
.path
= 'BackendService.getApiConfigs'
1023 return BuildCGIRequest(cgi_env
, request
, dev_appserver
)
1026 def VerifyResponse(response
, status_code
, content_type
=None):
1027 """Verifies that a response has the expected status and content type.
1030 response: Response to be checked.
1031 status_code: HTTP status code to be compared with response status.
1032 content_type: acceptable Content-Type: header value, None allows any.
1035 True if both status_code and content_type match, else False.
1037 if response
.status_code
!= status_code
:
1039 if content_type
is None:
1041 for header
in response
.headers
:
1042 if header
.lower() == 'content-type':
1043 return response
.headers
[header
].lower() == content_type
1048 def ParseCgiResponse(response
):
1049 """Parses a CGI response, returning a headers dict and body.
1052 response: a CGI response
1055 tuple of ({header: header_value, ...}, body)
1058 for header
in response
.headers
.headers
:
1059 header_name
, header_value
= header
.split(':', 1)
1060 header_dict
[header_name
.strip()] = header_value
.strip()
1063 body
= response
.body
.read()
1066 return header_dict
, body
1068 def HandleGetApiConfigsResponse(self
, dispatched_output
, outfile
):
1069 """Parses the result of getApiConfigs, returning True on success.
1072 dispatched_output: Output from the getApiConfigs call handler.
1073 outfile: CGI output handle, used for error conditions.
1076 True on success, False on failure
1078 response
= dev_appserver
.RewriteResponse(dispatched_output
)
1079 if self
.VerifyResponse(response
, 200, 'application/json'):
1080 self
.config_manager
.ParseApiConfigResponse(response
.body
.read())
1083 self
.FailRequest('BackendService.getApiConfigs Error', outfile
)
1086 def CallSpi(self
, outfile
):
1087 """Generate SPI call (from earlier-saved request).
1090 self.request is modified from Rest/JsonRPC format to apiserving format.
1093 outfile: File to write out CGI-style response in case of error.
1096 AppServerRequest for redirect or None to send immediate CGI response.
1099 method_config
= self
.LookupRpcMethod()
1102 method_config
, params
= self
.LookupRestMethod()
1105 self
.TransformRequest(params
, method_config
)
1106 discovery_service
= DiscoveryService(self
.config_manager
,
1107 self
.request
, outfile
)
1109 if not discovery_service
.HandleDiscoveryRequest(self
.request
.path
):
1110 self
._request
_stage
= self
.RequestState
.SPI_CALL
1111 return BuildCGIRequest(self
.request
.cgi_env
, self
.request
,
1113 except RequestRejectionError
, rejection_error
:
1115 return SendCGIRejectedResponse(rejection_error
, outfile
)
1118 cors_handler
= ApiserverDispatcher
.__CheckCorsHeaders
(self
.request
)
1119 return SendCGINotFoundResponse(outfile
, cors_handler
=cors_handler
)
1121 class __CheckCorsHeaders(object):
1122 """Track information about CORS headers and our response to them."""
1124 def __init__(self
, request
):
1125 self
.allow_cors_request
= False
1127 self
.cors_request_method
= None
1128 self
.cors_request_headers
= None
1130 self
.__CheckCorsRequest
(request
)
1132 def __CheckCorsRequest(self
, request
):
1133 """Check for a CORS request, and see if it gets a CORS response."""
1135 for orig_header
, orig_value
in request
.headers
.iteritems():
1136 if orig_header
.lower() == _CORS_HEADER_ORIGIN
:
1137 self
.origin
= orig_value
1138 if orig_header
.lower() == _CORS_HEADER_REQUEST_METHOD
:
1139 self
.cors_request_method
= orig_value
1140 if orig_header
.lower() == _CORS_HEADER_REQUEST_HEADERS
:
1141 self
.cors_request_headers
= orig_value
1145 ((self
.cors_request_method
is None) or
1146 (self
.cors_request_method
.upper() in _CORS_ALLOWED_METHODS
))):
1147 self
.allow_cors_request
= True
1149 def UpdateHeaders(self
, headers
):
1150 """Add CORS headers to the response, if needed."""
1151 if not self
.allow_cors_request
:
1155 headers
[_CORS_HEADER_ALLOW_ORIGIN
] = self
.origin
1156 headers
[_CORS_HEADER_ALLOW_METHODS
] = ','.join(
1157 tuple(_CORS_ALLOWED_METHODS
))
1158 if self
.cors_request_headers
is not None:
1159 headers
[_CORS_HEADER_ALLOW_HEADERS
] = self
.cors_request_headers
1161 def HandleSpiResponse(self
, dispatched_output
, outfile
):
1162 """Handle SPI response, transforming output as needed.
1165 dispatched_output: Response returned by SPI backend.
1166 outfile: File-like object to write transformed result.
1172 response
= dev_appserver
.AppServerResponse(
1173 response_file
=dispatched_output
)
1174 response_headers
, body
= self
.ParseCgiResponse(response
)
1179 for header
, value
in response_headers
.items():
1180 if (header
.lower() == 'content-type' and
1181 not value
.lower().startswith('application/json')):
1182 return self
.FailRequest('Non-JSON reply: %s' % body
, outfile
)
1183 elif header
.lower() not in ('content-length', 'content-type'):
1184 headers
[header
] = value
1187 body
= self
.TransformJsonrpcResponse(body
)
1190 cors_handler
= ApiserverDispatcher
.__CheckCorsHeaders
(self
.request
)
1191 return SendCGIResponse(response
.status_code
, headers
, body
, outfile
,
1192 cors_handler
=cors_handler
)
1194 def FailRequest(self
, message
, outfile
):
1195 """Write an immediate failure response to outfile, no redirect.
1198 message: Error message to be displayed to user (plain text).
1199 outfile: File-like object to write CGI response to.
1206 cors_handler
= ApiserverDispatcher
.__CheckCorsHeaders
(self
.request
)
1209 return SendCGIErrorResponse(message
, outfile
, cors_handler
=cors_handler
)
1211 def LookupRestMethod(self
):
1212 """Looks up and returns rest method for the currently-pending request.
1214 This method uses self.request as the currently-pending request.
1217 tuple of (method, parameters)
1219 method_name
, method
, params
= self
.config_manager
.LookupRestMethod(
1220 self
.request
.path
, self
.request
.http_method
)
1221 self
.request
.method_name
= method_name
1222 return method
, params
1224 def LookupRpcMethod(self
):
1225 """Looks up and returns RPC method for the currently-pending request.
1227 This method uses self.request as the currently-pending request.
1230 RPC method that was found for the current request.
1232 if not self
.request
.body_obj
:
1235 method_name
= self
.request
.body_obj
.get('method', '')
1236 except AttributeError:
1241 if len(self
.request
.body_obj
) != 1:
1242 raise NotImplementedError('Batch requests with more than 1 element '
1243 'not supported in dev_appserver. Found '
1244 '%d elements.' % len(self
.request
.body_obj
))
1245 logging
.info('Converting batch request to single request.')
1246 self
.request
.body_obj
= self
.request
.body_obj
[0]
1247 method_name
= self
.request
.body_obj
.get('method', '')
1248 self
._is
_batch
= True
1250 version
= self
.request
.body_obj
.get('apiVersion', '')
1251 self
.request
.method_name
= method_name
1252 return self
.config_manager
.LookupRpcMethod(method_name
, version
)
1254 def TransformRequest(self
, params
, method_config
):
1255 """Transforms self.request to apiserving request.
1257 This method uses self.request to determine the currently-pending request.
1258 This method accepts a rest-style or RPC-style request.
1261 Updates self.request to apiserving format. (e.g. updating path to be the
1262 method name, and moving request parameters to the body.)
1265 params: Path parameters dictionary for rest request
1266 method_config: API config of the method to be called
1269 self
.TransformJsonrpcRequest()
1271 method_params
= method_config
.get('request', {}).get('parameters', {})
1272 self
.TransformRestRequest(params
, method_params
)
1273 self
.request
.path
= method_config
.get('rosyMethod', '')
1275 def _CheckEnum(self
, parameter_name
, value
, field_parameter
):
1276 """Checks if the parameter value is valid if an enum.
1278 If the parameter is not an enum, does nothing. If it is, verifies that
1282 parameter_name: String; The name of the parameter, which is either just
1283 a variable name or the name with the index appended. For example 'var'
1285 value: String or list of Strings; The value(s) to be used as enum(s) for
1287 field_parameter: The dictionary containing information specific to the
1288 field in question. This is retrieved from request.parameters in the
1292 EnumRejectionError: If the given value is not among the accepted
1293 enum values in the field parameter.
1295 if 'enum' not in field_parameter
:
1298 enum_values
= [enum
['backendValue']
1299 for enum
in field_parameter
['enum'].values()
1300 if 'backendValue' in enum
]
1301 if value
not in enum_values
:
1302 raise EnumRejectionError(parameter_name
, value
, enum_values
)
1304 def _CheckParameter(self
, parameter_name
, value
, field_parameter
):
1305 """Checks if the parameter value is valid against all parameter rules.
1307 First checks if the value is a list and recursively calls _CheckParameter
1308 on the values in the list. Otherwise, checks all parameter rules for the
1311 In the list case, '[index-of-value]' is appended to the parameter name for
1312 error reporting purposes.
1314 Currently only checks if value adheres to enum rule, but more can be
1318 parameter_name: String; The name of the parameter, which is either just
1319 a variable name or the name with the index appended in the recursive
1320 case. For example 'var' or 'var[2]'.
1321 value: String or List of values; The value(s) to be used for the
1323 field_parameter: The dictionary containing information specific to the
1324 field in question. This is retrieved from request.parameters in the
1327 if isinstance(value
, list):
1328 for index
, element
in enumerate(value
):
1329 parameter_name_index
= '%s[%d]' % (parameter_name
, index
)
1330 self
._CheckParameter
(parameter_name_index
, element
, field_parameter
)
1333 self
._CheckEnum
(parameter_name
, value
, field_parameter
)
1335 def _AddMessageField(self
, field_name
, value
, params
):
1336 """Converts a . delimitied field name to a message field in parameters.
1341 {'a': {'b': {'c': ['foo']}}}
1344 field_name: String; the . delimitied name to be converted into a
1346 value: The value to be set.
1347 params: The dictionary holding all the parameters, where the value is
1350 if '.' not in field_name
:
1351 params
[field_name
] = value
1354 root
, remaining
= field_name
.split('.', 1)
1355 sub_params
= params
.setdefault(root
, {})
1356 self
._AddMessageField
(remaining
, value
, sub_params
)
1358 def _UpdateFromBody(self
, destination
, source
):
1359 """Updates the dictionary for an API payload with the request body.
1361 The values from the body should override those already in the payload, but
1362 for nested fields (message objects), the values can be combined
1366 destination: A dictionary containing an API payload parsed from the
1367 path and query parameters in a request.
1368 source: The object parsed from the body of the request.
1370 for key
, value
in source
.iteritems():
1371 destination_value
= destination
.get(key
)
1372 if isinstance(value
, dict) and isinstance(destination_value
, dict):
1373 self
._UpdateFromBody
(destination_value
, value
)
1375 destination
[key
] = value
1377 def TransformRestRequest(self
, params
, method_parameters
):
1378 """Translates a Rest request/response into an apiserving request/response.
1380 The request can receive values from the path, query and body and combine
1381 them before sending them along to the SPI server. In cases of collision,
1382 objects from the body take precedence over those from the query, which in
1383 turn take precedence over those from the path.
1385 In the case that a repeated value occurs in both the query and the path,
1386 those values can be combined, but if that value also occurred in the body,
1387 it would override any other values.
1389 In the case of nested values from message fields, non-colliding values
1390 from subfields can be combined. For example, if '?a.c=10' occurs in the
1391 query string and "{'a': {'b': 11}}" occurs in the body, then they will be
1401 before being sent to the SPI server.
1404 Updates self.request to apiserving format. (e.g. updating path to be the
1405 method name, and moving request parameters to the body.)
1408 params: URL path parameter dict extracted by config_manager lookup.
1409 method_parameters: Dictionary; The parameters for the request from the
1410 API config of the method.
1414 for key
, value
in params
.iteritems():
1417 body_obj
[key
] = [value
]
1419 if self
.request
.parameters
:
1421 for key
, value
in self
.request
.parameters
.iteritems():
1423 body_obj
[key
] = value
+ body_obj
[key
]
1425 body_obj
[key
] = value
1429 for key
, value
in body_obj
.items():
1430 current_parameter
= method_parameters
.get(key
, {})
1431 repeated
= current_parameter
.get('repeated', False)
1434 body_obj
[key
] = body_obj
[key
][0]
1442 self
._CheckParameter
(key
, body_obj
[key
], current_parameter
)
1444 message_value
= body_obj
.pop(key
)
1445 self
._AddMessageField
(key
, message_value
, body_obj
)
1447 if self
.request
.body_obj
:
1448 self
._UpdateFromBody
(body_obj
, self
.request
.body_obj
)
1450 self
.request
.body_obj
= body_obj
1451 self
.request
.body
= json
.dumps(body_obj
)
1453 def TransformJsonrpcRequest(self
):
1454 """Translates a JsonRpc request/response into apiserving request/response.
1457 Updates self.request to apiserving format. (e.g. updating path to be the
1458 method name, and moving request parameters to the body.)
1460 body_obj
= json
.loads(self
.request
.body
) if self
.request
.body
else {}
1462 self
.request
.request_id
= body_obj
.get('id')
1463 except AttributeError:
1468 assert self
._is
_batch
1469 if len(body_obj
) != 1:
1470 raise NotImplementedError('Batch requests with more than 1 element '
1471 'not supported in dev_appserver. Found '
1472 '%d elements.' % len(self
.request
.body_obj
))
1473 body_obj
= body_obj
[0]
1474 self
.request
.request_id
= body_obj
.get('id')
1475 body_obj
= body_obj
.get('params', {})
1476 self
.request
.body
= json
.dumps(body_obj
)
1478 def TransformJsonrpcResponse(self
, response_body
):
1479 """Translates a apiserving response to a JsonRpc response.
1482 Updates self.request to JsonRpc format. (e.g. restoring request id
1483 and moving body object into {'result': body_obj}
1486 response_body: Backend response to transform back to JsonRPC
1489 Updated, JsonRPC-formatted request body
1491 body_obj
= {'result': json
.loads(response_body
)}
1492 if self
.request
.request_id
is not None:
1493 body_obj
['id'] = self
.request
.request_id
1495 body_obj
= [body_obj
]
1496 return json
.dumps(body_obj
)
1498 return ApiserverDispatcher(config_manager
)
1501 def BuildCGIRequest(base_env_dict
, request
, dev_appserver
):
1502 """Build a CGI request to Call a method on an SPI backend.
1505 base_env_dict: CGI environment dict
1506 request: ApiRequest to be converted to a CGI request
1507 dev_appserver: Handle to dev_appserver to generate CGI request.
1510 dev_appserver.AppServerRequest internal redirect object
1512 if request
.headers
is None:
1513 request
.headers
= {}
1516 request
.headers
['Content-Type'] = 'application/json'
1517 url
= SPI_ROOT_FORMAT
% (request
.port
, request
.path
)
1518 base_env_dict
['REQUEST_METHOD'] = 'POST'
1525 header_outfile
= cStringIO
.StringIO()
1526 body_outfile
= cStringIO
.StringIO()
1527 WriteHeaders(request
.headers
, header_outfile
, len(request
.body
))
1528 body_outfile
.write(request
.body
)
1529 header_outfile
.seek(0)
1530 body_outfile
.seek(0)
1531 return dev_appserver
.AppServerRequest(
1532 url
, None, mimetools
.Message(header_outfile
), body_outfile
)
1535 def WriteHeaders(headers
, outfile
, content_len
=None):
1536 """Write headers to the output file, updating content length if needed.
1539 headers: Header dict to be written
1540 outfile: File-like object to send headers to
1541 content_len: Optional updated content length to update content-length with
1543 wrote_content_length
= False
1544 for header
, value
in headers
.iteritems():
1545 if header
.lower() == 'content-length' and content_len
is not None:
1547 wrote_content_length
= True
1548 outfile
.write('%s: %s\r\n' % (header
, value
))
1549 if not wrote_content_length
and content_len
:
1550 outfile
.write('Content-Length: %s\r\n' % content_len
)
1553 def SendCGINotFoundResponse(outfile
, cors_handler
=None):
1554 SendCGIResponse('404', {'Content-Type': 'text/plain'}, 'Not Found', outfile
,
1555 cors_handler
=cors_handler
)
1558 def SendCGIErrorResponse(message
, outfile
, cors_handler
=None):
1559 body
= json
.dumps({'error': {'message': message
}})
1560 SendCGIResponse('500', {'Content-Type': 'application/json'}, body
, outfile
,
1561 cors_handler
=cors_handler
)
1564 def SendCGIRejectedResponse(rejection_error
, outfile
, cors_handler
=None):
1565 body
= rejection_error
.ToJson()
1566 SendCGIResponse('400', {'Content-Type': 'application/json'}, body
, outfile
,
1567 cors_handler
=cors_handler
)
1570 def SendCGIRedirectResponse(redirect_location
, outfile
, cors_handler
=None):
1571 SendCGIResponse('302', {'Location': redirect_location
}, None, outfile
,
1572 cors_handler
=cors_handler
)
1575 def SendCGIResponse(status
, headers
, content
, outfile
, cors_handler
=None):
1576 """Dump reformatted response to CGI outfile.
1579 status: HTTP status code to send
1580 headers: Headers dictionary {header_name: header_value, ...}
1581 content: Body content to write
1582 outfile: File-like object where response will be written.
1583 cors_handler: A handler to process CORS request headers and update the
1584 headers in the response. Or this can be None, to bypass CORS checks.
1590 cors_handler
.UpdateHeaders(headers
)
1592 outfile
.write('Status: %s\r\n' % status
)
1593 WriteHeaders(headers
, outfile
, len(content
) if content
else None)
1594 outfile
.write('\r\n')
1596 outfile
.write(content
)