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
Subscription(std::shared_ptr<persistent_data::UserSubscription> userSubIn,const boost::urls::url_view_base & url,boost::asio::io_context & ioc)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
Subscription(crow::sse_socket::Connection & connIn)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
resHandler(const std::shared_ptr<Subscription> &,const crow::Response & res)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
sendHeartbeatEvent()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
scheduleNextHeartbeatEvent()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
heartbeatParametersChanged()140 void Subscription::heartbeatParametersChanged()
141 {
142 hbTimer.cancel();
143
144 if (userSub->sendHeartbeat)
145 {
146 scheduleNextHeartbeatEvent();
147 }
148 }
149
onHbTimeout(const std::weak_ptr<Subscription> & weakSelf,const boost::system::error_code & ec)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
sendEventToSubscriber(std::string && msg)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
sendTestEventLog()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
filterAndSendEventLogs(const std::vector<EventLogObjectsType> & eventRecords)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
filterAndSendReports(const std::string & reportId,const telemetry::TimestampReadings & var)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
updateRetryConfig(uint32_t retryAttempts,uint32_t retryTimeoutInterval)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
getEventSeqNum() const344 uint64_t Subscription::getEventSeqNum() const
345 {
346 return eventSeqNum;
347 }
348
matchSseId(const crow::sse_socket::Connection & thisConn)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
retryRespHandler(unsigned int respCode)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