From 9fba5354413c2650c74de065325dc451305de72e Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Tue, 2 Jul 2024 14:50:26 -0700 Subject: [PATCH 1/5] Refactoring in a hierarchy of the HTTP modules The former base class http::ModuleBase was split into the HTTP library neutral class http::Module and the QHTTP-specific intermediate base class http::QhttpModule. All existing modules that were based on the former were migrated to depend on the the latter. This refactoring prepared ground for introducing another intermediate base class for the HTTPLIB-based REST services. --- src/czar/HttpCzarIngestModule.cc | 6 +- src/czar/HttpCzarIngestModule.h | 4 +- src/czar/HttpModule.cc | 6 +- src/czar/HttpModule.h | 4 +- src/http/CMakeLists.txt | 5 +- src/http/MetaModule.cc | 11 +- src/http/MetaModule.h | 12 +- src/http/{ModuleBase.cc => Module.cc} | 62 ++++++---- src/http/{ModuleBase.h => Module.h} | 109 ++++++++++-------- src/http/QhttpModule.cc | 59 ++++++++++ src/http/QhttpModule.h | 86 ++++++++++++++ src/http/RequestBody.cc | 67 ----------- src/http/RequestBodyJSON.cc | 40 +++++++ src/http/{RequestBody.h => RequestBodyJSON.h} | 55 +++------ src/replica/contr/HttpConfigurationModule.cc | 1 + src/replica/contr/HttpControllersModule.cc | 1 + src/replica/contr/HttpExportModule.cc | 8 +- src/replica/contr/HttpIngestChunksModule.cc | 1 + src/replica/contr/HttpIngestConfigModule.cc | 1 + src/replica/contr/HttpIngestTransModule.cc | 1 + src/replica/contr/HttpJobsModule.cc | 1 + src/replica/contr/HttpModule.cc | 4 +- src/replica/contr/HttpModule.h | 6 +- src/replica/contr/HttpQservMonitorModule.cc | 1 + src/replica/contr/HttpRequestsModule.cc | 1 + src/replica/contr/HttpSqlIndexModule.cc | 9 +- src/replica/ingest/IngestDataHttpSvcMod.cc | 15 ++- src/replica/ingest/IngestDataHttpSvcMod.h | 24 ++-- src/replica/ingest/IngestHttpSvcMod.cc | 8 +- src/replica/ingest/IngestHttpSvcMod.h | 17 ++- src/replica/registry/RegistryHttpSvcMod.cc | 13 ++- src/replica/registry/RegistryHttpSvcMod.h | 17 ++- src/xrdsvc/HttpModule.cc | 6 +- src/xrdsvc/HttpModule.h | 4 +- src/xrdsvc/HttpReplicaMgtModule.cc | 2 +- 35 files changed, 404 insertions(+), 263 deletions(-) rename src/http/{ModuleBase.cc => Module.cc} (72%) rename src/http/{ModuleBase.h => Module.h} (78%) create mode 100644 src/http/QhttpModule.cc create mode 100644 src/http/QhttpModule.h delete mode 100644 src/http/RequestBody.cc create mode 100644 src/http/RequestBodyJSON.cc rename src/http/{RequestBody.h => RequestBodyJSON.h} (80%) diff --git a/src/czar/HttpCzarIngestModule.cc b/src/czar/HttpCzarIngestModule.cc index 8159d2692..c8ccdef7f 100644 --- a/src/czar/HttpCzarIngestModule.cc +++ b/src/czar/HttpCzarIngestModule.cc @@ -37,7 +37,7 @@ #include "http/BinaryEncoding.h" #include "http/Exceptions.h" #include "http/MetaModule.h" -#include "http/RequestBody.h" +#include "http/RequestBodyJSON.h" #include "qhttp/Request.h" #include "qhttp/Status.h" @@ -116,8 +116,8 @@ void HttpCzarIngestModule::process(asio::io_service& io_service, string const& c HttpCzarIngestModule::HttpCzarIngestModule(asio::io_service& io_service, string const& context, shared_ptr const& req, shared_ptr const& resp) - : http::ModuleBase(cconfig::CzarConfig::instance()->replicationAuthKey(), - cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), + : http::QhttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(), + cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), _io_service(io_service), _context(context), _registryBaseUrl("http://" + cconfig::CzarConfig::instance()->replicationRegistryHost() + ":" + diff --git a/src/czar/HttpCzarIngestModule.h b/src/czar/HttpCzarIngestModule.h index 6dfaa05b6..4ce9ea237 100644 --- a/src/czar/HttpCzarIngestModule.h +++ b/src/czar/HttpCzarIngestModule.h @@ -33,7 +33,7 @@ // Qserv headers #include "http/Method.h" -#include "http/ModuleBase.h" +#include "http/QhttpModule.h" // Forward declarations @@ -53,7 +53,7 @@ namespace lsst::qserv::czar { * Class HttpCzarIngestModule implements a handler for processing requests for ingesting * user-generated data prodicts via the HTTP-based frontend. */ -class HttpCzarIngestModule : public http::ModuleBase { +class HttpCzarIngestModule : public http::QhttpModule { public: /** * @note supported values for parameter 'subModuleName' are: diff --git a/src/czar/HttpModule.cc b/src/czar/HttpModule.cc index 1e1cc8957..680431010 100644 --- a/src/czar/HttpModule.cc +++ b/src/czar/HttpModule.cc @@ -28,7 +28,7 @@ // Qserv headers #include "cconfig/CzarConfig.h" #include "http/Exceptions.h" -#include "http/RequestBody.h" +#include "http/RequestBodyJSON.h" #include "http/RequestQuery.h" #include "qhttp/Request.h" @@ -38,8 +38,8 @@ namespace lsst::qserv::czar { HttpModule::HttpModule(string const& context, shared_ptr const& req, shared_ptr const& resp) - : http::ModuleBase(cconfig::CzarConfig::instance()->replicationAuthKey(), - cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), + : http::QhttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(), + cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), _context(context) {} string HttpModule::context() const { return _context; } diff --git a/src/czar/HttpModule.h b/src/czar/HttpModule.h index 566113ffb..f64b44ce7 100644 --- a/src/czar/HttpModule.h +++ b/src/czar/HttpModule.h @@ -26,7 +26,7 @@ #include // Qserv headers -#include "http/ModuleBase.h" +#include "http/QhttpModule.h" // Forward declarations namespace lsst::qserv::qhttp { @@ -40,7 +40,7 @@ namespace lsst::qserv::czar { /** * Class HttpModule is an intermediate base class of the Qserv Czar modules. */ -class HttpModule : public http::ModuleBase { +class HttpModule : public http::QhttpModule { public: HttpModule() = delete; HttpModule(HttpModule const&) = delete; diff --git a/src/http/CMakeLists.txt b/src/http/CMakeLists.txt index b7cb0f1ed..722c0736b 100644 --- a/src/http/CMakeLists.txt +++ b/src/http/CMakeLists.txt @@ -10,8 +10,9 @@ target_sources(http PRIVATE Exceptions.cc MetaModule.cc Method.cc - ModuleBase.cc - RequestBody.cc + Module.cc + QhttpModule.cc + RequestBodyJSON.cc RequestQuery.cc Url.cc ) diff --git a/src/http/MetaModule.cc b/src/http/MetaModule.cc index 2965eefaa..f64572b08 100644 --- a/src/http/MetaModule.cc +++ b/src/http/MetaModule.cc @@ -39,15 +39,16 @@ namespace lsst::qserv::http { unsigned int const MetaModule::version = 35; -void MetaModule::process(string const& context, nlohmann::json const& info, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp, string const& subModuleName) { +void MetaModule::process(string const& context, nlohmann::json const& info, + shared_ptr const& req, shared_ptr const& resp, + string const& subModuleName) { MetaModule module(context, info, req, resp); module.execute(subModuleName, ::authType); } -MetaModule::MetaModule(string const& context, nlohmann::json const& info, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp) - : http::ModuleBase(::authKey, ::adminAuthKey, req, resp), _context(context), _info(info) { +MetaModule::MetaModule(string const& context, nlohmann::json const& info, + shared_ptr const& req, shared_ptr const& resp) + : http::QhttpModule(::authKey, ::adminAuthKey, req, resp), _context(context), _info(info) { if (!_info.is_object()) { throw invalid_argument("MetaModule::" + string(__func__) + " parameter info must be an object."); } diff --git a/src/http/MetaModule.h b/src/http/MetaModule.h index 31750d6fe..b294a1b7e 100644 --- a/src/http/MetaModule.h +++ b/src/http/MetaModule.h @@ -29,7 +29,7 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "http/ModuleBase.h" +#include "http/QhttpModule.h" // This header declarations namespace lsst::qserv::http { @@ -38,7 +38,7 @@ namespace lsst::qserv::http { * Class MetaModule implements a handler for the metadata queries on the REST API itself. * The service responds with an information object provided at the creation time of the module. */ -class MetaModule : public http::ModuleBase { +class MetaModule : public http::QhttpModule { public: typedef std::shared_ptr Ptr; @@ -53,8 +53,8 @@ class MetaModule : public http::ModuleBase { * @throws std::invalid_argument for unknown values of parameter 'subModuleName' */ static void process(std::string const& context, nlohmann::json const& info, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, - std::string const& subModuleName); + std::shared_ptr const& req, + std::shared_ptr const& resp, std::string const& subModuleName); MetaModule() = delete; MetaModule(MetaModule const&) = delete; @@ -67,8 +67,8 @@ class MetaModule : public http::ModuleBase { virtual std::string context() const final; private: - MetaModule(std::string const& context, nlohmann::json const& info, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp); + MetaModule(std::string const& context, nlohmann::json const& info, + std::shared_ptr const& req, std::shared_ptr const& resp); nlohmann::json _version(); diff --git a/src/http/ModuleBase.cc b/src/http/Module.cc similarity index 72% rename from src/http/ModuleBase.cc rename to src/http/Module.cc index 82129724e..3156d7c3c 100644 --- a/src/http/ModuleBase.cc +++ b/src/http/Module.cc @@ -20,11 +20,12 @@ */ // Class header -#include "http/ModuleBase.h" +#include "http/Module.h" // Qserv headers #include "http/Exceptions.h" #include "http/MetaModule.h" +#include "http/RequestQuery.h" // LSST headers #include "lsst/log/Log.h" @@ -36,7 +37,7 @@ using namespace std; using json = nlohmann::json; namespace { -LOG_LOGGER _log = LOG_GET("lsst.qserv.http.ModuleBase"); +LOG_LOGGER _log = LOG_GET("lsst.qserv.http.Module"); string packWarnings(list const& warnings) { string packed; @@ -50,15 +51,12 @@ string packWarnings(list const& warnings) { namespace lsst::qserv::http { -ModuleBase::ModuleBase(string const& authKey, string const& adminAuthKey, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp) - : _authKey(authKey), _adminAuthKey(adminAuthKey), _req(req), _resp(resp), _query(req->query) {} +Module::Module(string const& authKey, string const& adminAuthKey) + : _authKey(authKey), _adminAuthKey(adminAuthKey) {} -ModuleBase::~ModuleBase() {} - -void ModuleBase::execute(string const& subModuleName, http::AuthType const authType) { +void Module::execute(string const& subModuleName, http::AuthType const authType) { try { - _body = RequestBody(_req); + _parseRequestBodyJSON(); if (authType == http::AuthType::REQUIRED) _enforceAuthorization(); json result = executeImpl(subModuleName); _sendData(result); @@ -73,7 +71,7 @@ void ModuleBase::execute(string const& subModuleName, http::AuthType const authT } } -void ModuleBase::checkApiVersion(string const& func, unsigned int minVersion, string const& warning) const { +void Module::checkApiVersion(string const& func, unsigned int minVersion, string const& warning) const { unsigned int const maxVersion = MetaModule::version; unsigned int version = 0; string const versionAttrName = "version"; @@ -86,7 +84,7 @@ void ModuleBase::checkApiVersion(string const& func, unsigned int minVersion, st // Note that requests sent w/o explicitly specified API version will still be // processed. In this case a warning will be sent in the response object. try { - if (req()->method == "GET") { + if (method() == "GET") { if (!query().has(versionAttrName)) { warn("No version number was provided in the request's query."); return; @@ -111,9 +109,9 @@ void ModuleBase::checkApiVersion(string const& func, unsigned int minVersion, st } } -void ModuleBase::enforceInstanceId(string const& func, string const& requiredInstanceId) const { - string const instanceId = req()->method == "GET" ? query().requiredString("instance_id") - : body().required("instance_id"); +void Module::enforceInstanceId(string const& func, string const& requiredInstanceId) const { + string const instanceId = method() == "GET" ? query().requiredString("instance_id") + : body().required("instance_id"); debug(func, "instance_id: " + instanceId); if (instanceId != requiredInstanceId) { throw invalid_argument(context() + func + " Qserv instance identifier mismatch. Client sent '" + @@ -121,36 +119,54 @@ void ModuleBase::enforceInstanceId(string const& func, string const& requiredIns } } -void ModuleBase::info(string const& msg) const { LOGS(_log, LOG_LVL_INFO, context() << msg); } +void Module::info(string const& msg) const { LOGS(_log, LOG_LVL_INFO, context() << msg); } -void ModuleBase::debug(string const& msg) const { LOGS(_log, LOG_LVL_DEBUG, context() << msg); } +void Module::debug(string const& msg) const { LOGS(_log, LOG_LVL_DEBUG, context() << msg); } -void ModuleBase::warn(string const& msg) const { +void Module::warn(string const& msg) const { LOGS(_log, LOG_LVL_WARN, context() << msg); _warnings.push_back(msg); } -void ModuleBase::error(string const& msg) const { LOGS(_log, LOG_LVL_ERROR, context() << msg); } +void Module::error(string const& msg) const { LOGS(_log, LOG_LVL_ERROR, context() << msg); } -void ModuleBase::_sendError(string const& func, string const& errorMsg, json const& errorExt) const { +void Module::_sendError(string const& func, string const& errorMsg, json const& errorExt) { error(func, errorMsg); json result; result["success"] = 0; result["error"] = errorMsg; result["error_ext"] = errorExt.is_null() ? json::object() : errorExt; result["warning"] = ::packWarnings(_warnings); - resp()->send(result.dump(), "application/json"); + sendResponse(result.dump(), "application/json"); } -void ModuleBase::_sendData(json& result) { +void Module::_sendData(json& result) { result["success"] = 1; result["error"] = ""; result["error_ext"] = json::object(); result["warning"] = ::packWarnings(_warnings); - resp()->send(result.dump(), "application/json"); + sendResponse(result.dump(), "application/json"); +} + +void Module::_parseRequestBodyJSON() { + string content; + getRequestBody(content, "application/json"); + if (!content.empty()) { + try { + _body.objJson = json::parse(content); + if (_body.objJson.is_null() || _body.objJson.is_object()) return; + } catch (...) { + // Not really interested in knowing specific details of the exception. + // All what matters here is that the string can't be parsed into + // a valid JSON object. This will be reported via another exception + // after this block ends. + ; + } + throw std::invalid_argument("invalid format of the request body. A simple JSON object was expected"); + } } -void ModuleBase::_enforceAuthorization() { +void Module::_enforceAuthorization() { if (body().has("admin_auth_key")) { auto const adminAuthKey = body().required("admin_auth_key"); if (adminAuthKey != _adminAuthKey) { diff --git a/src/http/ModuleBase.h b/src/http/Module.h similarity index 78% rename from src/http/ModuleBase.h rename to src/http/Module.h index 611ae9ed1..009d2a192 100644 --- a/src/http/ModuleBase.h +++ b/src/http/Module.h @@ -19,8 +19,8 @@ * the GNU General Public License along with this program. If not, * see . */ -#ifndef LSST_QSERV_HTTP_MODULEBASE_H -#define LSST_QSERV_HTTP_MODULEBASE_H +#ifndef LSST_QSERV_HTTP_MODULE_H +#define LSST_QSERV_HTTP_MODULE_H // System headers #include @@ -33,10 +33,12 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "http/RequestBody.h" -#include "http/RequestQuery.h" -#include "qhttp/Request.h" -#include "qhttp/Response.h" +#include "http/RequestBodyJSON.h" + +// Forward declarations +namespace lsst::qserv::http { +class RequestQuery; +} // namespace lsst::qserv::http // This header declarations namespace lsst::qserv::http { @@ -45,38 +47,35 @@ namespace lsst::qserv::http { /// module's authorization requirements. enum class AuthType { REQUIRED, NONE }; +/// Class AuthError represent exceptions thrown when the authorization +/// requirements aren't met. +class AuthError : public std::invalid_argument { +public: + using std::invalid_argument::invalid_argument; +}; + /** - * Class ModuleBase is a base class for requests processing modules - * of the HTTP servers built into the Replication system's services. + * Class Module is the very base class for the request processing modules of the HTTP servers. */ -class ModuleBase { +class Module { public: - /** - * Class AuthError represent exceptions thrown when the authorization - * requirements aren't met. - */ - class AuthError : public std::invalid_argument { - public: - using std::invalid_argument::invalid_argument; - }; - - ModuleBase() = delete; - ModuleBase(ModuleBase const&) = delete; - ModuleBase& operator=(ModuleBase const&) = delete; + Module() = delete; + Module(Module const&) = delete; + Module& operator=(Module const&) = delete; - virtual ~ModuleBase(); + virtual ~Module() = default; /** * Invokes a subclass-specific request processing provided by implementations - * of the pure virtual method ModuleBase::executeImpl(). The current method + * of the pure virtual method Module::executeImpl(). The current method * would also do an optional processing of exceptions thrown by the subclass-specific - * implementations of method ModuleBase::executeImpl(). These error conditions will + * implementations of method Module::executeImpl(). These error conditions will * be reported to as errors to callers. * * @param subModuleName this optional parameter allows modules to have * multiple sub-modules. A value of this parameter will be forwarded to * the subclass-specific implementation of the pure virtual method - * ModuleBase::executeImpl(). + * Module::executeImpl(). * @param authType Authorization requirements of the module. If 'http::AuthType::REQUIRED' is * requested then the method will enforce the authorization. A lack of required * authorization key in a request, or an incorrect value of such key would result @@ -100,26 +99,23 @@ class ModuleBase { /** * @param authKey An authorization key for operations which require extra security. * @param adminAuthKey An administrator-level authorization key. - * @param req The HTTP request. - * @param resp The HTTP response channel. */ - ModuleBase(std::string const& authKey, std::string const& adminAuthKey, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp); - - qhttp::Request::Ptr const& req() const { return _req; } - qhttp::Response::Ptr const& resp() const { return _resp; } + Module(std::string const& authKey, std::string const& adminAuthKey); /// @return Authorization level of the request. bool isAdmin() const { return _isAdmin; } - /// @return Parameters of a REST request. - std::unordered_map const& params() const { return _req->params; } + /// @return The method of a request. + virtual std::string method() const = 0; + + /// @return Captured URL path elements. + virtual std::unordered_map params() const = 0; /// @return Parameters of the request's query captured from the request's URL. - RequestQuery const& query() const { return _query; } + virtual RequestQuery query() const = 0; /// @return Optional parameters of a request extracted from the request's body (if any). - RequestBody const& body() const { return _body; } + RequestBodyJSON const& body() const { return _body; } // Message loggers for the corresponding log levels @@ -152,7 +148,7 @@ class ModuleBase { * in the "warning" attribute at the returned JSON object. * * The method will look for th eversion attribute in the query string of the "GET" - * requests. For requests that are called using methods "POIST", "PUT" or "DELETE" + * requests. For requests that are called using methods "POST", "PUT" or "DELETE" * the attribute will be located in the requests's body. * * @note Services that are calling the method should adjust the minimum version @@ -184,9 +180,17 @@ class ModuleBase { */ void enforceInstanceId(std::string const& func, std::string const& requiredInstanceId) const; + /** + * Get the raw body of a request if it's available and if the content type + * meets expectations. + * @note An assumption is made that the body is small enough to fit into memory. + * @param content The content of the body is set of a request if all conditions are met. + * @param requiredContentType The required content type of the body. + */ + virtual void getRequestBody(std::string& content, std::string const& requiredContentType) = 0; + /** * To implement a subclass-specific request processing. - * * @note All exceptions thrown by the implementations will be intercepted and * reported as errors to callers. Exceptions are now the only way to report * errors from modules. @@ -197,7 +201,21 @@ class ModuleBase { */ virtual nlohmann::json executeImpl(std::string const& subModuleName) = 0; + /** + * Send a response back to a requester of a service. + * @param content The content to be sent back. + * @param contentType The type of the content to be sent back. + */ + virtual void sendResponse(std::string const& content, std::string const& contentType) = 0; + private: + /** + * Pull the raw request body and translate it into a JSON object. + * @note The body will be set only if the request has a body and the content + * type is "application/json". Otherwise the body will be left empty. + */ + void _parseRequestBodyJSON(); + /** * Inspect the body of a request or a presence of a user-supplied authorization key. * Its value will be compared against a value of the corresponding configuration @@ -218,7 +236,7 @@ class ModuleBase { * @param errorExt (optional) The additional information on the error. */ void _sendError(std::string const& func, std::string const& errorMsg, - nlohmann::json const& errorExt = nlohmann::json::object()) const; + nlohmann::json const& errorExt = nlohmann::json::object()); /** * Report a result back to a requester of a service upon its successful @@ -231,19 +249,12 @@ class ModuleBase { std::string const _authKey; std::string const _adminAuthKey; - qhttp::Request::Ptr const _req; - qhttp::Response::Ptr const _resp; /// The flag indicating if a request has been granted the "administrator"-level privileges. bool _isAdmin = false; - /// The parser for parameters passed into the Web services via the optional - /// query part of a URL. The object gets initialized from the request. - RequestQuery const _query; - - /// The body of a request is initialized/parsed from the request before calling - /// the overloaded method HttpModule::executeImpl. - RequestBody _body; + /// The body of a request is initialized by Module::execute(). + RequestBodyJSON _body; /// The optional warning message to be sent to a caller if the API version /// number wasn't mentoned in the request. @@ -252,4 +263,4 @@ class ModuleBase { } // namespace lsst::qserv::http -#endif // LSST_QSERV_HTTP_MODULEBASE_H +#endif // LSST_QSERV_HTTP_MODULE_H diff --git a/src/http/QhttpModule.cc b/src/http/QhttpModule.cc new file mode 100644 index 000000000..7b492862b --- /dev/null +++ b/src/http/QhttpModule.cc @@ -0,0 +1,59 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ + +// Class header +#include "http/QhttpModule.h" + +// System headers +#include + +// Qserv headers +#include "http/RequestQuery.h" +#include "qhttp/Request.h" +#include "qhttp/Response.h" + +using namespace std; + +namespace lsst::qserv::http { + +QhttpModule::QhttpModule(string const& authKey, string const& adminAuthKey, + shared_ptr const& req, shared_ptr const& resp) + : Module(authKey, adminAuthKey), _req(req), _resp(resp) {} + +string QhttpModule::method() const { return _req->method; } + +unordered_map QhttpModule::params() const { return _req->params; } + +RequestQuery QhttpModule::query() const { return RequestQuery(_req->query); } + +void QhttpModule::getRequestBody(string& content, string const& requiredContentType) { + if (_req->header["Content-Type"] == requiredContentType) { + content.clear(); + content.reserve(_req->contentLengthBytes()); + content.append(istreambuf_iterator(_req->content), {}); + } +} + +void QhttpModule::sendResponse(string const& content, string const& contentType) { + _resp->send(content, contentType); +} + +} // namespace lsst::qserv::http diff --git a/src/http/QhttpModule.h b/src/http/QhttpModule.h new file mode 100644 index 000000000..db2721311 --- /dev/null +++ b/src/http/QhttpModule.h @@ -0,0 +1,86 @@ + +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ +#ifndef LSST_QSERV_HTTP_QHTTPMODULE_H +#define LSST_QSERV_HTTP_QHTTPMODULE_H + +// System headers +#include +#include +#include + +// Qserv headers +#include "http/Module.h" + +// Forward declarations + +namespace lsst::qserv::qhttp { +class Request; +class Response; +} // namespace lsst::qserv::qhttp + +namespace lsst::qserv::http { +class RequestBodyJSON; +class RequestQuery; +} // namespace lsst::qserv::http + +// This header declarations +namespace lsst::qserv::http { + +/** + * Class QhttpModule is an extended base class specialized for constructing + * the QHTTP request processing modules. + */ +class QhttpModule : public Module { +public: + QhttpModule() = delete; + QhttpModule(QhttpModule const&) = delete; + QhttpModule& operator=(QhttpModule const&) = delete; + + virtual ~QhttpModule() = default; + +protected: + /** + * @param authKey An authorization key for operations which require extra security. + * @param adminAuthKey An administrator-level authorization key. + * @param req The HTTP request. + * @param resp The HTTP response channel. + */ + QhttpModule(std::string const& authKey, std::string const& adminAuthKey, + std::shared_ptr const& req, std::shared_ptr const& resp); + + std::shared_ptr const& req() const { return _req; } + std::shared_ptr const& resp() const { return _resp; } + + virtual std::string method() const; + virtual std::unordered_map params() const; + virtual RequestQuery query() const; + virtual void getRequestBody(std::string& content, std::string const& requiredContentType); + virtual void sendResponse(std::string const& content, std::string const& contentType); + +private: + std::shared_ptr const _req; + std::shared_ptr const _resp; +}; + +} // namespace lsst::qserv::http + +#endif // LSST_QSERV_HTTP_QHTTPMODULE_H diff --git a/src/http/RequestBody.cc b/src/http/RequestBody.cc deleted file mode 100644 index 6c0a2da0e..000000000 --- a/src/http/RequestBody.cc +++ /dev/null @@ -1,67 +0,0 @@ -/* - * LSST Data Management System - * - * This product includes software developed by the - * LSST Project (http://www.lsst.org/). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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 LSST License Statement and - * the GNU General Public License along with this program. If not, - * see . - */ - -// Class header -#include "http/RequestBody.h" - -// System headers -#include - -using namespace std; -using json = nlohmann::json; - -namespace lsst::qserv::http { - -RequestBody::RequestBody(qhttp::Request::Ptr const& req) : objJson(json::object()) { - // This way of parsing the optional body allows requests which have no body. - - string const contentType = req->header["Content-Type"]; - string const requiredContentType = "application/json"; - - if (contentType == requiredContentType) { - string content(istreambuf_iterator(req->content), {}); - if (not content.empty()) { - try { - objJson = json::parse(content); - if (objJson.is_null() or objJson.is_object()) return; - } catch (...) { - // Not really interested in knowing specific details of the exception. - // All what matters here is that the string can't be parsed into - // a valid JSON object. This will be reported via another exception - // after this block ends. - ; - } - throw invalid_argument("invalid format of the request body. A simple JSON object was expected"); - } - } -} - -bool RequestBody::has(json const& obj, string const& name) const { - if (not obj.is_object()) { - throw invalid_argument("RequestBody::" + string(__func__) + - " parameter 'obj' is not a valid JSON object"); - } - return obj.find(name) != obj.end(); -} - -bool RequestBody::has(string const& name) const { return has(objJson, name); } - -} // namespace lsst::qserv::http \ No newline at end of file diff --git a/src/http/RequestBodyJSON.cc b/src/http/RequestBodyJSON.cc new file mode 100644 index 000000000..7e52a9d68 --- /dev/null +++ b/src/http/RequestBodyJSON.cc @@ -0,0 +1,40 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ + +// Class header +#include "http/RequestBodyJSON.h" + +using namespace std; +using json = nlohmann::json; + +namespace lsst::qserv::http { + +bool RequestBodyJSON::has(json const& obj, string const& name) const { + if (!obj.is_object()) { + throw invalid_argument("RequestBodyJSON::" + string(__func__) + + " parameter 'obj' is not a valid JSON object"); + } + return obj.find(name) != obj.end(); +} + +bool RequestBodyJSON::has(string const& name) const { return has(objJson, name); } + +} // namespace lsst::qserv::http \ No newline at end of file diff --git a/src/http/RequestBody.h b/src/http/RequestBodyJSON.h similarity index 80% rename from src/http/RequestBody.h rename to src/http/RequestBodyJSON.h index 40d121583..896250d32 100644 --- a/src/http/RequestBody.h +++ b/src/http/RequestBodyJSON.h @@ -18,8 +18,8 @@ * the GNU General Public License along with this program. If not, * see . */ -#ifndef LSST_QSERV_HTTP_REQUESTBODY_H -#define LSST_QSERV_HTTP_REQUESTBODY_H +#ifndef LSST_QSERV_HTTP_REQUESTBODYJSON_H +#define LSST_QSERV_HTTP_REQUESTBODYJSON_H // System headers #include @@ -29,46 +29,21 @@ // Third party headers #include "nlohmann/json.hpp" -// Qserv headers -#include "qhttp/Server.h" - // This header declarations namespace lsst::qserv::http { /** - * Helper class RequestBody parses a body of an HTTP request - * which has the following header: - * - * Content-Type: application/json - * - * Exceptions may be thrown by the constructor of the class if - * the request has an unexpected content type, or if its payload - * is not a proper JSON object. + * Class RequestBodyJSON represents the request body parsed into a JSON object. + * This type of an object is only available for requests that have the following + * header: 'Content-Type: application/json'. */ -class RequestBody { +class RequestBodyJSON { public: /// parsed body of the request nlohmann::json objJson = nlohmann::json::object(); - RequestBody() = default; - RequestBody(RequestBody const&) = default; - RequestBody& operator=(RequestBody const&) = default; - - ~RequestBody() = default; - - /** - * The constructor will parse and evaluate a body of an HTTP request - * and populate the 'kv' dictionary. Exceptions may be thrown in - * the following scenarios: - * - the required HTTP header is not found in the request - * - the body doesn't have a valid JSON string (unless the body is empty) - * - * @param req The request to be parsed. - */ - explicit RequestBody(qhttp::Request::Ptr const& req); - /** - * Check if thw specified parameter is present in the input JSON object. + * Check if the specified parameter is present in the input JSON object. * @param obj JSON object to be inspected. * @param name The name of a parameter. * @return 'true' if the parameter was found. @@ -95,11 +70,11 @@ class RequestBody { template static T required(nlohmann::json const& obj, std::string const& name) { if (not obj.is_object()) { - throw std::invalid_argument("RequestBody::" + std::string(__func__) + + throw std::invalid_argument("RequestBodyJSON::" + std::string(__func__) + "[static] parameter 'obj' is not a valid JSON object"); } if (obj.find(name) != obj.end()) return obj[name]; - throw std::invalid_argument("RequestBody::" + std::string(__func__) + + throw std::invalid_argument("RequestBodyJSON::" + std::string(__func__) + "[static] required parameter " + name + " is missing in the request body"); } @@ -127,7 +102,7 @@ class RequestBody { T required(std::string const& name, std::vector const& permitted) const { auto const value = required(objJson, name); if (_in(value, permitted)) return value; - throw std::invalid_argument("RequestBody::" + std::string(__func__) + + throw std::invalid_argument("RequestBodyJSON::" + std::string(__func__) + "(permitted) a value of parameter " + name + " is not allowed."); } @@ -155,7 +130,7 @@ class RequestBody { T optional(std::string const& name, T const& defaultValue, std::vector const& permitted) const { auto const value = optional(name, defaultValue); if (_in(value, permitted)) return value; - throw std::invalid_argument("RequestBody::" + std::string(__func__) + + throw std::invalid_argument("RequestBodyJSON::" + std::string(__func__) + "(permitted) a value of parameter " + name + " is not allowed."); } @@ -169,11 +144,11 @@ class RequestBody { std::vector requiredColl(std::string const& name) const { auto const itr = objJson.find(name); if (itr == objJson.end()) { - throw std::invalid_argument("RequestBody::" + std::string(__func__) + " required parameter " + - name + " is missing in the request body"); + throw std::invalid_argument("RequestBodyJSON::" + std::string(__func__) + + " required parameter " + name + " is missing in the request body"); } if (not itr->is_array()) { - throw std::invalid_argument("RequestBody::" + std::string(__func__) + + throw std::invalid_argument("RequestBodyJSON::" + std::string(__func__) + " a value of the required parameter " + name + " is not an array"); } std::vector coll; @@ -213,4 +188,4 @@ class RequestBody { } // namespace lsst::qserv::http -#endif // LSST_QSERV_HTTP_REQUESTBODY_H +#endif // LSST_QSERV_HTTP_REQUESTBODYJSON_H diff --git a/src/replica/contr/HttpConfigurationModule.cc b/src/replica/contr/HttpConfigurationModule.cc index 464601d52..cce6d06e3 100644 --- a/src/replica/contr/HttpConfigurationModule.cc +++ b/src/replica/contr/HttpConfigurationModule.cc @@ -30,6 +30,7 @@ // Qserv headers #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/config/Configuration.h" #include "replica/config/ConfigDatabase.h" #include "replica/config/ConfigurationSchema.h" diff --git a/src/replica/contr/HttpControllersModule.cc b/src/replica/contr/HttpControllersModule.cc index 1d0d96bbf..1032515e6 100644 --- a/src/replica/contr/HttpControllersModule.cc +++ b/src/replica/contr/HttpControllersModule.cc @@ -27,6 +27,7 @@ // Qserv headers #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/services/DatabaseServices.h" #include "replica/services/ServiceProvider.h" diff --git a/src/replica/contr/HttpExportModule.cc b/src/replica/contr/HttpExportModule.cc index 5af2f5a40..b021a9d07 100644 --- a/src/replica/contr/HttpExportModule.cc +++ b/src/replica/contr/HttpExportModule.cc @@ -27,7 +27,7 @@ // Qserv headers #include "http/Exceptions.h" -#include "http/RequestBody.h" +#include "http/RequestBodyJSON.h" #include "replica/config/Configuration.h" #include "replica/config/ConfigWorker.h" #include "replica/services/DatabaseServices.h" @@ -201,11 +201,11 @@ json HttpExportModule::_getTables() { for (auto&& tableJson : tablesJson) { TableSpec spec; - spec.tableName = http::RequestBody::required(tableJson, "table"); + spec.tableName = http::RequestBodyJSON::required(tableJson, "table"); spec.partitioned = database.findTable(spec.tableName).isPartitioned; if (spec.partitioned) { - spec.overlap = http::RequestBody::required(tableJson, "overlap"); - spec.chunk = http::RequestBody::required(tableJson, "chunk"); + spec.overlap = http::RequestBodyJSON::required(tableJson, "overlap"); + spec.chunk = http::RequestBodyJSON::required(tableJson, "chunk"); } ConfigWorker const worker = spec.partitioned ? findWorkerForChunk(spec.chunk) : allConfigWorkers[0]; diff --git a/src/replica/contr/HttpIngestChunksModule.cc b/src/replica/contr/HttpIngestChunksModule.cc index 24f39b3f4..9eac312f7 100644 --- a/src/replica/contr/HttpIngestChunksModule.cc +++ b/src/replica/contr/HttpIngestChunksModule.cc @@ -32,6 +32,7 @@ // Qserv headers #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/config/Configuration.h" #include "replica/services/DatabaseServices.h" #include "replica/services/ServiceProvider.h" diff --git a/src/replica/contr/HttpIngestConfigModule.cc b/src/replica/contr/HttpIngestConfigModule.cc index 2e1d74a92..e2c765acb 100644 --- a/src/replica/contr/HttpIngestConfigModule.cc +++ b/src/replica/contr/HttpIngestConfigModule.cc @@ -29,6 +29,7 @@ #include "global/stringUtil.h" #include "http/Client.h" #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/config/Configuration.h" #include "replica/services/DatabaseServices.h" diff --git a/src/replica/contr/HttpIngestTransModule.cc b/src/replica/contr/HttpIngestTransModule.cc index b49c8e8cc..5f34462d4 100644 --- a/src/replica/contr/HttpIngestTransModule.cc +++ b/src/replica/contr/HttpIngestTransModule.cc @@ -33,6 +33,7 @@ // Qserv headers #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/config/Configuration.h" #include "replica/jobs/AbortTransactionJob.h" #include "replica/jobs/DirectorIndexJob.h" diff --git a/src/replica/contr/HttpJobsModule.cc b/src/replica/contr/HttpJobsModule.cc index 1a049d30c..f1afda4c0 100644 --- a/src/replica/contr/HttpJobsModule.cc +++ b/src/replica/contr/HttpJobsModule.cc @@ -27,6 +27,7 @@ // Qserv headers #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/services/DatabaseServices.h" #include "replica/services/ServiceProvider.h" diff --git a/src/replica/contr/HttpModule.cc b/src/replica/contr/HttpModule.cc index c090aa5ad..7cbc3ef68 100644 --- a/src/replica/contr/HttpModule.cc +++ b/src/replica/contr/HttpModule.cc @@ -52,8 +52,8 @@ HttpModule::HttpModule(Controller::Ptr const& controller, string const& taskName HttpProcessorConfig const& processorConfig, qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp) : EventLogger(controller, taskName), - http::ModuleBase(controller->serviceProvider()->authKey(), - controller->serviceProvider()->adminAuthKey(), req, resp), + http::QhttpModule(controller->serviceProvider()->authKey(), + controller->serviceProvider()->adminAuthKey(), req, resp), _processorConfig(processorConfig) {} string HttpModule::context() const { return name() + " "; } diff --git a/src/replica/contr/HttpModule.h b/src/replica/contr/HttpModule.h index ae4a4dabc..bd97f51eb 100644 --- a/src/replica/contr/HttpModule.h +++ b/src/replica/contr/HttpModule.h @@ -26,7 +26,7 @@ #include // Qserv headers -#include "http/ModuleBase.h" +#include "http/QhttpModule.h" #include "qhttp/Request.h" #include "qhttp/Response.h" #include "replica/config/Configuration.h" @@ -53,7 +53,7 @@ namespace lsst::qserv::replica { * Class HttpModule is a base class for requests processing modules * of an HTTP server built into the Master Replication Controller. */ -class HttpModule : public EventLogger, public http::ModuleBase { +class HttpModule : public EventLogger, public http::QhttpModule { public: HttpModule() = delete; HttpModule(HttpModule const&) = delete; @@ -79,7 +79,7 @@ class HttpModule : public EventLogger, public http::ModuleBase { unsigned int qservSyncTimeoutSec() const { return _processorConfig.qservSyncTimeoutSec; } unsigned int workerReconfigTimeoutSec() const { return _processorConfig.workerReconfigTimeoutSec; } - /// @see http::ModuleBase::context() + /// @see http::Module::context() virtual std::string context() const final; /// @param database The name of a database to connect to. diff --git a/src/replica/contr/HttpQservMonitorModule.cc b/src/replica/contr/HttpQservMonitorModule.cc index 0d4006142..ade25e38f 100644 --- a/src/replica/contr/HttpQservMonitorModule.cc +++ b/src/replica/contr/HttpQservMonitorModule.cc @@ -35,6 +35,7 @@ #include "css/CssError.h" #include "global/intTypes.h" #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "qmeta/types.h" #include "replica/config/Configuration.h" #include "replica/config/ConfigDatabase.h" diff --git a/src/replica/contr/HttpRequestsModule.cc b/src/replica/contr/HttpRequestsModule.cc index 6fdb69981..bf706cbe3 100644 --- a/src/replica/contr/HttpRequestsModule.cc +++ b/src/replica/contr/HttpRequestsModule.cc @@ -27,6 +27,7 @@ // Qserv headers #include "http/Exceptions.h" +#include "http/RequestQuery.h" #include "replica/services/DatabaseServices.h" #include "replica/services/ServiceProvider.h" diff --git a/src/replica/contr/HttpSqlIndexModule.cc b/src/replica/contr/HttpSqlIndexModule.cc index 4bd8a76bd..3dcb1fb05 100644 --- a/src/replica/contr/HttpSqlIndexModule.cc +++ b/src/replica/contr/HttpSqlIndexModule.cc @@ -30,7 +30,8 @@ // Qserv headers #include "http/Exceptions.h" -#include "http/RequestBody.h" +#include "http/RequestBodyJSON.h" +#include "http/RequestQuery.h" #include "replica/config/Configuration.h" #include "replica/jobs/SqlCreateIndexesJob.h" #include "replica/jobs/SqlDropIndexesJob.h" @@ -154,15 +155,15 @@ json HttpSqlIndexModule::_createIndexes() { } vector indexColumns; for (auto&& columnJson : columnsJson) { - string const column = http::RequestBody::required(columnJson, "column"); + string const column = http::RequestBodyJSON::required(columnJson, "column"); if (!table.columns.empty() and table.columns.cend() == find_if(table.columns.cbegin(), table.columns.cend(), [&column](auto&& c) { return c.name == column; })) { throw invalid_argument(context() + "::" + string(__func__) + " requested column '" + column + "' has not been found in the table schema."); } - indexColumns.emplace_back(column, http::RequestBody::required(columnJson, "length"), - http::RequestBody::required(columnJson, "ascending")); + indexColumns.emplace_back(column, http::RequestBodyJSON::required(columnJson, "length"), + http::RequestBodyJSON::required(columnJson, "ascending")); } bool const allWorkers = true; diff --git a/src/replica/ingest/IngestDataHttpSvcMod.cc b/src/replica/ingest/IngestDataHttpSvcMod.cc index bc66a6b23..7007dbef3 100644 --- a/src/replica/ingest/IngestDataHttpSvcMod.cc +++ b/src/replica/ingest/IngestDataHttpSvcMod.cc @@ -26,6 +26,8 @@ #include "http/BinaryEncoding.h" #include "http/Exceptions.h" #include "http/Method.h" +#include "qhttp/Request.h" +#include "qhttp/Response.h" #include "replica/config/Configuration.h" #include "replica/services/DatabaseServices.h" #include "replica/util/Csv.h" @@ -43,7 +45,7 @@ namespace util = lsst::qserv::util; namespace { /// @return requestor's IP address -string senderIpAddr(qhttp::Request::Ptr const& req) { +string senderIpAddr(shared_ptr const& req) { ostringstream ss; ss << req->remoteAddr.address(); return ss.str(); @@ -69,16 +71,17 @@ bool isBinaryColumnType(string const& type) { namespace lsst::qserv::replica { void IngestDataHttpSvcMod::process(ServiceProvider::Ptr const& serviceProvider, string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, - string const& subModuleName, http::AuthType const authType) { + shared_ptr const& req, + shared_ptr const& resp, string const& subModuleName, + http::AuthType const authType) { IngestDataHttpSvcMod module(serviceProvider, workerName, req, resp); module.execute(subModuleName, authType); } IngestDataHttpSvcMod::IngestDataHttpSvcMod(ServiceProvider::Ptr const& serviceProvider, - string const& workerName, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp) - : http::ModuleBase(serviceProvider->authKey(), serviceProvider->adminAuthKey(), req, resp), + string const& workerName, shared_ptr const& req, + shared_ptr const& resp) + : http::QhttpModule(serviceProvider->authKey(), serviceProvider->adminAuthKey(), req, resp), IngestFileSvc(serviceProvider, workerName) {} string IngestDataHttpSvcMod::context() const { return "INGEST-DATA-HTTP-SVC "; } diff --git a/src/replica/ingest/IngestDataHttpSvcMod.h b/src/replica/ingest/IngestDataHttpSvcMod.h index bfe6ff84d..b92d3ae93 100644 --- a/src/replica/ingest/IngestDataHttpSvcMod.h +++ b/src/replica/ingest/IngestDataHttpSvcMod.h @@ -28,13 +28,18 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "http/ModuleBase.h" -#include "qhttp/Request.h" -#include "qhttp/Response.h" +#include "http/QhttpModule.h" #include "replica/ingest/IngestFileSvc.h" #include "replica/ingest/TransactionContrib.h" #include "replica/services/ServiceProvider.h" +// Forward declarations + +namespace lsst::qserv::qhttp { +class Request; +class Response; +} // namespace lsst::qserv::qhttp + // This header declarations namespace lsst::qserv::replica { @@ -44,7 +49,7 @@ namespace lsst::qserv::replica { * Unlike class IngestHttpSvcMod, the current class is meant to be used for ingesting * payloads that are pushed directly into the service over the HTTP protocol. */ -class IngestDataHttpSvcMod : public http::ModuleBase, public IngestFileSvc { +class IngestDataHttpSvcMod : public http::QhttpModule, public IngestFileSvc { public: IngestDataHttpSvcMod() = delete; IngestDataHttpSvcMod(IngestDataHttpSvcMod const&) = delete; @@ -70,21 +75,22 @@ class IngestDataHttpSvcMod : public http::ModuleBase, public IngestFileSvc { * @throws std::invalid_argument for unknown values of parameter 'subModuleName' */ static void process(ServiceProvider::Ptr const& serviceProvider, std::string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, - std::string const& subModuleName, + std::shared_ptr const& req, + std::shared_ptr const& resp, std::string const& subModuleName, http::AuthType const authType = http::AuthType::REQUIRED); protected: - /// @see http::ModuleBase::context() + /// @see http::Module::context() virtual std::string context() const final; - /// @see http::ModuleBase::executeImpl() + /// @see http::Module::executeImpl() virtual nlohmann::json executeImpl(std::string const& subModuleName) final; private: /// @see method IngestDataHttpSvcMod::create() IngestDataHttpSvcMod(ServiceProvider::Ptr const& serviceProvider, std::string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp); + std::shared_ptr const& req, + std::shared_ptr const& resp); /// Process a table contribution request (SYNC). nlohmann::json _syncProcessData(); diff --git a/src/replica/ingest/IngestHttpSvcMod.cc b/src/replica/ingest/IngestHttpSvcMod.cc index 154ad2979..7a44ed3ea 100644 --- a/src/replica/ingest/IngestHttpSvcMod.cc +++ b/src/replica/ingest/IngestHttpSvcMod.cc @@ -24,6 +24,8 @@ // Qserv header #include "http/Method.h" +#include "qhttp/Request.h" +#include "qhttp/Response.h" #include "replica/util/Csv.h" // System headers @@ -37,7 +39,7 @@ namespace lsst::qserv::replica { void IngestHttpSvcMod::process(ServiceProvider::Ptr const& serviceProvider, IngestRequestMgr::Ptr const& ingestRequestMgr, string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, + qhttp::Request::Ptr const& req, shared_ptr const& resp, string const& subModuleName, http::AuthType const authType) { IngestHttpSvcMod module(serviceProvider, ingestRequestMgr, workerName, req, resp); module.execute(subModuleName, authType); @@ -45,8 +47,8 @@ void IngestHttpSvcMod::process(ServiceProvider::Ptr const& serviceProvider, IngestHttpSvcMod::IngestHttpSvcMod(ServiceProvider::Ptr const& serviceProvider, IngestRequestMgr::Ptr const& ingestRequestMgr, string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp) - : http::ModuleBase(serviceProvider->authKey(), serviceProvider->adminAuthKey(), req, resp), + qhttp::Request::Ptr const& req, shared_ptr const& resp) + : http::QhttpModule(serviceProvider->authKey(), serviceProvider->adminAuthKey(), req, resp), _serviceProvider(serviceProvider), _ingestRequestMgr(ingestRequestMgr), _workerName(workerName) {} diff --git a/src/replica/ingest/IngestHttpSvcMod.h b/src/replica/ingest/IngestHttpSvcMod.h index a015e21ac..0e24f322b 100644 --- a/src/replica/ingest/IngestHttpSvcMod.h +++ b/src/replica/ingest/IngestHttpSvcMod.h @@ -28,9 +28,7 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "http/ModuleBase.h" -#include "qhttp/Request.h" -#include "qhttp/Response.h" +#include "http/QhttpModule.h" #include "replica/ingest/IngestRequest.h" #include "replica/ingest/IngestRequestMgr.h" #include "replica/services/ServiceProvider.h" @@ -42,7 +40,7 @@ namespace lsst::qserv::replica { * Class IngestHttpSvcMod processes chunk/table contribution requests made over HTTP. * The class is used by the HTTP server built into the worker Ingest service. */ -class IngestHttpSvcMod : public http::ModuleBase { +class IngestHttpSvcMod : public http::QhttpModule { public: IngestHttpSvcMod() = delete; IngestHttpSvcMod(IngestHttpSvcMod const&) = delete; @@ -81,22 +79,23 @@ class IngestHttpSvcMod : public http::ModuleBase { */ static void process(ServiceProvider::Ptr const& serviceProvider, IngestRequestMgr::Ptr const& ingestRequestMgr, std::string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, - std::string const& subModuleName, + std::shared_ptr const& req, + std::shared_ptr const& resp, std::string const& subModuleName, http::AuthType const authType = http::AuthType::REQUIRED); protected: - /// @see http::ModuleBase::context() + /// @see http::Module::context() virtual std::string context() const final; - /// @see http::ModuleBase::executeImpl() + /// @see http::Module::executeImpl() virtual nlohmann::json executeImpl(std::string const& subModuleName) final; private: /// @see method IngestHttpSvcMod::create() IngestHttpSvcMod(ServiceProvider::Ptr const& serviceProvider, IngestRequestMgr::Ptr const& ingestRequestMgr, std::string const& workerName, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp); + std::shared_ptr const& req, + std::shared_ptr const& resp); /// Process a table contribution request (SYNC). nlohmann::json _syncProcessRequest() const; diff --git a/src/replica/registry/RegistryHttpSvcMod.cc b/src/replica/registry/RegistryHttpSvcMod.cc index a150acd95..58461f01a 100644 --- a/src/replica/registry/RegistryHttpSvcMod.cc +++ b/src/replica/registry/RegistryHttpSvcMod.cc @@ -40,7 +40,7 @@ using namespace lsst::qserv; namespace { /// @return requestor's IP address -string senderIpAddr(qhttp::Request::Ptr const& req) { +string senderIpAddr(shared_ptr const& req) { ostringstream ss; ss << req->remoteAddr.address(); return ss.str(); @@ -62,16 +62,17 @@ bool isSecurityContextKey(string const& key) { namespace lsst::qserv::replica { void RegistryHttpSvcMod::process(ServiceProvider::Ptr const& serviceProvider, RegistryServices& services, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, - string const& subModuleName, http::AuthType const authType) { + shared_ptr const& req, + shared_ptr const& resp, string const& subModuleName, + http::AuthType const authType) { RegistryHttpSvcMod module(serviceProvider, services, req, resp); module.execute(subModuleName, authType); } RegistryHttpSvcMod::RegistryHttpSvcMod(ServiceProvider::Ptr const& serviceProvider, - RegistryServices& services, qhttp::Request::Ptr const& req, - qhttp::Response::Ptr const& resp) - : http::ModuleBase(serviceProvider->authKey(), serviceProvider->adminAuthKey(), req, resp), + RegistryServices& services, shared_ptr const& req, + shared_ptr const& resp) + : http::QhttpModule(serviceProvider->authKey(), serviceProvider->adminAuthKey(), req, resp), _serviceProvider(serviceProvider), _services(services) {} diff --git a/src/replica/registry/RegistryHttpSvcMod.h b/src/replica/registry/RegistryHttpSvcMod.h index f4477fd15..ae33a25bd 100644 --- a/src/replica/registry/RegistryHttpSvcMod.h +++ b/src/replica/registry/RegistryHttpSvcMod.h @@ -28,9 +28,7 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "http/ModuleBase.h" -#include "qhttp/Request.h" -#include "qhttp/Response.h" +#include "http/QhttpModule.h" #include "replica/services/ServiceProvider.h" // Forward declarations @@ -47,7 +45,7 @@ namespace lsst::qserv::replica { * @note Each worker entry represents a collection of attributes merged from * from two sources - Replication System's worker and Qserv worker. */ -class RegistryHttpSvcMod : public http::ModuleBase { +class RegistryHttpSvcMod : public http::QhttpModule { public: RegistryHttpSvcMod() = delete; RegistryHttpSvcMod(RegistryHttpSvcMod const&) = delete; @@ -79,21 +77,22 @@ class RegistryHttpSvcMod : public http::ModuleBase { * @throws std::invalid_argument for unknown values of parameter 'subModuleName' */ static void process(ServiceProvider::Ptr const& serviceProvider, RegistryServices& services, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp, - std::string const& subModuleName, + std::shared_ptr const& req, + std::shared_ptr const& resp, std::string const& subModuleName, http::AuthType const authType = http::AuthType::REQUIRED); protected: - /// @see http::ModuleBase::context() + /// @see http::Module::context() virtual std::string context() const final; - /// @see http::ModuleBase::executeImpl() + /// @see http::Module::executeImpl() virtual nlohmann::json executeImpl(std::string const& subModuleName) final; private: /// @see method RegistryHttpSvcMod::create() RegistryHttpSvcMod(ServiceProvider::Ptr const& serviceProvider, RegistryServices& services, - qhttp::Request::Ptr const& req, qhttp::Response::Ptr const& resp); + std::shared_ptr const& req, + std::shared_ptr const& resp); /// Return a collection of known services. nlohmann::json _getServices() const; diff --git a/src/xrdsvc/HttpModule.cc b/src/xrdsvc/HttpModule.cc index 53beae66b..46bc058e5 100644 --- a/src/xrdsvc/HttpModule.cc +++ b/src/xrdsvc/HttpModule.cc @@ -27,7 +27,7 @@ // Qserv headers #include "http/Exceptions.h" -#include "http/RequestBody.h" +#include "http/RequestBodyJSON.h" #include "http/RequestQuery.h" #include "qhttp/Request.h" #include "wbase/TaskState.h" @@ -41,8 +41,8 @@ namespace lsst::qserv::xrdsvc { HttpModule::HttpModule(string const& context, shared_ptr const& foreman, shared_ptr const& req, shared_ptr const& resp) - : http::ModuleBase(wconfig::WorkerConfig::instance()->replicationAuthKey(), - wconfig::WorkerConfig::instance()->replicationAdminAuthKey(), req, resp), + : http::QhttpModule(wconfig::WorkerConfig::instance()->replicationAuthKey(), + wconfig::WorkerConfig::instance()->replicationAdminAuthKey(), req, resp), _context(context), _foreman(foreman) {} diff --git a/src/xrdsvc/HttpModule.h b/src/xrdsvc/HttpModule.h index 31d85e162..fb122041d 100644 --- a/src/xrdsvc/HttpModule.h +++ b/src/xrdsvc/HttpModule.h @@ -26,7 +26,7 @@ #include // Qserv headers -#include "http/ModuleBase.h" +#include "http/QhttpModule.h" namespace lsst::qserv::qhttp { class Request; @@ -48,7 +48,7 @@ namespace lsst::qserv::xrdsvc { /** * Class HttpModule is an intermediate base class of the Qserv worker modules. */ -class HttpModule : public http::ModuleBase { +class HttpModule : public http::QhttpModule { public: HttpModule() = delete; HttpModule(HttpModule const&) = delete; diff --git a/src/xrdsvc/HttpReplicaMgtModule.cc b/src/xrdsvc/HttpReplicaMgtModule.cc index 5ffcb03f7..afa81d74c 100644 --- a/src/xrdsvc/HttpReplicaMgtModule.cc +++ b/src/xrdsvc/HttpReplicaMgtModule.cc @@ -32,7 +32,7 @@ // Qserv headers #include "http/Exceptions.h" -#include "http/RequestBody.h" +#include "http/RequestBodyJSON.h" #include "http/RequestQuery.h" #include "mysql/MySqlUtils.h" #include "util/String.h" From bba044ff5c8a8f016ccb27a77ef444267017ee0d Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Tue, 2 Jul 2024 18:10:58 -0700 Subject: [PATCH 2/5] Added an intermediate base class for the CPP-HTTPLIB-based REST services --- src/http/CMakeLists.txt | 2 + src/http/ChttpModule.cc | 69 +++++++++++++++++++++++++++++++++ src/http/ChttpModule.h | 85 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 src/http/ChttpModule.cc create mode 100644 src/http/ChttpModule.h diff --git a/src/http/CMakeLists.txt b/src/http/CMakeLists.txt index 722c0736b..d379c1603 100644 --- a/src/http/CMakeLists.txt +++ b/src/http/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(http SHARED) target_sources(http PRIVATE AsyncReq.cc BinaryEncoding.cc + ChttpModule.cc Client.cc ClientConnPool.cc ClientConfig.cc @@ -25,6 +26,7 @@ target_link_libraries(http PUBLIC Boost::filesystem Boost::regex Boost::system + cpp-httplib ) install(TARGETS http) diff --git a/src/http/ChttpModule.cc b/src/http/ChttpModule.cc new file mode 100644 index 000000000..8e806944a --- /dev/null +++ b/src/http/ChttpModule.cc @@ -0,0 +1,69 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ + +// Class header +#include "http/ChttpModule.h" + +// System headers +#include + +// Third-party headers +#include + +// Qserv headers +#include "http/RequestQuery.h" + +using namespace std; + +namespace lsst::qserv::http { + +ChttpModule::ChttpModule(string const& authKey, string const& adminAuthKey, httplib::Request const& req, + httplib::Response& resp) + : Module(authKey, adminAuthKey), _req(req), _resp(resp) {} + +string ChttpModule::method() const { return _req.method; } + +unordered_map ChttpModule::params() const { return _req.path_params; } + +RequestQuery ChttpModule::query() const { + // TODO: The query parameters in CPP-HTTPLIB are stored in the std::multimap + // container to allow accumulating values of non-unique keys. For now we need + // to convert the multimap to the std::unordered_map container. This may result + // in losing some query parameters if they have the same key but different values. + // Though, the correct solution is to fix the QHTTP library to support + // the std::multimap container for query parameters. + unordered_map queryParams; + for (auto const& [key, value] : _req.params) queryParams[key] = value; + return RequestQuery(queryParams); +} + +void ChttpModule::getRequestBody(string& content, string const& requiredContentType) { + auto itr = _req.headers.find("Content-Type"); + if (itr != _req.headers.end() && itr->second == requiredContentType) { + content = _req.body; + } +} + +void ChttpModule::sendResponse(string const& content, string const& contentType) { + _resp.set_content(content, contentType); +} + +} // namespace lsst::qserv::http diff --git a/src/http/ChttpModule.h b/src/http/ChttpModule.h new file mode 100644 index 000000000..3f07f8935 --- /dev/null +++ b/src/http/ChttpModule.h @@ -0,0 +1,85 @@ + +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ +#ifndef LSST_QSERV_HTTP_CHTTPMODULE_H +#define LSST_QSERV_HTTP_CHTTPMODULE_H + +// System headers +#include +#include + +// Qserv headers +#include "http/Module.h" + +// Forward declarations + +namespace httplib { +class Request; +class Response; +} // namespace httplib + +namespace lsst::qserv::http { +class RequestBodyJSON; +class RequestQuery; +} // namespace lsst::qserv::http + +// This header declarations +namespace lsst::qserv::http { + +/** + * Class ChttpModule is an extended base class specialized for constructing + * the CPP-HTTPLIB request processing modules. + */ +class ChttpModule : public Module { +public: + ChttpModule() = delete; + ChttpModule(ChttpModule const&) = delete; + ChttpModule& operator=(ChttpModule const&) = delete; + + virtual ~ChttpModule() = default; + +protected: + /** + * @param authKey An authorization key for operations which require extra security. + * @param adminAuthKey An administrator-level authorization key. + * @param req The HTTP request. + * @param resp The HTTP response channel. + */ + ChttpModule(std::string const& authKey, std::string const& adminAuthKey, httplib::Request const& req, + httplib::Response& resp); + + httplib::Request const& req() { return _req; } + httplib::Response& resp() { return _resp; } + + virtual std::string method() const; + virtual std::unordered_map params() const; + virtual RequestQuery query() const; + virtual void getRequestBody(std::string& content, std::string const& requiredContentType); + virtual void sendResponse(std::string const& content, std::string const& contentType); + +private: + httplib::Request const& _req; + httplib::Response& _resp; +}; + +} // namespace lsst::qserv::http + +#endif // LSST_QSERV_HTTP_CHTTPMODULE_H From afd6d1f14980480342dede233bde471547dad518 Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Tue, 2 Jul 2024 18:32:03 -0700 Subject: [PATCH 3/5] Added CPP-HTTPLIB-based version of the meta module --- src/http/CMakeLists.txt | 1 + src/http/ChttpMetaModule.cc | 71 ++++++++++++++++++++++++++++++++ src/http/ChttpMetaModule.h | 80 +++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src/http/ChttpMetaModule.cc create mode 100644 src/http/ChttpMetaModule.h diff --git a/src/http/CMakeLists.txt b/src/http/CMakeLists.txt index d379c1603..800a7be83 100644 --- a/src/http/CMakeLists.txt +++ b/src/http/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(http SHARED) target_sources(http PRIVATE AsyncReq.cc BinaryEncoding.cc + ChttpMetaModule.cc ChttpModule.cc Client.cc ClientConnPool.cc diff --git a/src/http/ChttpMetaModule.cc b/src/http/ChttpMetaModule.cc new file mode 100644 index 000000000..45aff9858 --- /dev/null +++ b/src/http/ChttpMetaModule.cc @@ -0,0 +1,71 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ + +// Class header +#include "http/ChttpMetaModule.h" + +// System headers +#include + +using namespace std; +using json = nlohmann::json; + +namespace { +// Authorization context is not required by this module +lsst::qserv::http::AuthType const authType = lsst::qserv::http::AuthType::NONE; +string const authKey; +string const adminAuthKey; +} // namespace + +namespace lsst::qserv::http { + +unsigned int const ChttpMetaModule::version = 35; + +void ChttpMetaModule::process(string const& context, nlohmann::json const& info, httplib::Request const& req, + httplib::Response& resp, string const& subModuleName) { + ChttpMetaModule module(context, info, req, resp); + module.execute(subModuleName, ::authType); +} + +ChttpMetaModule::ChttpMetaModule(string const& context, nlohmann::json const& info, + httplib::Request const& req, httplib::Response& resp) + : http::ChttpModule(::authKey, ::adminAuthKey, req, resp), _context(context), _info(info) { + if (!_info.is_object()) { + throw invalid_argument("ChttpMetaModule::" + string(__func__) + " parameter info must be an object."); + } +} + +json ChttpMetaModule::executeImpl(string const& subModuleName) { + if (subModuleName == "VERSION") return _version(); + throw invalid_argument(context() + "::" + string(__func__) + " unsupported sub-module: '" + + subModuleName + "'"); +} + +string ChttpMetaModule::context() const { return _context; } + +json ChttpMetaModule::_version() { + debug(__func__); + json result = _info; + result["version"] = ChttpMetaModule::version; + return result; +} + +} // namespace lsst::qserv::http diff --git a/src/http/ChttpMetaModule.h b/src/http/ChttpMetaModule.h new file mode 100644 index 000000000..1e500b8d5 --- /dev/null +++ b/src/http/ChttpMetaModule.h @@ -0,0 +1,80 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ +#ifndef LSST_QSERV_HTTP_CHTTPMETAMODULE_H +#define LSST_QSERV_HTTP_CHTTPMETAMODULE_H + +// System headers +#include +#include + +// Third party headers +#include "nlohmann/json.hpp" + +// Qserv headers +#include "http/ChttpModule.h" + +// This header declarations +namespace lsst::qserv::http { + +/** + * Class ChttpMetaModule implements a handler for the metadata queries on the REST API itself. + * The service responds with an information object provided at the creation time of the module. + */ +class ChttpMetaModule : public http::ChttpModule { +public: + typedef std::shared_ptr Ptr; + + /// The current version of the REST API + static unsigned int const version; + + /** + * @note supported values for parameter 'subModuleName' are: + * 'VERSION' - return a version of the REST API + * + * @param info The information object to be returned to clients of the service. + * @throws std::invalid_argument for unknown values of parameter 'subModuleName' + */ + static void process(std::string const& context, nlohmann::json const& info, httplib::Request const& req, + httplib::Response& resp, std::string const& subModuleName); + + ChttpMetaModule() = delete; + ChttpMetaModule(ChttpMetaModule const&) = delete; + ChttpMetaModule& operator=(ChttpMetaModule const&) = delete; + + ~ChttpMetaModule() final = default; + +protected: + virtual nlohmann::json executeImpl(std::string const& subModuleName) final; + virtual std::string context() const final; + +private: + ChttpMetaModule(std::string const& context, nlohmann::json const& info, httplib::Request const& req, + httplib::Response& resp); + + nlohmann::json _version(); + + std::string const _context; + nlohmann::json const _info; +}; + +} // namespace lsst::qserv::http + +#endif // LSST_QSERV_HTTP_CHTTPMETAMODULE_H From da1f607b2492c2d48aa7351cdd203a944fda1a91 Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Wed, 3 Jul 2024 16:36:26 -0700 Subject: [PATCH 4/5] Implemented HTTPS-based Czar front-end Eliminated classes of the QHTTP-based version of the Czar frontend --- .../python/lsst/qserv/admin/cli/entrypoint.py | 8 +- src/czar/CMakeLists.txt | 8 +- src/czar/ChttpModule.cc | 67 +++++++ src/czar/ChttpModule.h | 69 +++++++ src/czar/HttpCzarIngestModule.cc | 11 +- src/czar/HttpCzarIngestModule.h | 15 +- src/czar/HttpCzarQueryModule.cc | 11 +- src/czar/HttpCzarQueryModule.h | 15 +- src/czar/HttpCzarSvc.cc | 177 +++++++++--------- src/czar/HttpCzarSvc.h | 127 +++---------- src/czar/HttpMonitorModule.cc | 2 +- src/czar/HttpMonitorModule.h | 4 +- src/czar/{HttpModule.cc => QhttpModule.cc} | 10 +- src/czar/{HttpModule.h => QhttpModule.h} | 22 +-- src/czar/qserv-czar-http.cc | 20 +- 15 files changed, 319 insertions(+), 247 deletions(-) create mode 100644 src/czar/ChttpModule.cc create mode 100644 src/czar/ChttpModule.h rename src/czar/{HttpModule.cc => QhttpModule.cc} (88%) rename src/czar/{HttpModule.h => QhttpModule.h} (74%) diff --git a/src/admin/python/lsst/qserv/admin/cli/entrypoint.py b/src/admin/python/lsst/qserv/admin/cli/entrypoint.py index e992ebfa8..e679ee47c 100644 --- a/src/admin/python/lsst/qserv/admin/cli/entrypoint.py +++ b/src/admin/python/lsst/qserv/admin/cli/entrypoint.py @@ -137,7 +137,13 @@ class CommandInfo: "--lua-cpath=/usr/local/lua/qserv/lib/czarProxy.so --defaults-file={{proxy_cfg_path}}", )), ("czar-http", CommandInfo( - "qserv-czar-http http {{czar_cfg_path}} {{http_frontend_port}} {{http_frontend_threads}} ", + "qserv-czar-http " + "http " + "{{czar_cfg_path}} " + "{{http_frontend_port}} " + "{{http_frontend_threads}} " + "{{http_ssl_cert_file}} " + "{{http_ssl_private_key_file}}", )), ("cmsd-manager", CommandInfo( "cmsd -c {{cmsd_manager_cfg_path}} -n manager -I v4", diff --git a/src/czar/CMakeLists.txt b/src/czar/CMakeLists.txt index 613fdc243..4015913c7 100644 --- a/src/czar/CMakeLists.txt +++ b/src/czar/CMakeLists.txt @@ -1,14 +1,15 @@ add_library(czar OBJECT) target_sources(czar PRIVATE + ChttpModule.cc Czar.cc HttpCzarIngestModule.cc - HttpCzarSvc.cc HttpCzarQueryModule.cc - HttpModule.cc + HttpCzarSvc.cc HttpMonitorModule.cc HttpSvc.cc MessageTable.cc + QhttpModule.cc ) target_include_directories(czar PRIVATE @@ -23,6 +24,7 @@ target_link_libraries(czar PUBLIC util log XrdSsiLib + cpp-httplib ) function(CZAR_UTILS) @@ -51,4 +53,4 @@ endfunction() czar_utils( qserv-czar-http -) \ No newline at end of file +) diff --git a/src/czar/ChttpModule.cc b/src/czar/ChttpModule.cc new file mode 100644 index 000000000..f080b0219 --- /dev/null +++ b/src/czar/ChttpModule.cc @@ -0,0 +1,67 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ + +// Class header +#include "czar/ChttpModule.h" + +// System headers +#include + +// Qserv headers +#include "cconfig/CzarConfig.h" +#include "http/Exceptions.h" +#include "http/RequestBodyJSON.h" +#include "http/RequestQuery.h" + +using namespace std; + +namespace lsst::qserv::czar { + +ChttpModule::ChttpModule(string const& context, httplib::Request const& req, httplib::Response& resp) + : http::ChttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(), + cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), + _context(context) {} + +string ChttpModule::context() const { return _context; } + +void ChttpModule::enforceCzarName(string const& func) const { + string const czarNameAttrName = "czar"; + string czarName; + if (method() == "GET") { + if (!query().has(czarNameAttrName)) { + throw http::Error(func, "No Czar identifier was provided in the request query."); + } + czarName = query().requiredString(czarNameAttrName); + } else { + if (!body().has(czarNameAttrName)) { + throw http::Error(func, "No Czar identifier was provided in the request body."); + } + czarName = body().required(czarNameAttrName); + } + string const expectedCzarName = cconfig::CzarConfig::instance()->name(); + if (expectedCzarName != czarName) { + string const msg = "Requested Czar identifier '" + czarName + "' does not match the one '" + + expectedCzarName + "' of the current Czar."; + throw http::Error(func, msg); + } +} + +} // namespace lsst::qserv::czar diff --git a/src/czar/ChttpModule.h b/src/czar/ChttpModule.h new file mode 100644 index 000000000..efb91a15b --- /dev/null +++ b/src/czar/ChttpModule.h @@ -0,0 +1,69 @@ +/* + * LSST Data Management System + * + * This product includes software developed by the + * LSST Project (http://www.lsst.org/). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 LSST License Statement and + * the GNU General Public License along with this program. If not, + * see . + */ +#ifndef LSST_QSERV_CZAR_CHTTPMODULE_H +#define LSST_QSERV_CZAR_CHTTPMODULE_H + +// System headers +#include + +// Qserv headers +#include "http/ChttpModule.h" + +// Forward declarations +namespace httplib { +class Request; +class Response; +} // namespace httplib + +// This header declarations +namespace lsst::qserv::czar { + +/** + * Class ChttpModule is an intermediate base class of the Qserv Czar modules. + */ +class ChttpModule : public http::ChttpModule { +public: + ChttpModule() = delete; + ChttpModule(ChttpModule const&) = delete; + ChttpModule& operator=(ChttpModule const&) = delete; + + virtual ~ChttpModule() = default; + +protected: + ChttpModule(std::string const& context, httplib::Request const& req, httplib::Response& resp); + + virtual std::string context() const final; + + /** + * Check if Czar identifier is present in a request and if so then the identifier + * is the same as the one of the current Czar. Throw an exception in case of mismatch. + * @param func The name of the calling context (it's used for error reporting). + * @throws std::invalid_argument If the dentifiers didn't match. + */ + void enforceCzarName(std::string const& func) const; + +private: + std::string const _context; +}; + +} // namespace lsst::qserv::czar + +#endif // LSST_QSERV_CZAR_CHTTPMODULE_H diff --git a/src/czar/HttpCzarIngestModule.cc b/src/czar/HttpCzarIngestModule.cc index c8ccdef7f..a0443d6a2 100644 --- a/src/czar/HttpCzarIngestModule.cc +++ b/src/czar/HttpCzarIngestModule.cc @@ -38,7 +38,6 @@ #include "http/Exceptions.h" #include "http/MetaModule.h" #include "http/RequestBodyJSON.h" -#include "qhttp/Request.h" #include "qhttp/Status.h" using namespace std; @@ -106,17 +105,15 @@ void setProtocolFields(json& data) { namespace lsst::qserv::czar { void HttpCzarIngestModule::process(asio::io_service& io_service, string const& context, - shared_ptr const& req, - shared_ptr const& resp, string const& subModuleName, - http::AuthType const authType) { + httplib::Request const& req, httplib::Response& resp, + string const& subModuleName, http::AuthType const authType) { HttpCzarIngestModule module(io_service, context, req, resp); module.execute(subModuleName, authType); } HttpCzarIngestModule::HttpCzarIngestModule(asio::io_service& io_service, string const& context, - shared_ptr const& req, - shared_ptr const& resp) - : http::QhttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(), + httplib::Request const& req, httplib::Response& resp) + : http::ChttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(), cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), _io_service(io_service), _context(context), diff --git a/src/czar/HttpCzarIngestModule.h b/src/czar/HttpCzarIngestModule.h index 4ce9ea237..25b53bdf3 100644 --- a/src/czar/HttpCzarIngestModule.h +++ b/src/czar/HttpCzarIngestModule.h @@ -32,8 +32,8 @@ #include "nlohmann/json.hpp" // Qserv headers +#include "http/ChttpModule.h" #include "http/Method.h" -#include "http/QhttpModule.h" // Forward declarations @@ -41,10 +41,10 @@ namespace lsst::qserv::http { class AsyncReq; } // namespace lsst::qserv::http -namespace lsst::qserv::qhttp { +namespace httplib { class Request; class Response; -} // namespace lsst::qserv::qhttp +} // namespace httplib // This header declarations namespace lsst::qserv::czar { @@ -53,7 +53,7 @@ namespace lsst::qserv::czar { * Class HttpCzarIngestModule implements a handler for processing requests for ingesting * user-generated data prodicts via the HTTP-based frontend. */ -class HttpCzarIngestModule : public http::QhttpModule { +class HttpCzarIngestModule : public http::ChttpModule { public: /** * @note supported values for parameter 'subModuleName' are: @@ -64,8 +64,8 @@ class HttpCzarIngestModule : public http::QhttpModule { * @throws std::invalid_argument for unknown values of parameter 'subModuleName' */ static void process(boost::asio::io_service& io_service, std::string const& context, - std::shared_ptr const& req, - std::shared_ptr const& resp, std::string const& subModuleName, + httplib::Request const& req, httplib::Response& resp, + std::string const& subModuleName, http::AuthType const authType = http::AuthType::NONE); HttpCzarIngestModule() = delete; @@ -80,8 +80,7 @@ class HttpCzarIngestModule : public http::QhttpModule { private: HttpCzarIngestModule(boost::asio::io_service& io_service, std::string const& context, - std::shared_ptr const& req, - std::shared_ptr const& resp); + httplib::Request const& req, httplib::Response& resp); nlohmann::json _ingestData(); nlohmann::json _deleteDatabase(); diff --git a/src/czar/HttpCzarQueryModule.cc b/src/czar/HttpCzarQueryModule.cc index 5d45e77d0..ec10497f4 100644 --- a/src/czar/HttpCzarQueryModule.cc +++ b/src/czar/HttpCzarQueryModule.cc @@ -52,16 +52,15 @@ vector const binTypes = {"BIT", "BINARY", "VARBINARY", "TINYBLOB", "BLOB namespace lsst::qserv::czar { -void HttpCzarQueryModule::process(string const& context, shared_ptr const& req, - shared_ptr const& resp, string const& subModuleName, - http::AuthType const authType) { +void HttpCzarQueryModule::process(string const& context, httplib::Request const& req, httplib::Response& resp, + string const& subModuleName, http::AuthType const authType) { HttpCzarQueryModule module(context, req, resp); module.execute(subModuleName, authType); } -HttpCzarQueryModule::HttpCzarQueryModule(string const& context, shared_ptr const& req, - shared_ptr const& resp) - : HttpModule(context, req, resp) {} +HttpCzarQueryModule::HttpCzarQueryModule(string const& context, httplib::Request const& req, + httplib::Response& resp) + : ChttpModule(context, req, resp) {} json HttpCzarQueryModule::executeImpl(string const& subModuleName) { string const func = string(__func__) + "[sub-module='" + subModuleName + "']"; diff --git a/src/czar/HttpCzarQueryModule.h b/src/czar/HttpCzarQueryModule.h index 0f267ba07..eb8ee22a9 100644 --- a/src/czar/HttpCzarQueryModule.h +++ b/src/czar/HttpCzarQueryModule.h @@ -29,7 +29,7 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "czar/HttpModule.h" +#include "czar/ChttpModule.h" #include "global/intTypes.h" #include "http/BinaryEncoding.h" @@ -39,10 +39,10 @@ namespace lsst::qserv::czar { struct SubmitResult; } // namespace lsst::qserv::czar -namespace lsst::qserv::qhttp { +namespace httplib { class Request; class Response; -} // namespace lsst::qserv::qhttp +} // namespace httplib namespace lsst::qserv::sql { class SqlResults; @@ -56,7 +56,7 @@ namespace lsst::qserv::czar { * Class HttpCzarQueryModule implements a handler for processing user * queries submitted to Czar via the HTTP-based frontend. */ -class HttpCzarQueryModule : public czar::HttpModule { +class HttpCzarQueryModule : public czar::ChttpModule { public: /** * @note supported values for parameter 'subModuleName' are: @@ -68,8 +68,8 @@ class HttpCzarQueryModule : public czar::HttpModule { * * @throws std::invalid_argument for unknown values of parameter 'subModuleName' */ - static void process(std::string const& context, std::shared_ptr const& req, - std::shared_ptr const& resp, std::string const& subModuleName, + static void process(std::string const& context, httplib::Request const& req, httplib::Response& resp, + std::string const& subModuleName, http::AuthType const authType = http::AuthType::NONE); HttpCzarQueryModule() = delete; @@ -82,8 +82,7 @@ class HttpCzarQueryModule : public czar::HttpModule { virtual nlohmann::json executeImpl(std::string const& subModuleName) final; private: - HttpCzarQueryModule(std::string const& context, std::shared_ptr const& req, - std::shared_ptr const& resp); + HttpCzarQueryModule(std::string const& context, httplib::Request const& req, httplib::Response& resp); nlohmann::json _submit(); nlohmann::json _submitAsync(); diff --git a/src/czar/HttpCzarSvc.cc b/src/czar/HttpCzarSvc.cc index ca4326904..026855a54 100644 --- a/src/czar/HttpCzarSvc.cc +++ b/src/czar/HttpCzarSvc.cc @@ -25,12 +25,17 @@ // System headers #include +// Third-party headers +#ifndef CPPHTTPLIB_OPENSSL_SUPPORT +#define CPPHTTPLIB_OPENSSL_SUPPORT 1 +#endif +#include + // Qserv headers #include "cconfig/CzarConfig.h" #include "czar/HttpCzarIngestModule.h" #include "czar/HttpCzarQueryModule.h" -#include "http/MetaModule.h" -#include "qhttp/Server.h" +#include "http/ChttpMetaModule.h" // LSST headers #include "lsst/log/Log.h" @@ -44,114 +49,108 @@ LOG_LOGGER _log = LOG_GET("lsst.qserv.czar.HttpCzarSvc"); string const serviceName = "CZAR-FRONTEND "; +template +void throwIf(bool condition, string const& message) { + if (condition) throw T(message); +} + } // namespace namespace lsst::qserv::czar { -shared_ptr HttpCzarSvc::create(uint16_t port, unsigned int numThreads) { - return shared_ptr(new HttpCzarSvc(port, numThreads)); +shared_ptr HttpCzarSvc::create(int port, unsigned int numThreads, string const& sslCertFile, + string const& sslPrivateKeyFile) { + return shared_ptr(new HttpCzarSvc(port, numThreads, sslCertFile, sslPrivateKeyFile)); } -HttpCzarSvc::HttpCzarSvc(uint16_t port, unsigned int numThreads) : _port(port), _numThreads(numThreads) {} +HttpCzarSvc::HttpCzarSvc(int port, unsigned int numThreads, string const& sslCertFile, + string const& sslPrivateKeyFile) + : _port(port), + _numThreads(numThreads), + _sslCertFile(sslCertFile), + _sslPrivateKeyFile(sslPrivateKeyFile) { + _createAndConfigure(); +} -uint16_t HttpCzarSvc::start() { +void HttpCzarSvc::startAndWait() { string const context = "czar::HttpCzarSvc::" + string(__func__) + " "; - if (_httpServerPtr != nullptr) { - throw logic_error(context + "the service is already running."); - } - _httpServerPtr = qhttp::Server::create(_io_service, _port); - auto const self = shared_from_this(); + // IMPORTANT: Request handlers can't be registered in the constructor + // because of the shared_from_this() call. This is because the shared + // pointer is not yet initialized at the time of the constructor call. + _registerHandlers(); - // Make sure the handlers are registered and the server is started before - // launching any BOOST ASIO threads. This will prevent threads from finishing - // due to a lack of work to be done. - _httpServerPtr->addHandlers( - {{"GET", "/meta/version", - [self](shared_ptr const& req, shared_ptr const& resp) { - json const info = json::object( - {{"kind", "qserv-czar-query-frontend"}, - {"id", cconfig::CzarConfig::instance()->id()}, - {"instance_id", cconfig::CzarConfig::instance()->replicationInstanceId()}}); - http::MetaModule::process(::serviceName, info, req, resp, "VERSION"); - }}}); - _httpServerPtr->addHandlers( - {{"POST", "/query", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarQueryModule::process(::serviceName, req, resp, "SUBMIT"); - }}}); - _httpServerPtr->addHandlers( - {{"POST", "/query-async", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarQueryModule::process(::serviceName, req, resp, "SUBMIT-ASYNC"); - }}}); - _httpServerPtr->addHandlers( - {{"DELETE", "/query-async/:qid", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarQueryModule::process(::serviceName, req, resp, "CANCEL"); - }}}); - _httpServerPtr->addHandlers( - {{"GET", "/query-async/status/:qid", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarQueryModule::process(::serviceName, req, resp, "STATUS"); - }}}); - _httpServerPtr->addHandlers( - {{"GET", "/query-async/result/:qid", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarQueryModule::process(::serviceName, req, resp, "RESULT"); - }}}); - _httpServerPtr->addHandlers( - {{"POST", "/ingest/data", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarIngestModule::process(self->_io_service, ::serviceName, req, resp, "INGEST-DATA"); - }}}); - _httpServerPtr->addHandlers( - {{"DELETE", "/ingest/database/:database", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarIngestModule::process(self->_io_service, ::serviceName, req, resp, - "DELETE-DATABASE"); - }}}); - _httpServerPtr->addHandlers( - {{"DELETE", "/ingest/table/:database/:table", - [self](shared_ptr const& req, shared_ptr const& resp) { - HttpCzarIngestModule::process(self->_io_service, ::serviceName, req, resp, "DELETE-TABLE"); - }}}); - _httpServerPtr->start(); + // This will prevent the I/O service from exiting the .run() + // method event when it will run out of any requests to process. + // Unless the service will be explicitly stopped. + _work.reset(new boost::asio::io_service::work(_io_service)); // Initialize the I/O context and start the service threads. At this point // the server will be ready to service incoming requests. - for (unsigned int i = 0; i < _numThreads; ++i) { - _threads.push_back(make_unique([self]() { self->_io_service.run(); })); + for (unsigned int i = 0; i < _numBoostAsioThreads; ++i) { + _threads.push_back(make_unique([self = shared_from_this()]() { self->_io_service.run(); })); } - auto const actualPort = _httpServerPtr->getPort(); - LOGS(_log, LOG_LVL_INFO, context + "started on port " + to_string(actualPort)); - return actualPort; + bool const started = _svr->listen_after_bind(); + ::throwIf(!started, context + "Failed to start the server"); } -void HttpCzarSvc::stop() { +void HttpCzarSvc::_createAndConfigure() { string const context = "czar::HttpCzarSvc::" + string(__func__) + " "; - if (_httpServerPtr == nullptr) { - throw logic_error(context + "the service is not running."); - } - // Stopping the server and resetting the I/O context will abort the ongoing - // requests and unblock the service threads. - _httpServerPtr->stop(); - _httpServerPtr = nullptr; - _io_service.reset(); + ::throwIf(_sslCertFile.empty(), context + "SSL certificate file is not valid"); + ::throwIf(_sslPrivateKeyFile.empty(), context + "SSL private key file is not valid"); - LOGS(_log, LOG_LVL_INFO, context + "stopped"); -} + _svr = make_unique(_sslCertFile.data(), _sslPrivateKeyFile.data()); + ::throwIf(!_svr->is_valid(), context + "Failed to create the server"); -void HttpCzarSvc::wait() { - string const context = "czar::HttpCzarSvc::" + string(__func__) + " "; - if (_httpServerPtr == nullptr) { - throw logic_error(context + "the service is not running."); - } - for (auto&& t : _threads) { - t->join(); + _svr->new_task_queue = [&] { return new httplib::ThreadPool(_numThreads, _maxQueuedRequests); }; + if (_port == 0) { + _port = _svr->bind_to_any_port(_bindAddr, _port); + ::throwIf(_port < 0, context + "Failed to bind the server to any port"); + } else { + bool const bound = _svr->bind_to_port(_bindAddr, _port); + ::throwIf(!bound, + context + "Failed to bind the server to the port: " + to_string(_port)); } - LOGS(_log, LOG_LVL_INFO, context + "unlocked"); + LOGS(_log, LOG_LVL_INFO, context + "started on port " + to_string(_port)); +} + +void HttpCzarSvc::_registerHandlers() { + ::throwIf(_svr == nullptr, + "czar::HttpCzarSvc::" + string(__func__) + " the server is not initialized"); + auto const self = shared_from_this(); + _svr->Get("/meta/version", [self](httplib::Request const& req, httplib::Response& resp) { + json const info = + json::object({{"kind", "qserv-czar-query-frontend"}, + {"id", cconfig::CzarConfig::instance()->id()}, + {"instance_id", cconfig::CzarConfig::instance()->replicationInstanceId()}}); + http::ChttpMetaModule::process(::serviceName, info, req, resp, "VERSION"); + }); + _svr->Post("/query", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarQueryModule::process(::serviceName, req, resp, "SUBMIT"); + }); + _svr->Post("/query-async", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarQueryModule::process(::serviceName, req, resp, "SUBMIT-ASYNC"); + }); + _svr->Delete("/query-async/:qid", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarQueryModule::process(::serviceName, req, resp, "CANCEL"); + }); + _svr->Get("/query-async/status/:qid", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarQueryModule::process(::serviceName, req, resp, "STATUS"); + }); + _svr->Get("/query-async/result/:qid", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarQueryModule::process(::serviceName, req, resp, "RESULT"); + }); + _svr->Post("/ingest/data", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarIngestModule::process(self->_io_service, ::serviceName, req, resp, "INGEST-DATA"); + }); + _svr->Delete("/ingest/database/:database", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarIngestModule::process(self->_io_service, ::serviceName, req, resp, "DELETE-DATABASE"); + }); + _svr->Delete( + "/ingest/table/:database/:table", [self](httplib::Request const& req, httplib::Response& resp) { + HttpCzarIngestModule::process(self->_io_service, ::serviceName, req, resp, "DELETE-TABLE"); + }); } } // namespace lsst::qserv::czar diff --git a/src/czar/HttpCzarSvc.h b/src/czar/HttpCzarSvc.h index b85e8313e..6106d458e 100644 --- a/src/czar/HttpCzarSvc.h +++ b/src/czar/HttpCzarSvc.h @@ -30,9 +30,10 @@ // Third party headers #include "boost/asio.hpp" -namespace lsst::qserv::qhttp { -class Server; -} // namespace lsst::qserv::qhttp +// Forward declarations +namespace httplib { +class SSLServer; +} // namespace httplib namespace lsst::qserv::wcontrol { class Foreman; @@ -43,106 +44,38 @@ namespace lsst::qserv::czar { /** * Class HttpCzarSvc is the HTTP server for processing user requests. - * - * The server creates and manages its own collection of BOOST ASIO service threads. - * The number of threads is specified via the corresponding parameter of the class's - * constructor. - * - * Typical usage of the class: - * @code - * // Create the server. Note, it won't run yet until explicitly started. - * uint16_t const port = 0; // The port will be dynamically allocated at start - * unsigned int const numThreads = 2; // The number of BOOST ASIO threads - * auto const svc = czar::HttpCzarSvc::create(port, numThreads); - * - * // Start the server and get the actual port number. - * uint16_t const actualPort = svc->start(); - * std::cout << "HTTP server is running on port " << actualPort << std::endl; - * - * // Stop the server to release resources. - * svc->stop(); - * svc.reset(); - * @code - * Alternatively, one may wait before the service will finish. In this case - * the server would need to be stopped from some other thread. For example: - * @code - * auto const svc = ... - * svc->start(); - * std::thread([svc]() { - * std::this_thread::sleep_for(60s); - * svc->stop(); - * }); - * svc->wait(); - * @code - * @note The class implementation is NOT thread safe. A correct ordering of - * calls to the methods 'start -> {stop | wait}' is required. */ class HttpCzarSvc : public std::enable_shared_from_this { public: - /** - * The factory will not initialize ASIO context and threads, or start - * the server. This has to be done by calling method HttpCzarSvc::start() - * - * @param port The number of a port to bind to. - * @param numThreads The number of BOOST ASIO threads. - * @return The shared pointer to the running server. - */ - static std::shared_ptr create(uint16_t port, unsigned int numThreads); - - HttpCzarSvc() = delete; - HttpCzarSvc(HttpCzarSvc const&) = delete; - HttpCzarSvc& operator=(HttpCzarSvc const&) = delete; - - ~HttpCzarSvc() = default; - - /** - * Initialize ASIO context and threads, and start the server. - * - * @note Once the server is started it has to be explicitly stopped - * using the counterpart method stop() to allow releasing allocated - * resources and letting the destructor to be executed. Note that - * service threads started by the curent method and the HTTP server - * incerement the reference counter on the shared pointer that is - * returned by the class's factory method. - * - * @return The actual port number on which the server is run. - * @throws std::logic_error If the server is already running. - */ - uint16_t start(); - - /** - * Stop the server and release the relevant resources. - * @throws std::logic_error If the server is not running. - */ - void stop(); - - /** - * Block the calling thread waiting before the server threads will finish. - * @throws std::logic_error If the server is not running. - */ - void wait(); + static std::shared_ptr create(int port, unsigned int numThreads, + std::string const& sslCertFile, + std::string const& sslPrivateKeyFile); + int port() const { return _port; } + void startAndWait(); private: - /** - * The constructor will not initialize ASIO context and threads, or start - * the server. This has to be done by calling method HttpCzarSvc::start() - * @param port The number of a port to bind to. - * @param numThreads The number of BOOST ASIO threads. - */ - HttpCzarSvc(uint16_t port, unsigned int numThreads); - - // Input parameters - - uint16_t const _port; ///< The input port number (could be 0 to allow autoallocation). - unsigned int const _numThreads; ///< The number of the BOOST ASIO service threads. - - /// Worker management requests are processed by this server. - std::shared_ptr _httpServerPtr; - - /// The BOOST ASIO I/O services. + HttpCzarSvc(int port, unsigned int numThreads, std::string const& sslCertFile, + std::string const& sslPrivateKeyFile); + void _createAndConfigure(); + void _registerHandlers(); + + int _port; + unsigned int const _numThreads; + std::string const _sslCertFile; + std::string const _sslPrivateKeyFile; + std::size_t const _maxQueuedRequests = 0; // 0 means unlimited + std::string const _bindAddr = "0.0.0.0"; + std::unique_ptr _svr; + + // The BOOST ASIO I/O services and a thread pool for async communication with + // the Replication Controller and workers. + // TODO: Consider a configuration option for setting the desired number + // of threads in the pool. + + unsigned int const _numBoostAsioThreads = 2; + + std::unique_ptr _work; boost::asio::io_service _io_service; - - /// The thread pool for running ASIO services. std::vector> _threads; }; diff --git a/src/czar/HttpMonitorModule.cc b/src/czar/HttpMonitorModule.cc index f8c58e503..491e2402c 100644 --- a/src/czar/HttpMonitorModule.cc +++ b/src/czar/HttpMonitorModule.cc @@ -48,7 +48,7 @@ void HttpMonitorModule::process(string const& context, shared_ptr const& req, shared_ptr const& resp) - : HttpModule(context, req, resp) {} + : QhttpModule(context, req, resp) {} json HttpMonitorModule::executeImpl(string const& subModuleName) { string const func = string(__func__) + "[sub-module='" + subModuleName + "']"; diff --git a/src/czar/HttpMonitorModule.h b/src/czar/HttpMonitorModule.h index 82183e853..0982472ab 100644 --- a/src/czar/HttpMonitorModule.h +++ b/src/czar/HttpMonitorModule.h @@ -29,7 +29,7 @@ #include "nlohmann/json.hpp" // Qserv headers -#include "czar/HttpModule.h" +#include "czar/QhttpModule.h" // Forward declarations namespace lsst::qserv::qhttp { @@ -44,7 +44,7 @@ namespace lsst::qserv::czar { * Class HttpMonitorModule implements a handler for reporting various run-time * monitoring metrics and statistics collected at the Qserv worker. */ -class HttpMonitorModule : public czar::HttpModule { +class HttpMonitorModule : public QhttpModule { public: /** * @note supported values for parameter 'subModuleName' are: diff --git a/src/czar/HttpModule.cc b/src/czar/QhttpModule.cc similarity index 88% rename from src/czar/HttpModule.cc rename to src/czar/QhttpModule.cc index 680431010..390b63ab3 100644 --- a/src/czar/HttpModule.cc +++ b/src/czar/QhttpModule.cc @@ -20,7 +20,7 @@ */ // Class header -#include "czar/HttpModule.h" +#include "czar/QhttpModule.h" // System headers #include @@ -36,15 +36,15 @@ using namespace std; namespace lsst::qserv::czar { -HttpModule::HttpModule(string const& context, shared_ptr const& req, - shared_ptr const& resp) +QhttpModule::QhttpModule(string const& context, shared_ptr const& req, + shared_ptr const& resp) : http::QhttpModule(cconfig::CzarConfig::instance()->replicationAuthKey(), cconfig::CzarConfig::instance()->replicationAdminAuthKey(), req, resp), _context(context) {} -string HttpModule::context() const { return _context; } +string QhttpModule::context() const { return _context; } -void HttpModule::enforceCzarName(string const& func) const { +void QhttpModule::enforceCzarName(string const& func) const { string const czarNameAttrName = "czar"; string czarName; if (req()->method == "GET") { diff --git a/src/czar/HttpModule.h b/src/czar/QhttpModule.h similarity index 74% rename from src/czar/HttpModule.h rename to src/czar/QhttpModule.h index f64b44ce7..dc7eb8b75 100644 --- a/src/czar/HttpModule.h +++ b/src/czar/QhttpModule.h @@ -18,8 +18,8 @@ * the GNU General Public License along with this program. If not, * see . */ -#ifndef LSST_QSERV_CZAR_HTTPMODULE_H -#define LSST_QSERV_CZAR_HTTPMODULE_H +#ifndef LSST_QSERV_CZAR_QHTTPMODULE_H +#define LSST_QSERV_CZAR_QHTTPMODULE_H // System headers #include @@ -38,19 +38,19 @@ class Response; namespace lsst::qserv::czar { /** - * Class HttpModule is an intermediate base class of the Qserv Czar modules. + * Class QhttpModule is an intermediate base class of the Qserv Czar modules. */ -class HttpModule : public http::QhttpModule { +class QhttpModule : public http::QhttpModule { public: - HttpModule() = delete; - HttpModule(HttpModule const&) = delete; - HttpModule& operator=(HttpModule const&) = delete; + QhttpModule() = delete; + QhttpModule(QhttpModule const&) = delete; + QhttpModule& operator=(QhttpModule const&) = delete; - virtual ~HttpModule() = default; + virtual ~QhttpModule() = default; protected: - HttpModule(std::string const& context, std::shared_ptr const& req, - std::shared_ptr const& resp); + QhttpModule(std::string const& context, std::shared_ptr const& req, + std::shared_ptr const& resp); virtual std::string context() const final; @@ -68,4 +68,4 @@ class HttpModule : public http::QhttpModule { } // namespace lsst::qserv::czar -#endif // LSST_QSERV_CZAR_HTTPMODULE_H +#endif // LSST_QSERV_CZAR_QHTTPMODULE_H diff --git a/src/czar/qserv-czar-http.cc b/src/czar/qserv-czar-http.cc index 65950647e..e97467c24 100644 --- a/src/czar/qserv-czar-http.cc +++ b/src/czar/qserv-czar-http.cc @@ -20,7 +20,7 @@ */ /** - * The HTTP-based frontend for Czar. + * The CPP-HTTPLIB-based frontend for Czar. */ // System headers @@ -32,15 +32,17 @@ // Qserv headers #include "czar/Czar.h" #include "czar/HttpCzarSvc.h" -#include "global/stringUtil.h" +#include "global/stringUtil.h" // for qserv::stoui using namespace std; namespace czar = lsst::qserv::czar; namespace qserv = lsst::qserv; namespace { -string const usage = "Usage: "; -} + +string const usage = "Usage: "; + +} // namespace int main(int argc, char* argv[]) { // Parse command-line parameters to get: @@ -73,13 +75,13 @@ int main(int argc, char* argv[]) { } try { auto const czar = czar::Czar::createCzar(configFilePath, czarName); - auto const svc = czar::HttpCzarSvc::create(port, numThreads); - port = svc->start(); - cout << __func__ << ": HTTP-based query processing service of Czar started on port " << port << endl; - svc->wait(); + auto const svc = czar::HttpCzarSvc::create(port, numThreads, sslCertFile, sslPrivateKeyFile); + cout << __func__ << ": HTTP-based query processing service of Czar bound to port: " << svc->port() + << endl; + svc->startAndWait(); } catch (exception const& ex) { cerr << __func__ << ": the application failed, exception: " << ex.what() << endl; return 1; } return 0; -} \ No newline at end of file +} From d47dea34e7ae0eeb2177ad71aa82e3c85380a94c Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Fri, 28 Jun 2024 23:16:46 -0700 Subject: [PATCH 5/5] Extended the entry point for Czar HTTP frontend to support SSL/TLS config Switched to the SSL-based REST services for testing. --- admin/local/docker/compose/docker-compose.yml | 2 ++ admin/tools/docker/base/Dockerfile | 3 +- src/admin/etc/integration_tests.yaml | 2 +- .../python/lsst/qserv/admin/cli/entrypoint.py | 16 ++++++++- .../python/lsst/qserv/admin/cli/script.py | 33 +++++++++++++++++++ src/admin/python/lsst/qserv/admin/itest.py | 11 ++++--- src/czar/qserv-czar-http.cc | 6 +++- 7 files changed, 65 insertions(+), 8 deletions(-) diff --git a/admin/local/docker/compose/docker-compose.yml b/admin/local/docker/compose/docker-compose.yml index 1af25e667..a8c13e90d 100644 --- a/admin/local/docker/compose/docker-compose.yml +++ b/admin/local/docker/compose/docker-compose.yml @@ -399,6 +399,8 @@ services: --xrootd-manager czar_xrootd --http-frontend-port 4048 --http-frontend-threads 4 + --http-ssl-cert-file /config-etc/ssl/czar-cert.pem + --http-ssl-private-key-file /config-etc/ssl/czar-key.pem --log-cfg-file=/config-etc/log/log-czar-proxy.cnf --repl-instance-id qserv_proj --repl-auth-key replauthkey diff --git a/admin/tools/docker/base/Dockerfile b/admin/tools/docker/base/Dockerfile index c9ea08095..71bc86d55 100644 --- a/admin/tools/docker/base/Dockerfile +++ b/admin/tools/docker/base/Dockerfile @@ -232,9 +232,10 @@ WORKDIR /home/qserv RUN mkdir -p /qserv/data && \ mkdir /config-etc && \ + mkdir /config-etc/ssl && \ mkdir -p /qserv/run/tmp && \ mkdir -p /var/run/xrootd && \ - chown qserv:qserv /qserv/data /config-etc /qserv/run/tmp /var/run/xrootd + chown qserv:qserv /qserv/data /config-etc /config-etc/ssl /qserv/run/tmp /var/run/xrootd RUN alternatives --install /usr/bin/python python /usr/bin/python3.9 1 ENV PYTHONPATH "${PYTHONPATH}:/usr/local/python" diff --git a/src/admin/etc/integration_tests.yaml b/src/admin/etc/integration_tests.yaml index d28634082..64a4783c3 100644 --- a/src/admin/etc/integration_tests.yaml +++ b/src/admin/etc/integration_tests.yaml @@ -3,7 +3,7 @@ reference-db-uri: mysql://qsmaster@integration-test-reference:3306 reference-db-admin-uri: mysql://root:CHANGEME@integration-test-reference:3306 replication-controller-uri: repl://@repl_controller:25081 qserv-uri: qserv://qsmaster@czar_proxy:4040 -qserv-http-uri: http://czar_http:4048 +qserv-http-uri: https://czar_http:4048 czar-db-admin-uri: mysql://root:CHANGEME@czar_mariadb:3306 # The folder where the itest sources will be mounted in the container: qserv-testdata-dir: /tmp/qserv/itest_src diff --git a/src/admin/python/lsst/qserv/admin/cli/entrypoint.py b/src/admin/python/lsst/qserv/admin/cli/entrypoint.py index e679ee47c..34905ec26 100644 --- a/src/admin/python/lsst/qserv/admin/cli/entrypoint.py +++ b/src/admin/python/lsst/qserv/admin/cli/entrypoint.py @@ -569,6 +569,18 @@ def proxy(ctx: click.Context, **kwargs: Any) -> None: help="The number of threads for the HTTP server of the frontend. The value of http_frontend_threads is passed" " as a command-line parameter to the application." ) +@click.option( + "--http-ssl-cert-file", + help="A location of a file containing an SSL/TSL certificate.", + default="/config-etc/ssl/czar-cert.pem", + show_default=True, +) +@click.option( + "--http-ssl-private-key-file", + help="A location of a file containing an SSL/TSL private key.", + default="/config-etc/ssl/czar-key.pem", + show_default=True, +) @click.option( "--czar-cfg-file", help="Path to the czar config file.", @@ -601,8 +613,10 @@ def czar_http(ctx: click.Context, **kwargs: Any) -> None: db_uri=targs["db_uri"], czar_cfg_file=targs["czar_cfg_file"], czar_cfg_path=targs["czar_cfg_path"], - cmd=targs["cmd"], log_cfg_file=targs["log_cfg_file"], + http_ssl_cert_file=targs["http_ssl_cert_file"], + http_ssl_private_key_file=targs["http_ssl_private_key_file"], + cmd=targs["cmd"], ) diff --git a/src/admin/python/lsst/qserv/admin/cli/script.py b/src/admin/python/lsst/qserv/admin/cli/script.py index 1fb0a0a73..7dbbf3471 100644 --- a/src/admin/python/lsst/qserv/admin/cli/script.py +++ b/src/admin/python/lsst/qserv/admin/cli/script.py @@ -23,6 +23,7 @@ import logging import os import shlex +import socket import subprocess import sys import time @@ -657,6 +658,8 @@ def enter_czar_http( czar_cfg_file: str, czar_cfg_path: str, log_cfg_file: str, + http_ssl_cert_file : str, + http_ssl_private_key_file : str, cmd: str, ) -> None: """Entrypoint script for the proxy container. @@ -673,6 +676,10 @@ def enter_czar_http( Location to render the czar config file. log_cfg_file : `str` Location of the log4cxx config file. + http_ssl_cert_file : `str` + The path to the SSL certificate file. + http_ssl_private_key_file : `str` + The path to the SSL private key file. cmd : `str` The jinja2 template for the command for this function to execute. """ @@ -700,6 +707,32 @@ def enter_czar_http( _do_smig_block(qmeta_smig_dir, "qmeta", db_uri) + # check if the SSL certificate and private key files exist and create + # them if they don't. + if not os.path.exists(http_ssl_cert_file) or not os.path.exists(http_ssl_private_key_file): + _log.info("Generating self-signed SSL/TLS certificate %s and private key %s for HTTPS", + http_ssl_cert_file, http_ssl_private_key_file) + country = "US" + state = "California" + loc = "Menlo Park" + org = "SLAC National Accelerator Laboratory" + org_unit = "Rubin Observatory" + hostname = socket.gethostbyaddr(socket.gethostname())[0] # FQDN if available + subj = f"/C={country}/ST={state}/L={loc}/O={org}/OU={org_unit}/CN={hostname}" + openssl_cmd = [ + "openssl", "req", + "-x509", + "-newkey", "rsa:4096", + "-out", http_ssl_cert_file, + "-keyout", http_ssl_private_key_file, + "-sha256", + "-days", "365", + "-nodes", + "-subj", subj] + ret = subprocess.run(openssl_cmd, env=dict(os.environ,), cwd="/home/qserv") + if ret.returncode != 0: + raise RuntimeError("Failed to create SSL certificate and private key files.") + env = dict( os.environ, LD_PRELOAD=ld_preload, diff --git a/src/admin/python/lsst/qserv/admin/itest.py b/src/admin/python/lsst/qserv/admin/itest.py index 89ea7d200..9235cfd65 100644 --- a/src/admin/python/lsst/qserv/admin/itest.py +++ b/src/admin/python/lsst/qserv/admin/itest.py @@ -21,6 +21,7 @@ from filecmp import dircmp import json import logging +import urllib3 import requests import os import re @@ -510,7 +511,7 @@ def run_attached_http(self, connection: str, database: str) -> None: # Submit the query, check and analyze the completion status svc = str(urljoin(connection, '/query')) - req = requests.post(svc, json={'query': query, 'database': database, 'binary_encoding': 'hex'}) + req = requests.post(svc, json={'query': query, 'database': database, 'binary_encoding': 'hex'}, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -534,7 +535,7 @@ def run_detached_http(self, connection: str, database: str) -> None: # Submit the query via the async service, check and analyze the completion status svc = str(urljoin(connection, '/query-async')) - req = requests.post(svc, json={'query': query, 'database': database}) + req = requests.post(svc, json={'query': query, 'database': database}, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -547,7 +548,7 @@ def run_detached_http(self, connection: str, database: str) -> None: while time.time() < end_time: # Submit a request to check a status of the query svc = str(urljoin(connection, f"/query-async/status/{query_id}")) - req = requests.get(svc) + req = requests.get(svc, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -561,7 +562,7 @@ def run_detached_http(self, connection: str, database: str) -> None: # Make another request to pull the result set svc = str(urljoin(connection, f"/query-async/result/{query_id}?binary_encoding=hex")) - req = requests.get(svc) + req = requests.get(svc, verify=False) req.raise_for_status() res = req.json() if res['success'] == 0: @@ -747,6 +748,8 @@ def __init__( self.db_name, skip_numbers, ) + # Supress the warning about the self-signed certificate + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def run(self) -> None: """Run the test queries in a test case. diff --git a/src/czar/qserv-czar-http.cc b/src/czar/qserv-czar-http.cc index e97467c24..85a0c451d 100644 --- a/src/czar/qserv-czar-http.cc +++ b/src/czar/qserv-czar-http.cc @@ -51,7 +51,9 @@ int main(int argc, char* argv[]) { // - the port number (0 value would result in allocating the first available port) // - the number of service threads (0 value would assume the number of host machine's // hardware threads) - if (argc != 5) { + // - a location of the SSL/TSL certificate for the secure connections + // - a location of the SSL/TSL private key + if (argc != 7) { cerr << __func__ << ": insufficient number of the command-line parameters\n" << ::usage << endl; return 1; } @@ -73,6 +75,8 @@ int main(int argc, char* argv[]) { cerr << __func__ << ": failed to parse command line parameters\n" << ::usage << endl; return 1; } + string const sslCertFile = argv[nextArg++]; + string const sslPrivateKeyFile = argv[nextArg++]; try { auto const czar = czar::Czar::createCzar(configFilePath, czarName); auto const svc = czar::HttpCzarSvc::create(port, numThreads, sslCertFile, sslPrivateKeyFile);