| Index: third_party/google-endpoints/google/api/control/wsgi.py
|
| diff --git a/third_party/google-endpoints/google/api/control/wsgi.py b/third_party/google-endpoints/google/api/control/wsgi.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ff8b88bc82851ea52492f2339510b3878ea948d7
|
| --- /dev/null
|
| +++ b/third_party/google-endpoints/google/api/control/wsgi.py
|
| @@ -0,0 +1,624 @@
|
| +# Copyright 2016 Google Inc. All Rights Reserved.
|
| +#
|
| +# Licensed under the Apache License, Version 2.0 (the "License");
|
| +# you may not use this file except in compliance with the License.
|
| +# You may obtain a copy of the License at
|
| +#
|
| +# http://www.apache.org/licenses/LICENSE-2.0
|
| +#
|
| +# Unless required by applicable law or agreed to in writing, software
|
| +# distributed under the License is distributed on an "AS IS" BASIS,
|
| +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| +# See the License for the specific language governing permissions and
|
| +# limitations under the License.
|
| +
|
| +"""wsgi implement behaviour that provides service control as wsgi
|
| +middleware.
|
| +
|
| +It provides the :class:`Middleware`, which is a WSGI middleware implementation
|
| +that wraps another WSGI application to uses a provided
|
| +:class:`google.api.control.client.Client` to provide service control.
|
| +
|
| +"""
|
| +#pylint: disable=too-many-arguments
|
| +
|
| +from __future__ import absolute_import
|
| +
|
| +from datetime import datetime
|
| +import httplib
|
| +import logging
|
| +import os
|
| +import socket
|
| +import uuid
|
| +import urllib2
|
| +import urlparse
|
| +import wsgiref.util
|
| +
|
| +from google.api.auth import suppliers, tokens
|
| +from . import check_request, messages, report_request, service
|
| +
|
| +
|
| +logger = logging.getLogger(__name__)
|
| +
|
| +
|
| +_CONTENT_LENGTH = 'content-length'
|
| +_DEFAULT_LOCATION = 'global'
|
| +
|
| +_METADATA_SERVER_URL = 'http://metadata.google.internal'
|
| +
|
| +
|
| +def _running_on_gce():
|
| + headers = {'Metadata-Flavor': 'Google'}
|
| +
|
| + try:
|
| + request = urllib2.Request(_METADATA_SERVER_URL, headers=headers)
|
| + response = urllib2.urlopen(request)
|
| + if response.info().getheader('Metadata-Flavor') == 'Google':
|
| + return True
|
| + except (urllib2.URLError, socket.error):
|
| + pass
|
| +
|
| + return False
|
| +
|
| +
|
| +def _get_platform():
|
| + server_software = os.environ.get('SERVER_SOFTWARE', '')
|
| +
|
| + if server_software.startswith('Development'):
|
| + return report_request.ReportedPlatforms.DEVELOPMENT
|
| + elif os.environ.get('KUBERNETES_SERVICE_HOST'):
|
| + return report_request.ReportedPlatforms.GKE
|
| + elif _running_on_gce():
|
| + # We're either in GAE Flex or GCE
|
| + if os.environ.get('GAE_MODULE_NAME'):
|
| + return report_request.ReportedPlatforms.GAE_FLEX
|
| + else:
|
| + return report_request.ReportedPlatforms.GCE
|
| + elif os.environ.get('GAE_MODULE_NAME'):
|
| + return report_request.ReportedPlatforms.GAE_STANDARD
|
| +
|
| + return report_request.ReportedPlatforms.UNKNOWN
|
| +
|
| +
|
| +platform = _get_platform()
|
| +
|
| +
|
| +def running_on_devserver():
|
| + return platform == report_request.ReportedPlatforms.DEVELOPMENT
|
| +
|
| +
|
| +def add_all(application, project_id, control_client,
|
| + loader=service.Loaders.FROM_SERVICE_MANAGEMENT):
|
| + """Adds all endpoints middleware to a wsgi application.
|
| +
|
| + Sets up application to use all default endpoints middleware.
|
| +
|
| + Example:
|
| +
|
| + >>> application = MyWsgiApp() # an existing WSGI application
|
| + >>>
|
| + >>> # the name of the controlled service
|
| + >>> service_name = 'my-service-name'
|
| + >>>
|
| + >>> # A GCP project with service control enabled
|
| + >>> project_id = 'my-project-id'
|
| + >>>
|
| + >>> # wrap the app for service control
|
| + >>> from google.api.control import wsgi
|
| + >>> control_client = client.Loaders.DEFAULT.load(service_name)
|
| + >>> control_client.start()
|
| + >>> wrapped_app = add_all(application, project_id, control_client)
|
| + >>>
|
| + >>> # now use wrapped_app in place of app
|
| +
|
| + Args:
|
| + application: the wrapped wsgi application
|
| + project_id: the project_id thats providing service control support
|
| + control_client: the service control client instance
|
| + loader (:class:`google.api.control.service.Loader`): loads the service
|
| + instance that configures this instance's behaviour
|
| + """
|
| + a_service = loader.load()
|
| + if not a_service:
|
| + raise ValueError("Failed to load service config")
|
| + authenticator = _create_authenticator(a_service)
|
| +
|
| + wrapped_app = Middleware(application, project_id, control_client)
|
| + if authenticator:
|
| + wrapped_app = AuthenticationMiddleware(wrapped_app, authenticator)
|
| + return EnvironmentMiddleware(wrapped_app, a_service)
|
| +
|
| +
|
| +def _next_operation_uuid():
|
| + return uuid.uuid4().hex
|
| +
|
| +
|
| +class EnvironmentMiddleware(object):
|
| + """A WSGI middleware that sets related variables in the environment.
|
| +
|
| + It attempts to add the following vars:
|
| +
|
| + - google.api.config.service
|
| + - google.api.config.service_name
|
| + - google.api.config.method_registry
|
| + - google.api.config.reporting_rules
|
| + - google.api.config.method_info
|
| + """
|
| + # pylint: disable=too-few-public-methods
|
| +
|
| + SERVICE = 'google.api.config.service'
|
| + SERVICE_NAME = 'google.api.config.service_name'
|
| + METHOD_REGISTRY = 'google.api.config.method_registry'
|
| + METHOD_INFO = 'google.api.config.method_info'
|
| + REPORTING_RULES = 'google.api.config.reporting_rules'
|
| +
|
| + def __init__(self, application, a_service):
|
| + """Initializes a new Middleware instance.
|
| +
|
| + Args:
|
| + application: the wrapped wsgi application
|
| + a_service (:class:`google.api.gen.servicecontrol_v1_messages.Service`):
|
| + a service instance
|
| + """
|
| + if not isinstance(a_service, messages.Service):
|
| + raise ValueError("service is None or not an instance of Service")
|
| +
|
| + self._application = application
|
| + self._service = a_service
|
| +
|
| + method_registry, reporting_rules = self._configure()
|
| + self._method_registry = method_registry
|
| + self._reporting_rules = reporting_rules
|
| +
|
| + def _configure(self):
|
| + registry = service.MethodRegistry(self._service)
|
| + logs, metric_names, label_names = service.extract_report_spec(self._service)
|
| + reporting_rules = report_request.ReportingRules.from_known_inputs(
|
| + logs=logs,
|
| + metric_names=metric_names,
|
| + label_names=label_names)
|
| +
|
| + return registry, reporting_rules
|
| +
|
| + def __call__(self, environ, start_response):
|
| + environ[self.SERVICE] = self._service
|
| + environ[self.SERVICE_NAME] = self._service.name
|
| + environ[self.METHOD_REGISTRY] = self._method_registry
|
| + environ[self.REPORTING_RULES] = self._reporting_rules
|
| + parsed_uri = urlparse.urlparse(wsgiref.util.request_uri(environ))
|
| + http_method = environ.get('REQUEST_METHOD')
|
| + method_info = self._method_registry.lookup(http_method, parsed_uri.path)
|
| + if method_info:
|
| + environ[self.METHOD_INFO] = method_info
|
| +
|
| + return self._application(environ, start_response)
|
| +
|
| +
|
| +class Middleware(object):
|
| + """A WSGI middleware implementation that provides service control.
|
| +
|
| + Example:
|
| +
|
| + >>> app = MyWsgiApp() # an existing WSGI application
|
| + >>>
|
| + >>> # the name of the controlled service
|
| + >>> service_name = 'my-service-name'
|
| + >>>
|
| + >>> # A GCP project with service control enabled
|
| + >>> project_id = 'my-project-id'
|
| + >>>
|
| + >>> # wrap the app for service control
|
| + >>> from google.api.control import client, wsgi, service
|
| + >>> control_client = client.Loaders.DEFAULT.load(service_name)
|
| + >>> control_client.start()
|
| + >>> wrapped_app = wsgi.Middleware(app, control_client, project_id)
|
| + >>> env_app = wsgi.EnvironmentMiddleware(wrapped,app)
|
| + >>>
|
| + >>> # now use env_app in place of app
|
| +
|
| + """
|
| + # pylint: disable=too-few-public-methods, fixme
|
| + _NO_API_KEY_MSG = (
|
| + 'Method does not allow callers without established identity.'
|
| + ' Please use an API key or other form of API consumer identity'
|
| + ' to call this API.'
|
| + )
|
| +
|
| + def __init__(self,
|
| + application,
|
| + project_id,
|
| + control_client,
|
| + next_operation_id=_next_operation_uuid,
|
| + timer=datetime.utcnow):
|
| + """Initializes a new Middleware instance.
|
| +
|
| + Args:
|
| + application: the wrapped wsgi application
|
| + project_id: the project_id thats providing service control support
|
| + control_client: the service control client instance
|
| + next_operation_id (func): produces the next operation
|
| + timer (func[[datetime.datetime]]): a func that obtains the current time
|
| + """
|
| + self._application = application
|
| + self._project_id = project_id
|
| + self._next_operation_id = next_operation_id
|
| + self._control_client = control_client
|
| + self._timer = timer
|
| +
|
| + def __call__(self, environ, start_response):
|
| + # pylint: disable=too-many-locals
|
| + method_info = environ.get(EnvironmentMiddleware.METHOD_INFO)
|
| + if not method_info:
|
| + # just allow the wrapped application to handle the request
|
| + logger.debug('method_info not present in the wsgi environment'
|
| + ', no service control')
|
| + return self._application(environ, start_response)
|
| +
|
| + latency_timer = _LatencyTimer(self._timer)
|
| + latency_timer.start()
|
| +
|
| + # Determine if the request can proceed
|
| + http_method = environ.get('REQUEST_METHOD')
|
| + parsed_uri = urlparse.urlparse(wsgiref.util.request_uri(environ))
|
| + app_info = _AppInfo()
|
| + # TODO: determine if any of the more complex ways of getting the request size
|
| + # (e.g) buffering and counting the wsgi input stream is more appropriate here
|
| + try:
|
| + app_info.request_size = int(environ.get('CONTENT_LENGTH',
|
| + report_request.SIZE_NOT_SET))
|
| + except ValueError:
|
| + logger.warn('ignored bad content-length: %s', environ.get('CONTENT_LENGTH'))
|
| +
|
| + app_info.http_method = http_method
|
| + app_info.url = parsed_uri
|
| +
|
| + check_info = self._create_check_info(method_info, parsed_uri, environ)
|
| + if not check_info.api_key and not method_info.allow_unregistered_calls:
|
| + logger.debug("skipping %s, no api key was provided", parsed_uri)
|
| + error_msg = self._handle_missing_api_key(app_info, start_response)
|
| + else:
|
| + check_req = check_info.as_check_request()
|
| + logger.debug('checking %s with %s', method_info, check_request)
|
| + check_resp = self._control_client.check(check_req)
|
| + error_msg = self._handle_check_response(app_info, check_resp, start_response)
|
| +
|
| + if error_msg:
|
| + # send a report request that indicates that the request failed
|
| + rules = environ.get(EnvironmentMiddleware.REPORTING_RULES)
|
| + latency_timer.end()
|
| + report_req = self._create_report_request(method_info,
|
| + check_info,
|
| + app_info,
|
| + latency_timer,
|
| + rules)
|
| + logger.debug('scheduling report_request %s', report_req)
|
| + self._control_client.report(report_req)
|
| + return error_msg
|
| +
|
| + # update the client with the response
|
| + latency_timer.app_start()
|
| +
|
| + # run the application request in an inner handler that sets the status
|
| + # and response code on app_info
|
| + def inner_start_response(status, response_headers, exc_info=None):
|
| + app_info.response_code = int(status.partition(' ')[0])
|
| + for name, value in response_headers:
|
| + if name.lower() == _CONTENT_LENGTH:
|
| + app_info.response_size = int(value)
|
| + break
|
| + return start_response(status, response_headers, exc_info)
|
| +
|
| + result = self._application(environ, inner_start_response)
|
| +
|
| + # perform reporting, result must be joined otherwise the latency record
|
| + # is incorrect
|
| + result = b''.join(result)
|
| + latency_timer.end()
|
| + app_info.response_size = len(result)
|
| + rules = environ.get(EnvironmentMiddleware.REPORTING_RULES)
|
| + report_req = self._create_report_request(method_info,
|
| + check_info,
|
| + app_info,
|
| + latency_timer,
|
| + rules)
|
| + logger.debug('scheduling report_request %s', report_req)
|
| + self._control_client.report(report_req)
|
| + return result
|
| +
|
| + def _create_report_request(self,
|
| + method_info,
|
| + check_info,
|
| + app_info,
|
| + latency_timer,
|
| + reporting_rules):
|
| + # TODO: determine how to obtain the consumer_project_id and the location
|
| + # correctly
|
| + report_info = report_request.Info(
|
| + api_key=check_info.api_key,
|
| + api_key_valid=app_info.api_key_valid,
|
| + api_method=method_info.selector,
|
| + consumer_project_id=self._project_id, # TODO: see above
|
| + location=_DEFAULT_LOCATION, # TODO: see above
|
| + method=app_info.http_method,
|
| + operation_id=check_info.operation_id,
|
| + operation_name=check_info.operation_name,
|
| + backend_time=latency_timer.backend_time,
|
| + overhead_time=latency_timer.overhead_time,
|
| + platform=platform,
|
| + producer_project_id=self._project_id,
|
| + protocol=report_request.ReportedProtocols.HTTP,
|
| + request_size=app_info.request_size,
|
| + request_time=latency_timer.request_time,
|
| + response_code=app_info.response_code,
|
| + response_size=app_info.response_size,
|
| + referer=check_info.referer,
|
| + service_name=check_info.service_name,
|
| + url=app_info.url
|
| + )
|
| + return report_info.as_report_request(reporting_rules, timer=self._timer)
|
| +
|
| + def _create_check_info(self, method_info, parsed_uri, environ):
|
| + service_name = environ.get(EnvironmentMiddleware.SERVICE_NAME)
|
| + operation_id = self._next_operation_id()
|
| + api_key_valid = False
|
| + api_key = _find_api_key_param(method_info, parsed_uri)
|
| + if not api_key:
|
| + api_key = _find_api_key_header(method_info, environ)
|
| + if not api_key:
|
| + api_key = _find_default_api_key_param(parsed_uri)
|
| +
|
| + if api_key:
|
| + api_key_valid = True
|
| +
|
| + check_info = check_request.Info(
|
| + api_key=api_key,
|
| + api_key_valid=api_key_valid,
|
| + client_ip=environ.get('REMOTE_ADDR', ''),
|
| + consumer_project_id=self._project_id, # TODO: switch this to producer_project_id
|
| + operation_id=operation_id,
|
| + operation_name=method_info.selector,
|
| + referer=environ.get('HTTP_REFERER', ''),
|
| + service_name=service_name
|
| + )
|
| + return check_info
|
| +
|
| + def _handle_check_response(self, app_info, check_resp, start_response):
|
| + code, detail, api_key_valid = check_request.convert_response(
|
| + check_resp, self._project_id)
|
| + if code == httplib.OK:
|
| + return None # the check was OK
|
| +
|
| + # there was problem; the request cannot proceed
|
| + logger.warn('Check failed %d, %s', code, detail)
|
| + error_msg = '%d %s' % (code, detail)
|
| + start_response(error_msg, [])
|
| + app_info.response_code = code
|
| + app_info.api_key_valid = api_key_valid
|
| + return error_msg # the request cannot continue
|
| +
|
| + def _handle_missing_api_key(self, app_info, start_response):
|
| + code = httplib.UNAUTHORIZED
|
| + detail = self._NO_API_KEY_MSG
|
| + logger.warn('Check not performed %d, %s', code, detail)
|
| + error_msg = '%d %s' % (code, detail)
|
| + start_response(error_msg, [])
|
| + app_info.response_code = code
|
| + app_info.api_key_valid = False
|
| + return error_msg # the request cannot continue
|
| +
|
| +
|
| +class _AppInfo(object):
|
| + # pylint: disable=too-few-public-methods
|
| +
|
| + def __init__(self):
|
| + self.api_key_valid = True
|
| + self.response_code = httplib.INTERNAL_SERVER_ERROR
|
| + self.response_size = report_request.SIZE_NOT_SET
|
| + self.request_size = report_request.SIZE_NOT_SET
|
| + self.http_method = None
|
| + self.url = None
|
| +
|
| +
|
| +class _LatencyTimer(object):
|
| +
|
| + def __init__(self, timer):
|
| + self._timer = timer
|
| + self._start = None
|
| + self._app_start = None
|
| + self._end = None
|
| +
|
| + def start(self):
|
| + self._start = self._timer()
|
| +
|
| + def app_start(self):
|
| + self._app_start = self._timer()
|
| +
|
| + def end(self):
|
| + self._end = self._timer()
|
| + if self._app_start is None:
|
| + self._app_start = self._end
|
| +
|
| + @property
|
| + def request_time(self):
|
| + if self._start and self._end:
|
| + return self._end - self._start
|
| + return None
|
| +
|
| + @property
|
| + def overhead_time(self):
|
| + if self._start and self._app_start:
|
| + return self._app_start - self._start
|
| + return None
|
| +
|
| + @property
|
| + def backend_time(self):
|
| + if self._end and self._app_start:
|
| + return self._end - self._app_start
|
| + return None
|
| +
|
| +
|
| +def _find_api_key_param(info, parsed_uri):
|
| + params = info.api_key_url_query_params
|
| + if not params:
|
| + return None
|
| +
|
| + param_dict = urlparse.parse_qs(parsed_uri.query)
|
| + if not param_dict:
|
| + return None
|
| +
|
| + for q in params:
|
| + value = param_dict.get(q)
|
| + if value:
|
| + # param's values are lists, assume the first value
|
| + # is what's needed
|
| + return value[0]
|
| +
|
| + return None
|
| +
|
| +
|
| +_DEFAULT_API_KEYS = ('key', 'api_key')
|
| +
|
| +
|
| +def _find_default_api_key_param(parsed_uri):
|
| + param_dict = urlparse.parse_qs(parsed_uri.query)
|
| + if not param_dict:
|
| + return None
|
| +
|
| + for q in _DEFAULT_API_KEYS:
|
| + value = param_dict.get(q)
|
| + if value:
|
| + # param's values are lists, assume the first value
|
| + # is what's needed
|
| + return value[0]
|
| +
|
| + return None
|
| +
|
| +
|
| +def _find_api_key_header(info, environ):
|
| + headers = info.api_key_http_header
|
| + if not headers:
|
| + return None
|
| +
|
| + for h in headers:
|
| + value = environ.get('HTTP_' + h.upper())
|
| + if value:
|
| + return value # headers have single values
|
| +
|
| + return None
|
| +
|
| +def _create_authenticator(a_service):
|
| + """Create an instance of :class:`google.auth.tokens.Authenticator`.
|
| +
|
| + Args:
|
| + a_service (:class:`google.api.gen.servicecontrol_v1_messages.Service`): a
|
| + service instance
|
| + """
|
| + if not isinstance(a_service, messages.Service):
|
| + raise ValueError("service is None or not an instance of Service")
|
| +
|
| + authentication = a_service.authentication
|
| + if not authentication:
|
| + logger.info("authentication is not configured in service, "
|
| + "authentication checks will be disabled")
|
| + return
|
| +
|
| + issuers_to_provider_ids = {}
|
| + issuer_uri_configs = {}
|
| + for provider in authentication.providers:
|
| + issuer = provider.issuer
|
| + jwks_uri = provider.jwksUri
|
| +
|
| + # Enable openID discovery if jwks_uri is unset
|
| + open_id = jwks_uri is None
|
| + issuer_uri_configs[issuer] = suppliers.IssuerUriConfig(open_id, jwks_uri)
|
| + issuers_to_provider_ids[issuer] = provider.id
|
| +
|
| + key_uri_supplier = suppliers.KeyUriSupplier(issuer_uri_configs)
|
| + jwks_supplier = suppliers.JwksSupplier(key_uri_supplier)
|
| + authenticator = tokens.Authenticator(issuers_to_provider_ids, jwks_supplier)
|
| + return authenticator
|
| +
|
| +
|
| +class AuthenticationMiddleware(object):
|
| + """A WSGI middleware that does authentication checks for incoming
|
| + requests.
|
| +
|
| + In environments where os.environ is replaced with a request-local and
|
| + thread-independent copy (e.g. Google Appengine), authentication result is
|
| + added to os.environ so that the wrapped application can make use of the
|
| + authentication result.
|
| + """
|
| + # pylint: disable=too-few-public-methods
|
| +
|
| + USER_INFO = "google.api.auth.user_info"
|
| +
|
| + def __init__(self, application, authenticator):
|
| + """Initializes an authentication middleware instance.
|
| +
|
| + Args:
|
| + application: a WSGI application to be wrapped
|
| + authenticator (:class:`google.auth.tokens.Authenticator`): an
|
| + authenticator that authenticates incoming requests
|
| + """
|
| + if not isinstance(authenticator, tokens.Authenticator):
|
| + raise ValueError("Invalid authenticator")
|
| +
|
| + self._application = application
|
| + self._authenticator = authenticator
|
| +
|
| + def __call__(self, environ, start_response):
|
| + method_info = environ.get(EnvironmentMiddleware.METHOD_INFO)
|
| + if not method_info or not method_info.auth_info:
|
| + # No authentication configuration for this method
|
| + logger.debug("authentication is not configured")
|
| + return self._application(environ, start_response)
|
| +
|
| + auth_token = _extract_auth_token(environ)
|
| + user_info = None
|
| + if not auth_token:
|
| + logger.debug("No auth token is attached to the request")
|
| + else:
|
| + try:
|
| + service_name = environ.get(EnvironmentMiddleware.SERVICE_NAME)
|
| + user_info = self._authenticator.authenticate(auth_token,
|
| + method_info.auth_info,
|
| + service_name)
|
| + except Exception: # pylint: disable=broad-except
|
| + logger.debug("Cannot decode and verify the auth token. The backend "
|
| + "will not be able to retrieve user info", exc_info=True)
|
| +
|
| + environ[self.USER_INFO] = user_info
|
| +
|
| + # pylint: disable=protected-access
|
| + if user_info and not isinstance(os.environ, os._Environ):
|
| + # Set user info into os.environ only if os.environ is replaced
|
| + # with a request-local copy
|
| + os.environ[self.USER_INFO] = user_info
|
| +
|
| + response = self._application(environ, start_response)
|
| +
|
| + # Erase user info from os.environ for safety and sanity.
|
| + if self.USER_INFO in os.environ:
|
| + del os.environ[self.USER_INFO]
|
| +
|
| + return response
|
| +
|
| +
|
| +_ACCESS_TOKEN_PARAM_NAME = "access_token"
|
| +_BEARER_TOKEN_PREFIX = "Bearer "
|
| +_BEARER_TOKEN_PREFIX_LEN = len(_BEARER_TOKEN_PREFIX)
|
| +
|
| +
|
| +def _extract_auth_token(environ):
|
| + # First try to extract auth token from HTTP authorization header.
|
| + auth_header = environ.get("HTTP_AUTHORIZATION")
|
| + if auth_header:
|
| + if auth_header.startswith(_BEARER_TOKEN_PREFIX):
|
| + return auth_header[_BEARER_TOKEN_PREFIX_LEN:]
|
| + return
|
| +
|
| + # Then try to read auth token from query.
|
| + parameters = urlparse.parse_qs(environ.get("QUERY_STRING", ""))
|
| + if _ACCESS_TOKEN_PARAM_NAME in parameters:
|
| + auth_token, = parameters[_ACCESS_TOKEN_PARAM_NAME]
|
| + return auth_token
|
|
|