1 /*
2 Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
3
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License, version 2.0,
6 as published by the Free Software Foundation.
7
8 This program is also distributed with certain software (including
9 but not limited to OpenSSL) that is licensed under separate terms,
10 as designated in a particular file or component or in included license
11 documentation. The authors of MySQL hereby grant you an additional
12 permission to link the program and your derivative works with the
13 separately licensed software that they have included with MySQL.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
23 */
24
25 /**
26 * REST API plugin.
27 */
28 #include "rest_api_plugin.h"
29
30 #include <array>
31 #include <string>
32
33 #include "mysql/harness/config_parser.h"
34 #include "mysql/harness/loader.h"
35 #include "mysql/harness/plugin.h"
36 #include "mysql/harness/utility/string.h" // ::join()
37 #include "mysqlrouter/http_server_component.h"
38 #include "mysqlrouter/plugin_config.h"
39 #include "mysqlrouter/rest_api_utils.h"
40
41 #include "rest_api.h"
42 IMPORT_LOG_FUNCTIONS()
43
44 static const char kSectionName[]{"rest_api"};
45
46 // one shared setting
47 std::string require_realm_api;
48
49 class RestApiPluginConfig : public mysqlrouter::BasePluginConfig {
50 public:
51 std::string require_realm;
52
RestApiPluginConfig(const mysql_harness::ConfigSection * section)53 explicit RestApiPluginConfig(const mysql_harness::ConfigSection *section)
54 : mysqlrouter::BasePluginConfig(section),
55 require_realm(get_option_string(section, "require_realm")) {}
56
get_default(const std::string &) const57 std::string get_default(const std::string & /* option */) const override {
58 return {};
59 }
60
is_required(const std::string &) const61 bool is_required(const std::string & /* option */) const override {
62 return false;
63 }
64 };
65
init(mysql_harness::PluginFuncEnv * env)66 static void init(mysql_harness::PluginFuncEnv *env) {
67 const mysql_harness::AppInfo *info = get_app_info(env);
68
69 if (nullptr == info->config) {
70 return;
71 }
72
73 try {
74 std::set<std::string> known_realms;
75 for (const mysql_harness::ConfigSection *section :
76 info->config->sections()) {
77 if (section->name == "http_auth_realm") {
78 known_realms.emplace(section->key);
79 }
80 }
81 for (const mysql_harness::ConfigSection *section :
82 info->config->sections()) {
83 if (section->name != kSectionName) {
84 continue;
85 }
86
87 if (!section->key.empty()) {
88 log_error("[%s] section does not expect a key, found '%s'",
89 kSectionName, section->key.c_str());
90 set_error(env, mysql_harness::kConfigInvalidArgument,
91 "[%s] section does not expect a key, found '%s'",
92 kSectionName, section->key.c_str());
93 return;
94 }
95
96 RestApiPluginConfig config{section};
97
98 if (!config.require_realm.empty() &&
99 (known_realms.find(config.require_realm) == known_realms.end())) {
100 throw std::invalid_argument(
101 "unknown authentication realm for [" + std::string(kSectionName) +
102 "] '" + section->key + "': " + config.require_realm +
103 ", known realm(s): " + mysql_harness::join(known_realms, ","));
104 }
105
106 require_realm_api = config.require_realm;
107 }
108 } catch (const std::invalid_argument &exc) {
109 set_error(env, mysql_harness::kConfigInvalidArgument, "%s", exc.what());
110 } catch (const std::exception &exc) {
111 set_error(env, mysql_harness::kRuntimeError, "%s", exc.what());
112 } catch (...) {
113 set_error(env, mysql_harness::kUndefinedError, "Unexpected exception");
114 }
115 }
116
RestApi(const std::string & uri_prefix,const std::string & uri_prefix_regex)117 RestApi::RestApi(const std::string &uri_prefix,
118 const std::string &uri_prefix_regex)
119 : uri_prefix_(uri_prefix), uri_prefix_regex_(uri_prefix_regex) {
120 auto &allocator = spec_doc_.GetAllocator();
121 spec_doc_.SetObject()
122 .AddMember("swagger", "2.0", allocator)
123 .AddMember("info",
124 RestApiComponent::JsonValue(rapidjson::kObjectType)
125 .AddMember("title", "MySQL Router", allocator)
126 .AddMember("description", "API of MySQL Router", allocator)
127 .AddMember("version", kRestAPIVersion, allocator),
128 allocator)
129 .AddMember("basePath",
130 RestApiComponent::JsonValue(uri_prefix.c_str(),
131 uri_prefix.size(), allocator),
132 allocator)
133 .AddMember("tags",
134 RestApiComponent::JsonValue(rapidjson::kArrayType).Move(),
135 allocator)
136 .AddMember("paths",
137 RestApiComponent::JsonValue(rapidjson::kObjectType).Move(),
138 allocator)
139 .AddMember("definitions",
140 RestApiComponent::JsonValue(rapidjson::kObjectType).Move(),
141 allocator)
142 //
143 ;
144 }
145
process_spec(RestApiComponent::SpecProcessor spec_processor)146 void RestApi::process_spec(RestApiComponent::SpecProcessor spec_processor) {
147 std::lock_guard<std::mutex> mx(spec_doc_mutex_);
148
149 spec_processor(spec_doc_);
150 }
151
spec()152 std::string RestApi::spec() {
153 rapidjson::StringBuffer json_buf;
154 {
155 rapidjson::Writer<rapidjson::StringBuffer> json_writer(json_buf);
156
157 std::lock_guard<std::mutex> mx(spec_doc_mutex_);
158 spec_doc_.Accept(json_writer);
159 }
160
161 return {json_buf.GetString(), json_buf.GetSize()};
162 }
163
add_path(const std::string & path,std::unique_ptr<BaseRestApiHandler> handler)164 void RestApi::add_path(const std::string &path,
165 std::unique_ptr<BaseRestApiHandler> handler) {
166 std::unique_lock<std::shared_timed_mutex> mx(rest_api_handler_mutex_);
167 // ensure path is unique
168 if (rest_api_handlers_.end() !=
169 std::find_if(
170 rest_api_handlers_.begin(), rest_api_handlers_.end(),
171 [&path](const auto &value) { return std::get<0>(value) == path; })) {
172 throw std::invalid_argument("path already exists in rest_api: " + path);
173 }
174
175 rest_api_handlers_.emplace_back(path, std::regex(path), std::move(handler));
176 }
177
remove_path(const std::string & path)178 void RestApi::remove_path(const std::string &path) {
179 std::unique_lock<std::shared_timed_mutex> mx(rest_api_handler_mutex_);
180
181 rest_api_handlers_.erase(
182 std::remove_if(
183 rest_api_handlers_.begin(), rest_api_handlers_.end(),
184 [&path](const auto &value) { return std::get<0>(value) == path; }),
185 rest_api_handlers_.end());
186 }
187
handle_paths(HttpRequest & req)188 void RestApi::handle_paths(HttpRequest &req) {
189 std::string uri_path(req.get_uri().get_path());
190
191 // strip prefix from uri path
192 std::string uri_suffix;
193 {
194 std::smatch m;
195 if (!std::regex_search(uri_path, m, std::regex(uri_prefix_regex_))) {
196 send_rfc7807_not_found_error(req);
197 return;
198 }
199 uri_suffix = m.suffix().str();
200 }
201
202 if (uri_suffix.empty() || uri_suffix[0] == '/') {
203 std::smatch m;
204 std::shared_lock<std::shared_timed_mutex> mx(rest_api_handler_mutex_);
205 for (const auto &path : rest_api_handlers_) {
206 if (std::regex_match(uri_suffix, m, std::get<1>(path))) {
207 std::vector<std::string> matches;
208
209 for (const auto &match : m) {
210 matches.emplace_back(match.str());
211 }
212 if (std::get<2>(path)->try_handle_request(req, uri_prefix(), matches)) {
213 return;
214 }
215 }
216 }
217 }
218
219 // if nothing matched, send a generic 404 handler
220 send_rfc7807_not_found_error(req);
221 }
222
223 static std::shared_ptr<RestApi> rest_api;
224
start(mysql_harness::PluginFuncEnv * env)225 static void start(mysql_harness::PluginFuncEnv *env) {
226 try {
227 auto &http_srv = HttpServerComponent::get_instance();
228 auto &rest_api_srv = RestApiComponent::get_instance();
229
230 rest_api =
231 std::make_shared<RestApi>(std::string("/api/") + kRestAPIVersion,
232 std::string("^/api/") + kRestAPIVersion);
233
234 rest_api->add_path("/swagger.json$", std::make_unique<RestApiSpecHandler>(
235 rest_api, require_realm_api));
236
237 rest_api_srv.init(rest_api);
238
239 http_srv.add_route(rest_api->uri_prefix_regex(),
240 std::make_unique<RestApiHttpRequestHandler>(rest_api));
241
242 wait_for_stop(env, 0);
243
244 http_srv.remove_route(rest_api->uri_prefix_regex());
245 rest_api->remove_path("/swagger.json$");
246 } catch (const std::runtime_error &exc) {
247 set_error(env, mysql_harness::kRuntimeError, "%s", exc.what());
248 } catch (...) {
249 set_error(env, mysql_harness::kUndefinedError, "Unexpected exception");
250 }
251 }
252
deinit(mysql_harness::PluginFuncEnv *)253 static void deinit(mysql_harness::PluginFuncEnv * /* env */) {
254 // destroy the rest_api after all rest_api users are stopped.
255 rest_api.reset();
256 }
257
258 #if defined(_MSC_VER) && defined(rest_api_EXPORTS)
259 /* We are building this library */
260 #define DLLEXPORT __declspec(dllexport)
261 #else
262 #define DLLEXPORT
263 #endif
264
265 static const std::array<const char *, 2> plugin_requires = {{
266 "http_server",
267 "logger",
268 }};
269
270 extern "C" {
271 mysql_harness::Plugin DLLEXPORT harness_plugin_rest_api = {
272 mysql_harness::PLUGIN_ABI_VERSION, mysql_harness::ARCHITECTURE_DESCRIPTOR,
273 "REST_API", VERSION_NUMBER(0, 0, 1),
274 // requires
275 plugin_requires.size(), plugin_requires.data(),
276 // conflicts
277 0, nullptr,
278 init, // init
279 deinit, // deinit
280 start, // start
281 nullptr, // stop
282 };
283 }
284