| #ifndef PRODUCTION_SUSHID_SAFEPOWER_AGENT_BMC_MOCK_SERVER_H_ |
| #define PRODUCTION_SUSHID_SAFEPOWER_AGENT_BMC_MOCK_SERVER_H_ |
| |
| #include <chrono> // NOLINT(build/c++11) |
| #include <cstdint> |
| #include <cstdlib> |
| #include <ctime> |
| #include <iostream> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| // NOLINTBEGIN(readability/boost) |
| #include "absl/strings/string_view.h" |
| #include "boost/asio/io_context.hpp" |
| #include "boost/beast/core.hpp" |
| #include "boost/beast/http.hpp" |
| #include "boost/beast/version.hpp" |
| // NOLINTEND(readability/boost) |
| |
| namespace safepower_agent { |
| namespace mock_server_test { |
| |
| // boost ref server |
| // https://www.boost.org/doc/libs/1_85_0/libs/beast/example/http/server/small/http_server_small.cpp |
| // |
| // using a boost http server to test the http client used in gpowerd |
| |
| namespace beast = boost::beast; // from <boost/beast.hpp> |
| namespace http = beast::http; // from <boost/beast/http.hpp> |
| namespace net = boost::asio; // from <boost/asio.hpp> |
| using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp> |
| |
| constexpr absl::string_view goodJson = R"( |
| { |
| "@Message.ExtendedInfo": [ |
| { |
| "@odata.type": "#Message.v1_1_1.Message", |
| "Message": "The request completed successfully.", |
| "MessageArgs": [], |
| "MessageId": "Base.1.13.0.Success", |
| "MessageSeverity": "OK", |
| "Resolution": "None" |
| } |
| ] |
| })"; |
| |
| constexpr absl::string_view testingPowerStateJson = R"( |
| { |
| "@odata.id": "/redfish/v1/Systems/system", |
| "@odata.type": "#ComputerSystem.v1_16_0.ComputerSystem", |
| "Actions": { |
| "#ComputerSystem.Reset": { |
| "@Redfish.ActionInfo": "/redfish/v1/Systems/system/ResetActionInfo", |
| "target": "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset" |
| } |
| }, |
| "PowerState": "On", |
| "PowerState_On": "On", |
| "PowerState_Off": "Off", |
| "PowerState_Paused": "Paused", |
| "PowerState_PoweringOn": "PoweringOn", |
| "PowerState_PoweringOff": "PoweringOff", |
| "PowerState_UNSPECIFIED_1": "", |
| "PowerState_UNSPECIFIED_2": "a" |
| })"; |
| |
| constexpr absl::string_view malformedJson = R"( |
| "@Message.ExtendedInfo": |
| "@odata.type": "#Message.v1_1_1.Message", |
| "Message": "The request completed successfully.", |
| "MessageArgs": ], |
| "MessageId": "Base.1.13.0.Success", |
| "MessageSeverity": "OK", |
| "Resolution": "None" |
| } |
| ] |
| })"; |
| |
| constexpr absl::string_view goodSystemGet = R"( |
| { |
| "@odata.id": "/redfish/v1/Chassis", |
| "@odata.type": "#ChassisCollection.ChassisCollection", |
| "Members": [ |
| { |
| "@odata.id": "/redfish/v1/Chassis/A" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/B" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/D" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/i0" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/i1" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/i2" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/w1" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/w2" |
| }, |
| { |
| "@odata.id": "/redfish/v1/Chassis/w3" |
| } |
| ], |
| "Members@odata.count": 9, |
| "Name": "Chassis Collection" |
| })"; |
| |
| class http_connection : public std::enable_shared_from_this<http_connection> { |
| public: |
| explicit http_connection(tcp::socket socket) |
| : socket_(std::move(socket)), skip_write_(false) { |
| std::cerr << "Test server connection made" << std::endl; |
| } |
| |
| // Initiate the asynchronous operations associated with the connection. |
| void start() { |
| read_request(); |
| check_deadline(); |
| } |
| |
| private: |
| // The socket for the currently connected client. |
| tcp::socket socket_; |
| |
| // The buffer for performing reads. |
| beast::flat_buffer buffer_{8192}; |
| |
| // The request message. |
| http::request<http::dynamic_body> request_; |
| |
| // The response message. |
| http::response<http::dynamic_body> response_; |
| |
| // The timer for putting a deadline on connection processing. |
| net::steady_timer deadline_{socket_.get_executor(), std::chrono::seconds(60)}; |
| // Asynchronously receive a complete request message. |
| |
| bool skip_write_; |
| |
| void read_request() { |
| auto self = shared_from_this(); |
| |
| http::async_read( |
| socket_, buffer_, request_, |
| [self](beast::error_code ec, std::size_t bytes_transferred) { |
| boost::ignore_unused(bytes_transferred); |
| if (!ec) { |
| self->process_request(); |
| } |
| }); |
| } |
| |
| void process_request() { |
| response_.version(request_.version()); |
| response_.keep_alive(false); |
| |
| switch (request_.method()) { |
| case http::verb::post: |
| response_.result(http::status::ok); |
| response_.set(http::field::server, "Beast"); |
| response_.set(http::field::content_type, "text/plain"); |
| create_post_response(); |
| break; |
| |
| case http::verb::get: |
| response_.result(http::status::ok); |
| response_.set(http::field::server, "Beast"); |
| create_get_response(); |
| break; |
| |
| default: |
| // We return responses indicating an error if |
| // we do not recognize the request method. |
| response_.result(http::status::bad_request); |
| response_.set(http::field::content_type, "text/plain"); |
| beast::ostream(response_.body()) |
| << "Invalid request-method '" |
| << std::string(request_.method_string()) << "'"; |
| break; |
| } |
| if (!skip_write_) { |
| write_response(); |
| } |
| } |
| // Construct a response message based on the program state. |
| void create_post_response() { |
| response_.set(http::field::content_type, "text/html"); |
| if (request_.target() == "/count") { |
| beast::ostream(response_.body()) << "count_answer"; |
| } else if (request_.target() == "/time") { |
| beast::ostream(response_.body()) << "time_answer"; |
| } else if (request_.target() == |
| "/redfish/v1/Systems/system/Actions/Power.Reset") { |
| beast::ostream(response_.body()) << goodJson; |
| } else if (request_.target() == |
| "/redfish/v1/Systems/system/Actions/Power.Reset_never_write") { |
| skip_write_ = true; |
| } else if (request_.target() == |
| "/redfish/v1/Systems/system/Actions/Power.Reset_malformed") { |
| beast::ostream(response_.body()) << malformedJson; |
| } else { |
| std::cerr << "request_.target():" << request_.target() << std::endl; |
| response_.result(http::status::not_found); |
| response_.set(http::field::content_type, "text/plain"); |
| beast::ostream(response_.body()) << "File not found\r\n"; |
| } |
| } |
| void create_get_response() { |
| if (request_.target() == "/count") { |
| response_.set(http::field::content_type, "text/html"); |
| beast::ostream(response_.body()) << "count_answer"; |
| } else if (request_.target() == "/redfish/v1/Systems/system_get") { |
| response_.set(http::field::content_type, "text/html"); |
| beast::ostream(response_.body()) << goodSystemGet; |
| } else if (request_.target() == |
| "/redfish/v1/Systems/system/_testingPowerState") { |
| beast::ostream(response_.body()) << testingPowerStateJson; |
| } else { |
| std::cerr << "request_.target():" << request_.target() << std::endl; |
| response_.result(http::status::not_found); |
| response_.set(http::field::content_type, "text/plain"); |
| beast::ostream(response_.body()) << "File not found\r\n"; |
| } |
| } |
| |
| // Asynchronously transmit the response message. |
| void write_response() { |
| auto self = shared_from_this(); |
| |
| response_.content_length(response_.body().size()); |
| |
| http::async_write(socket_, response_, |
| [self](beast::error_code ec, std::size_t) { |
| self->socket_.shutdown(tcp::socket::shutdown_send, ec); |
| self->deadline_.cancel(); |
| }); |
| } |
| |
| // Check whether we have spent enough time on this connection. |
| void check_deadline() { |
| auto self = shared_from_this(); |
| |
| deadline_.async_wait([self](beast::error_code ec) { |
| if (!ec) { |
| // Close socket to cancel any outstanding operation. |
| self->socket_.close(ec); |
| } |
| }); |
| } |
| }; |
| |
| inline void http_server(tcp::acceptor& acceptor, tcp::socket& socket) { |
| acceptor.async_accept(socket, [&](beast::error_code ec) { |
| if (!ec) { |
| std::make_shared<http_connection>(std::move(socket))->start(); |
| } else { |
| std::cerr << "http_server error:" << ec.what() << std::endl; |
| } |
| http_server(acceptor, socket); |
| }); |
| } |
| |
| class TestBoostServer { |
| public: |
| std::shared_ptr<tcp::acceptor> acceptor_; |
| std::shared_ptr<tcp::socket> socket_; |
| |
| void Start(boost::asio::io_context& io_ctx) { |
| auto const address = net::ip::make_address("0.0.0.0"); |
| uint16_t const port = 80; |
| auto const doc_root = std::make_shared<std::string>("/redfish/v1/"); |
| boost::asio::ip::tcp::endpoint ep(address, port); |
| |
| socket_ = std::make_shared<tcp::socket>(io_ctx); |
| acceptor_ = std::make_shared<tcp::acceptor>(io_ctx, ep); |
| |
| http_server(*acceptor_, *socket_); |
| } |
| ~TestBoostServer() { |
| acceptor_->cancel(); |
| socket_->close(); |
| } |
| }; |
| |
| } // namespace mock_server_test |
| } // namespace safepower_agent |
| |
| #endif // PRODUCTION_SUSHID_SAFEPOWER_AGENT_BMC_MOCK_SERVER_H_ |