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