1 /* 2 Copyright (c) 2020 Intel Corporation 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 #include "subscription.hpp" 17 18 #include "dbus_singleton.hpp" 19 #include "event_log.hpp" 20 #include "event_logs_object_type.hpp" 21 #include "event_matches_filter.hpp" 22 #include "event_service_store.hpp" 23 #include "filter_expr_executor.hpp" 24 #include "heartbeat_messages.hpp" 25 #include "http_client.hpp" 26 #include "http_response.hpp" 27 #include "logging.hpp" 28 #include "metric_report.hpp" 29 #include "server_sent_event.hpp" 30 #include "ssl_key_handler.hpp" 31 #include "utils/time_utils.hpp" 32 33 #include <boost/asio/error.hpp> 34 #include <boost/asio/io_context.hpp> 35 #include <boost/asio/steady_timer.hpp> 36 #include <boost/beast/http/verb.hpp> 37 #include <boost/system/errc.hpp> 38 #include <boost/url/format.hpp> 39 #include <boost/url/url_view_base.hpp> 40 #include <nlohmann/json.hpp> 41 42 #include <algorithm> 43 #include <chrono> 44 #include <cstdint> 45 #include <cstdlib> 46 #include <ctime> 47 #include <format> 48 #include <functional> 49 #include <memory> 50 #include <span> 51 #include <string> 52 #include <string_view> 53 #include <utility> 54 #include <vector> 55 56 namespace redfish 57 { 58 59 Subscription::Subscription( 60 std::shared_ptr<persistent_data::UserSubscription> userSubIn, 61 const boost::urls::url_view_base& url, boost::asio::io_context& ioc) : 62 userSub{std::move(userSubIn)}, 63 policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc) 64 { 65 userSub->destinationUrl = url; 66 client.emplace(ioc, policy); 67 // Subscription constructor 68 policy->invalidResp = retryRespHandler; 69 } 70 71 Subscription::Subscription(crow::sse_socket::Connection& connIn) : 72 userSub{std::make_shared<persistent_data::UserSubscription>()}, 73 sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context()) 74 {} 75 76 // callback for subscription sendData 77 void Subscription::resHandler(const std::shared_ptr<Subscription>& /*unused*/, 78 const crow::Response& res) 79 { 80 BMCWEB_LOG_DEBUG("Response handled with return code: {}", res.resultInt()); 81 82 if (!client) 83 { 84 BMCWEB_LOG_ERROR( 85 "Http client wasn't filled but http client callback was called."); 86 return; 87 } 88 89 if (userSub->retryPolicy != "TerminateAfterRetries") 90 { 91 return; 92 } 93 if (client->isTerminated()) 94 { 95 hbTimer.cancel(); 96 if (deleter) 97 { 98 BMCWEB_LOG_INFO("Subscription {} is deleted after MaxRetryAttempts", 99 userSub->id); 100 deleter(); 101 } 102 } 103 } 104 105 void Subscription::sendHeartbeatEvent() 106 { 107 // send the heartbeat message 108 nlohmann::json eventMessage = messages::redfishServiceFunctional(); 109 110 std::string heartEventId = std::to_string(eventSeqNum); 111 eventMessage["EventId"] = heartEventId; 112 eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first; 113 eventMessage["OriginOfCondition"] = 114 std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id); 115 eventMessage["MemberId"] = "0"; 116 117 nlohmann::json::array_t eventRecord; 118 eventRecord.emplace_back(std::move(eventMessage)); 119 120 nlohmann::json msgJson; 121 msgJson["@odata.type"] = "#Event.v1_4_0.Event"; 122 msgJson["Name"] = "Heartbeat"; 123 msgJson["Id"] = heartEventId; 124 msgJson["Events"] = std::move(eventRecord); 125 126 std::string strMsg = 127 msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 128 sendEventToSubscriber(std::move(strMsg)); 129 eventSeqNum++; 130 } 131 132 void Subscription::scheduleNextHeartbeatEvent() 133 { 134 hbTimer.expires_after(std::chrono::minutes(userSub->hbIntervalMinutes)); 135 hbTimer.async_wait( 136 std::bind_front(&Subscription::onHbTimeout, this, weak_from_this())); 137 } 138 139 void Subscription::heartbeatParametersChanged() 140 { 141 hbTimer.cancel(); 142 143 if (userSub->sendHeartbeat) 144 { 145 scheduleNextHeartbeatEvent(); 146 } 147 } 148 149 void Subscription::onHbTimeout(const std::weak_ptr<Subscription>& weakSelf, 150 const boost::system::error_code& ec) 151 { 152 if (ec == boost::asio::error::operation_aborted) 153 { 154 BMCWEB_LOG_DEBUG("heartbeat timer async_wait is aborted"); 155 return; 156 } 157 if (ec == boost::system::errc::operation_canceled) 158 { 159 BMCWEB_LOG_DEBUG("heartbeat timer async_wait canceled"); 160 return; 161 } 162 if (ec) 163 { 164 BMCWEB_LOG_CRITICAL("heartbeat timer async_wait failed: {}", ec); 165 return; 166 } 167 168 std::shared_ptr<Subscription> self = weakSelf.lock(); 169 if (!self) 170 { 171 BMCWEB_LOG_CRITICAL("onHbTimeout failed on Subscription"); 172 return; 173 } 174 175 // Timer expired. 176 sendHeartbeatEvent(); 177 178 // reschedule heartbeat timer 179 scheduleNextHeartbeatEvent(); 180 } 181 182 bool Subscription::sendEventToSubscriber(std::string&& msg) 183 { 184 persistent_data::EventServiceConfig eventServiceConfig = 185 persistent_data::EventServiceStore::getInstance() 186 .getEventServiceConfig(); 187 if (!eventServiceConfig.enabled) 188 { 189 return false; 190 } 191 192 if (client) 193 { 194 client->sendDataWithCallback( 195 std::move(msg), userSub->destinationUrl, 196 static_cast<ensuressl::VerifyCertificate>( 197 userSub->verifyCertificate), 198 userSub->httpHeaders, boost::beast::http::verb::post, 199 std::bind_front(&Subscription::resHandler, this, 200 shared_from_this())); 201 return true; 202 } 203 204 if (sseConn != nullptr) 205 { 206 eventSeqNum++; 207 sseConn->sendSseEvent(std::to_string(eventSeqNum), msg); 208 } 209 return true; 210 } 211 212 bool Subscription::sendTestEventLog(TestEvent& testEvent) 213 { 214 nlohmann::json::array_t logEntryArray; 215 nlohmann::json& logEntryJson = logEntryArray.emplace_back(); 216 217 if (testEvent.eventGroupId) 218 { 219 logEntryJson["EventGroupId"] = *testEvent.eventGroupId; 220 } 221 222 if (testEvent.eventId) 223 { 224 logEntryJson["EventId"] = *testEvent.eventId; 225 } 226 227 if (testEvent.eventTimestamp) 228 { 229 logEntryJson["EventTimestamp"] = *testEvent.eventTimestamp; 230 } 231 232 if (testEvent.originOfCondition) 233 { 234 logEntryJson["OriginOfCondition"]["@odata.id"] = 235 *testEvent.originOfCondition; 236 } 237 if (testEvent.severity) 238 { 239 logEntryJson["Severity"] = *testEvent.severity; 240 } 241 242 if (testEvent.message) 243 { 244 logEntryJson["Message"] = *testEvent.message; 245 } 246 247 if (testEvent.resolution) 248 { 249 logEntryJson["Resolution"] = *testEvent.resolution; 250 } 251 252 if (testEvent.messageId) 253 { 254 logEntryJson["MessageId"] = *testEvent.messageId; 255 } 256 257 if (testEvent.messageArgs) 258 { 259 logEntryJson["MessageArgs"] = *testEvent.messageArgs; 260 } 261 // MemberId is 0 : since we are sending one event record. 262 logEntryJson["MemberId"] = "0"; 263 264 nlohmann::json msg; 265 msg["@odata.type"] = "#Event.v1_4_0.Event"; 266 msg["Id"] = std::to_string(eventSeqNum); 267 msg["Name"] = "Event Log"; 268 msg["Events"] = logEntryArray; 269 270 std::string strMsg = 271 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 272 return sendEventToSubscriber(std::move(strMsg)); 273 } 274 275 void Subscription::filterAndSendEventLogs( 276 const std::vector<EventLogObjectsType>& eventRecords) 277 { 278 nlohmann::json::array_t logEntryArray; 279 for (const EventLogObjectsType& logEntry : eventRecords) 280 { 281 std::vector<std::string_view> messageArgsView( 282 logEntry.messageArgs.begin(), logEntry.messageArgs.end()); 283 284 nlohmann::json::object_t bmcLogEntry; 285 if (event_log::formatEventLogEntry( 286 logEntry.id, logEntry.messageId, messageArgsView, 287 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0) 288 { 289 BMCWEB_LOG_DEBUG("Read eventLog entry failed"); 290 continue; 291 } 292 293 if (!eventMatchesFilter(*userSub, bmcLogEntry, "")) 294 { 295 BMCWEB_LOG_DEBUG("Event {} did not match the filter", 296 nlohmann::json(bmcLogEntry).dump()); 297 continue; 298 } 299 300 if (filter) 301 { 302 if (!memberMatches(bmcLogEntry, *filter)) 303 { 304 BMCWEB_LOG_DEBUG("Filter didn't match"); 305 continue; 306 } 307 } 308 309 logEntryArray.emplace_back(std::move(bmcLogEntry)); 310 } 311 312 if (logEntryArray.empty()) 313 { 314 BMCWEB_LOG_DEBUG("No log entries available to be transferred."); 315 return; 316 } 317 318 nlohmann::json msg; 319 msg["@odata.type"] = "#Event.v1_4_0.Event"; 320 msg["Id"] = std::to_string(eventSeqNum); 321 msg["Name"] = "Event Log"; 322 msg["Events"] = std::move(logEntryArray); 323 std::string strMsg = 324 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 325 sendEventToSubscriber(std::move(strMsg)); 326 eventSeqNum++; 327 } 328 329 void Subscription::filterAndSendReports(const std::string& reportId, 330 const telemetry::TimestampReadings& var) 331 { 332 boost::urls::url mrdUri = boost::urls::format( 333 "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId); 334 335 // Empty list means no filter. Send everything. 336 if (!userSub->metricReportDefinitions.empty()) 337 { 338 if (std::ranges::find(userSub->metricReportDefinitions, 339 mrdUri.buffer()) == 340 userSub->metricReportDefinitions.end()) 341 { 342 return; 343 } 344 } 345 346 nlohmann::json msg; 347 if (!telemetry::fillReport(msg, reportId, var)) 348 { 349 BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus " 350 "Report with id {}", 351 reportId); 352 return; 353 } 354 355 // Context is set by user during Event subscription and it must be 356 // set for MetricReport response. 357 if (!userSub->customText.empty()) 358 { 359 msg["Context"] = userSub->customText; 360 } 361 362 std::string strMsg = 363 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 364 sendEventToSubscriber(std::move(strMsg)); 365 } 366 367 void Subscription::updateRetryConfig(uint32_t retryAttempts, 368 uint32_t retryTimeoutInterval) 369 { 370 if (policy == nullptr) 371 { 372 BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set"); 373 return; 374 } 375 policy->maxRetryAttempts = retryAttempts; 376 policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval); 377 } 378 379 uint64_t Subscription::getEventSeqNum() const 380 { 381 return eventSeqNum; 382 } 383 384 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn) 385 { 386 return &thisConn == sseConn; 387 } 388 389 // Check used to indicate what response codes are valid as part of our retry 390 // policy. 2XX is considered acceptable 391 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode) 392 { 393 BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent"); 394 if ((respCode < 200) || (respCode >= 300)) 395 { 396 return boost::system::errc::make_error_code( 397 boost::system::errc::result_out_of_range); 398 } 399 400 // Return 0 if the response code is valid 401 return boost::system::errc::make_error_code(boost::system::errc::success); 402 } 403 404 } // namespace redfish 405