/* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2.0, as published by the Free Software Foundation. This program is also distributed with certain software (including but not limited to OpenSSL) that is licensed under separate terms, as designated in a particular file or component or in included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the separately licensed software that they have included with MySQL. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include #include #include #include #ifdef RAPIDJSON_NO_SIZETYPEDEFINE // if we build within the server, it will set RAPIDJSON_NO_SIZETYPEDEFINE // globally and require to include my_rapidjson_size_t.h #include "my_rapidjson_size_t.h" #endif #include #include #include "mysqlrouter/rest_client.h" #include "rest_api_testutils.h" #include "config_builder.h" static const std::chrono::milliseconds kMaxRestEndpointNotAvailableCheckTime{ 1500}; static const std::chrono::milliseconds kMaxRestEndpointNotAvailableStepTime{50}; static const std::string rest_api_openapi_json = std::string(rest_api_basepath) + "/swagger.json"; // wait for the endpoint to return 404 static bool wait_endpoint_404( RestClient &rest_client, const std::string &uri, std::chrono::milliseconds max_wait_time) noexcept { while (max_wait_time.count() > 0) { auto req = rest_client.request_sync(HttpMethod::Get, uri); if (req && req.get_response_code() != 0) return (req.get_response_code() == 404); auto wait_time = std::min(kMaxRestEndpointNotAvailableStepTime, max_wait_time); std::this_thread::sleep_for(wait_time); max_wait_time -= wait_time; } return false; } void fetch_json(RestClient &rest_client, const std::string &uri, JsonDocument &json_doc) { request_json(rest_client, uri, HttpMethod::Get, HttpStatusCode::Ok, json_doc); } void request_json(RestClient &rest_client, const std::string &uri, HttpMethod::type http_method, HttpStatusCode::key_type http_status_code, JsonDocument &json_doc, const std::string &expected_content_type) { SCOPED_TRACE("// make a http connections for " + uri); auto req = rest_client.request_sync(http_method, uri); SCOPED_TRACE("// checking HTTP response"); ASSERT_TRUE(req) << "HTTP Request to failed (early): " << req.error_msg() << std::endl; ASSERT_GT(req.get_response_code(), 0u) << "HTTP Request failed: " << req.error_msg() << std::endl; ASSERT_EQ(req.get_response_code(), http_status_code); if (!expected_content_type.empty()) { ASSERT_THAT(req.get_input_headers().get("Content-Type"), ::testing::StrEq(expected_content_type)); } // HEAD doesn't return a body if (http_method != HttpMethod::Head && http_status_code != HttpStatusCode::Unauthorized) { auto resp_body = req.get_input_buffer(); ASSERT_GT(resp_body.length(), 0u); auto resp_body_content = resp_body.pop_front(resp_body.length()); // parse json std::string json_payload(resp_body_content.begin(), resp_body_content.end()); // for methods Options, Connect and Trace libevent returns "not implemented" // html if (expected_content_type == "text/html") return; json_doc.Parse(json_payload.c_str()); ASSERT_FALSE(json_doc.HasParseError()) << rapidjson::GetParseError_En(json_doc.GetParseError()) << " at pos " << json_doc.GetErrorOffset() << " in document retrieved from " << uri << " :\n" << json_payload; } } JsonValue *openapi_get_or_deref(JsonDocument &json_doc, const JsonPointer &pointer) { if (JsonValue *schm = pointer.Get(json_doc)) { if (auto *ref = JsonPointer("/$ref").Get(*schm)) { // we have a ref, follow it return JsonPointer(ref->GetString()).Get(json_doc); } return schm; } return nullptr; } void json_schema_validate(const JsonDocument &json_doc, const JsonValue &schema) { ASSERT_TRUE(schema.IsObject()); JsonSchemaDocument schema_doc(schema); JsonSchemaValidator validator(schema_doc); ASSERT_TRUE(json_doc.Accept(validator)) << validator << "\n" << "schema: " << *validator.GetInvalidSchemaPointer().Get(schema) << "\n" << "document: " << json_doc << "\n"; } void mark_object_additional_properties(JsonValue &v, JsonDocument::AllocatorType &allocator) { ASSERT_TRUE(v.IsObject()) << v; if (v.HasMember("type")) { ASSERT_TRUE(v["type"].IsString()); std::string v_type = v["type"].GetString(); if (v_type == "object") { if (v.HasMember("properties")) { ASSERT_TRUE(v["properties"].IsObject()); for (auto &m : v["properties"].GetObject()) { mark_object_additional_properties(m.value, allocator); } } if (!v.HasMember("additionalProperties")) { v.AddMember("additionalProperties", false, allocator); } } else if (v_type == "array") { if (v.HasMember("items")) { mark_object_additional_properties(v["items"], allocator); } } } } std::string http_method_to_string(const HttpMethod::type method) { switch (method) { case HttpMethod::Get: return "GET"; case HttpMethod::Post: return "POST"; case HttpMethod::Head: return "HEAD"; case HttpMethod::Put: return "PUT"; case HttpMethod::Delete: return "DELETE"; case HttpMethod::Options: return "OPTIONS"; case HttpMethod::Trace: return "TRACE"; case HttpMethod::Connect: return "CONNECT"; case HttpMethod::Patch: return "PATCH"; } return "UNKNOWN"; } bool wait_for_rest_endpoint_ready( const std::string &uri, const uint16_t http_port, const std::string &username, const std::string &password, const std::string &http_host, std::chrono::milliseconds max_wait_time, const std::chrono::milliseconds step_time) noexcept { IOContext io_ctx; RestClient rest_client(io_ctx, http_host, http_port, username, password); while (max_wait_time.count() > 0) { auto req = rest_client.request_sync(HttpMethod::Get, uri); if (req && req.get_response_code() != 0 && req.get_response_code() != 404) return true; auto wait_time = std::min(step_time, max_wait_time); std::this_thread::sleep_for(wait_time); max_wait_time -= wait_time; } return false; } std::string RestApiComponentTest::create_password_file() { const std::string userfile = mysql_harness::Path(conf_dir_.name()).join("users").str(); { auto &cmd = launch_command(get_origin().join("mysqlrouter_passwd").str(), {"set", userfile, kRestApiUsername}, EXIT_SUCCESS, true); cmd.register_response("Please enter password", std::string(kRestApiPassword) + "\n"); check_exit_code(cmd, EXIT_SUCCESS); } return userfile; } std::vector RestApiComponentTest::get_restapi_config( const std::string &component, const std::string &userfile, const bool request_authentication, const std::string &realm_name) { std::vector authentication; if (request_authentication) { authentication.push_back({"require_realm", realm_name}); } const std::vector config_sections{ ConfigBuilder::build_section("http_server", { {"port", std::to_string(http_port_)}, }), ConfigBuilder::build_section(component, authentication), ConfigBuilder::build_section("http_auth_realm:somerealm", { {"backend", "somebackend"}, {"method", "basic"}, {"name", "Some Realm"}, }), ConfigBuilder::build_section("http_auth_backend:somebackend", { {"backend", "file"}, {"filename", userfile}, }), }; return config_sections; } static void verify_swagger_content( const JsonDocument &openapi_json_doc, const std::vector &expected_paths) { // swagger ASSERT_TRUE(openapi_json_doc.HasMember("swagger")); const auto &swagger = openapi_json_doc["swagger"]; ASSERT_TRUE(swagger.IsString()); ASSERT_STREQ(swagger.GetString(), "2.0"); // info ASSERT_TRUE(openapi_json_doc.HasMember("info")); const auto &info = openapi_json_doc["info"]; ASSERT_TRUE(info.IsObject()); // info/title ASSERT_TRUE(info.HasMember("title")); const auto &title = info["title"]; ASSERT_TRUE(title.IsString()); ASSERT_STREQ(title.GetString(), "MySQL Router"); // info/description ASSERT_TRUE(info.HasMember("description")); const auto &descr = info["description"]; ASSERT_TRUE(descr.IsString()); ASSERT_STREQ(descr.GetString(), "API of MySQL Router"); // info/version ASSERT_TRUE(info.HasMember("version")); const auto &version = info["version"]; ASSERT_TRUE(version.IsString()); ASSERT_STREQ(version.GetString(), kRestAPIVersion); // paths ASSERT_TRUE(openapi_json_doc.HasMember("paths")); const auto &paths = openapi_json_doc["paths"]; ASSERT_TRUE(paths.IsObject()); for (const auto &expected_path : expected_paths) { const char *path_name = expected_path.path_name.c_str(); ASSERT_TRUE(paths.HasMember(path_name)); const auto &path = paths[path_name]; ASSERT_TRUE(path.IsObject()); // /path/get ASSERT_TRUE(path.HasMember("get")); const auto &path_get = path["get"]; ASSERT_TRUE(path_get.IsObject()); // /path/get/description ASSERT_TRUE(path_get.HasMember("description")); const auto &path_get_desc = path_get["description"]; ASSERT_TRUE(path_get_desc.IsString()); ASSERT_STREQ(path_get_desc.GetString(), expected_path.description.c_str()); // /path/get/responses ASSERT_TRUE(path_get.HasMember("responses")); const auto &path_get_responses = path_get["responses"]; ASSERT_TRUE(path_get_responses.IsObject()); // /path/get/responses/200 ASSERT_TRUE(path_get_responses.HasMember("200")); const auto &path_get_response_200 = path_get_responses["200"]; ASSERT_TRUE(path_get_response_200.IsObject()); ASSERT_TRUE(path_get_response_200.HasMember("description")); const auto &path_get_response_200_desc = path_get_response_200["description"]; ASSERT_TRUE(path_get_response_200_desc.IsString()); ASSERT_STREQ(path_get_response_200_desc.GetString(), expected_path.response_200.c_str()); // /path/get/responses/404 if (expected_path.response_404.empty()) { ASSERT_FALSE(path_get_responses.HasMember("404")); } else { ASSERT_TRUE(path_get_responses.HasMember("404")); const auto &path_get_response_404 = path_get_responses["404"]; ASSERT_TRUE(path_get_response_404.IsObject()); ASSERT_TRUE(path_get_response_404.HasMember("description")); const auto &path_get_response_404_desc = path_get_response_404["description"]; ASSERT_TRUE(path_get_response_404_desc.IsString()); ASSERT_STREQ(path_get_response_404_desc.GetString(), expected_path.response_404.c_str()); } } } void RestApiComponentTest::fetch_and_validate_schema_and_resource( const RestApiTestParams &test_params, ProcessWrapper &http_server, const std::string &http_hostname) { #define STR(s) \ { s, strlen(s), rapidjson::kPointerInvalidIndex } const std::array schema_pointer_tokens{ {STR("paths"), STR(test_params.api_path.c_str()), STR("get"), STR("responses"), STR("200"), STR("schema")}}; #undef STR IOContext io_ctx; RestClient rest_client(io_ctx, http_hostname, http_port_, test_params.user_name, test_params.user_password); // if 404 is expected make sure this is what we are getting and leave if (test_params.status_code == HttpStatusCode::NotFound) { ASSERT_TRUE(wait_endpoint_404(rest_client, test_params.uri, kMaxRestEndpointNotAvailableCheckTime)); return; } SCOPED_TRACE("// wait for REST endpoint: " + test_params.uri); ASSERT_TRUE(wait_for_rest_endpoint_ready(test_params.uri, http_port_, test_params.user_name, test_params.user_password)) << http_server.get_full_output() << http_server.get_full_logfile(); for (HttpMethod::pos_type ndx = 0; ndx < HttpMethod::Pos::_LAST; ++ndx) { if (test_params.methods.test(ndx)) { const auto method = 1 << ndx; SCOPED_TRACE("// fetching openapi spec"); JsonDocument openapi_json_doc; { // if we test for authorization failure this will still return Ok as // accessing swagger.json does not require authorization, // same with InternalError, BadRequest from a path, that does not affect // the swagger HttpStatusCode::key_type expected_code = test_params.status_code == HttpStatusCode::Unauthorized || test_params.status_code == HttpStatusCode::BadRequest || test_params.status_code == HttpStatusCode::InternalError ? HttpStatusCode::Ok : test_params.status_code; std::string expected_content_type = test_params.status_code == HttpStatusCode::Unauthorized || test_params.status_code == HttpStatusCode::BadRequest || test_params.status_code == HttpStatusCode::InternalError ? kContentTypeJson : test_params.expected_content_type; // also if the method is HEAD it's not really invalid method for // swagger.json file, it's only invalid for the path (API call) itself // later if (method == HttpMethod::Head && test_params.status_code == HttpStatusCode::MethodNotAllowed) { expected_code = HttpStatusCode::Ok; expected_content_type = kContentTypeJson; } ASSERT_NO_FATAL_FAILURE(request_json( rest_client, rest_api_openapi_json, method, expected_code, openapi_json_doc, expected_content_type)); } // verify response against the schema of the openapi spec SCOPED_TRACE("// API call"); JsonDocument json_doc; ASSERT_NO_FATAL_FAILURE(request_json(rest_client, test_params.uri, method, test_params.status_code, json_doc, test_params.expected_content_type)); SCOPED_TRACE("// validating schema"); if (HttpStatusCode::Ok == test_params.status_code) { verify_swagger_content(openapi_json_doc, test_params.swagger_paths); auto schema_pointer = JsonPointer(schema_pointer_tokens.data(), schema_pointer_tokens.size()); // points to either a $ref or a schema object auto *schema_val = openapi_get_or_deref(openapi_json_doc, schema_pointer); ASSERT_TRUE(schema_val != nullptr); ASSERT_TRUE(schema_val->IsObject()); ASSERT_NO_FATAL_FAILURE(mark_object_additional_properties( *schema_val, openapi_json_doc.GetAllocator())); ASSERT_NO_FATAL_FAILURE(json_schema_validate(json_doc, *schema_val)); } SCOPED_TRACE("// validating values"); for (const auto &kv : test_params.value_checks) { validate_value(json_doc, kv.first, kv.second); } } } } /*static*/ const std::vector< std::pair> RestApiComponentTest::kProblemJsonMethodNotAllowed{ {"/status", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsInt()); ASSERT_EQ(value->GetInt(), HttpStatusCode::MethodNotAllowed); }}, {"/title", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsString()); ASSERT_STREQ(value->GetString(), "HTTP Method not allowed"); }}, {"/detail", [](const JsonValue *value) -> void { ASSERT_NE(value, nullptr); ASSERT_TRUE(value->IsString()); ASSERT_STREQ(value->GetString(), "only HTTP Methods GET,HEAD are supported"); }}, }; void RestApiComponentTest::validate_value( const JsonDocument &json_doc, const std::string &value_json_pointer, const RestApiTestParams::value_check_func value_check) { const auto jp = JsonPointer(value_json_pointer.c_str()); ASSERT_TRUE(jp.IsValid()) << value_json_pointer; SCOPED_TRACE("// validating field: " + value_json_pointer); ASSERT_NO_FATAL_FAILURE(value_check(jp.Get(json_doc))); }