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 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
scheduleNextHeartbeatEvent()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
heartbeatParametersChanged()139 void Subscription::heartbeatParametersChanged()
140 {
141 hbTimer.cancel();
142
143 if (userSub->sendHeartbeat)
144 {
145 scheduleNextHeartbeatEvent();
146 }
147 }
148
onHbTimeout(const std::weak_ptr<Subscription> & weakSelf,const boost::system::error_code & ec)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
sendEventToSubscriber(uint64_t eventId,std::string && msg)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
filterAndSendEventLogs(uint64_t eventId,const std::vector<EventLogObjectsType> & eventRecords)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
filterAndSendReports(uint64_t eventId,const std::string & reportId,const telemetry::TimestampReadings & var)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
updateRetryConfig(uint32_t retryAttempts,uint32_t retryTimeoutInterval)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
matchSseId(const crow::sse_socket::Connection & thisConn)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
retryRespHandler(unsigned int respCode)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