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