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
Subscription(std::shared_ptr<persistent_data::UserSubscription> userSubIn,const boost::urls::url_view_base & url,boost::asio::io_context & ioc)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
Subscription(crow::sse_socket::Connection & connIn)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
resHandler(const crow::Response & res)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
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 boost::beast::http::fields httpHeadersCopy(userSub->httpHeaders);
196 httpHeadersCopy.set(boost::beast::http::field::content_type,
197 "application/json");
198 client->sendDataWithCallback(
199 std::move(msg), userSub->destinationUrl,
200 static_cast<ensuressl::VerifyCertificate>(
201 userSub->verifyCertificate),
202 httpHeadersCopy, boost::beast::http::verb::post,
203 std::bind_front(&Subscription::resHandler, this));
204 return true;
205 }
206
207 if (sseConn != nullptr)
208 {
209 eventSeqNum++;
210 sseConn->sendSseEvent(std::to_string(eventSeqNum), msg);
211 }
212 return true;
213 }
214
sendTestEventLog(TestEvent & testEvent)215 bool Subscription::sendTestEventLog(TestEvent& testEvent)
216 {
217 nlohmann::json::array_t logEntryArray;
218 nlohmann::json& logEntryJson = logEntryArray.emplace_back();
219
220 if (testEvent.eventGroupId)
221 {
222 logEntryJson["EventGroupId"] = *testEvent.eventGroupId;
223 }
224
225 if (testEvent.eventId)
226 {
227 logEntryJson["EventId"] = *testEvent.eventId;
228 }
229
230 if (testEvent.eventTimestamp)
231 {
232 logEntryJson["EventTimestamp"] = *testEvent.eventTimestamp;
233 }
234
235 if (testEvent.originOfCondition)
236 {
237 logEntryJson["OriginOfCondition"]["@odata.id"] =
238 *testEvent.originOfCondition;
239 }
240 if (testEvent.severity)
241 {
242 logEntryJson["Severity"] = *testEvent.severity;
243 }
244
245 if (testEvent.message)
246 {
247 logEntryJson["Message"] = *testEvent.message;
248 }
249
250 if (testEvent.resolution)
251 {
252 logEntryJson["Resolution"] = *testEvent.resolution;
253 }
254
255 if (testEvent.messageId)
256 {
257 logEntryJson["MessageId"] = *testEvent.messageId;
258 }
259
260 if (testEvent.messageArgs)
261 {
262 logEntryJson["MessageArgs"] = *testEvent.messageArgs;
263 }
264 // MemberId is 0 : since we are sending one event record.
265 logEntryJson["MemberId"] = "0";
266
267 nlohmann::json msg;
268 msg["@odata.type"] = "#Event.v1_4_0.Event";
269 msg["Id"] = std::to_string(eventSeqNum);
270 msg["Name"] = "Event Log";
271 msg["Events"] = logEntryArray;
272
273 std::string strMsg =
274 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
275 return sendEventToSubscriber(std::move(strMsg));
276 }
277
filterAndSendEventLogs(const std::vector<EventLogObjectsType> & eventRecords)278 void Subscription::filterAndSendEventLogs(
279 const std::vector<EventLogObjectsType>& eventRecords)
280 {
281 nlohmann::json::array_t logEntryArray;
282 for (const EventLogObjectsType& logEntry : eventRecords)
283 {
284 std::vector<std::string_view> messageArgsView(
285 logEntry.messageArgs.begin(), logEntry.messageArgs.end());
286
287 nlohmann::json::object_t bmcLogEntry;
288 if (event_log::formatEventLogEntry(
289 logEntry.id, logEntry.messageId, messageArgsView,
290 logEntry.timestamp, userSub->customText, bmcLogEntry) != 0)
291 {
292 BMCWEB_LOG_DEBUG("Read eventLog entry failed");
293 continue;
294 }
295
296 if (!eventMatchesFilter(*userSub, bmcLogEntry, ""))
297 {
298 BMCWEB_LOG_DEBUG("Event {} did not match the filter",
299 nlohmann::json(bmcLogEntry).dump());
300 continue;
301 }
302
303 if (filter)
304 {
305 if (!memberMatches(bmcLogEntry, *filter))
306 {
307 BMCWEB_LOG_DEBUG("Filter didn't match");
308 continue;
309 }
310 }
311
312 logEntryArray.emplace_back(std::move(bmcLogEntry));
313 }
314
315 if (logEntryArray.empty())
316 {
317 BMCWEB_LOG_DEBUG("No log entries available to be transferred.");
318 return;
319 }
320
321 nlohmann::json msg;
322 msg["@odata.type"] = "#Event.v1_4_0.Event";
323 msg["Id"] = std::to_string(eventSeqNum);
324 msg["Name"] = "Event Log";
325 msg["Events"] = std::move(logEntryArray);
326 std::string strMsg =
327 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
328 sendEventToSubscriber(std::move(strMsg));
329 eventSeqNum++;
330 }
331
filterAndSendReports(const std::string & reportId,const telemetry::TimestampReadings & var)332 void Subscription::filterAndSendReports(const std::string& reportId,
333 const telemetry::TimestampReadings& var)
334 {
335 boost::urls::url mrdUri = boost::urls::format(
336 "/redfish/v1/TelemetryService/MetricReportDefinitions/{}", reportId);
337
338 // Empty list means no filter. Send everything.
339 if (!userSub->metricReportDefinitions.empty())
340 {
341 if (std::ranges::find(userSub->metricReportDefinitions,
342 mrdUri.buffer()) ==
343 userSub->metricReportDefinitions.end())
344 {
345 return;
346 }
347 }
348
349 nlohmann::json msg;
350 if (!telemetry::fillReport(msg, reportId, var))
351 {
352 BMCWEB_LOG_ERROR("Failed to fill the MetricReport for DBus "
353 "Report with id {}",
354 reportId);
355 return;
356 }
357
358 // Context is set by user during Event subscription and it must be
359 // set for MetricReport response.
360 if (!userSub->customText.empty())
361 {
362 msg["Context"] = userSub->customText;
363 }
364
365 std::string strMsg =
366 msg.dump(2, ' ', true, nlohmann::json::error_handler_t::replace);
367 sendEventToSubscriber(std::move(strMsg));
368 }
369
updateRetryConfig(uint32_t retryAttempts,uint32_t retryTimeoutInterval)370 void Subscription::updateRetryConfig(uint32_t retryAttempts,
371 uint32_t retryTimeoutInterval)
372 {
373 if (policy == nullptr)
374 {
375 BMCWEB_LOG_DEBUG("Retry policy was nullptr, ignoring set");
376 return;
377 }
378 policy->maxRetryAttempts = retryAttempts;
379 policy->retryIntervalSecs = std::chrono::seconds(retryTimeoutInterval);
380 }
381
getEventSeqNum() const382 uint64_t Subscription::getEventSeqNum() const
383 {
384 return eventSeqNum;
385 }
386
matchSseId(const crow::sse_socket::Connection & thisConn)387 bool Subscription::matchSseId(const crow::sse_socket::Connection& thisConn)
388 {
389 return &thisConn == sseConn;
390 }
391
392 // Check used to indicate what response codes are valid as part of our retry
393 // policy. 2XX is considered acceptable
retryRespHandler(unsigned int respCode)394 boost::system::error_code Subscription::retryRespHandler(unsigned int respCode)
395 {
396 BMCWEB_LOG_DEBUG("Checking response code validity for SubscriptionEvent");
397 if ((respCode < 200) || (respCode >= 300))
398 {
399 return boost::system::errc::make_error_code(
400 boost::system::errc::result_out_of_range);
401 }
402
403 // Return 0 if the response code is valid
404 return boost::system::errc::make_error_code(boost::system::errc::success);
405 }
406
407 } // namespace redfish
408