| Index: third_party/google-endpoints/endpoints/api_config_manager.py
|
| diff --git a/third_party/google-endpoints/endpoints/api_config_manager.py b/third_party/google-endpoints/endpoints/api_config_manager.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ab3fa94e24735252a348e17751afa264baa6b69b
|
| --- /dev/null
|
| +++ b/third_party/google-endpoints/endpoints/api_config_manager.py
|
| @@ -0,0 +1,375 @@
|
| +# 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.
|
| +
|
| +"""Configuration manager to store API configurations."""
|
| +
|
| +# pylint: disable=g-bad-name
|
| +import base64
|
| +import logging
|
| +import re
|
| +import threading
|
| +import urllib
|
| +
|
| +import discovery_service
|
| +
|
| +
|
| +# Internal constants
|
| +_PATH_VARIABLE_PATTERN = r'[a-zA-Z_][a-zA-Z_.\d]*'
|
| +_PATH_VALUE_PATTERN = r'[^:/?#\[\]{}]*'
|
| +
|
| +
|
| +class ApiConfigManager(object):
|
| + """Manages loading api configs and method lookup."""
|
| +
|
| + def __init__(self):
|
| + self._rpc_method_dict = {}
|
| + self._rest_methods = []
|
| + self._configs = {}
|
| + self._config_lock = threading.Lock()
|
| +
|
| + @property
|
| + def configs(self):
|
| + """Return a dict with the current configuration mappings.
|
| +
|
| + Returns:
|
| + A dict with the current configuration mappings.
|
| + """
|
| + with self._config_lock:
|
| + return self._configs.copy()
|
| +
|
| + def process_api_config_response(self, config_json):
|
| + """Parses a JSON API config and registers methods for dispatch.
|
| +
|
| + Side effects:
|
| + Parses method name, etc. for all methods and updates the indexing
|
| + data structures with the information.
|
| +
|
| + Args:
|
| + config_json: A dict, the JSON body of the getApiConfigs response.
|
| + """
|
| + with self._config_lock:
|
| + self._add_discovery_config()
|
| + for config in config_json.get('items', []):
|
| + lookup_key = config.get('name', ''), config.get('version', '')
|
| + self._configs[lookup_key] = config
|
| +
|
| + for config in self._configs.itervalues():
|
| + name = config.get('name', '')
|
| + version = config.get('version', '')
|
| + sorted_methods = self._get_sorted_methods(config.get('methods', {}))
|
| +
|
| + for method_name, method in sorted_methods:
|
| + self._save_rpc_method(method_name, version, method)
|
| + self._save_rest_method(method_name, name, version, method)
|
| +
|
| + def _get_sorted_methods(self, methods):
|
| + """Get a copy of 'methods' sorted the way they would be on the live server.
|
| +
|
| + Args:
|
| + methods: JSON configuration of an API's methods.
|
| +
|
| + Returns:
|
| + The same configuration with the methods sorted based on what order
|
| + they'll be checked by the server.
|
| + """
|
| + if not methods:
|
| + return methods
|
| +
|
| + # Comparison function we'll use to sort the methods:
|
| + def _sorted_methods_comparison(method_info1, method_info2):
|
| + """Sort method info by path and http_method.
|
| +
|
| + Args:
|
| + method_info1: Method name and info for the first method to compare.
|
| + method_info2: Method name and info for the method to compare to.
|
| +
|
| + Returns:
|
| + Negative if the first method should come first, positive if the
|
| + first method should come after the second. Zero if they're
|
| + equivalent.
|
| + """
|
| +
|
| + def _score_path(path):
|
| + """Calculate the score for this path, used for comparisons.
|
| +
|
| + Higher scores have priority, and if scores are equal, the path text
|
| + is sorted alphabetically. Scores are based on the number and location
|
| + of the constant parts of the path. The server has some special handling
|
| + for variables with regexes, which we don't handle here.
|
| +
|
| + Args:
|
| + path: The request path that we're calculating a score for.
|
| +
|
| + Returns:
|
| + The score for the given path.
|
| + """
|
| + score = 0
|
| + parts = path.split('/')
|
| + for part in parts:
|
| + score <<= 1
|
| + if not part or part[0] != '{':
|
| + # Found a constant.
|
| + score += 1
|
| + # Shift by 31 instead of 32 because some (!) versions of Python like
|
| + # to convert the int to a long if we shift by 32, and the sorted()
|
| + # function that uses this blows up if it receives anything but an int.
|
| + score <<= 31 - len(parts)
|
| + return score
|
| +
|
| + # Higher path scores come first.
|
| + path_score1 = _score_path(method_info1[1].get('path', ''))
|
| + path_score2 = _score_path(method_info2[1].get('path', ''))
|
| + if path_score1 != path_score2:
|
| + return path_score2 - path_score1
|
| +
|
| + # Compare by path text next, sorted alphabetically.
|
| + path_result = cmp(method_info1[1].get('path', ''),
|
| + method_info2[1].get('path', ''))
|
| + if path_result != 0:
|
| + return path_result
|
| +
|
| + # All else being equal, sort by HTTP method.
|
| + method_result = cmp(method_info1[1].get('httpMethod', ''),
|
| + method_info2[1].get('httpMethod', ''))
|
| + return method_result
|
| +
|
| + return sorted(methods.items(), _sorted_methods_comparison)
|
| +
|
| + @staticmethod
|
| + def _get_path_params(match):
|
| + """Gets path parameters from a regular expression match.
|
| +
|
| + Args:
|
| + match: A regular expression Match object for a path.
|
| +
|
| + Returns:
|
| + A dictionary containing the variable names converted from base64.
|
| + """
|
| + result = {}
|
| + for var_name, value in match.groupdict().iteritems():
|
| + actual_var_name = ApiConfigManager._from_safe_path_param_name(var_name)
|
| + result[actual_var_name] = urllib.unquote_plus(value)
|
| + return result
|
| +
|
| + def lookup_rpc_method(self, method_name, version):
|
| + """Lookup the JsonRPC method at call time.
|
| +
|
| + The method is looked up in self._rpc_method_dict, the dictionary that
|
| + it is saved in for SaveRpcMethod().
|
| +
|
| + Args:
|
| + method_name: A string containing the name of the method.
|
| + version: A string containing the version of the API.
|
| +
|
| + Returns:
|
| + Method descriptor as specified in the API configuration.
|
| + """
|
| + with self._config_lock:
|
| + method = self._rpc_method_dict.get((method_name, version))
|
| + return method
|
| +
|
| + def lookup_rest_method(self, path, http_method):
|
| + """Look up the rest method at call time.
|
| +
|
| + The method is looked up in self._rest_methods, the list it is saved
|
| + in for SaveRestMethod.
|
| +
|
| + Args:
|
| + path: A string containing the path from the URL of the request.
|
| + http_method: A string containing HTTP method of the request.
|
| +
|
| + Returns:
|
| + Tuple of (<method name>, <method>, <params>)
|
| + Where:
|
| + <method name> is the string name of the method that was matched.
|
| + <method> is the descriptor as specified in the API configuration. -and-
|
| + <params> is a dict of path parameters matched in the rest request.
|
| + """
|
| + with self._config_lock:
|
| + path = urllib.unquote(path)
|
| + for compiled_path_pattern, unused_path, methods in self._rest_methods:
|
| + match = compiled_path_pattern.match(path)
|
| + if match:
|
| + params = self._get_path_params(match)
|
| + method_key = http_method.lower()
|
| + method_name, method = methods.get(method_key, (None, None))
|
| + if method:
|
| + break
|
| + else:
|
| + logging.warn('No endpoint found for path: %s', path)
|
| + method_name = None
|
| + method = None
|
| + params = None
|
| + return method_name, method, params
|
| +
|
| + def _add_discovery_config(self):
|
| + """Add the Discovery configuration to our list of configs.
|
| +
|
| + This should only be called with self._config_lock. The code here assumes
|
| + the lock is held.
|
| + """
|
| + lookup_key = (discovery_service.DiscoveryService.API_CONFIG['name'],
|
| + discovery_service.DiscoveryService.API_CONFIG['version'])
|
| + self._configs[lookup_key] = discovery_service.DiscoveryService.API_CONFIG
|
| +
|
| + def save_config(self, lookup_key, config):
|
| + """Save a configuration to the cache of configs.
|
| +
|
| + Args:
|
| + lookup_key: A string containing the cache lookup key.
|
| + config: The dict containing the configuration to save to the cache.
|
| + """
|
| + with self._config_lock:
|
| + self._configs[lookup_key] = config
|
| +
|
| + @staticmethod
|
| + def _to_safe_path_param_name(matched_parameter):
|
| + """Creates a safe string to be used as a regex group name.
|
| +
|
| + Only alphanumeric characters and underscore are allowed in variable name
|
| + tokens, and numeric are not allowed as the first character.
|
| +
|
| + We cast the matched_parameter to base32 (since the alphabet is safe),
|
| + strip the padding (= not safe) and prepend with _, since we know a token
|
| + can begin with underscore.
|
| +
|
| + Args:
|
| + matched_parameter: A string containing the parameter matched from the URL
|
| + template.
|
| +
|
| + Returns:
|
| + A string that's safe to be used as a regex group name.
|
| + """
|
| + return '_' + base64.b32encode(matched_parameter).rstrip('=')
|
| +
|
| + @staticmethod
|
| + def _from_safe_path_param_name(safe_parameter):
|
| + """Takes a safe regex group name and converts it back to the original value.
|
| +
|
| + Only alphanumeric characters and underscore are allowed in variable name
|
| + tokens, and numeric are not allowed as the first character.
|
| +
|
| + The safe_parameter is a base32 representation of the actual value.
|
| +
|
| + Args:
|
| + safe_parameter: A string that was generated by _to_safe_path_param_name.
|
| +
|
| + Returns:
|
| + A string, the parameter matched from the URL template.
|
| + """
|
| + assert safe_parameter.startswith('_')
|
| + safe_parameter_as_base32 = safe_parameter[1:]
|
| +
|
| + padding_length = - len(safe_parameter_as_base32) % 8
|
| + padding = '=' * padding_length
|
| + return base64.b32decode(safe_parameter_as_base32 + padding)
|
| +
|
| + @staticmethod
|
| + def _compile_path_pattern(pattern):
|
| + r"""Generates a compiled regex pattern for a path pattern.
|
| +
|
| + e.g. '/MyApi/v1/notes/{id}'
|
| + returns re.compile(r'/MyApi/v1/notes/(?P<id>[^:/?#\[\]{}]*)')
|
| +
|
| + Args:
|
| + pattern: A string, the parameterized path pattern to be checked.
|
| +
|
| + Returns:
|
| + A compiled regex object to match this path pattern.
|
| + """
|
| +
|
| + def replace_variable(match):
|
| + """Replaces a {variable} with a regex to match it by name.
|
| +
|
| + Changes the string corresponding to the variable name to the base32
|
| + representation of the string, prepended by an underscore. This is
|
| + necessary because we can have message variable names in URL patterns
|
| + (e.g. via {x.y}) but the character '.' can't be in a regex group name.
|
| +
|
| + Args:
|
| + match: A regex match object, the matching regex group as sent by
|
| + re.sub().
|
| +
|
| + Returns:
|
| + A string regex to match the variable by name, if the full pattern was
|
| + matched.
|
| + """
|
| + if match.lastindex > 1:
|
| + var_name = ApiConfigManager._to_safe_path_param_name(match.group(2))
|
| + return '%s(?P<%s>%s)' % (match.group(1), var_name,
|
| + _PATH_VALUE_PATTERN)
|
| + return match.group(0)
|
| +
|
| + pattern = re.sub('(/|^){(%s)}(?=/|$)' % _PATH_VARIABLE_PATTERN,
|
| + replace_variable, pattern)
|
| + return re.compile(pattern + '/?$')
|
| +
|
| + def _save_rpc_method(self, method_name, version, method):
|
| + """Store JsonRpc api methods in a map for lookup at call time.
|
| +
|
| + (rpcMethodName, apiVersion) => method.
|
| +
|
| + Args:
|
| + method_name: A string containing the name of the API method.
|
| + version: A string containing the version of the API.
|
| + method: A dict containing the method descriptor (as in the api config
|
| + file).
|
| + """
|
| + self._rpc_method_dict[(method_name, version)] = method
|
| +
|
| + def _save_rest_method(self, method_name, api_name, version, method):
|
| + """Store Rest api methods in a list for lookup at call time.
|
| +
|
| + The list is self._rest_methods, a list of tuples:
|
| + [(<compiled_path>, <path_pattern>, <method_dict>), ...]
|
| + where:
|
| + <compiled_path> is a compiled regex to match against the incoming URL
|
| + <path_pattern> is a string representing the original path pattern,
|
| + checked on insertion to prevent duplicates. -and-
|
| + <method_dict> is a dict of httpMethod => (method_name, method)
|
| +
|
| + This structure is a bit complex, it supports use in two contexts:
|
| + Creation time:
|
| + - SaveRestMethod is called repeatedly, each method will have a path,
|
| + which we want to be compiled for fast lookup at call time
|
| + - We want to prevent duplicate incoming path patterns, so store the
|
| + un-compiled path, not counting on a compiled regex being a stable
|
| + comparison as it is not documented as being stable for this use.
|
| + - Need to store the method that will be mapped at calltime.
|
| + - Different methods may have the same path but different http method.
|
| + Call time:
|
| + - Quickly scan through the list attempting .match(path) on each
|
| + compiled regex to find the path that matches.
|
| + - When a path is matched, look up the API method from the request
|
| + and get the method name and method config for the matching
|
| + API method and method name.
|
| +
|
| + Args:
|
| + method_name: A string containing the name of the API method.
|
| + api_name: A string containing the name of the API.
|
| + version: A string containing the version of the API.
|
| + method: A dict containing the method descriptor (as in the api config
|
| + file).
|
| + """
|
| + path_pattern = '/'.join((api_name, version, method.get('path', '')))
|
| + http_method = method.get('httpMethod', '').lower()
|
| + for _, path, methods in self._rest_methods:
|
| + if path == path_pattern:
|
| + methods[http_method] = method_name, method
|
| + break
|
| + else:
|
| + self._rest_methods.append(
|
| + (self._compile_path_pattern(path_pattern),
|
| + path_pattern,
|
| + {http_method: (method_name, method)}))
|
|
|