| // Ignore the following warnings in this header when doing Yocto builds. |
| #include <cstdint> |
| #include <cstring> |
| #include <string_view> |
| |
| #include "absl/status/status.h" |
| #include "absl/strings/numbers.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/string_view.h" |
| #include "absl/time/time.h" |
| #include "tlbmc/subscription/manager.h" |
| #include "tlbmc/subscription/manager_fake.h" |
| #include "payload.pb.h" |
| #include "sensor_identifier.pb.h" |
| #include "sensor_payload.pb.h" |
| |
| #pragma GCC diagnostic push |
| #pragma GCC diagnostic warning \ |
| "-Wdeprecated-declarations" // There is not a good way to avoid this as we |
| // need backward compatibility |
| |
| #include <chrono> // NOLINT |
| #include <cstddef> |
| #include <filesystem> // NOLINT |
| #include <fstream> |
| #include <functional> |
| #include <iterator> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <queue> |
| #include <string> |
| #include <system_error> // NOLINT |
| #include <thread> // NOLINT |
| #include <unordered_set> |
| #include <utility> |
| |
| #include "absl/base/thread_annotations.h" |
| #include "absl/container/flat_hash_map.h" |
| #include "absl/strings/match.h" |
| #include "absl/synchronization/mutex.h" |
| #include "absl/time/clock.h" |
| #include "subscription.h" |
| #include "nlohmann/json_fwd.hpp" |
| #include "tlbmc/service/hft_service.h" |
| #include "server.h" |
| |
| #ifdef ENABLE_LOAS3_VALIDATION |
| #include "security/zatar/loas3_validation/validation/validation.h" |
| #endif |
| |
| #include "absl/log/log.h" |
| #include "boost/asio/error.hpp" // NOLINT |
| #include "boost/asio/io_context.hpp" // NOLINT |
| #include "boost/asio/post.hpp" // NOLINT |
| #include "boost/beast/http/verb.hpp" // NOLINT |
| #include "boost/uuid/uuid.hpp" // NOLINT |
| #include "boost/uuid/uuid_generators.hpp" // NOLINT |
| #include "boost/uuid/uuid_io.hpp" // NOLINT |
| #include "redfish_v1.grpc.pb.h" |
| #include "redfish_v1.pb.h" |
| #include "redfish_v1_grpc_include.h" |
| #include "zatar/struct_proto_conversion.h" |
| #include "grpcpp/ext/proto_server_reflection_plugin.h" |
| #include "grpcpp/security/auth_context.h" |
| #include "grpcpp/security/server_credentials.h" |
| #include "grpcpp/server_builder.h" |
| #include "grpcpp/server_context.h" |
| #include "grpcpp/support/server_callback.h" |
| #include "grpcpp/support/status.h" |
| #include "grpcpp/support/string_ref.h" |
| #include "nlohmann/json.hpp" |
| #include "config_parser.h" |
| #include "oauth_utils.h" |
| #include "tlbmc/hal/shared_mem/server.h" |
| #include "tlbmc/redfish/app.h" |
| #include "tlbmc/redfish/request.h" |
| #include "tlbmc/redfish/response.h" |
| #include "tlbmc/trace/tracer.h" |
| #include "bmcweb_authorizer_singleton.h" |
| #include "app.hpp" |
| #include "zatar/bmcweb_cert_provider.h" |
| #include "zatar/bmcweb_cert_provider_impl.h" |
| #include "zatar/bmcweb_credentials.h" |
| |
| namespace milotic { |
| |
| using ::milotic_tlbmc::RedfishRequest; |
| using ::milotic_tlbmc::RedfishResponse; |
| using ::milotic_tlbmc::SharedMemoryServer; |
| |
| grpc::Status SubscriptionPreCheck( |
| const grpc::AuthContext& auth_context, |
| const std::multimap<grpc::string_ref, grpc::string_ref>& client_metadata, |
| const ::redfish::v1::Request* request, |
| ::milotic::redfish::BmcWebCertProvider::ServerStatus server_status) { |
| ::milotic::authz::BmcWebAuthorizerSingleton& authorizer = |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance(); |
| // Abort if it is not mutual authenticated |
| if (server_status != ::milotic::redfish::BmcWebCertProvider::ServerStatus:: |
| kWithRootCertsAndProdSignedCert) { |
| return grpc::Status(grpc::StatusCode::PERMISSION_DENIED, |
| "The server is not in mutual authenticated mode thus " |
| "no subscription is " |
| "allowed. Please check Server's credential status."); |
| } |
| |
| if (!auth_context.IsPeerAuthenticated()) { |
| return grpc::Status( |
| grpc::StatusCode::PERMISSION_DENIED, |
| "The request is not authenticated thus no subscription is " |
| "allowed. Please check Server's credential status."); |
| } |
| |
| // Abort if target doesn't match |
| if (grpc::Status status = |
| authorizer.CheckTarget(auth_context, client_metadata, *request); |
| !status.ok()) { |
| return status; |
| } |
| |
| return grpc::Status::OK; |
| } |
| |
| namespace internal { |
| |
| // The implementation of the ServiceWriteReactor. |
| // This implementation maintains a queue of events, which is supposed to be |
| // filled by the SubscriptionService. |
| class Reactor final : public grpc::ServerWriteReactor<::redfish::v1::Response> { |
| public: |
| // The status of the reactor (also the stream). |
| enum class Status : char { |
| kIdle, // The stream is idle, no write is in the flight. |
| kWriteInFlight, // There is a write in flight. |
| kFinishCalled, // The stream will be terminated soon at any given time as |
| // `Finish` has been called. A stream can fall into this |
| // state because of "previous write failed" or "client |
| // cancelled the stream". The stream is waiting for OnDone |
| // to be called to declare the stream fully finished. |
| kFinished, // The stream has been terminated. The `reactor` pointer might |
| // be still valid but will be released at any given time. |
| }; |
| |
| Reactor(ecclesia::SubscriptionService* subscription_service, |
| std::function<void(Reactor*)>&& on_done, |
| size_t maximum_event_queue_size) |
| : subscription_service_(subscription_service), |
| on_done_(std::move(on_done)), |
| maximum_event_queue_size_(maximum_event_queue_size) {} |
| |
| // NOTE: set this once before any StartWrite is called. |
| void SetPeerSpiffeIdentity(const ::milotic::authz::PeerSpiffeIdentity& peer) { |
| peer_ = peer; |
| } |
| |
| void SafeFinish(const grpc::Status& status) { |
| absl::MutexLock lock(&status_mutex_); |
| if (status_ == Status::kFinishCalled || status_ == Status::kFinished) { |
| return; |
| } |
| status_ = Status::kFinishCalled; |
| Finish(status); |
| } |
| |
| void OnWriteDone(bool ok) override { |
| { |
| absl::MutexLock status_lock(&status_mutex_); |
| if (status_ != Status::kWriteInFlight) { |
| return; |
| } |
| if (!ok) { |
| LOG(ERROR) << "Failed to write response"; |
| status_ = Status::kFinishCalled; |
| Finish(grpc::Status(grpc::StatusCode::INTERNAL, |
| "Failed to write response")); |
| return; |
| } |
| // Now pop the last event as it won't be referenced again |
| { |
| absl::MutexLock events_lock(&events_mutex_); |
| events_.pop(); |
| } |
| status_ = Status::kIdle; |
| } |
| if (is_testing_events_) { |
| GenerateSingleTestingEvent(); |
| } |
| // The above code block will ensure no lock is held when the block goes out |
| // of scope. |
| WriteAnEventIfAny(); |
| } |
| |
| void SetToTestMode() { is_testing_events_ = true; } |
| |
| void OnCancel() override { |
| absl::MutexLock lock(&status_mutex_); |
| if (status_ == Status::kFinishCalled || status_ == Status::kFinished) { |
| return; |
| } |
| status_ = Status::kFinishCalled; |
| Finish(grpc::Status(grpc::StatusCode::CANCELLED, |
| "Client cancelled requests.")); |
| } |
| |
| void OnDone() override { |
| { |
| absl::MutexLock lock(&status_mutex_); |
| status_ = Status::kFinished; |
| } |
| if (peer_) { |
| grpc::Status status = |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance() |
| .RecordNewUnsubscription(*peer_); |
| if (!status.ok()) { |
| // Conversion from grpc::Status to std::ostream is not supported in grpc |
| // v1.56.2 which is what was used causing builds to break. See |
| // b/323972900 |
| LOG(ERROR) << "RecordNewUnsubscription failed with code " |
| << status.error_code() << " and message " |
| << status.error_message(); |
| } |
| } |
| { |
| absl::MutexLock lock(&subscription_id_mutex_); |
| if (subscription_id_ && subscription_service_ != nullptr) { |
| subscription_service_->DeleteSubscription(*subscription_id_); |
| } |
| } |
| if (on_done_) { |
| on_done_(this); |
| } |
| } |
| |
| void SetSubscriptionId(const ecclesia::SubscriptionId& id) { |
| absl::MutexLock lock(&subscription_id_mutex_); |
| subscription_id_ = id; |
| } |
| |
| void AddAnEvent(const ::redfish::v1::Response& event) { |
| // We only push an event to the queue if the stream is idle or has a write |
| // in flight. |
| absl::MutexLock status_lock(&status_mutex_); |
| if (status_ != Status::kIdle && status_ != Status::kWriteInFlight) { |
| return; |
| } |
| // TODO: check subscription IDs match before enqueue. |
| absl::MutexLock events_lock(&events_mutex_); |
| if (events_.size() >= maximum_event_queue_size_) { |
| LOG(WARNING) << "Too many events to queue; new events have been ignored"; |
| return; |
| } |
| events_.push(event); |
| } |
| |
| Status GetStatus() { |
| absl::MutexLock lock(&status_mutex_); |
| return status_; |
| } |
| |
| void WriteAnEventIfAny() { |
| // We only start a new StartWrite if the stream is idle. |
| absl::MutexLock status_lock(&status_mutex_); |
| if (status_ != Status::kIdle) { |
| return; |
| } |
| absl::MutexLock lock(&events_mutex_); |
| if (events_.empty()) { |
| return; |
| } |
| status_ = Status::kWriteInFlight; |
| StartWrite(&events_.front()); |
| } |
| |
| void StartTestingEvents() { |
| GenerateSingleTestingEvent(); |
| WriteAnEventIfAny(); |
| } |
| |
| protected: |
| void GenerateSingleTestingEvent() { |
| ::redfish::v1::Response event; |
| nlohmann::json json_response; |
| event.set_code(200); |
| json_response["number"] = test_event_count_; |
| event.set_json_str(json_response.dump()); |
| test_event_count_++; |
| AddAnEvent(event); |
| } |
| |
| private: |
| // The data members below are guaranteed to be created before OnWrite starts, |
| // afterwards they are read-only, so no need to be protected by a lock |
| std::optional<::milotic::authz::PeerSpiffeIdentity> peer_; |
| bool is_testing_events_ = false; |
| ecclesia::SubscriptionService* subscription_service_ = nullptr; |
| std::function<void(Reactor*)> on_done_; |
| size_t maximum_event_queue_size_ = 0; |
| |
| // The data members below is guaranteed to be accessed only by one thread at a |
| // time. |
| int test_event_count_ = 0; |
| |
| // `events_` might be modified by different thread: gRPC Event Engine threads, |
| // subscription service threads. |
| absl::Mutex events_mutex_; |
| std::queue<::redfish::v1::Response> events_ ABSL_GUARDED_BY(events_mutex_); |
| |
| // `subscription_id_` might be modified by different thread. For example, the |
| // stream gets closed very early before any SetSubscriptionId is called. |
| absl::Mutex subscription_id_mutex_; |
| std::optional<ecclesia::SubscriptionId> subscription_id_ |
| ABSL_GUARDED_BY(subscription_id_mutex_); |
| |
| // `status_` might be modified by different thread. |
| // `status_` is to avoid extra Write to be fired when the stream is |
| // supposed to be finished. |
| absl::Mutex status_mutex_; |
| Status status_ ABSL_GUARDED_BY(status_mutex_) = Status::kIdle; |
| }; |
| |
| class RedfishV1Impl final : public ecclesia::GrpcRedfishV1::CallbackService { |
| public: |
| RedfishV1Impl( |
| App* app, const milotic_tlbmc::RedfishApp* tlbmc_app, |
| const std::shared_ptr<boost::asio::io_context>& io_context_main_thread, |
| const std::shared_ptr<boost::asio::io_context>& io_context_worker_threads, |
| ::milotic::redfish::BmcWebCertProvider& cert_provider, |
| RedfishServiceConfig& config, |
| ecclesia::SubscriptionService* subscription_service) |
| : hardware_concurrency_(std::thread::hardware_concurrency()), |
| app_(app), |
| tlbmc_app_(tlbmc_app), |
| io_context_main_thread_(io_context_main_thread), |
| io_context_worker_threads_(io_context_worker_threads), |
| cert_provider_(cert_provider), |
| config_(config), |
| subscription_service_(subscription_service) {} |
| |
| ~RedfishV1Impl() override = default; |
| |
| inline grpc::ServerUnaryReactor* Get( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request, |
| ::redfish::v1::Response* response) override { |
| return RequestUri(context, boost::beast::http::verb::get, request, |
| response); |
| } |
| inline grpc::ServerUnaryReactor* Post( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request, |
| ::redfish::v1::Response* response) override { |
| return RequestUri(context, boost::beast::http::verb::post, request, |
| response); |
| } |
| inline grpc::ServerUnaryReactor* Put( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request, |
| ::redfish::v1::Response* response) override { |
| return RequestUri(context, boost::beast::http::verb::put, request, |
| response); |
| } |
| inline grpc::ServerUnaryReactor* Patch( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request, |
| ::redfish::v1::Response* response) override { |
| return RequestUri(context, boost::beast::http::verb::patch, request, |
| response); |
| } |
| inline grpc::ServerUnaryReactor* Delete( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request, |
| ::redfish::v1::Response* response) override { |
| return RequestUri(context, boost::beast::http::verb::delete_, request, |
| response); |
| } |
| |
| grpc::ServerUnaryReactor* GetOverridePolicy( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::GetOverridePolicyRequest* /*unused*/, |
| ::redfish::v1::GetOverridePolicyResponse* response) override { |
| grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); |
| |
| namespace fs = std::filesystem; |
| fs::file_status status = |
| fs::status(fs::path(config_.redfish_override_policy_path)); |
| if (!fs::is_regular_file(status)) { |
| reactor->Finish(grpc::Status( |
| grpc::StatusCode::NOT_FOUND, |
| "Policy file not found: " + config_.redfish_override_policy_path)); |
| return reactor; |
| } |
| |
| std::ifstream policy_file(config_.redfish_override_policy_path); |
| |
| if (!policy_file) { |
| reactor->Finish(grpc::Status( |
| grpc::StatusCode::INTERNAL, |
| "Policy file read error: " + config_.redfish_override_policy_path)); |
| return reactor; |
| } |
| |
| response->set_policy( |
| std::string(std::istreambuf_iterator<char>{policy_file}, {})); |
| reactor->Finish(grpc::Status::OK); |
| return reactor; |
| } |
| |
| grpc::ServerWriteReactor<::redfish::v1::Response>* Subscribe( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request) override { |
| // A reactor will only released when all references are cleared. References |
| // include gRPC core layer, and the subscription service. |
| auto reactor = std::make_shared<Reactor>( |
| subscription_service_, |
| [this](Reactor* reactor) { RemoveReactor(reactor); }, |
| config_.maximum_event_queue_size); |
| StoreReactor(reactor); |
| |
| if (!config_.generate_testing_events && |
| (config_.disable_eventing || subscription_service_ == nullptr)) { |
| reactor->SafeFinish( |
| grpc::Status(grpc::StatusCode::UNIMPLEMENTED, |
| "Eventing is disabled or not implemented.")); |
| return reactor.get(); |
| } |
| |
| if (config_.generate_testing_events) { |
| reactor->SetToTestMode(); |
| } |
| |
| // Sanity check: the server has to be in mutual TLS mode and the requester |
| // has been authenticated |
| if (grpc::Status status = SubscriptionPreCheck( |
| *context->auth_context(), context->client_metadata(), request, |
| cert_provider_.GetServerStatus()); |
| !status.ok()) { |
| reactor->SafeFinish(status); |
| LOG(WARNING) << "AuthorizeSubscription failed: " |
| << status.error_message(); |
| return reactor.get(); |
| } |
| |
| // Rate limit |
| ::milotic::authz::PeerSpiffeIdentity peer; |
| if (grpc::Status status = ::milotic::authz::BmcWebAuthorizerSingleton:: |
| GetPeerIdentityFromAuthContext(*context->auth_context(), peer); |
| !status.ok()) { |
| reactor->SafeFinish(status); |
| LOG(WARNING) << "GetPeerIdentityFromAuthContext failed" |
| << status.error_message(); |
| return reactor.get(); |
| } |
| if (grpc::Status status = |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance() |
| .RecordNewSubscription(peer); |
| !status.ok()) { |
| reactor->SafeFinish(status); |
| LOG(WARNING) << "RecordNewSubscription failed" << status.error_message(); |
| return reactor.get(); |
| } |
| reactor->SetPeerSpiffeIdentity(peer); |
| |
| nlohmann::json event_config = |
| nlohmann::json::parse(request->json_str(), nullptr, false); |
| |
| // Create subscription asynchronously. |
| if (subscription_service_ != nullptr) { |
| std::unordered_set<std::string> peer_privileges; |
| if (grpc::Status status = |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance() |
| .GetPrivilegesViaMTls(*context->auth_context(), |
| peer_privileges); |
| !status.ok()) { |
| reactor->SafeFinish(status); |
| LOG(WARNING) << "GetPrivilegesViaMTls failed" << status.error_message(); |
| return reactor.get(); |
| } |
| subscription_service_->CreateSubscription( |
| event_config, peer_privileges, |
| [this, reactor]( |
| const absl::StatusOr<ecclesia::SubscriptionId>& subscription_id) { |
| if (!subscription_id.ok()) { |
| reactor->SafeFinish( |
| grpc::Status(grpc::StatusCode::INTERNAL, |
| subscription_id.status().ToString())); |
| return; |
| } |
| reactor->SetSubscriptionId(*subscription_id); |
| |
| // Note: any StartWrite must be after any Finish calls above |
| if (config_.generate_testing_events) { |
| reactor->StartTestingEvents(); |
| } |
| }, |
| [reactor](const nlohmann::json& event_json) mutable { |
| if (reactor == nullptr) return; |
| // If reactor ever reports that it is finished, we should never |
| // access it again. |
| if (reactor->GetStatus() == Reactor::Status::kFinished) { |
| reactor = nullptr; |
| return; |
| } |
| // If reactor is about to finish, we should return and wait until |
| // it is actually finished. We must not start a new StartWrite(). |
| if (reactor->GetStatus() == Reactor::Status::kFinishCalled) { |
| return; |
| } |
| ::redfish::v1::Response event; |
| event.set_json_str(event_json.dump()); |
| event.set_code(200); |
| reactor->AddAnEvent(event); |
| reactor->WriteAnEventIfAny(); |
| }); |
| return reactor.get(); |
| } |
| |
| if (config_.generate_testing_events) { |
| reactor->StartTestingEvents(); |
| } |
| return reactor.get(); |
| } |
| |
| private: |
| struct ProfilingTimeStamps { |
| std::chrono::time_point<std::chrono::high_resolution_clock> |
| request_uri_start; |
| std::chrono::time_point<std::chrono::high_resolution_clock> |
| entering_io_context; |
| std::chrono::time_point<std::chrono::high_resolution_clock> handler_start; |
| std::chrono::time_point<std::chrono::high_resolution_clock> handler_end; |
| std::chrono::time_point<std::chrono::high_resolution_clock> response_end; |
| absl::Time request_start; |
| }; |
| |
| std::optional<crow::Request> GetInternalRequestFromProto( |
| boost::beast::http::request<boost::beast::http::string_body>&& |
| boost_request, |
| grpc::ServerUnaryReactor* reactor, |
| grpc::CallbackServerContext* context) const { |
| std::error_code error; |
| crow::Request crow_request(std::move(boost_request), error); |
| |
| if (error) { |
| LOG(ERROR) << "Request failed to construct " << error; |
| reactor->Finish(grpc::Status(grpc::StatusCode::INTERNAL, |
| "Request failed to construct")); |
| return std::nullopt; |
| } |
| |
| // If enable_insecure_server is true, set fromGrpc to false; otherwise |
| // fromGrpc should be false. |
| crow_request.fromGrpc = !config_.enable_insecure_server; |
| |
| crow_request.with_trust_bundle = |
| cert_provider_.GetServerStatus() == |
| ::milotic::redfish::BmcWebCertProvider::ServerStatus:: |
| kWithRootCertsAndProdSignedCert || |
| cert_provider_.GetServerStatus() == |
| ::milotic::redfish::BmcWebCertProvider::ServerStatus:: |
| kWithRootCertsAndSelfSignedCert; |
| |
| crow_request.peer_authenticated = |
| context->auth_context()->IsPeerAuthenticated(); |
| return crow_request; |
| } |
| |
| grpc::Status SetPeerPrivileges( |
| const grpc::AuthContext& auth_context, |
| const ::redfish::v1::Request* request, |
| std::unordered_set<std::string>& peer_privileges, |
| bool with_trust_bundle) const { |
| std::optional<std::string> oauth_token = |
| ::milotic::authz::GetOAuthTokenFromRequest(*request); |
| |
| grpc::Status privileges_status; |
| if (config_.enable_insecure_server) { |
| peer_privileges.clear(); |
| privileges_status = grpc::Status::OK; |
| } else if (oauth_token != std::nullopt) { |
| privileges_status = |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance() |
| .GetPrivilegesViaOAuth(auth_context, *oauth_token, |
| peer_privileges); |
| } else { |
| privileges_status = |
| ::milotic::authz::BmcWebAuthorizerSingleton::GetInstance() |
| .GetPrivilegesViaMTls(auth_context, peer_privileges); |
| // Allow recovery RPCs when there is no trust bundle. |
| if (!privileges_status.ok() && !with_trust_bundle) { |
| LOG(WARNING) << "Server doesn't have trust bundle; only recovery RPCs " |
| "are allowed"; |
| peer_privileges.clear(); |
| privileges_status = grpc::Status::OK; |
| } |
| } |
| |
| return privileges_status; |
| } |
| |
| static std::chrono::time_point<std::chrono::high_resolution_clock> |
| GetCurrentTime(bool do_profiling) { |
| if (!do_profiling) |
| return std::chrono::high_resolution_clock::time_point::min(); |
| return std::chrono::high_resolution_clock::now(); |
| } |
| |
| static void LogProfileTimeInResp( |
| std::chrono::time_point<std::chrono::high_resolution_clock> start, |
| std::chrono::time_point<std::chrono::high_resolution_clock> end, |
| const std::string& profile_label, bool do_profiling, |
| crow::Response& res) { |
| if (!do_profiling) return; |
| auto time_delta = |
| std::chrono::duration_cast<std::chrono::nanoseconds>(end - start); |
| res.jsonValue[profile_label] = time_delta.count(); |
| } |
| |
| static void UpdateResponseMetrics(int code, std::string_view resource_url, |
| bool tlbmc_enabled, |
| absl::Time request_start) { |
| if (tlbmc_enabled) { |
| absl::Duration response_time = absl::Now() - request_start; |
| SharedMemoryServer::GetInstance().UpdateMetricsResponse( |
| response_time, code, resource_url); |
| } |
| } |
| |
| static void AsyncResponseHandler(::redfish::v1::Response* response, |
| grpc::ServerUnaryReactor* reactor, |
| crow::Response& res, |
| ProfilingTimeStamps profiling_time_stamps, |
| bool do_profiling, bool tlbmc_enabled, |
| std::string_view resource_url) { |
| if (!res.stringResponse) { |
| LOG(ERROR) << "HTTP response is empty!"; |
| reactor->Finish(grpc::Status(grpc::StatusCode::INTERNAL, |
| "HTTP response from BMCWeb is empty.")); |
| UpdateResponseMetrics(static_cast<int>(res.result()), resource_url, |
| tlbmc_enabled, profiling_time_stamps.request_start); |
| return; |
| } |
| |
| auto* headers = response->mutable_headers(); |
| for (const auto& element : res.stringResponse->base()) { |
| // Repeated headers are combined into a ',' separated list. See |
| // https://www.rfc-editor.org/rfc/rfc9110.html#section-5.3-3. |
| auto found = headers->find(element.name_string()); |
| if (found == headers->end()) { |
| (*headers)[element.name_string()] = element.value(); |
| } else { |
| found->second += ","; |
| found->second += element.value(); |
| } |
| } |
| |
| profiling_time_stamps.response_end = GetCurrentTime(do_profiling); |
| |
| LogProfileTimeInResp(profiling_time_stamps.request_uri_start, |
| profiling_time_stamps.response_end, |
| "time-total-response", do_profiling, res); |
| |
| if ((*res.stringResponse)["Content-Type"] == "application/octet-stream" || |
| (*res.stringResponse)["OData-Version"] != "4.0") { |
| *response->mutable_octet_stream() = res.body(); |
| } else { |
| *response->mutable_json_str() = res.jsonValue.dump( |
| 2, ' ', true, nlohmann::json::error_handler_t::replace); |
| } |
| response->set_code(static_cast<unsigned int>(res.result())); |
| reactor->Finish(grpc::Status::OK); |
| UpdateResponseMetrics(static_cast<int>(res.result()), resource_url, |
| tlbmc_enabled, profiling_time_stamps.request_start); |
| } |
| |
| void HandleTlbmcRequest( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* grpc_request, |
| boost::beast::http::request<boost::beast::http::string_body>&& |
| boost_request, |
| ::redfish::v1::Response* grpc_response, grpc::ServerUnaryReactor* reactor, |
| bool with_trust_bundle, bool enable_insecure_server) const { |
| milotic_tlbmc::Tracer::GetInstance().AddRepeatedDatapoint( |
| "Tlbmc-Parse-Request-Begin", absl::Now()); |
| absl::StatusOr<milotic_tlbmc::RedfishRequest> request = |
| milotic_tlbmc::RedfishRequest::Create(std::move(boost_request)); |
| if (!request.ok()) { |
| reactor->Finish(grpc::Status(grpc::StatusCode::INTERNAL, |
| request.status().ToString())); |
| return; |
| } |
| request->SetWithTrustBundle(with_trust_bundle); |
| // If enable_insecure_server is true, set `FromGrpc` to false; otherwise |
| // `FromGrpc` should be true. |
| request->SetFromGrpc(!enable_insecure_server); |
| request->SetPeerAuthenticated( |
| context->auth_context()->IsPeerAuthenticated()); |
| |
| LOG(INFO) << "Received TLBMC request; |url|=" << request->Target() |
| << "; |method|=" << static_cast<int>(request->Method()); |
| |
| std::unordered_set<std::string> peer_privileges; |
| if (grpc::Status privileges_status = |
| SetPeerPrivileges(*context->auth_context(), grpc_request, |
| peer_privileges, with_trust_bundle); |
| !privileges_status.ok() && !enable_insecure_server) { |
| reactor->Finish(privileges_status); |
| return; |
| } |
| |
| request->SetPeerPrivileges(peer_privileges); |
| milotic_tlbmc::Tracer::GetInstance().AddRepeatedDatapoint( |
| "Tlbmc-Parse-Request-End", absl::Now()); |
| |
| RedfishResponse response(reactor, *grpc_response); |
| milotic_tlbmc::Tracer::GetInstance().AddRepeatedDatapoint( |
| "Tlbmc-Redfish-Routing-Begin", absl::Now()); |
| tlbmc_app_->Handle(std::move(*request), std::move(response)); |
| } |
| |
| void HandleNonTlbmcRequest( |
| grpc::CallbackServerContext* context, |
| const ::redfish::v1::Request* request, |
| boost::beast::http::request<boost::beast::http::string_body>&& |
| boost_request, |
| ::redfish::v1::Response* response, grpc::ServerUnaryReactor* reactor, |
| ProfilingTimeStamps profiling_time_stamps) const { |
| std::optional<crow::Request> crow_request = |
| GetInternalRequestFromProto(std::move(boost_request), reactor, context); |
| if (!crow_request) { |
| return; |
| } |
| |
| LOG(INFO) << "Received non-TLBMC request; |url|=" << crow_request->target() |
| << "; |method|=" << crow_request->methodString(); |
| |
| if (grpc::Status privileges_status = SetPeerPrivileges( |
| *context->auth_context(), request, crow_request->peer_privileges, |
| crow_request->with_trust_bundle); |
| !privileges_status.ok()) { |
| reactor->Finish(privileges_status); |
| return; |
| } |
| |
| auto async_resp = std::make_shared<bmcweb::AsyncResp>(nullptr); |
| if (auto it = request->headers().find(bmcweb::BMCWEB_HINT_MAX_AGE_SEC); |
| it != request->headers().end()) { |
| int64_t max_age_sec{}; |
| bool success = absl::SimpleAtoi(it->second, &max_age_sec); |
| if (!success || max_age_sec <= 0) { |
| reactor->Finish(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, |
| "Invalid BMCWEB_HINT_MAX_AGE_SEC," |
| "must be a positive integer")); |
| return; |
| } |
| async_resp->hintMaxAgeSec = max_age_sec; |
| async_resp->hintCacheMaxAgeSec = max_age_sec; |
| } |
| |
| profiling_time_stamps.request_start = absl::Now(); |
| std::string resource_url = std::string(crow_request->target()); |
| async_resp->res.setCompleteRequestHandler( |
| [response, reactor, profiling_time_stamps, |
| do_profiling{config_.do_profiling}, |
| tlbmc_enabled{config_.enable_tlbmc}, |
| resource_url(std::move(resource_url))](crow::Response& res) { |
| AsyncResponseHandler(response, reactor, res, profiling_time_stamps, |
| do_profiling, tlbmc_enabled, resource_url); |
| }); |
| |
| profiling_time_stamps.entering_io_context = |
| GetCurrentTime(config_.do_profiling); |
| |
| if (crow_request->method() == boost::beast::http::verb::get && |
| config_.multi_thread_get) { |
| auto strand = std::make_shared<boost::asio::io_context::strand>( |
| *io_context_worker_threads_); |
| async_resp->strand_ = strand; |
| strand->post([this, crow_request{std::move(crow_request)}, |
| async_resp{std::move(async_resp)}]() mutable { |
| app_->handle(*crow_request, async_resp); |
| }); |
| } else { |
| boost::asio::post( |
| *io_context_main_thread_, [crow_request{std::move(crow_request)}, |
| /*async_resp has to be moved to prevent |
| the handler being called in the grpc |
| thread due to expand queries.*/ |
| async_resp{std::move(async_resp)}, |
| profiling_time_stamps, this]() mutable { |
| profiling_time_stamps.handler_start = |
| GetCurrentTime(config_.do_profiling); |
| |
| app_->handle(*crow_request, async_resp); |
| |
| profiling_time_stamps.handler_end = |
| GetCurrentTime(config_.do_profiling); |
| |
| LogProfileTimeInResp(profiling_time_stamps.request_uri_start, |
| profiling_time_stamps.entering_io_context, |
| "time-before-io-context", config_.do_profiling, |
| async_resp->res); |
| LogProfileTimeInResp(profiling_time_stamps.entering_io_context, |
| profiling_time_stamps.handler_start, |
| "time-io-context-queue-wait", |
| config_.do_profiling, async_resp->res); |
| LogProfileTimeInResp(profiling_time_stamps.handler_start, |
| profiling_time_stamps.handler_end, |
| "time-handler", config_.do_profiling, |
| async_resp->res); |
| }); |
| } |
| } |
| |
| static bool IsTlbmcRequest(const ::redfish::v1::Request* request) { |
| return absl::StartsWith(request->url(), milotic_tlbmc::kTlbmcPrefix); |
| } |
| |
| static std::optional< |
| boost::beast::http::request<boost::beast::http::string_body>> |
| CreateBoostRequest(const ::redfish::v1::Request* request, |
| boost::beast::http::verb method) { |
| boost::beast::http::request<boost::beast::http::string_body> boost_request; |
| |
| boost::system::result<boost::urls::url> url = |
| boost::urls::parse_relative_ref(request->url()); |
| if (!url) { |
| LOG(INFO) << absl::StrFormat("URL %s is invalid", request->url()); |
| return std::nullopt; |
| } |
| |
| // https://www.dmtf.org/sites/default/files/standards/documents/DSP0266_1.8.0.pdf |
| // Redfish spec states that # is URL must be a fragment. (6.1) |
| // Fragments will be ignored by the server. (Also 6.1) |
| boost_request.target(url->remove_fragment()); |
| boost_request.method(method); |
| |
| // Check if json_str here first since that's preferred. |
| if (!request->json_str().empty()) { |
| boost_request.body() = request->json_str(); |
| } else if (request->has_json()) { |
| nlohmann::json json_req = ecclesia::StructToJson(request->json()); |
| if (!json_req.is_null()) { |
| boost_request.body() = json_req.dump(); |
| } |
| } else if (request->has_octet_stream()) { |
| boost_request.body() = request->octet_stream(); |
| } |
| |
| for (const auto& [key, value] : request->headers()) { |
| boost_request.base().insert(key, value); |
| }; |
| |
| return boost_request; |
| } |
| grpc::ServerUnaryReactor* RequestUri(grpc::CallbackServerContext* context, |
| boost::beast::http::verb method, |
| const ::redfish::v1::Request* request, |
| ::redfish::v1::Response* response) { |
| ProfilingTimeStamps profiling_time_stamps; |
| profiling_time_stamps.request_uri_start = |
| GetCurrentTime(config_.do_profiling); |
| |
| using ::milotic::authz::BmcWebAuthorizerSingleton; |
| |
| grpc::ServerUnaryReactor* reactor = context->DefaultReactor(); |
| |
| milotic_tlbmc::Tracer::GetInstance().AddRepeatedDatapoint( |
| "Authz-CheckTarget-Begin", absl::Now()); |
| // Check target only when there is trust bundle and server is in secure |
| // mode. |
| bool with_trust_bundle = |
| (cert_provider_.GetServerStatus() == |
| ::milotic::redfish::BmcWebCertProvider::ServerStatus:: |
| kWithRootCertsAndProdSignedCert || |
| cert_provider_.GetServerStatus() == |
| ::milotic::redfish::BmcWebCertProvider::ServerStatus:: |
| kWithRootCertsAndSelfSignedCert); |
| if (!config_.enable_insecure_server && with_trust_bundle) { |
| grpc::Status status = |
| BmcWebAuthorizerSingleton::GetInstance().CheckTarget( |
| *context->auth_context(), context->client_metadata(), *request); |
| if (!status.ok()) { |
| LOG(WARNING) << "Check target failed: " << status.error_message(); |
| reactor->Finish(status); |
| return reactor; |
| } |
| } |
| |
| std::optional<boost::beast::http::request<boost::beast::http::string_body>> |
| boost_request = CreateBoostRequest(request, method); |
| if (!boost_request) { |
| reactor->Finish( |
| grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Invalid URL")); |
| return reactor; |
| } |
| |
| milotic_tlbmc::Tracer::GetInstance().AddRepeatedDatapoint( |
| "Authz-CheckTarget-End", absl::Now()); |
| |
| if (config_.enable_tlbmc && IsTlbmcRequest(request)) { |
| HandleTlbmcRequest(context, request, std::move(boost_request.value()), |
| response, reactor, with_trust_bundle, |
| config_.enable_insecure_server); |
| } else { |
| HandleNonTlbmcRequest(context, request, std::move(boost_request.value()), |
| response, reactor, profiling_time_stamps); |
| } |
| |
| return reactor; |
| } |
| |
| void StoreReactor(const std::shared_ptr<Reactor>& reactor) { |
| absl::MutexLock lock(&mutex_); |
| reactors_store_.insert({reactor.get(), reactor}); |
| } |
| |
| void RemoveReactor(Reactor* reactor) { |
| absl::MutexLock lock(&mutex_); |
| reactors_store_.erase(reactor); |
| } |
| |
| const unsigned int hardware_concurrency_; |
| App* app_; |
| const milotic_tlbmc::RedfishApp* tlbmc_app_; |
| std::shared_ptr<boost::asio::io_context> io_context_main_thread_; |
| std::shared_ptr<boost::asio::io_context> io_context_worker_threads_; |
| ::milotic::redfish::BmcWebCertProvider& cert_provider_; |
| RedfishServiceConfig& config_; |
| ::redfish::v1::Response first_test_response_; |
| ecclesia::SubscriptionService* subscription_service_ = nullptr; |
| |
| absl::Mutex mutex_; |
| absl::flat_hash_map<Reactor*, std::shared_ptr<Reactor>> reactors_store_ |
| ABSL_GUARDED_BY(mutex_); |
| }; |
| |
| } // namespace internal |
| |
| void GrpcRedfishService::Shutdown() { |
| LOG(INFO) << "Dumping trace: \n" |
| << milotic_tlbmc::Tracer::GetInstance().Dump(); |
| LOG(WARNING) << "Shutdown gRPC server..."; |
| server_->Shutdown(std::chrono::system_clock::now() + std::chrono::seconds(1)); |
| server_->Wait(); |
| LOG(WARNING) << "gRPC server stopped!"; |
| } |
| |
| GrpcRedfishService::GrpcRedfishService( |
| App* app, const milotic_tlbmc::RedfishApp* tlbmc_app, |
| const std::shared_ptr<boost::asio::io_context>& io_context_main_thread, |
| const std::shared_ptr<boost::asio::io_context>& io_context_worker_threads, |
| const RedfishServiceConfig& config, |
| ecclesia::SubscriptionService* subscription_service) |
| : cert_provider_( |
| std::make_unique<::milotic::redfish::BmcWebCertProviderImpl>( |
| config.trust_bundle_path, config.private_key_path, |
| config.own_cert_path, config.self_signed_key_cert_path, |
| boost::uuids::to_string(boost::uuids::random_generator()()), |
| config.authority_policy_file_binary, |
| /*enable_insecure_server=*/config.enable_insecure_server, |
| /*check_loas3_policy=*/config.check_loas3_policy)), |
| config_(config), |
| subscription_service_(subscription_service) { |
| using ::milotic::authz::BmcWebAuthorizerSingleton; |
| using ::milotic::redfish::BmcWebCertProvider; |
| using ::milotic::redfish::BmcWebCertProviderImpl; |
| using ::milotic::redfish::GetCredentialsWithoutAuthz; |
| using ::milotic_hft::HftServiceImpl; |
| using ::milotic_hft::HighFrequencySensorReading; |
| using ::milotic_hft::HighFrequencySensorsReadings; |
| using ::milotic_hft::Payload; |
| using ::milotic_hft::SubscriptionManager; |
| using ::milotic_hft::SubscriptionManagerFake; |
| |
| LOG(WARNING) << "Start gRPC server..."; |
| LOG(WARNING) << "Initialize authorization layer..."; |
| #ifdef ENABLE_LOAS3_VALIDATION |
| security::SetCertificateAuthorityPolicyFilePath( |
| config_.authority_policy_file_binary); |
| #endif |
| BmcWebAuthorizerSingleton::Initialize( |
| { |
| .configuration_path = config_.authz_config_path, |
| .platform_configuration_path = config_.authz_platform_config_path, |
| .base_privileges_folder = config_.persistent_base_privileges_folder, |
| .pattern_entity_overrides_path = |
| config.pattern_to_entity_overrides_path, |
| .offline_node_entities_path = config.offline_node_entity_path, |
| .google_machine_identity_path = config_.gmi_file_path, |
| }, |
| /*oauth_key_path=*/config.oauth_key_path); |
| if (!BmcWebAuthorizerSingleton::GetInstance() |
| .IsBasePrivilegeRegistryFound()) { |
| LOG(WARNING) |
| << "Could not find Privilege Registry at /var/google/authz_policies"; |
| BmcWebAuthorizerSingleton::GetInstance().SetBasePrivilegesFolder( |
| config_.rofs_base_privileges_folder); |
| } |
| |
| LOG(WARNING) << "Authorization layer initialized!"; |
| grpc::reflection::InitProtoReflectionServerBuilderPlugin(); |
| grpc::ServerBuilder builder; |
| |
| if (config_.enable_insecure_server) { |
| builder.AddListeningPort("[::]:" + std::to_string(config_.port), |
| grpc::InsecureServerCredentials()); |
| } else { |
| builder.AddListeningPort( |
| "[::]:" + std::to_string(config_.port), |
| GetCredentialsWithoutAuthz(*cert_provider_, config_.crl_directory)); |
| } |
| |
| service_ = std::make_unique<internal::RedfishV1Impl>( |
| app, tlbmc_app, io_context_main_thread, io_context_worker_threads, |
| *cert_provider_, config_, subscription_service_); |
| |
| std::unique_ptr<SubscriptionManager> subscription_manager; |
| if (config_.enable_hft_fake_manager) { |
| auto fake_manager = |
| std::make_unique<SubscriptionManagerFake>(); |
| Payload payload; |
| HighFrequencySensorsReadings* readings = |
| payload.mutable_high_frequency_sensors_readings_batch() |
| ->add_high_frequency_sensors(); |
| readings->mutable_sensor_identifier()->set_name("test_sensor"); |
| HighFrequencySensorReading* timestamped_reading = |
| readings->add_timestamped_readings(); |
| timestamped_reading->set_timestamp_ns(1234567890); |
| timestamped_reading->set_float_reading(1.23456789f); |
| fake_manager->SetFakeData(std::move(payload)); |
| subscription_manager = std::move(fake_manager); |
| } |
| |
| hft_service_ = std::make_unique<milotic_hft::HftServiceImpl>( |
| milotic_hft::HftServiceOptions{ |
| .cert_provider = cert_provider_.get(), |
| }, |
| std::move(subscription_manager)); |
| builder.RegisterService(service_.get()); |
| |
| if (config_.enable_hft) { |
| builder.RegisterService(hft_service_.get()); |
| } |
| |
| server_ = builder.BuildAndStart(); |
| } |
| |
| } // namespace milotic |
| |
| #pragma GCC diagnostic pop |