OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 Google Inc. All Rights Reserved. |
| 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with the License. |
| 5 # You may obtain a copy of the License at |
| 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. |
| 14 |
| 15 """Dispatcher middleware for Cloud Endpoints API server. |
| 16 |
| 17 This middleware does simple transforms on requests that come into the base path |
| 18 and then re-dispatches them to the main backend. It does not do any |
| 19 authentication, quota checking, DoS checking, etc. |
| 20 |
| 21 In addition, the middleware loads API configs prior to each call, in case the |
| 22 configuration has changed. |
| 23 """ |
| 24 |
| 25 # pylint: disable=g-bad-name |
| 26 import cStringIO |
| 27 import httplib |
| 28 import json |
| 29 import logging |
| 30 import re |
| 31 import urlparse |
| 32 import wsgiref |
| 33 |
| 34 import api_config_manager |
| 35 import api_request |
| 36 import discovery_api_proxy |
| 37 import discovery_service |
| 38 import errors |
| 39 import parameter_converter |
| 40 import util |
| 41 |
| 42 |
| 43 __all__ = ['EndpointsDispatcherMiddleware'] |
| 44 |
| 45 _SERVER_SOURCE_IP = '0.2.0.3' |
| 46 |
| 47 # Internal constants |
| 48 _CORS_HEADER_ORIGIN = 'Origin' |
| 49 _CORS_HEADER_REQUEST_METHOD = 'Access-Control-Request-Method' |
| 50 _CORS_HEADER_REQUEST_HEADERS = 'Access-Control-Request-Headers' |
| 51 _CORS_HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin' |
| 52 _CORS_HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods' |
| 53 _CORS_HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers' |
| 54 _CORS_HEADER_ALLOW_CREDS = 'Access-Control-Allow-Credentials' |
| 55 _CORS_HEADER_EXPOSE_HEADERS = 'Access-Control-Expose-Headers' |
| 56 _CORS_ALLOWED_METHODS = frozenset(('DELETE', 'GET', 'PATCH', 'POST', 'PUT')) |
| 57 _CORS_EXPOSED_HEADERS = frozenset( |
| 58 ('Content-Encoding', 'Content-Length', 'Date', 'ETag', 'Server') |
| 59 ) |
| 60 |
| 61 |
| 62 class EndpointsDispatcherMiddleware(object): |
| 63 """Dispatcher that handles requests to the built-in apiserver handlers.""" |
| 64 |
| 65 _API_EXPLORER_URL = 'https://apis-explorer.appspot.com/apis-explorer/?base=' |
| 66 |
| 67 def __init__(self, backend_wsgi_app, config_manager=None): |
| 68 """Constructor for EndpointsDispatcherMiddleware. |
| 69 |
| 70 Args: |
| 71 backend_wsgi_app: A WSGI server that serves the app's endpoints. |
| 72 config_manager: An ApiConfigManager instance that allows a caller to |
| 73 set up an existing configuration for testing. |
| 74 """ |
| 75 if config_manager is None: |
| 76 config_manager = api_config_manager.ApiConfigManager() |
| 77 self.config_manager = config_manager |
| 78 |
| 79 self._backend = backend_wsgi_app |
| 80 self._dispatchers = [] |
| 81 for base_path in self._backend.base_paths: |
| 82 self._add_dispatcher('%sexplorer/?$' % base_path, |
| 83 self.handle_api_explorer_request) |
| 84 self._add_dispatcher('%sstatic/.*$' % base_path, |
| 85 self.handle_api_static_request) |
| 86 |
| 87 def _add_dispatcher(self, path_regex, dispatch_function): |
| 88 """Add a request path and dispatch handler. |
| 89 |
| 90 Args: |
| 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. |
| 95 """ |
| 96 self._dispatchers.append((re.compile(path_regex), dispatch_function)) |
| 97 |
| 98 def __call__(self, environ, start_response): |
| 99 """Handle an incoming request. |
| 100 |
| 101 Args: |
| 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. |
| 108 |
| 109 Yields: |
| 110 An iterable over strings containing the body of the HTTP response. |
| 111 """ |
| 112 request = api_request.ApiRequest(environ, |
| 113 base_paths=self._backend.base_paths) |
| 114 |
| 115 # PEP-333 requires that we return an iterator that iterates over the |
| 116 # response body. Yielding the returned body accomplishes this. |
| 117 yield self.dispatch(request, start_response) |
| 118 |
| 119 def dispatch(self, request, start_response): |
| 120 """Handles dispatch to apiserver handlers. |
| 121 |
| 122 This typically ends up calling start_response and returning the entire |
| 123 body of the response. |
| 124 |
| 125 Args: |
| 126 request: An ApiRequest, the request from the user. |
| 127 start_response: A function with semantics defined in PEP-333. |
| 128 |
| 129 Returns: |
| 130 A string, the body of the response. |
| 131 """ |
| 132 # Check if this matches any of our special handlers. |
| 133 dispatched_response = self.dispatch_non_api_requests(request, |
| 134 start_response) |
| 135 if dispatched_response is not None: |
| 136 return dispatched_response |
| 137 |
| 138 # Get API configuration first. We need this so we know how to |
| 139 # call the back end. |
| 140 api_config_response = self.get_api_configs() |
| 141 if api_config_response: |
| 142 self.config_manager.process_api_config_response(api_config_response) |
| 143 else: |
| 144 return self.fail_request(request, 'get_api_configs Error', |
| 145 start_response) |
| 146 |
| 147 # Call the service. |
| 148 try: |
| 149 return self.call_backend(request, start_response) |
| 150 except errors.RequestError as error: |
| 151 return self._handle_request_error(request, error, start_response) |
| 152 |
| 153 def dispatch_non_api_requests(self, request, start_response): |
| 154 """Dispatch this request if this is a request to a reserved URL. |
| 155 |
| 156 If the request matches one of our reserved URLs, this calls |
| 157 start_response and returns the response body. This also handles OPTIONS |
| 158 CORS requests. |
| 159 |
| 160 Args: |
| 161 request: An ApiRequest, the request from the user. |
| 162 start_response: A function with semantics defined in PEP-333. |
| 163 |
| 164 Returns: |
| 165 None if the request doesn't match one of the reserved URLs this |
| 166 handles. Otherwise, returns the response body. |
| 167 """ |
| 168 for path_regex, dispatch_function in self._dispatchers: |
| 169 if path_regex.match(request.relative_url): |
| 170 return dispatch_function(request, start_response) |
| 171 |
| 172 if request.http_method == 'OPTIONS': |
| 173 cors_handler = self._create_cors_handler(request) |
| 174 if cors_handler.allow_cors_request: |
| 175 # The server returns 200 rather than 204, for some reason. |
| 176 return util.send_wsgi_response('200', [], '', start_response, |
| 177 cors_handler) |
| 178 |
| 179 return None |
| 180 |
| 181 def handle_api_explorer_request(self, request, start_response): |
| 182 """Handler for requests to {base_path}/explorer. |
| 183 |
| 184 This calls start_response and returns the response body. |
| 185 |
| 186 Args: |
| 187 request: An ApiRequest, the request from the user. |
| 188 start_response: A function with semantics defined in PEP-333. |
| 189 |
| 190 Returns: |
| 191 A string containing the response body (which is empty, in this case). |
| 192 """ |
| 193 protocol = 'http' if 'localhost' in request.server else 'https' |
| 194 base_path = request.base_path.strip('/') |
| 195 base_url = '{0}://{1}:{2}/{3}'.format( |
| 196 protocol, request.server, request.port, base_path) |
| 197 redirect_url = self._API_EXPLORER_URL + base_url |
| 198 return util.send_wsgi_redirect_response(redirect_url, start_response) |
| 199 |
| 200 def handle_api_static_request(self, request, start_response): |
| 201 """Handler for requests to {base_path}/static/.*. |
| 202 |
| 203 This calls start_response and returns the response body. |
| 204 |
| 205 Args: |
| 206 request: An ApiRequest, the request from the user. |
| 207 start_response: A function with semantics defined in PEP-333. |
| 208 |
| 209 Returns: |
| 210 A string containing the response body. |
| 211 """ |
| 212 discovery_api = discovery_api_proxy.DiscoveryApiProxy() |
| 213 response, body = discovery_api.get_static_file(request.relative_url) |
| 214 status_string = '%d %s' % (response.status, response.reason) |
| 215 if response.status == 200: |
| 216 # Some of the headers that come back from the server can't be passed |
| 217 # along in our response. Specifically, the response from the server has |
| 218 # transfer-encoding: chunked, which doesn't apply to the response that |
| 219 # we're forwarding. There may be other problematic headers, so we strip |
| 220 # off everything but Content-Type. |
| 221 return util.send_wsgi_response(status_string, |
| 222 [('Content-Type', |
| 223 response.getheader('Content-Type'))], |
| 224 body, start_response) |
| 225 else: |
| 226 logging.error('Discovery API proxy failed on %s with %d. Details: %s', |
| 227 request.relative_url, response.status, body) |
| 228 return util.send_wsgi_response(status_string, response.getheaders(), body, |
| 229 start_response) |
| 230 |
| 231 def get_api_configs(self): |
| 232 return self._backend.get_api_configs() |
| 233 |
| 234 @staticmethod |
| 235 def verify_response(response, status_code, content_type=None): |
| 236 """Verifies that a response has the expected status and content type. |
| 237 |
| 238 Args: |
| 239 response: The ResponseTuple to be checked. |
| 240 status_code: An int, the HTTP status code to be compared with response |
| 241 status. |
| 242 content_type: A string with the acceptable Content-Type header value. |
| 243 None allows any content type. |
| 244 |
| 245 Returns: |
| 246 True if both status_code and content_type match, else False. |
| 247 """ |
| 248 status = int(response.status.split(' ', 1)[0]) |
| 249 if status != status_code: |
| 250 return False |
| 251 |
| 252 if content_type is None: |
| 253 return True |
| 254 |
| 255 for header, value in response.headers: |
| 256 if header.lower() == 'content-type': |
| 257 return value == content_type |
| 258 |
| 259 # If we fall through to here, the verification has failed, so return False. |
| 260 return False |
| 261 |
| 262 def prepare_backend_environ(self, host, method, relative_url, headers, body, |
| 263 source_ip, port): |
| 264 """Build an environ object for the backend to consume. |
| 265 |
| 266 Args: |
| 267 host: A string containing the host serving the request. |
| 268 method: A string containing the HTTP method of the request. |
| 269 relative_url: A string containing path and query string of the request. |
| 270 headers: A list of (key, value) tuples where key and value are both |
| 271 strings. |
| 272 body: A string containing the request body. |
| 273 source_ip: The source IP address for the request. |
| 274 port: The port to which to direct the request. |
| 275 |
| 276 Returns: |
| 277 An environ object with all the information necessary for the backend to |
| 278 process the request. |
| 279 """ |
| 280 if isinstance(body, unicode): |
| 281 body = body.encode('ascii') |
| 282 |
| 283 url = urlparse.urlsplit(relative_url) |
| 284 if port != 80: |
| 285 host = '%s:%s' % (host, port) |
| 286 else: |
| 287 host = host |
| 288 environ = {'CONTENT_LENGTH': str(len(body)), |
| 289 'PATH_INFO': url.path, |
| 290 'QUERY_STRING': url.query, |
| 291 'REQUEST_METHOD': method, |
| 292 'REMOTE_ADDR': source_ip, |
| 293 'SERVER_NAME': host, |
| 294 'SERVER_PORT': str(port), |
| 295 'SERVER_PROTOCOL': 'HTTP/1.1', |
| 296 'wsgi.version': (1, 0), |
| 297 'wsgi.url_scheme': 'http', |
| 298 'wsgi.errors': cStringIO.StringIO(), |
| 299 'wsgi.multithread': True, |
| 300 'wsgi.multiprocess': True, |
| 301 'wsgi.input': cStringIO.StringIO(body)} |
| 302 util.put_headers_in_environ(headers, environ) |
| 303 environ['HTTP_HOST'] = host |
| 304 return environ |
| 305 |
| 306 def call_backend(self, orig_request, start_response): |
| 307 """Generate API call (from earlier-saved request). |
| 308 |
| 309 This calls start_response and returns the response body. |
| 310 |
| 311 Args: |
| 312 orig_request: An ApiRequest, the original request from the user. |
| 313 start_response: A function with semantics defined in PEP-333. |
| 314 |
| 315 Returns: |
| 316 A string containing the response body. |
| 317 """ |
| 318 if orig_request.is_rpc(): |
| 319 method_config = self.lookup_rpc_method(orig_request) |
| 320 params = None |
| 321 else: |
| 322 method_config, params = self.lookup_rest_method(orig_request) |
| 323 if not method_config: |
| 324 cors_handler = self._create_cors_handler(orig_request) |
| 325 return util.send_wsgi_not_found_response(start_response, |
| 326 cors_handler=cors_handler) |
| 327 |
| 328 # Prepare the request for the back end. |
| 329 transformed_request = self.transform_request( |
| 330 orig_request, params, method_config) |
| 331 |
| 332 # Check if this call is for the Discovery service. If so, route |
| 333 # it to our Discovery handler. |
| 334 discovery = discovery_service.DiscoveryService( |
| 335 self.config_manager, self._backend) |
| 336 discovery_response = discovery.handle_discovery_request( |
| 337 transformed_request.path, transformed_request, start_response) |
| 338 if discovery_response: |
| 339 return discovery_response |
| 340 |
| 341 url = transformed_request.base_path + transformed_request.path |
| 342 transformed_request.headers['Content-Type'] = 'application/json' |
| 343 transformed_environ = self.prepare_backend_environ( |
| 344 orig_request.server, 'POST', url, transformed_request.headers.items(), |
| 345 transformed_request.body, transformed_request.source_ip, |
| 346 orig_request.port) |
| 347 |
| 348 # Send the transformed request to the backend app and capture the response. |
| 349 with util.StartResponseProxy() as start_response_proxy: |
| 350 body_iter = self._backend(transformed_environ, start_response_proxy.Proxy) |
| 351 status = start_response_proxy.response_status |
| 352 headers = start_response_proxy.response_headers |
| 353 |
| 354 # Get response body |
| 355 body = start_response_proxy.response_body |
| 356 # In case standard WSGI behavior is implemented later... |
| 357 if not body: |
| 358 body = ''.join(body_iter) |
| 359 |
| 360 return self.handle_backend_response(orig_request, transformed_request, |
| 361 status, headers, body, method_config, |
| 362 start_response) |
| 363 |
| 364 class __CheckCorsHeaders(object): |
| 365 """Track information about CORS headers and our response to them.""" |
| 366 |
| 367 def __init__(self, request): |
| 368 self.allow_cors_request = False |
| 369 self.origin = None |
| 370 self.cors_request_method = None |
| 371 self.cors_request_headers = None |
| 372 |
| 373 self.__check_cors_request(request) |
| 374 |
| 375 def __check_cors_request(self, request): |
| 376 """Check for a CORS request, and see if it gets a CORS response.""" |
| 377 # Check for incoming CORS headers. |
| 378 self.origin = request.headers[_CORS_HEADER_ORIGIN] |
| 379 self.cors_request_method = request.headers[_CORS_HEADER_REQUEST_METHOD] |
| 380 self.cors_request_headers = request.headers[ |
| 381 _CORS_HEADER_REQUEST_HEADERS] |
| 382 |
| 383 # Check if the request should get a CORS response. |
| 384 if (self.origin and |
| 385 ((self.cors_request_method is None) or |
| 386 (self.cors_request_method.upper() in _CORS_ALLOWED_METHODS))): |
| 387 self.allow_cors_request = True |
| 388 |
| 389 def update_headers(self, headers_in): |
| 390 """Add CORS headers to the response, if needed.""" |
| 391 if not self.allow_cors_request: |
| 392 return |
| 393 |
| 394 # Add CORS headers. |
| 395 headers = wsgiref.headers.Headers(headers_in) |
| 396 headers[_CORS_HEADER_ALLOW_CREDS] = 'true' |
| 397 headers[_CORS_HEADER_ALLOW_ORIGIN] = self.origin |
| 398 headers[_CORS_HEADER_ALLOW_METHODS] = ','.join(tuple( |
| 399 _CORS_ALLOWED_METHODS)) |
| 400 headers[_CORS_HEADER_EXPOSE_HEADERS] = ','.join(tuple( |
| 401 _CORS_EXPOSED_HEADERS)) |
| 402 if self.cors_request_headers is not None: |
| 403 headers[_CORS_HEADER_ALLOW_HEADERS] = self.cors_request_headers |
| 404 |
| 405 def _create_cors_handler(self, request): |
| 406 return EndpointsDispatcherMiddleware.__CheckCorsHeaders(request) |
| 407 |
| 408 def handle_backend_response(self, orig_request, backend_request, |
| 409 response_status, response_headers, |
| 410 response_body, method_config, start_response): |
| 411 """Handle backend response, transforming output as needed. |
| 412 |
| 413 This calls start_response and returns the response body. |
| 414 |
| 415 Args: |
| 416 orig_request: An ApiRequest, the original request from the user. |
| 417 backend_request: An ApiRequest, the transformed request that was |
| 418 sent to the backend handler. |
| 419 response_status: A string, the status from the response. |
| 420 response_headers: A dict, the headers from the response. |
| 421 response_body: A string, the body of the response. |
| 422 method_config: A dict, the API config of the method to be called. |
| 423 start_response: A function with semantics defined in PEP-333. |
| 424 |
| 425 Returns: |
| 426 A string containing the response body. |
| 427 """ |
| 428 # Verify that the response is json. If it isn't treat, the body as an |
| 429 # error message and wrap it in a json error response. |
| 430 for header, value in response_headers: |
| 431 if (header.lower() == 'content-type' and |
| 432 not value.lower().startswith('application/json')): |
| 433 return self.fail_request(orig_request, |
| 434 'Non-JSON reply: %s' % response_body, |
| 435 start_response) |
| 436 |
| 437 self.check_error_response(response_body, response_status) |
| 438 |
| 439 # Need to check is_rpc() against the original request, because the |
| 440 # incoming request here has had its path modified. |
| 441 if orig_request.is_rpc(): |
| 442 body = self.transform_jsonrpc_response(backend_request, response_body) |
| 443 else: |
| 444 # Check if the response from the API was empty. Empty REST responses |
| 445 # generate a HTTP 204. |
| 446 empty_response = self.check_empty_response(orig_request, method_config, |
| 447 start_response) |
| 448 if empty_response is not None: |
| 449 return empty_response |
| 450 |
| 451 body = self.transform_rest_response(response_body) |
| 452 |
| 453 cors_handler = self._create_cors_handler(orig_request) |
| 454 return util.send_wsgi_response(response_status, response_headers, body, |
| 455 start_response, cors_handler=cors_handler) |
| 456 |
| 457 def fail_request(self, orig_request, message, start_response): |
| 458 """Write an immediate failure response to outfile, no redirect. |
| 459 |
| 460 This calls start_response and returns the error body. |
| 461 |
| 462 Args: |
| 463 orig_request: An ApiRequest, the original request from the user. |
| 464 message: A string containing the error message to be displayed to user. |
| 465 start_response: A function with semantics defined in PEP-333. |
| 466 |
| 467 Returns: |
| 468 A string containing the body of the error response. |
| 469 """ |
| 470 cors_handler = self._create_cors_handler(orig_request) |
| 471 return util.send_wsgi_error_response( |
| 472 message, start_response, cors_handler=cors_handler) |
| 473 |
| 474 def lookup_rest_method(self, orig_request): |
| 475 """Looks up and returns rest method for the currently-pending request. |
| 476 |
| 477 Args: |
| 478 orig_request: An ApiRequest, the original request from the user. |
| 479 |
| 480 Returns: |
| 481 A tuple of (method descriptor, parameters), or (None, None) if no method |
| 482 was found for the current request. |
| 483 """ |
| 484 method_name, method, params = self.config_manager.lookup_rest_method( |
| 485 orig_request.path, orig_request.http_method) |
| 486 orig_request.method_name = method_name |
| 487 return method, params |
| 488 |
| 489 def lookup_rpc_method(self, orig_request): |
| 490 """Looks up and returns RPC method for the currently-pending request. |
| 491 |
| 492 Args: |
| 493 orig_request: An ApiRequest, the original request from the user. |
| 494 |
| 495 Returns: |
| 496 The RPC method descriptor that was found for the current request, or None |
| 497 if none was found. |
| 498 """ |
| 499 if not orig_request.body_json: |
| 500 return None |
| 501 method_name = orig_request.body_json.get('method', '') |
| 502 version = orig_request.body_json.get('apiVersion', '') |
| 503 orig_request.method_name = method_name |
| 504 return self.config_manager.lookup_rpc_method(method_name, version) |
| 505 |
| 506 def transform_request(self, orig_request, params, method_config): |
| 507 """Transforms orig_request to apiserving request. |
| 508 |
| 509 This method uses orig_request to determine the currently-pending request |
| 510 and returns a new transformed request ready to send to the backend. This |
| 511 method accepts a rest-style or RPC-style request. |
| 512 |
| 513 Args: |
| 514 orig_request: An ApiRequest, the original request from the user. |
| 515 params: A dictionary containing path parameters for rest requests, or |
| 516 None for an RPC request. |
| 517 method_config: A dict, the API config of the method to be called. |
| 518 |
| 519 Returns: |
| 520 An ApiRequest that's a copy of the current request, modified so it can |
| 521 be sent to the backend. The path is updated and parts of the body or |
| 522 other properties may also be changed. |
| 523 """ |
| 524 if orig_request.is_rpc(): |
| 525 request = self.transform_jsonrpc_request(orig_request) |
| 526 else: |
| 527 method_params = method_config.get('request', {}).get('parameters', {}) |
| 528 request = self.transform_rest_request(orig_request, params, method_params) |
| 529 request.path = method_config.get('rosyMethod', '') |
| 530 return request |
| 531 |
| 532 def _add_message_field(self, field_name, value, params): |
| 533 """Converts a . delimitied field name to a message field in parameters. |
| 534 |
| 535 This adds the field to the params dict, broken out so that message |
| 536 parameters appear as sub-dicts within the outer param. |
| 537 |
| 538 For example: |
| 539 {'a.b.c': ['foo']} |
| 540 becomes: |
| 541 {'a': {'b': {'c': ['foo']}}} |
| 542 |
| 543 Args: |
| 544 field_name: A string containing the '.' delimitied name to be converted |
| 545 into a dictionary. |
| 546 value: The value to be set. |
| 547 params: The dictionary holding all the parameters, where the value is |
| 548 eventually set. |
| 549 """ |
| 550 if '.' not in field_name: |
| 551 params[field_name] = value |
| 552 return |
| 553 |
| 554 root, remaining = field_name.split('.', 1) |
| 555 sub_params = params.setdefault(root, {}) |
| 556 self._add_message_field(remaining, value, sub_params) |
| 557 |
| 558 def _update_from_body(self, destination, source): |
| 559 """Updates the dictionary for an API payload with the request body. |
| 560 |
| 561 The values from the body should override those already in the payload, but |
| 562 for nested fields (message objects) the values can be combined |
| 563 recursively. |
| 564 |
| 565 Args: |
| 566 destination: A dictionary containing an API payload parsed from the |
| 567 path and query parameters in a request. |
| 568 source: A dictionary parsed from the body of the request. |
| 569 """ |
| 570 for key, value in source.iteritems(): |
| 571 destination_value = destination.get(key) |
| 572 if isinstance(value, dict) and isinstance(destination_value, dict): |
| 573 self._update_from_body(destination_value, value) |
| 574 else: |
| 575 destination[key] = value |
| 576 |
| 577 def transform_rest_request(self, orig_request, params, method_parameters): |
| 578 """Translates a Rest request into an apiserving request. |
| 579 |
| 580 This makes a copy of orig_request and transforms it to apiserving |
| 581 format (moving request parameters to the body). |
| 582 |
| 583 The request can receive values from the path, query and body and combine |
| 584 them before sending them along to the backend. In cases of collision, |
| 585 objects from the body take precedence over those from the query, which in |
| 586 turn take precedence over those from the path. |
| 587 |
| 588 In the case that a repeated value occurs in both the query and the path, |
| 589 those values can be combined, but if that value also occurred in the body, |
| 590 it would override any other values. |
| 591 |
| 592 In the case of nested values from message fields, non-colliding values |
| 593 from subfields can be combined. For example, if '?a.c=10' occurs in the |
| 594 query string and "{'a': {'b': 11}}" occurs in the body, then they will be |
| 595 combined as |
| 596 |
| 597 { |
| 598 'a': { |
| 599 'b': 11, |
| 600 'c': 10, |
| 601 } |
| 602 } |
| 603 |
| 604 before being sent to the backend. |
| 605 |
| 606 Args: |
| 607 orig_request: An ApiRequest, the original request from the user. |
| 608 params: A dict with URL path parameters extracted by the config_manager |
| 609 lookup. |
| 610 method_parameters: A dictionary containing the API configuration for the |
| 611 parameters for the request. |
| 612 |
| 613 Returns: |
| 614 A copy of the current request that's been modified so it can be sent |
| 615 to the backend. The body is updated to include parameters from the |
| 616 URL. |
| 617 """ |
| 618 request = orig_request.copy() |
| 619 body_json = {} |
| 620 |
| 621 # Handle parameters from the URL path. |
| 622 for key, value in params.iteritems(): |
| 623 # Values need to be in a list to interact with query parameter values |
| 624 # and to account for case of repeated parameters |
| 625 body_json[key] = [value] |
| 626 |
| 627 # Add in parameters from the query string. |
| 628 if request.parameters: |
| 629 # For repeated elements, query and path work together |
| 630 for key, value in request.parameters.iteritems(): |
| 631 if key in body_json: |
| 632 body_json[key] = value + body_json[key] |
| 633 else: |
| 634 body_json[key] = value |
| 635 |
| 636 # Validate all parameters we've merged so far and convert any '.' delimited |
| 637 # parameters to nested parameters. We don't use iteritems since we may |
| 638 # modify body_json within the loop. For instance, 'a.b' is not a valid key |
| 639 # and would be replaced with 'a'. |
| 640 for key, value in body_json.items(): |
| 641 current_parameter = method_parameters.get(key, {}) |
| 642 repeated = current_parameter.get('repeated', False) |
| 643 |
| 644 if not repeated: |
| 645 body_json[key] = body_json[key][0] |
| 646 |
| 647 # Order is important here. Parameter names are dot-delimited in |
| 648 # parameters instead of nested in dictionaries as a message field is, so |
| 649 # we need to call transform_parameter_value on them before calling |
| 650 # _add_message_field. |
| 651 body_json[key] = parameter_converter.transform_parameter_value( |
| 652 key, body_json[key], current_parameter) |
| 653 # Remove the old key and try to convert to nested message value |
| 654 message_value = body_json.pop(key) |
| 655 self._add_message_field(key, message_value, body_json) |
| 656 |
| 657 # Add in values from the body of the request. |
| 658 if request.body_json: |
| 659 self._update_from_body(body_json, request.body_json) |
| 660 |
| 661 request.body_json = body_json |
| 662 request.body = json.dumps(request.body_json) |
| 663 return request |
| 664 |
| 665 def transform_jsonrpc_request(self, orig_request): |
| 666 """Translates a JsonRpc request/response into apiserving request/response. |
| 667 |
| 668 Args: |
| 669 orig_request: An ApiRequest, the original request from the user. |
| 670 |
| 671 Returns: |
| 672 A new request with the request_id updated and params moved to the body. |
| 673 """ |
| 674 request = orig_request.copy() |
| 675 request.request_id = request.body_json.get('id') |
| 676 request.body_json = request.body_json.get('params', {}) |
| 677 request.body = json.dumps(request.body_json) |
| 678 return request |
| 679 |
| 680 def check_error_response(self, body, status): |
| 681 """Raise an exception if the response from the backend was an error. |
| 682 |
| 683 Args: |
| 684 body: A string containing the backend response body. |
| 685 status: A string containing the backend response status. |
| 686 |
| 687 Raises: |
| 688 BackendError if the response is an error. |
| 689 """ |
| 690 status_code = int(status.split(' ', 1)[0]) |
| 691 if status_code >= 300: |
| 692 raise errors.BackendError(body, status) |
| 693 |
| 694 def check_empty_response(self, orig_request, method_config, start_response): |
| 695 """If the response from the backend is empty, return a HTTP 204 No Content. |
| 696 |
| 697 Args: |
| 698 orig_request: An ApiRequest, the original request from the user. |
| 699 method_config: A dict, the API config of the method to be called. |
| 700 start_response: A function with semantics defined in PEP-333. |
| 701 |
| 702 Returns: |
| 703 If the backend response was empty, this returns a string containing the |
| 704 response body that should be returned to the user. If the backend |
| 705 response wasn't empty, this returns None, indicating that we should not |
| 706 exit early with a 204. |
| 707 """ |
| 708 response_config = method_config.get('response', {}).get('body') |
| 709 if response_config == 'empty': |
| 710 # The response to this function should be empty. We should return a 204. |
| 711 # Note that it's possible that the backend returned something, but we'll |
| 712 # ignore it. This matches the behavior in the Endpoints server. |
| 713 cors_handler = self._create_cors_handler(orig_request) |
| 714 return util.send_wsgi_no_content_response(start_response, cors_handler) |
| 715 |
| 716 def transform_rest_response(self, response_body): |
| 717 """Translates an apiserving REST response so it's ready to return. |
| 718 |
| 719 Currently, the only thing that needs to be fixed here is indentation, |
| 720 so it's consistent with what the live app will return. |
| 721 |
| 722 Args: |
| 723 response_body: A string containing the backend response. |
| 724 |
| 725 Returns: |
| 726 A reformatted version of the response JSON. |
| 727 """ |
| 728 body_json = json.loads(response_body) |
| 729 return json.dumps(body_json, indent=1, sort_keys=True) |
| 730 |
| 731 def transform_jsonrpc_response(self, backend_request, response_body): |
| 732 """Translates an apiserving response to a JsonRpc response. |
| 733 |
| 734 Args: |
| 735 backend_request: An ApiRequest, the transformed request that was sent to |
| 736 the backend handler. |
| 737 response_body: A string containing the backend response to transform |
| 738 back to JsonRPC. |
| 739 |
| 740 Returns: |
| 741 A string with the updated, JsonRPC-formatted request body. |
| 742 """ |
| 743 body_json = {'result': json.loads(response_body)} |
| 744 return self._finish_rpc_response(backend_request.request_id, |
| 745 backend_request.is_batch(), body_json) |
| 746 |
| 747 def _finish_rpc_response(self, request_id, is_batch, body_json): |
| 748 """Finish adding information to a JSON RPC response. |
| 749 |
| 750 Args: |
| 751 request_id: None if the request didn't have a request ID. Otherwise, this |
| 752 is a string containing the request ID for the request. |
| 753 is_batch: A boolean indicating whether the request is a batch request. |
| 754 body_json: A dict containing the JSON body of the response. |
| 755 |
| 756 Returns: |
| 757 A string with the updated, JsonRPC-formatted request body. |
| 758 """ |
| 759 if request_id is not None: |
| 760 body_json['id'] = request_id |
| 761 if is_batch: |
| 762 body_json = [body_json] |
| 763 return json.dumps(body_json, indent=1, sort_keys=True) |
| 764 |
| 765 def _handle_request_error(self, orig_request, error, start_response): |
| 766 """Handle a request error, converting it to a WSGI response. |
| 767 |
| 768 Args: |
| 769 orig_request: An ApiRequest, the original request from the user. |
| 770 error: A RequestError containing information about the error. |
| 771 start_response: A function with semantics defined in PEP-333. |
| 772 |
| 773 Returns: |
| 774 A string containing the response body. |
| 775 """ |
| 776 headers = [('Content-Type', 'application/json')] |
| 777 if orig_request.is_rpc(): |
| 778 # JSON RPC errors are returned with status 200 OK and the |
| 779 # error details in the body. |
| 780 status_code = 200 |
| 781 body = self._finish_rpc_response(orig_request.body_json.get('id'), |
| 782 orig_request.is_batch(), |
| 783 error.rpc_error()) |
| 784 else: |
| 785 status_code = error.status_code() |
| 786 body = error.rest_error() |
| 787 |
| 788 response_status = '%d %s' % (status_code, |
| 789 httplib.responses.get(status_code, |
| 790 'Unknown Error')) |
| 791 cors_handler = self._create_cors_handler(orig_request) |
| 792 return util.send_wsgi_response(response_status, headers, body, |
| 793 start_response, cors_handler=cors_handler) |
OLD | NEW |