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 "server_sent_event.hpp" 17 #include "ssl_key_handler.hpp" 18 #include "telemetry_readings.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 <format> 36 #include <functional> 37 #include <memory> 38 #include <span> 39 #include <string> 40 #include <string_view> 41 #include <utility> 42 #include <vector> 43 44 namespace redfish 45 { 46 47 Subscription::Subscription( 48 std::shared_ptr<persistent_data::UserSubscription> userSubIn, 49 const boost::urls::url_view_base& url, boost::asio::io_context& ioc) : 50 userSub{std::move(userSubIn)}, 51 policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc) 52 { 53 userSub->destinationUrl = url; 54 client.emplace(ioc, policy); 55 // Subscription constructor 56 policy->invalidResp = retryRespHandler; 57 } 58 59 Subscription::Subscription(crow::sse_socket::Connection& connIn) : 60 userSub{std::make_shared<persistent_data::UserSubscription>()}, 61 sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context()) 62 {} 63 64 // callback for subscription sendData 65 void Subscription::resHandler(const std::shared_ptr<Subscription>& /*self*/, 66 const crow::Response& res) 67 { 68 BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt()); 69 70 if (!client) 71 { 72 BMCWEB_LOG_ERROR( 73 "Http client wasn't filled but http client callback was called."); 74 return; 75 } 76 77 if (userSub->retryPolicy != "TerminateAfterRetries") 78 { 79 return; 80 } 81 if (client->isTerminated()) 82 { 83 hbTimer.cancel(); 84 if (deleter) 85 { 86 BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts", 87 userSub->id); 88 deleter(); 89 } 90 } 91 } 92 93 void Subscription::sendHeartbeatEvent() 94 { 95 // send the heartbeat message 96 nlohmann::json eventMessage = messages::redfishServiceFunctional(); 97 eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first; 98 eventMessage["OriginOfCondition"] = 99 std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id); 100 eventMessage["MemberId"] = "0"; 101 102 nlohmann::json::array_t eventRecord; 103 eventRecord.emplace_back(std::move(eventMessage)); 104 105 nlohmann::json msgJson; 106 msgJson["@odata.type"] = "#Event.v1_4_0.Event"; 107 msgJson["Name"] = "Heartbeat"; 108 msgJson["Events"] = std::move(eventRecord); 109 110 std::string strMsg = 111 msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 112 113 // Note, eventId here is always zero, because this is a a per subscription 114 // event and doesn't have an "ID" 115 uint64_t eventId = 0; 116 sendEventToSubscriber(eventId, std::move(strMsg)); 117 } 118 119 void Subscription::scheduleNextHeartbeatEvent() 120 { 121 hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes)); 122 hbTimer.async_wait( 123 std::bind_front(&Subscription::onHbTimeout, this, weak_from_this())); 124 } 125 126 void Subscription::heartbeatParametersChanged() 127 { 128 hbTimer.cancel(); 129 130 if (userSub->sendHeartbeat) 131 { 132 scheduleNextHeartbeatEvent(); 133 } 134 } 135 136 void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf, 137 const boost::system::error_code& ec) 138 { 139 if (ec == boost::asio::error::operation_aborted) 140 { 141 BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted"); 142 return; 143 } 144 if (ec == boost::system::errc::operation_canceled) 145 { 146 BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled"); 147 return; 148 } 149 if (ec) 150 { 151 BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec); 152 return; 153 } 154 155 std::shared_ptr<Subscription> self = weakSelf.lock(); 156 if (!self) 157 { 158 BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription"); 159 return; 160 } 161 162 // Timer expired. 163 sendHeartbeatEvent(); 164 165 // reschedule heartbeat timer 166 scheduleNextHeartbeatEvent(); 167 } 168 169 bool Subscription::sendEventToSubscriber(uint64_t eventId, std::string&& msg) 170 { 171 persistent_data::EventServiceConfig eventServiceConfig = 172 persistent_data::EventServiceStore::getInstance() 173 .getEventServiceConfig(); 174 if (!eventServiceConfig.enabled) 175 { 176 return false; 177 } 178 179 if (client) 180 { 181 boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders); 182 httpHeadersCopy.set(boost::beast::http::field::content_type, 183 "application/json"); 184 client->sendDataWithCallback( 185 std::move(msg), userSub->destinationUrl, 186 static_cast<ensuressl::VerifyCertificate>( 187 userSub->verifyCertificate), 188 httpHeadersCopy, boost::beast::http::verb::post, 189 std::bind_front(&Subscription::resHandler, this, 190 shared_from_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