blob: 51409a5711ca8f8daebc2d69b4b7e3105c32a91b [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# 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"""Hook into the live Discovery service and get API configuration info."""
16
17# pylint: disable=g-bad-name
18from __future__ import absolute_import
19
20import json
21import logging
22
23from . import api_config
24from . import directory_list_generator
25from . import discovery_generator
26from . import util
27
28_logger = logging.getLogger(__name__)
29
30
31class DiscoveryService(object):
32 """Implements the local discovery service.
33
34 This has a static minimal version of the discoverable part of the
35 discovery .api file.
36
37 It only handles returning the discovery doc and directory, and ignores
38 directory parameters to filter the results.
39
40 The discovery docs/directory are created by calling a Cloud Endpoints
41 discovery service to generate the discovery docs/directory from an .api
42 file/set of .api files.
43 """
44
45 _GET_REST_API = 'apisdev.getRest'
46 _GET_RPC_API = 'apisdev.getRpc'
47 _LIST_API = 'apisdev.list'
48 API_CONFIG = {
49 'name': 'discovery',
50 'version': 'v1',
51 'api_version': 'v1',
52 'path_version': 'v1',
53 'methods': {
54 'discovery.apis.getRest': {
55 'path': 'apis/{api}/{version}/rest',
56 'httpMethod': 'GET',
57 'rosyMethod': _GET_REST_API,
58 },
59 'discovery.apis.getRpc': {
60 'path': 'apis/{api}/{version}/rpc',
61 'httpMethod': 'GET',
62 'rosyMethod': _GET_RPC_API,
63 },
64 'discovery.apis.list': {
65 'path': 'apis',
66 'httpMethod': 'GET',
67 'rosyMethod': _LIST_API,
68 },
69 }
70 }
71
72 def __init__(self, config_manager, backend):
73 """Initializes an instance of the DiscoveryService.
74
75 Args:
76 config_manager: An instance of ApiConfigManager.
77 backend: An _ApiServer instance for API config generation.
78 """
79 self._config_manager = config_manager
80 self._backend = backend
81
82 def _send_success_response(self, response, start_response):
83 """Sends an HTTP 200 json success response.
84
85 This calls start_response and returns the response body.
86
87 Args:
88 response: A string containing the response body to return.
89 start_response: A function with semantics defined in PEP-333.
90
91 Returns:
92 A string, the response body.
93 """
94 headers = [('Content-Type', 'application/json; charset=UTF-8')]
95 return util.send_wsgi_response('200 OK', headers, response, start_response)
96
97 def _get_rest_doc(self, request, start_response):
98 """Sends back HTTP response with API directory.
99
100 This calls start_response and returns the response body. It will return
101 the discovery doc for the requested api/version.
102
103 Args:
104 request: An ApiRequest, the transformed request sent to the Discovery API.
105 start_response: A function with semantics defined in PEP-333.
106
107 Returns:
108 A string, the response body.
109 """
110 api = request.body_json['api']
111 version = request.body_json['version']
112
113 generator = discovery_generator.DiscoveryGenerator(request=request)
114 services = [s for s in self._backend.api_services if
115 s.api_info.name == api and s.api_info.api_version == version]
116 doc = generator.pretty_print_config_to_json(services)
117 if not doc:
118 error_msg = ('Failed to convert .api to discovery doc for '
119 'version %s of api %s') % (version, api)
120 _logger.error('%s', error_msg)
121 return util.send_wsgi_error_response(error_msg, start_response)
122 return self._send_success_response(doc, start_response)
123
124 def _generate_api_config_with_root(self, request):
125 """Generate an API config with a specific root hostname.
126
127 This uses the backend object and the ApiConfigGenerator to create an API
128 config specific to the hostname of the incoming request. This allows for
129 flexible API configs for non-standard environments, such as localhost.
130
131 Args:
132 request: An ApiRequest, the transformed request sent to the Discovery API.
133
134 Returns:
135 A string representation of the generated API config.
136 """
137 actual_root = self._get_actual_root(request)
138 generator = api_config.ApiConfigGenerator()
139 api = request.body_json['api']
140 version = request.body_json['version']
141 lookup_key = (api, version)
142
143 service_factories = self._backend.api_name_version_map.get(lookup_key)
144 if not service_factories:
145 return None
146
147 service_classes = [service_factory.service_class
148 for service_factory in service_factories]
149 config_dict = generator.get_config_dict(
150 service_classes, hostname=actual_root)
151
152 # Save to cache
153 for config in config_dict.get('items', []):
154 lookup_key_with_root = (
155 config.get('name', ''), config.get('version', ''), actual_root)
156 self._config_manager.save_config(lookup_key_with_root, config)
157
158 return config_dict
159
160 def _get_actual_root(self, request):
161 url = request.server
162
163 # Append the port if not the default
164 if ((request.url_scheme == 'https' and request.port != '443') or
165 (request.url_scheme != 'https' and request.port != '80')):
166 url += ':%s' % request.port
167
168 return url
169
170 def _list(self, request, start_response):
171 """Sends HTTP response containing the API directory.
172
173 This calls start_response and returns the response body.
174
175 Args:
176 request: An ApiRequest, the transformed request sent to the Discovery API.
177 start_response: A function with semantics defined in PEP-333.
178
179 Returns:
180 A string containing the response body.
181 """
182 configs = []
183 generator = directory_list_generator.DirectoryListGenerator(request)
184 for config in self._config_manager.configs.values():
185 if config != self.API_CONFIG:
186 configs.append(config)
187 directory = generator.pretty_print_config_to_json(configs)
188 if not directory:
189 _logger.error('Failed to get API directory')
190 # By returning a 404, code explorer still works if you select the
191 # API in the URL
192 return util.send_wsgi_not_found_response(start_response)
193 return self._send_success_response(directory, start_response)
194
195 def handle_discovery_request(self, path, request, start_response):
196 """Returns the result of a discovery service request.
197
198 This calls start_response and returns the response body.
199
200 Args:
201 path: A string containing the API path (the portion of the path
202 after /_ah/api/).
203 request: An ApiRequest, the transformed request sent to the Discovery API.
204 start_response: A function with semantics defined in PEP-333.
205
206 Returns:
207 The response body. Or returns False if the request wasn't handled by
208 DiscoveryService.
209 """
210 if path == self._GET_REST_API:
211 return self._get_rest_doc(request, start_response)
212 elif path == self._GET_RPC_API:
213 error_msg = ('RPC format documents are no longer supported with the '
214 'Endpoints Framework for Python. Please use the REST '
215 'format.')
216 _logger.error('%s', error_msg)
217 return util.send_wsgi_error_response(error_msg, start_response)
218 elif path == self._LIST_API:
219 return self._list(request, start_response)
220 return False