1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 // SPDX-FileCopyrightText: Copyright 2020 Intel Corporation 4 #include "subscription.hpp" 5 6 #include "dbus_singleton.hpp" 7 #include "event_log.hpp" 8 #include "event_logs_object_type.hpp" 9 #include "event_matches_filter.hpp" 10 #include "event_service_store.hpp" 11 #include "filter_expr_executor.hpp" 12 #include "heartbeat_messages.hpp" 13 #include "http_client.hpp" 14 #include "http_response.hpp" 15 #include "logging.hpp" 16 #include "metric_report.hpp" 17 #include "server_sent_event.hpp" 18 #include "ssl_key_handler.hpp" 19 #include "utils/time_utils.hpp" 20 21 #include <boost/asio/error.hpp> 22 #include <boost/asio/io_context.hpp> 23 #include <boost/asio/steady_timer.hpp> 24 #include <boost/beast/http/field.hpp> 25 #include <boost/beast/http/fields.hpp> 26 #include <boost/beast/http/verb.hpp> 27 #include <boost/system/errc.hpp> 28 #include <boost/url/format.hpp> 29 #include <boost/url/url_view_base.hpp> 30 #include <nlohmann/json.hpp> 31 32 #include <algorithm> 33 #include <chrono> 34 #include <cstdint> 35 #include <cstdlib> 36 #include <ctime> 37 #include <format> 38 #include <functional> 39 #include <memory> 40 #include <span> 41 #include <string> 42 #include <string_view> 43 #include <utility> 44 #include <vector> 45 46 namespace redfish 47 { 48 49 Subscription::Subscription( 50 std::shared_ptr<persistent_data::UserSubscription> userSubIn, 51 const boost::urls::url_view_base& url, boost::asio::io_context& ioc) : 52 userSub{std::move(userSubIn)}, 53 policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc) 54 { 55 userSub->destinationUrl = url; 56 client.emplace(ioc, policy); 57 // Subscription constructor 58 policy->invalidResp = retryRespHandler; 59 } 60 61 Subscription::Subscription(crow::sse_socket::Connection& connIn) : 62 userSub{std::make_shared<persistent_data::UserSubscription>()}, 63 sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context()) 64 {} 65 66 // callback for subscription sendData 67 void Subscription::resHandler(const crow::Response& res) 68 { 69 BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt()); 70 71 if (!client) 72 { 73 BMCWEB_LOG_ERROR( 74 "Http client wasn't filled but http client callback was called."); 75 return; 76 } 77 78 if (userSub->retryPolicy != "TerminateAfterRetries") 79 { 80 return; 81 } 82 if (client->isTerminated()) 83 { 84 hbTimer.cancel(); 85 if (deleter) 86 { 87 BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts", 88 userSub->id); 89 deleter(); 90 } 91 } 92 } 93 94 void Subscription::sendHeartbeatEvent() 95 { 96 // send the heartbeat message 97 nlohmann::json eventMessage = messages::redfishServiceFunctional(); 98 eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first; 99 eventMessage["OriginOfCondition"] = 100 std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id); 101 eventMessage["MemberId"] = "0"; 102 103 nlohmann::json::array_t eventRecord; 104 eventRecord.emplace_back(std::move(eventMessage)); 105 106 nlohmann::json msgJson; 107 msgJson["@odata.type"] = "#Event.v1_4_0.Event"; 108 msgJson["Name"] = "Heartbeat"; 109 msgJson["Events"] = std::move(eventRecord); 110 111 std::string strMsg = 112 msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 113 114 // Note, eventId here is always zero, because this is a a per subscription 115 // event and doesn't have an "ID" 116 uint64_t eventId = 0; 117 sendEventToSubscriber(eventId, std::move(strMsg)); 118 } 119 120 void Subscription::scheduleNextHeartbeatEvent() 121 { 122 hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes)); 123 hbTimer.async_wait( 124 std::bind_front(&Subscription::onHbTimeout, this, weak_from_this())); 125 } 126 127 void Subscription::heartbeatParametersChanged() 128 { 129 hbTimer.cancel(); 130 131 if (userSub->sendHeartbeat) 132 { 133 scheduleNextHeartbeatEvent(); 134 } 135 } 136 137 void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf, 138 const boost::system::error_code& ec) 139 { 140 if (ec == boost::asio::error::operation_aborted) 141 { 142 BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted"); 143 return; 144 } 145 if (ec == boost::system::errc::operation_canceled) 146 { 147 BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled"); 148 return; 149 } 150 if (ec) 151 { 152 BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec); 153 return; 154 } 155 156 std::shared_ptr<Subscription> self = weakSelf.lock(); 157 if (!self) 158 { 159 BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription"); 160 return; 161 } 162 163 // Timer expired. 164 sendHeartbeatEvent(); 165 166 // reschedule heartbeat timer 167 scheduleNextHeartbeatEvent(); 168 } 169 170 bool Subscription::sendEventToSubscriber(uint64_t eventId, std::string&& msg) 171 { 172 persistent_data::EventServiceConfig eventServiceConfig = 173 persistent_data::EventServiceStore::getInstance() 174 .getEventServiceConfig(); 175 if (!eventServiceConfig.enabled) 176 { 177 return false; 178 } 179 180 if (client) 181 { 182 boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders); 183 httpHeadersCopy.set(boost::beast::http::field::content_type, 184 "application/json"); 185 client->sendDataWithCallback( 186 std::move(msg), userSub->destinationUrl, 187 static_cast<ensuressl::VerifyCertificate>( 188 userSub->verifyCertificate), 189 httpHeadersCopy, boost::beast::http::verb::post, 190 std::bind_front(&Subscription::resHandler, this)); 191 return true; 192 } 193 194 if (sseConn != nullptr) 195 { 196 sseConn->sendSseEvent(std::to_string(eventId), msg); 197 } 198 return true; 199 } 200 201 void Subscription::filterAndSendEventLogs( 202 uint64_t eventId, const std::vector<EventLogObjectsType>& eventRecords) 203 { 204 nlohmann::json::array_t logEntryArray; 205 for (const EventLogObjectsType& logEntry : eventRecords) 206 { 207 BMCWEB_LOG_DEBUG("Processing logEntry: {}, {} '{}'", logEntry.id, 208 logEntry.timestamp, logEntry.messageId); 209 std::vector<std::string_view> messageArgsView( 210 logEntry.messageArgs.begin(), logEntry.messageArgs.end()); 211 212 nlohmann::json::object_t bmcLogEntry; 213 if (event_log::formatEventLogEntry( 214 eventId, logEntry.id, logEntry.messageId, messageArgsView, 215 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0) 216 { 217 BMCWEB_LOG_WARNING("Read eventLog entry failed"); 218 continue; 219 } 220 221 if (!eventMatchesFilter(*userSub, bmcLogEntry, "")) 222 { 223 BMCWEB_LOG_DEBUG("Event {} did not match the filter", 224 nlohmann::json(bmcLogEntry).dump()); 225 continue; 226 } 227 228 if (filter) 229 { 230 if (!memberMatches(bmcLogEntry, *filter)) 231 { 232 BMCWEB_LOG_DEBUG("Filter didn't match"); 233 continue; 234 } 235 } 236 237 logEntryArray.emplace_back(std::move(bmcLogEntry)); 238 eventId++; 239 } 240 241 if (logEntryArray.empty()) 242 { 243 BMCWEB_LOG_DEBUG("No log entries available to be transferred."); 244 return; 245 } 246 247 nlohmann::json msg; 248 msg["@odata.type"] = "#Event.v1_4_0.Event"; 249 msg["Id"] = std::to_string(eventId); 250 msg["Name"] = "Event Log"; 251 msg["Events"] = std::move(logEntryArray); 252 std::string strMsg = 253 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 254 sendEventToSubscriber(eventId, std::move(strMsg)); 255 } 256 257 void Subscription::filterAndSendReports(uint64_t eventId, 258 const std::string& reportId, 259 const telemetry::TimestampReadings& var) 260 { 261 boost::urls::url mrdUri = boost::urls::format( 262 "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId); 263 264 // Empty list means no filter. Send everything. 265 if (!userSub->metricReportDefinitions.empty()) 266 { 267 if (std::ranges::find(userSub->metricReportDefinitions, 268 mrdUri.buffer()) == 269 userSub->metricReportDefinitions.end()) 270 { 271 return; 272 } 273 } 274 275 nlohmann::json msg; 276 if (!telemetry::fillReport(msg, reportId, var)) 277 { 278 BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus " 279 "Report with id {}", 280 reportId); 281 return; 282 } 283 284 // Context is set by user during Event subscription and it must be 285 // set for MetricReport response. 286 if (!userSub->customText.empty()) 287 { 288 msg["Context"] = userSub->customText; 289 } 290 291 std::string strMsg = 292 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 293 sendEventToSubscriber(eventId, std::move(strMsg)); 294 } 295 296 void Subscription::updateRetryConfig(uint32_t retryAttempts, 297 uint32_t retryTimeoutInterval) 298 { 299 if (policy == nullptr) 300 { 301 BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set"); 302 return; 303 } 304 policy->maxRetryAttempts = retryAttempts; 305 policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval); 306 } 307 308 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn) 309 { 310 return &thisConn == sseConn; 311 } 312 313 // Check used to indicate what response codes are valid as part of our retry 314 // policy. 2XX is considered acceptable 315 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode) 316 { 317 BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent"); 318 if ((respCode < 200) || (respCode >= 300)) 319 { 320 return boost::system::errc::make_error_code( 321 boost::system::errc::result_out_of_range); 322 } 323 324 // Return 0 if the response code is valid 325 return boost::system::errc::make_error_code(boost::system::errc::success); 326 } 327 328 } // namespace redfish 329