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/field.hpp> 37 #include <boost/beast/http/fields.hpp> 38 #include <boost/beast/http/verb.hpp> 39 #include <boost/system/errc.hpp> 40 #include <boost/url/format.hpp> 41 #include <boost/url/url_view_base.hpp> 42 #include <nlohmann/json.hpp> 43 44 #include <algorithm> 45 #include <chrono> 46 #include <cstdint> 47 #include <cstdlib> 48 #include <ctime> 49 #include <format> 50 #include <functional> 51 #include <memory> 52 #include <span> 53 #include <string> 54 #include <string_view> 55 #include <utility> 56 #include <vector> 57 58 namespace redfish 59 { 60 61 Subscription::Subscription( 62 std::shared_ptr<persistent_data::UserSubscription> userSubIn, 63 const boost::urls::url_view_base& url, boost::asio::io_context& ioc) : 64 userSub{std::move(userSubIn)}, 65 policy(std::make_shared<crow::ConnectionPolicy>()), hbTimer(ioc) 66 { 67 userSub->destinationUrl = url; 68 client.emplace(ioc, policy); 69 // Subscription constructor 70 policy->invalidResp = retryRespHandler; 71 } 72 73 Subscription::Subscription(crow::sse_socket::Connection& connIn) : 74 userSub{std::make_shared<persistent_data::UserSubscription>()}, 75 sseConn(&connIn), hbTimer(crow::connections::systemBus->get_io_context()) 76 {} 77 78 // callback for subscription sendData 79 void Subscription::resHandler(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 eventMessage["EventTimestamp"] = time_utils::getDateTimeOffsetNow().first; 111 eventMessage["OriginOfCondition"] = 112 std::format("/redfish/v1/EventService/Subscriptions/{}", userSub->id); 113 eventMessage["MemberId"] = "0"; 114 115 nlohmann::json::array_t eventRecord; 116 eventRecord.emplace_back(std::move(eventMessage)); 117 118 nlohmann::json msgJson; 119 msgJson["@odata.type"] = "#Event.v1_4_0.Event"; 120 msgJson["Name"] = "Heartbeat"; 121 msgJson["Events"] = std::move(eventRecord); 122 123 std::string strMsg = 124 msgJson.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 125 126 // Note, eventId here is always zero, because this is a a per subscription 127 // event and doesn't have an "ID" 128 uint64_t eventId = 0; 129 sendEventToSubscriber(eventId, std::move(strMsg)); 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(uint64_t eventId, 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 boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders); 195 httpHeadersCopy.set(boost::beast::http::field::content_type, 196 "application/json"); 197 client->sendDataWithCallback( 198 std::move(msg), userSub->destinationUrl, 199 static_cast<ensuressl::VerifyCertificate>( 200 userSub->verifyCertificate), 201 httpHeadersCopy, boost::beast::http::verb::post, 202 std::bind_front(&Subscription::resHandler, this)); 203 return true; 204 } 205 206 if (sseConn != nullptr) 207 { 208 sseConn->sendSseEvent(std::to_string(eventId), msg); 209 } 210 return true; 211 } 212 213 void Subscription::filterAndSendEventLogs( 214 uint64_t eventId, const std::vector<EventLogObjectsType>& eventRecords) 215 { 216 nlohmann::json::array_t logEntryArray; 217 for (const EventLogObjectsType& logEntry : eventRecords) 218 { 219 BMCWEB_LOG_DEBUG("Processing logEntry: {}, {} '{}'", logEntry.id, 220 logEntry.timestamp, logEntry.messageId); 221 std::vector<std::string_view> messageArgsView( 222 logEntry.messageArgs.begin(), logEntry.messageArgs.end()); 223 224 nlohmann::json::object_t bmcLogEntry; 225 if (event_log::formatEventLogEntry( 226 eventId, logEntry.id, logEntry.messageId, messageArgsView, 227 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0) 228 { 229 BMCWEB_LOG_WARNING("Read eventLog entry failed"); 230 continue; 231 } 232 233 if (!eventMatchesFilter(*userSub, bmcLogEntry, "")) 234 { 235 BMCWEB_LOG_DEBUG("Event {} did not match the filter", 236 nlohmann::json(bmcLogEntry).dump()); 237 continue; 238 } 239 240 if (filter) 241 { 242 if (!memberMatches(bmcLogEntry, *filter)) 243 { 244 BMCWEB_LOG_DEBUG("Filter didn't match"); 245 continue; 246 } 247 } 248 249 logEntryArray.emplace_back(std::move(bmcLogEntry)); 250 eventId++; 251 } 252 253 if (logEntryArray.empty()) 254 { 255 BMCWEB_LOG_DEBUG("No log entries available to be transferred."); 256 return; 257 } 258 259 nlohmann::json msg; 260 msg["@odata.type"] = "#Event.v1_4_0.Event"; 261 msg["Id"] = std::to_string(eventId); 262 msg["Name"] = "Event Log"; 263 msg["Events"] = std::move(logEntryArray); 264 std::string strMsg = 265 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 266 sendEventToSubscriber(eventId, std::move(strMsg)); 267 } 268 269 void Subscription::filterAndSendReports(uint64_t eventId, 270 const std::string& reportId, 271 const telemetry::TimestampReadings& var) 272 { 273 boost::urls::url mrdUri = boost::urls::format( 274 "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId); 275 276 // Empty list means no filter. Send everything. 277 if (!userSub->metricReportDefinitions.empty()) 278 { 279 if (std::ranges::find(userSub->metricReportDefinitions, 280 mrdUri.buffer()) == 281 userSub->metricReportDefinitions.end()) 282 { 283 return; 284 } 285 } 286 287 nlohmann::json msg; 288 if (!telemetry::fillReport(msg, reportId, var)) 289 { 290 BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus " 291 "Report with id {}", 292 reportId); 293 return; 294 } 295 296 // Context is set by user during Event subscription and it must be 297 // set for MetricReport response. 298 if (!userSub->customText.empty()) 299 { 300 msg["Context"] = userSub->customText; 301 } 302 303 std::string strMsg = 304 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace); 305 sendEventToSubscriber(eventId, std::move(strMsg)); 306 } 307 308 void Subscription::updateRetryConfig(uint32_t retryAttempts, 309 uint32_t retryTimeoutInterval) 310 { 311 if (policy == nullptr) 312 { 313 BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set"); 314 return; 315 } 316 policy->maxRetryAttempts = retryAttempts; 317 policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval); 318 } 319 320 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn) 321 { 322 return &thisConn == sseConn; 323 } 324 325 // Check used to indicate what response codes are valid as part of our retry 326 // policy. 2XX is considered acceptable 327 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode) 328 { 329 BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent"); 330 if ((respCode < 200) || (respCode >= 300)) 331 { 332 return boost::system::errc::make_error_code( 333 boost::system::errc::result_out_of_range); 334 } 335 336 // Return 0 if the response code is valid 337 return boost::system::errc::make_error_code(boost::system::errc::success); 338 } 339 340 } // namespace redfish 341