1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4
5 #include "aggregation_utils.hpp"
6 #include "async_resp.hpp"
7 #include "dbus_utility.hpp"
8 #include "error_messages.hpp"
9 #include "http_client.hpp"
10 #include "http_request.hpp"
11 #include "http_response.hpp"
12 #include "io_context_singleton.hpp"
13 #include "logging.hpp"
14 #include "parsing.hpp"
15 #include "ssl_key_handler.hpp"
16 #include "utility.hpp"
17
18 #include <boost/beast/http/field.hpp>
19 #include <boost/beast/http/status.hpp>
20 #include <boost/beast/http/verb.hpp>
21 #include <boost/system/errc.hpp>
22 #include <boost/system/result.hpp>
23 #include <boost/url/param.hpp>
24 #include <boost/url/parse.hpp>
25 #include <boost/url/segments_ref.hpp>
26 #include <boost/url/segments_view.hpp>
27 #include <boost/url/url.hpp>
28 #include <boost/url/url_view.hpp>
29 #include <nlohmann/json.hpp>
30 #include <sdbusplus/message/native_types.hpp>
31
32 #include <algorithm>
33 #include <array>
34 #include <chrono>
35 #include <cstddef>
36 #include <cstdint>
37 #include <functional>
38 #include <limits>
39 #include <memory>
40 #include <ranges>
41 #include <string>
42 #include <string_view>
43 #include <system_error>
44 #include <unordered_map>
45 #include <utility>
46 #include <variant>
47
48 namespace redfish
49 {
50
51 constexpr unsigned int aggregatorReadBodyLimit = 50 * 1024 * 1024; // 50MB
52
53 enum class Result
54 {
55 LocalHandle,
56 NoLocalHandle
57 };
58
59 enum class SearchType
60 {
61 Collection,
62 CollOrCon,
63 ContainsSubordinate,
64 Resource
65 };
66
67 // clang-format off
68 // These are all of the properties as of version 2022.2 of the Redfish Resource
69 // and Schema Guide whose Type is "string (URI)" and the name does not end in a
70 // case-insensitive form of "uri". That version of the schema is associated
71 // with version 1.16.0 of the Redfish Specification. Going forward, new URI
72 // properties should end in URI so this list should not need to be maintained as
73 // the spec is updated. NOTE: These have been pre-sorted in order to be
74 // compatible with binary search
75 constexpr std::array nonUriProperties{
76 "@Redfish.ActionInfo",
77 // "@odata.context", // We can't fix /redfish/v1/$metadata URIs
78 "@odata.id",
79 // "Destination", // Only used by EventService and won't be a Redfish URI
80 // "HostName", // Isn't actually a Redfish URI
81 "Image",
82 "MetricProperty",
83 // "OriginOfCondition", // Is URI when in request, but is object in response
84 "TaskMonitor",
85 "target", // normal string, but target URI for POST to invoke an action
86 };
87 // clang-format on
88
89 // Search the top collection array to determine if the passed URI is of a
90 // desired type
searchCollectionsArray(std::string_view uri,const SearchType searchType)91 inline bool searchCollectionsArray(std::string_view uri,
92 const SearchType searchType)
93 {
94 boost::system::result<boost::urls::url> parsedUrl =
95 boost::urls::parse_relative_ref(uri);
96 if (!parsedUrl)
97 {
98 BMCWEB_LOG_ERROR("Failed to get target URI from {}", uri);
99 return false;
100 }
101
102 parsedUrl->normalize();
103 boost::urls::segments_ref segments = parsedUrl->segments();
104 if (!segments.is_absolute())
105 {
106 return false;
107 }
108
109 // The passed URI must begin with "/redfish/v1", but we have to strip it
110 // from the URI since topCollections does not include it in its URIs.
111 if (segments.size() < 2)
112 {
113 return false;
114 }
115 if (segments.front() != "redfish")
116 {
117 return false;
118 }
119 segments.erase(segments.begin());
120 if (segments.front() != "v1")
121 {
122 return false;
123 }
124 segments.erase(segments.begin());
125
126 // Exclude the trailing "/" if it exists such as in "/redfish/v1/".
127 if (!segments.empty() && segments.back().empty())
128 {
129 segments.pop_back();
130 }
131
132 // If no segments then the passed URI was either "/redfish/v1" or
133 // "/redfish/v1/".
134 if (segments.empty())
135 {
136 return (searchType == SearchType::ContainsSubordinate) ||
137 (searchType == SearchType::CollOrCon);
138 }
139 std::string_view url = segments.buffer();
140 const auto* it = std::ranges::lower_bound(topCollections, url);
141 if (it == topCollections.end())
142 {
143 // parsedUrl is alphabetically after the last entry in the array so it
144 // can't be a top collection or up tree from a top collection
145 return false;
146 }
147
148 boost::urls::url collectionUrl(*it);
149 boost::urls::segments_view collectionSegments = collectionUrl.segments();
150 boost::urls::segments_view::iterator itCollection =
151 collectionSegments.begin();
152 const boost::urls::segments_view::const_iterator endCollection =
153 collectionSegments.end();
154
155 // Each segment in the passed URI should match the found collection
156 for (const auto& segment : segments)
157 {
158 if (itCollection == endCollection)
159 {
160 // Leftover segments means the target is for an aggregation
161 // supported resource
162 return searchType == SearchType::Resource;
163 }
164
165 if (segment != (*itCollection))
166 {
167 return false;
168 }
169 itCollection++;
170 }
171
172 // No remaining segments means the passed URI was a top level collection
173 if (searchType == SearchType::Collection)
174 {
175 return itCollection == endCollection;
176 }
177 if (searchType == SearchType::ContainsSubordinate)
178 {
179 return itCollection != endCollection;
180 }
181
182 // Return this check instead of "true" in case other SearchTypes get added
183 return searchType == SearchType::CollOrCon;
184 }
185
186 // Determines if the passed property contains a URI. Those property names
187 // either end with a case-insensitive version of "uri" or are specifically
188 // defined in the above array.
isPropertyUri(std::string_view propertyName)189 inline bool isPropertyUri(std::string_view propertyName)
190 {
191 if (propertyName.ends_with("uri") || propertyName.ends_with("Uri") ||
192 propertyName.ends_with("URI"))
193 {
194 return true;
195 }
196 return std::binary_search(nonUriProperties.begin(), nonUriProperties.end(),
197 propertyName);
198 }
199
addPrefixToStringItem(std::string & strValue,std::string_view prefix)200 inline void addPrefixToStringItem(std::string& strValue,
201 std::string_view prefix)
202 {
203 // Make sure the value is a properly formatted URI
204 auto parsed = boost::urls::parse_relative_ref(strValue);
205 if (!parsed)
206 {
207 // Note that DMTF URIs such as
208 // https://redfish.dmtf.org/registries/Base.1.15.0.json will fail this
209 // check and that's okay
210 BMCWEB_LOG_DEBUG("Couldn't parse URI from resource {}", strValue);
211 return;
212 }
213
214 const boost::urls::url_view& thisUrl = *parsed;
215
216 // We don't need to aggregate JsonSchemas due to potential issues such as
217 // version mismatches between aggregator and satellite BMCs. For now
218 // assume that the aggregator has all the schemas and versions that the
219 // aggregated server has.
220 if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas",
221 crow::utility::OrMorePaths()))
222 {
223 BMCWEB_LOG_DEBUG("Skipping JsonSchemas URI prefix fixing");
224 return;
225 }
226
227 // The first two segments should be "/redfish/v1". We need to check that
228 // before we can search topCollections
229 if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1",
230 crow::utility::OrMorePaths()))
231 {
232 return;
233 }
234
235 // Check array adding a segment each time until collection is identified
236 // Add prefix to segment after the collection
237 const boost::urls::segments_view urlSegments = thisUrl.segments();
238 bool addedPrefix = false;
239 boost::urls::url url("/");
240 boost::urls::segments_view::const_iterator it = urlSegments.begin();
241 const boost::urls::segments_view::const_iterator end = urlSegments.end();
242
243 // Skip past the leading "/redfish/v1"
244 it++;
245 it++;
246 for (; it != end; it++)
247 {
248 // Trailing "/" will result in an empty segment. In that case we need
249 // to return so we don't apply a prefix to top level collections such
250 // as "/redfish/v1/Chassis/"
251 if ((*it).empty())
252 {
253 return;
254 }
255
256 if (std::binary_search(topCollections.begin(), topCollections.end(),
257 url.buffer()))
258 {
259 std::string collectionItem(prefix);
260 collectionItem += "_" + (*it);
261 url.segments().push_back(collectionItem);
262 it++;
263 addedPrefix = true;
264 break;
265 }
266
267 url.segments().push_back(*it);
268 }
269
270 // Finish constructing the URL here (if needed) to avoid additional checks
271 for (; it != end; it++)
272 {
273 url.segments().push_back(*it);
274 }
275
276 if (addedPrefix)
277 {
278 url.segments().insert(url.segments().begin(), {"redfish", "v1"});
279 strValue = url.buffer();
280 }
281 }
282
addPrefixToItem(nlohmann::json & item,std::string_view prefix)283 inline void addPrefixToItem(nlohmann::json& item, std::string_view prefix)
284 {
285 std::string* strValue = item.get_ptr<std::string*>();
286 if (strValue == nullptr)
287 {
288 // Values for properties like "InvalidURI" and "ResourceMissingAtURI"
289 // from within the Base Registry are objects instead of strings and will
290 // fall into this check
291 BMCWEB_LOG_DEBUG("Field was not a string");
292 return;
293 }
294 addPrefixToStringItem(*strValue, prefix);
295 item = *strValue;
296 }
297
addAggregatedHeaders(crow::Response & asyncResp,const crow::Response & resp,std::string_view prefix)298 inline void addAggregatedHeaders(crow::Response& asyncResp,
299 const crow::Response& resp,
300 std::string_view prefix)
301 {
302 if (!resp.getHeaderValue("Content-Type").empty())
303 {
304 asyncResp.addHeader(boost::beast::http::field::content_type,
305 resp.getHeaderValue("Content-Type"));
306 }
307 if (!resp.getHeaderValue("Allow").empty())
308 {
309 asyncResp.addHeader(boost::beast::http::field::allow,
310 resp.getHeaderValue("Allow"));
311 }
312 std::string_view header = resp.getHeaderValue("Location");
313 if (!header.empty())
314 {
315 std::string location(header);
316 addPrefixToStringItem(location, prefix);
317 asyncResp.addHeader(boost::beast::http::field::location, location);
318 }
319 if (!resp.getHeaderValue("Retry-After").empty())
320 {
321 asyncResp.addHeader(boost::beast::http::field::retry_after,
322 resp.getHeaderValue("Retry-After"));
323 }
324 // TODO: we need special handling for Link Header Value
325 }
326
327 // Fix HTTP headers which appear in responses from Task resources among others
addPrefixToHeadersInResp(nlohmann::json & json,std::string_view prefix)328 inline void addPrefixToHeadersInResp(nlohmann::json& json,
329 std::string_view prefix)
330 {
331 // The passed in "HttpHeaders" should be an array of headers
332 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
333 if (array == nullptr)
334 {
335 BMCWEB_LOG_ERROR("Field wasn't an array_t????");
336 return;
337 }
338
339 for (nlohmann::json& item : *array)
340 {
341 // Each header is a single string with the form "<Field>: <Value>"
342 std::string* strHeader = item.get_ptr<std::string*>();
343 if (strHeader == nullptr)
344 {
345 BMCWEB_LOG_CRITICAL("Field wasn't a string????");
346 continue;
347 }
348
349 constexpr std::string_view location = "Location: ";
350 if (strHeader->starts_with(location))
351 {
352 std::string header = strHeader->substr(location.size());
353 addPrefixToStringItem(header, prefix);
354 *strHeader = std::string(location) + header;
355 }
356 }
357 }
358
359 // Search the json for all URIs and add the supplied prefix if the URI is for
360 // an aggregated resource.
addPrefixes(nlohmann::json & json,std::string_view prefix)361 inline void addPrefixes(nlohmann::json& json, std::string_view prefix)
362 {
363 nlohmann::json::object_t* object =
364 json.get_ptr<nlohmann::json::object_t*>();
365 if (object != nullptr)
366 {
367 for (std::pair<const std::string, nlohmann::json>& item : *object)
368 {
369 if (isPropertyUri(item.first))
370 {
371 addPrefixToItem(item.second, prefix);
372 continue;
373 }
374
375 // "HttpHeaders" contains HTTP headers. Among those we need to
376 // attempt to fix the "Location" header
377 if (item.first == "HttpHeaders")
378 {
379 addPrefixToHeadersInResp(item.second, prefix);
380 continue;
381 }
382
383 // Recursively parse the rest of the json
384 addPrefixes(item.second, prefix);
385 }
386 return;
387 }
388 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>();
389 if (array != nullptr)
390 {
391 for (nlohmann::json& item : *array)
392 {
393 addPrefixes(item, prefix);
394 }
395 }
396 }
397
aggregationRetryHandler(unsigned int respCode)398 inline boost::system::error_code aggregationRetryHandler(unsigned int respCode)
399 {
400 // Allow all response codes because we want to surface any satellite
401 // issue to the client
402 BMCWEB_LOG_DEBUG("Received {} response from satellite", respCode);
403 return boost::system::errc::make_error_code(boost::system::errc::success);
404 }
405
getAggregationPolicy()406 inline crow::ConnectionPolicy getAggregationPolicy()
407 {
408 return {.maxRetryAttempts = 0,
409 .requestByteLimit = aggregatorReadBodyLimit,
410 .maxConnections = 20,
411 .retryPolicyAction = "TerminateAfterRetries",
412 .retryIntervalSecs = std::chrono::seconds(0),
413 .invalidResp = aggregationRetryHandler};
414 }
415
416 struct AggregationSource
417 {
418 boost::urls::url url;
419 std::string username;
420 std::string password;
421 };
422
423 class RedfishAggregator
424 {
425 private:
426 crow::HttpClient client;
427
428 // Dummy callback used by the Constructor so that it can report the number
429 // of satellite configs when the class is first created
constructorCallback(const boost::system::error_code & ec,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)430 static void constructorCallback(
431 const boost::system::error_code& ec,
432 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
433 {
434 if (ec)
435 {
436 BMCWEB_LOG_ERROR("Something went wrong while querying dbus!");
437 return;
438 }
439
440 BMCWEB_LOG_DEBUG("There were {} satellite configs found at startup",
441 std::to_string(satelliteInfo.size()));
442 }
443
444 // Search D-Bus objects for satellite config objects and add their
445 // information if valid
findSatelliteConfigs(const dbus::utility::ManagedObjectType & objects,std::unordered_map<std::string,boost::urls::url> & satelliteInfo)446 static void findSatelliteConfigs(
447 const dbus::utility::ManagedObjectType& objects,
448 std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
449 {
450 for (const auto& objectPath : objects)
451 {
452 for (const auto& interface : objectPath.second)
453 {
454 if (interface.first ==
455 "xyz.openbmc_project.Configuration.SatelliteController")
456 {
457 BMCWEB_LOG_DEBUG("Found Satellite Controller at {}",
458 objectPath.first.str);
459
460 if (!satelliteInfo.empty())
461 {
462 BMCWEB_LOG_ERROR(
463 "Redfish Aggregation only supports one satellite!");
464 BMCWEB_LOG_DEBUG("Clearing all satellite data");
465 satelliteInfo.clear();
466 return;
467 }
468
469 addSatelliteConfig(interface.second, satelliteInfo);
470 }
471 }
472 }
473 }
474
475 // Parse the properties of a satellite config object and add the
476 // configuration if the properties are valid
addSatelliteConfig(const dbus::utility::DBusPropertiesMap & properties,std::unordered_map<std::string,boost::urls::url> & satelliteInfo)477 static void addSatelliteConfig(
478 const dbus::utility::DBusPropertiesMap& properties,
479 std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
480 {
481 boost::urls::url url;
482 std::string prefix;
483
484 for (const auto& prop : properties)
485 {
486 if (prop.first == "Hostname")
487 {
488 const std::string* propVal =
489 std::get_if<std::string>(&prop.second);
490 if (propVal == nullptr)
491 {
492 BMCWEB_LOG_ERROR("Invalid Hostname value");
493 return;
494 }
495 url.set_host(*propVal);
496 }
497
498 else if (prop.first == "Port")
499 {
500 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second);
501 if (propVal == nullptr)
502 {
503 BMCWEB_LOG_ERROR("Invalid Port value");
504 return;
505 }
506
507 if (*propVal > std::numeric_limits<uint16_t>::max())
508 {
509 BMCWEB_LOG_ERROR("Port value out of range");
510 return;
511 }
512 url.set_port(std::to_string(static_cast<uint16_t>(*propVal)));
513 }
514
515 else if (prop.first == "AuthType")
516 {
517 const std::string* propVal =
518 std::get_if<std::string>(&prop.second);
519 if (propVal == nullptr)
520 {
521 BMCWEB_LOG_ERROR("Invalid AuthType value");
522 return;
523 }
524
525 // For now assume authentication not required to communicate
526 // with the satellite BMC
527 if (*propVal != "None")
528 {
529 BMCWEB_LOG_ERROR(
530 "Unsupported AuthType value: {}, only \"none\" is supported",
531 *propVal);
532 return;
533 }
534 url.set_scheme("http");
535 }
536 else if (prop.first == "Name")
537 {
538 const std::string* propVal =
539 std::get_if<std::string>(&prop.second);
540 if (propVal != nullptr && !propVal->empty())
541 {
542 prefix = *propVal;
543 BMCWEB_LOG_DEBUG("Using Name property {} as prefix",
544 prefix);
545 }
546 else
547 {
548 BMCWEB_LOG_DEBUG(
549 "Invalid or empty Name property, invalid satellite config");
550 return;
551 }
552 }
553 } // Finished reading properties
554
555 // Make sure all required config information was made available
556 if (url.host().empty())
557 {
558 BMCWEB_LOG_ERROR("Satellite config {} missing Host", prefix);
559 return;
560 }
561
562 if (!url.has_port())
563 {
564 BMCWEB_LOG_ERROR("Satellite config {} missing Port", prefix);
565 return;
566 }
567
568 if (!url.has_scheme())
569 {
570 BMCWEB_LOG_ERROR("Satellite config {} missing AuthType", prefix);
571 return;
572 }
573
574 std::string resultString;
575 auto result = satelliteInfo.insert_or_assign(prefix, std::move(url));
576 if (result.second)
577 {
578 resultString = "Added new satellite config ";
579 }
580 else
581 {
582 resultString = "Updated existing satellite config ";
583 }
584
585 BMCWEB_LOG_DEBUG("{}{} at {}://{}", resultString, prefix,
586 result.first->second.scheme(),
587 result.first->second.encoded_host_and_port());
588 }
589
590 enum AggregationType
591 {
592 Collection,
593 ContainsSubordinate,
594 Resource,
595 };
596
startAggregation(AggregationType aggType,const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp) const597 void startAggregation(
598 AggregationType aggType, const crow::Request& thisReq,
599 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) const
600 {
601 if (thisReq.method() != boost::beast::http::verb::get)
602 {
603 if (aggType == AggregationType::Collection)
604 {
605 BMCWEB_LOG_DEBUG(
606 "Only aggregate GET requests to top level collections");
607 return;
608 }
609
610 if (aggType == AggregationType::ContainsSubordinate)
611 {
612 BMCWEB_LOG_DEBUG(
613 "Only aggregate GET requests when uptree of a top level collection");
614 return;
615 }
616 }
617
618 // Create a copy of thisReq so we we can still locally process the req
619 std::error_code ec;
620 auto localReq = std::make_shared<crow::Request>(thisReq.copy());
621 if (ec)
622 {
623 BMCWEB_LOG_ERROR("Failed to create copy of request");
624 if (aggType == AggregationType::Resource)
625 {
626 messages::internalError(asyncResp->res);
627 }
628 return;
629 }
630
631 if (aggType == AggregationType::Collection)
632 {
633 boost::urls::url& urlNew = localReq->url();
634 auto paramsIt = urlNew.params().begin();
635 while (paramsIt != urlNew.params().end())
636 {
637 const boost::urls::param& param = *paramsIt;
638 // only and $skip, params can't be passed to satellite
639 // as applying these filters twice results in different results.
640 // Removing them will cause them to only be processed in the
641 // aggregator. Note, this still doesn't work for collections
642 // that might return less than the complete collection by
643 // default, but hopefully those are rare/nonexistent in top
644 // collections. bmcweb doesn't implement any of these.
645 if (param.key == "only" || param.key == "$skip")
646 {
647 BMCWEB_LOG_DEBUG(
648 "Erasing \"{}\" param from request to top level collection",
649 param.key);
650
651 paramsIt = urlNew.params().erase(paramsIt);
652 continue;
653 }
654 // Pass all other parameters
655 paramsIt++;
656 }
657 localReq->target(urlNew.buffer());
658 }
659
660 getSatelliteConfigs(
661 std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp));
662 }
663
findSatellite(const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo,std::string_view memberName)664 static void findSatellite(
665 const crow::Request& req,
666 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
667 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
668 std::string_view memberName)
669 {
670 // Determine if the resource ID begins with a known prefix
671 for (const auto& satellite : satelliteInfo)
672 {
673 std::string targetPrefix = satellite.first;
674 targetPrefix += "_";
675 if (memberName.starts_with(targetPrefix))
676 {
677 BMCWEB_LOG_DEBUG("\"{}\" is a known prefix", satellite.first);
678
679 // Remove the known prefix from the request's URI and
680 // then forward to the associated satellite BMC
681 getInstance().forwardRequest(req, asyncResp, satellite.first,
682 satelliteInfo);
683 return;
684 }
685 }
686
687 // We didn't recognize the prefix and need to return a 404
688 std::string nameStr = req.url().segments().back();
689 messages::resourceNotFound(asyncResp->res, "", nameStr);
690 }
691
692 // Intended to handle an incoming request based on if Redfish Aggregation
693 // is enabled. Forwards request to satellite BMC if it exists.
aggregateAndHandle(AggregationType aggType,const std::shared_ptr<crow::Request> & sharedReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const boost::system::error_code & ec,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)694 static void aggregateAndHandle(
695 AggregationType aggType,
696 const std::shared_ptr<crow::Request>& sharedReq,
697 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
698 const boost::system::error_code& ec,
699 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
700 {
701 if (sharedReq == nullptr)
702 {
703 return;
704 }
705 // Something went wrong while querying dbus
706 if (ec)
707 {
708 messages::internalError(asyncResp->res);
709 return;
710 }
711
712 // No satellite configs means we don't need to keep attempting to
713 // aggregate
714 if (satelliteInfo.empty())
715 {
716 // For collections or resources that can contain a subordinate
717 // top level collection we'll also handle the request locally so we
718 // don't need to write an error code
719 if (aggType == AggregationType::Resource)
720 {
721 std::string nameStr = sharedReq->url().segments().back();
722 messages::resourceNotFound(asyncResp->res, "", nameStr);
723 }
724 return;
725 }
726
727 const crow::Request& thisReq = *sharedReq;
728 BMCWEB_LOG_DEBUG("Aggregation is enabled, begin processing of {}",
729 thisReq.target());
730
731 // We previously determined the request is for a collection. No need to
732 // check again
733 if (aggType == AggregationType::Collection)
734 {
735 BMCWEB_LOG_DEBUG("Aggregating a collection");
736 // We need to use a specific response handler and send the
737 // request to all known satellites
738 getInstance().forwardCollectionRequests(thisReq, asyncResp,
739 satelliteInfo);
740 return;
741 }
742
743 // We previously determined the request may contain a subordinate
744 // collection. No need to check again
745 if (aggType == AggregationType::ContainsSubordinate)
746 {
747 BMCWEB_LOG_DEBUG(
748 "Aggregating what may have a subordinate collection");
749 // We need to use a specific response handler and send the
750 // request to all known satellites
751 getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp,
752 satelliteInfo);
753 return;
754 }
755
756 const boost::urls::segments_view urlSegments = thisReq.url().segments();
757 boost::urls::url currentUrl("/");
758 boost::urls::segments_view::const_iterator it = urlSegments.begin();
759 boost::urls::segments_view::const_iterator end = urlSegments.end();
760
761 // Skip past the leading "/redfish/v1"
762 it++;
763 it++;
764 for (; it != end; it++)
765 {
766 if (std::binary_search(topCollections.begin(), topCollections.end(),
767 currentUrl.buffer()))
768 {
769 // We've matched a resource collection so this current segment
770 // must contain an aggregation prefix
771 findSatellite(thisReq, asyncResp, satelliteInfo, *it);
772 return;
773 }
774
775 currentUrl.segments().push_back(*it);
776 }
777
778 // We shouldn't reach this point since we should've hit one of the
779 // previous exits
780 messages::internalError(asyncResp->res);
781 }
782
783 // Attempt to forward a request to the satellite BMC associated with the
784 // prefix.
forwardRequest(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & prefix,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)785 void forwardRequest(
786 const crow::Request& thisReq,
787 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
788 const std::string& prefix,
789 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
790 {
791 const auto& sat = satelliteInfo.find(prefix);
792 if (sat == satelliteInfo.end())
793 {
794 // Realistically this shouldn't get called since we perform an
795 // earlier check to make sure the prefix exists
796 BMCWEB_LOG_ERROR("Unrecognized satellite prefix \"{}\"", prefix);
797 return;
798 }
799
800 // We need to strip the prefix from the request's path
801 boost::urls::url targetURI(thisReq.target());
802 std::string path = thisReq.url().path();
803 size_t pos = path.find(prefix + "_");
804 if (pos == std::string::npos)
805 {
806 // If this fails then something went wrong
807 BMCWEB_LOG_ERROR("Error removing prefix \"{}_\" from request URI",
808 prefix);
809 messages::internalError(asyncResp->res);
810 return;
811 }
812 path.erase(pos, prefix.size() + 1);
813
814 std::function<void(crow::Response&)> cb =
815 std::bind_front(processResponse, prefix, asyncResp);
816
817 std::string data = thisReq.body();
818 boost::urls::url url(sat->second);
819 url.set_path(path);
820 if (targetURI.has_query())
821 {
822 url.set_query(targetURI.query());
823 }
824
825 // Prepare request headers
826 boost::beast::http::fields requestFields =
827 prepareAggregationHeaders(thisReq.fields(), prefix);
828
829 client.sendDataWithCallback(std::move(data), url,
830 ensuressl::VerifyCertificate::Verify,
831 requestFields, thisReq.method(), cb);
832 }
833
834 // Forward a request for a collection URI to each known satellite BMC
forwardCollectionRequests(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)835 void forwardCollectionRequests(
836 const crow::Request& thisReq,
837 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
838 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
839 {
840 for (const auto& sat : satelliteInfo)
841 {
842 std::function<void(crow::Response&)> cb = std::bind_front(
843 processCollectionResponse, sat.first, asyncResp);
844
845 boost::urls::url url(sat.second);
846 url.set_path(thisReq.url().path());
847 if (thisReq.url().has_query())
848 {
849 url.set_query(thisReq.url().query());
850 }
851 std::string data = thisReq.body();
852
853 // Prepare request headers
854 boost::beast::http::fields requestFields =
855 prepareAggregationHeaders(thisReq.fields(), sat.first);
856
857 client.sendDataWithCallback(std::move(data), url,
858 ensuressl::VerifyCertificate::Verify,
859 requestFields, thisReq.method(), cb);
860 }
861 }
862
863 // Forward request for a URI that is uptree of a top level collection to
864 // each known satellite BMC
forwardContainsSubordinateRequests(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)865 void forwardContainsSubordinateRequests(
866 const crow::Request& thisReq,
867 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
868 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
869 {
870 for (const auto& sat : satelliteInfo)
871 {
872 std::function<void(crow::Response&)> cb = std::bind_front(
873 processContainsSubordinateResponse, sat.first, asyncResp);
874
875 // will ignore an expanded resource in the response if that resource
876 // is not already supported by the aggregating BMC
877 // TODO: Improve the processing so that we don't have to strip query
878 // params in this specific case
879 boost::urls::url url(sat.second);
880 url.set_path(thisReq.url().path());
881
882 std::string data = thisReq.body();
883
884 // Prepare request headers
885 boost::beast::http::fields requestFields =
886 prepareAggregationHeaders(thisReq.fields(), sat.first);
887
888 client.sendDataWithCallback(std::move(data), url,
889 ensuressl::VerifyCertificate::Verify,
890 requestFields, thisReq.method(), cb);
891 }
892 }
893
894 public:
RedfishAggregator()895 explicit RedfishAggregator() :
896 client(getIoContext(),
897 std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy()))
898 {
899 getSatelliteConfigs(constructorCallback);
900 }
901 RedfishAggregator(const RedfishAggregator&) = delete;
902 RedfishAggregator& operator=(const RedfishAggregator&) = delete;
903 RedfishAggregator(RedfishAggregator&&) = delete;
904 RedfishAggregator& operator=(RedfishAggregator&&) = delete;
905 ~RedfishAggregator() = default;
906
getInstance()907 static RedfishAggregator& getInstance()
908 {
909 static RedfishAggregator handler;
910 return handler;
911 }
912
913 // Aggregation sources with their URLs and optional credentials
914 std::unordered_map<std::string, AggregationSource> aggregationSources;
915
916 // Helper function to prepare headers for aggregated satellite BMC requests
prepareAggregationHeaders(const boost::beast::http::fields & originalFields,const std::string & prefix) const917 boost::beast::http::fields prepareAggregationHeaders(
918 const boost::beast::http::fields& originalFields,
919 const std::string& prefix) const
920 {
921 boost::beast::http::fields fields = originalFields;
922
923 // POST AggregationService can only parse JSON
924 fields.set(boost::beast::http::field::accept, "application/json");
925
926 // Add authentication if credentials exist for this prefix
927 auto it = aggregationSources.find(prefix);
928 if (it != aggregationSources.end())
929 {
930 const auto& source = it->second;
931 // Only add auth header if both username and password are provided
932 if (!source.username.empty() && !source.password.empty())
933 {
934 std::string authHeader = crow::utility::createBasicAuthHeader(
935 source.username, source.password);
936 fields.set(boost::beast::http::field::authorization,
937 authHeader);
938 }
939 }
940 return fields;
941 }
942
943 // Polls D-Bus to get all available satellite config information
944 // Expects a handler which interacts with the returned configs
getSatelliteConfigs(std::function<void (const boost::system::error_code &,const std::unordered_map<std::string,boost::urls::url> &)> handler) const945 void getSatelliteConfigs(
946 std::function<
947 void(const boost::system::error_code&,
948 const std::unordered_map<std::string, boost::urls::url>&)>
949 handler) const
950 {
951 BMCWEB_LOG_DEBUG("Gathering satellite configs");
952
953 // Extract just the URLs from aggregationSources for the handler
954 std::unordered_map<std::string, boost::urls::url> satelliteInfo;
955 for (const auto& [prefix, source] : aggregationSources)
956 {
957 satelliteInfo.emplace(prefix, source.url);
958 }
959
960 sdbusplus::message::object_path path("/xyz/openbmc_project/inventory");
961 dbus::utility::getManagedObjects(
962 "xyz.openbmc_project.EntityManager", path,
963 [handler{std::move(handler)},
964 satelliteInfo = std::move(satelliteInfo)](
965 const boost::system::error_code& ec,
966 const dbus::utility::ManagedObjectType& objects) mutable {
967 if (ec)
968 {
969 BMCWEB_LOG_ERROR("DBUS response error {}, {}", ec.value(),
970 ec.message());
971 handler(ec, satelliteInfo);
972 return;
973 }
974
975 // Maps a chosen alias representing a satellite BMC to a url
976 // containing the information required to create a http
977 // connection to the satellite
978 findSatelliteConfigs(objects, satelliteInfo);
979
980 if (!satelliteInfo.empty())
981 {
982 BMCWEB_LOG_DEBUG(
983 "Redfish Aggregation enabled with {} satellite BMCs",
984 std::to_string(satelliteInfo.size()));
985 }
986 else
987 {
988 BMCWEB_LOG_DEBUG(
989 "No satellite BMCs detected. Redfish Aggregation not enabled");
990 }
991 handler(ec, satelliteInfo);
992 });
993 }
994
995 // Processes the response returned by a satellite BMC and loads its
996 // contents into asyncResp
processResponse(std::string_view prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)997 static void processResponse(
998 std::string_view prefix,
999 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1000 crow::Response& resp)
1001 {
1002 // 429 and 502 mean we didn't actually send the request so don't
1003 // overwrite the response headers in that case
1004 if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1005 (resp.result() == boost::beast::http::status::bad_gateway))
1006 {
1007 asyncResp->res.result(resp.result());
1008 return;
1009 }
1010
1011 // We want to attempt prefix fixing regardless of response code
1012 // The resp will not have a json component
1013 // We need to create a json from resp's stringResponse
1014 if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1015 {
1016 nlohmann::json jsonVal =
1017 nlohmann::json::parse(*resp.body(), nullptr, false);
1018 if (jsonVal.is_discarded())
1019 {
1020 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1021 messages::operationFailed(asyncResp->res);
1022 return;
1023 }
1024
1025 BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1026
1027 addPrefixes(jsonVal, prefix);
1028
1029 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response");
1030
1031 asyncResp->res.result(resp.result());
1032 asyncResp->res.jsonValue = std::move(jsonVal);
1033
1034 BMCWEB_LOG_DEBUG("Finished writing asyncResp");
1035 }
1036 else
1037 {
1038 // We allow any Content-Type that is not "application/json" now
1039 asyncResp->res.result(resp.result());
1040 asyncResp->res.copyBody(resp);
1041 }
1042 addAggregatedHeaders(asyncResp->res, resp, prefix);
1043 }
1044
1045 // Processes the collection response returned by a satellite BMC and merges
1046 // its "@odata.id" values
processCollectionResponse(const std::string & prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)1047 static void processCollectionResponse(
1048 const std::string& prefix,
1049 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1050 crow::Response& resp)
1051 {
1052 // 429 and 502 mean we didn't actually send the request so don't
1053 // overwrite the response headers in that case
1054 if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1055 (resp.result() == boost::beast::http::status::bad_gateway))
1056 {
1057 return;
1058 }
1059
1060 if (resp.resultInt() != 200)
1061 {
1062 BMCWEB_LOG_DEBUG(
1063 "Collection resource does not exist in satellite BMC \"{}\"",
1064 prefix);
1065 // Return the error if we haven't had any successes
1066 if (asyncResp->res.resultInt() != 200)
1067 {
1068 asyncResp->res.result(resp.result());
1069 asyncResp->res.copyBody(resp);
1070 }
1071 return;
1072 }
1073
1074 // The resp will not have a json component
1075 // We need to create a json from resp's stringResponse
1076 if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1077 {
1078 nlohmann::json jsonVal =
1079 nlohmann::json::parse(*resp.body(), nullptr, false);
1080 if (jsonVal.is_discarded())
1081 {
1082 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1083
1084 // Notify the user if doing so won't overwrite a valid response
1085 if (asyncResp->res.resultInt() != 200)
1086 {
1087 messages::operationFailed(asyncResp->res);
1088 }
1089 return;
1090 }
1091
1092 BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1093
1094 // Now we need to add the prefix to the URIs contained in the
1095 // response.
1096 addPrefixes(jsonVal, prefix);
1097
1098 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response");
1099
1100 // If this resource collection does not exist on the aggregating bmc
1101 // and has not already been added from processing the response from
1102 // a different satellite then we need to completely overwrite
1103 // asyncResp
1104 if (asyncResp->res.resultInt() != 200)
1105 {
1106 // We only want to aggregate collections that contain a
1107 // "Members" array
1108 if ((!jsonVal.contains("Members")) &&
1109 (!jsonVal["Members"].is_array()))
1110 {
1111 BMCWEB_LOG_DEBUG(
1112 "Skipping aggregating unsupported resource");
1113 return;
1114 }
1115
1116 BMCWEB_LOG_DEBUG(
1117 "Collection does not exist, overwriting asyncResp");
1118 asyncResp->res.result(resp.result());
1119 asyncResp->res.jsonValue = std::move(jsonVal);
1120 asyncResp->res.addHeader("Content-Type", "application/json");
1121
1122 BMCWEB_LOG_DEBUG("Finished overwriting asyncResp");
1123 }
1124 else
1125 {
1126 // We only want to aggregate collections that contain a
1127 // "Members" array
1128 if ((!asyncResp->res.jsonValue.contains("Members")) &&
1129 (!asyncResp->res.jsonValue["Members"].is_array()))
1130
1131 {
1132 BMCWEB_LOG_DEBUG(
1133 "Skipping aggregating unsupported resource");
1134 return;
1135 }
1136
1137 BMCWEB_LOG_DEBUG(
1138 "Adding aggregated resources from \"{}\" to collection",
1139 prefix);
1140
1141 // TODO: This is a potential race condition with multiple
1142 // satellites and the aggregating bmc attempting to write to
1143 // update this array. May need to cascade calls to the next
1144 // satellite at the end of this function.
1145 // This is presumably not a concern when there is only a single
1146 // satellite since the aggregating bmc should have completed
1147 // before the response is received from the satellite.
1148
1149 auto& members = asyncResp->res.jsonValue["Members"];
1150 auto& satMembers = jsonVal["Members"];
1151 for (auto& satMem : satMembers)
1152 {
1153 members.emplace_back(std::move(satMem));
1154 }
1155 asyncResp->res.jsonValue["Members@odata.count"] =
1156 members.size();
1157
1158 // TODO: Do we need to sort() after updating the array?
1159 }
1160 }
1161 else
1162 {
1163 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"",
1164 prefix);
1165 // We received a response that was not a json.
1166 // Notify the user only if we did not receive any valid responses
1167 // and if the resource collection does not already exist on the
1168 // aggregating BMC
1169 if (asyncResp->res.resultInt() != 200)
1170 {
1171 messages::operationFailed(asyncResp->res);
1172 }
1173 }
1174 } // End processCollectionResponse()
1175
1176 // Processes the response returned by a satellite BMC and merges any
1177 // properties whose "@odata.id" value is the URI or either a top level
1178 // collection or is uptree from a top level collection
processContainsSubordinateResponse(const std::string & prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)1179 static void processContainsSubordinateResponse(
1180 const std::string& prefix,
1181 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1182 crow::Response& resp)
1183 {
1184 // 429 and 502 mean we didn't actually send the request so don't
1185 // overwrite the response headers in that case
1186 if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1187 (resp.result() == boost::beast::http::status::bad_gateway))
1188 {
1189 return;
1190 }
1191
1192 if (resp.resultInt() != 200)
1193 {
1194 BMCWEB_LOG_DEBUG(
1195 "Resource uptree from Collection does not exist in satellite BMC \"{}\"",
1196 prefix);
1197 // Return the error if we haven't had any successes
1198 if (asyncResp->res.resultInt() != 200)
1199 {
1200 asyncResp->res.result(resp.result());
1201 asyncResp->res.copyBody(resp);
1202 }
1203 return;
1204 }
1205
1206 // The resp will not have a json component
1207 // We need to create a json from resp's stringResponse
1208 if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1209 {
1210 bool addedLinks = false;
1211 nlohmann::json jsonVal =
1212 nlohmann::json::parse(*resp.body(), nullptr, false);
1213 if (jsonVal.is_discarded())
1214 {
1215 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1216
1217 // Notify the user if doing so won't overwrite a valid response
1218 if (asyncResp->res.resultInt() != 200)
1219 {
1220 messages::operationFailed(asyncResp->res);
1221 }
1222 return;
1223 }
1224
1225 BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1226
1227 // Parse response and add properties missing from the AsyncResp
1228 // Valid properties will be of the form <property>.@odata.id and
1229 // @odata.id is a <URI>. In other words, the json should contain
1230 // multiple properties such that
1231 // {"<property>":{"@odata.id": "<URI>"}}
1232 nlohmann::json::object_t* object =
1233 jsonVal.get_ptr<nlohmann::json::object_t*>();
1234 if (object == nullptr)
1235 {
1236 BMCWEB_LOG_ERROR("Parsed JSON was not an object?");
1237 return;
1238 }
1239
1240 for (std::pair<const std::string, nlohmann::json>& prop : *object)
1241 {
1242 if (!prop.second.contains("@odata.id"))
1243 {
1244 continue;
1245 }
1246
1247 std::string* strValue =
1248 prop.second["@odata.id"].get_ptr<std::string*>();
1249 if (strValue == nullptr)
1250 {
1251 BMCWEB_LOG_CRITICAL("Field wasn't a string????");
1252 continue;
1253 }
1254 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon))
1255 {
1256 continue;
1257 }
1258
1259 addedLinks = true;
1260 if (!asyncResp->res.jsonValue.contains(prop.first))
1261 {
1262 // Only add the property if it did not already exist
1263 BMCWEB_LOG_DEBUG("Adding link for {} from BMC {}",
1264 *strValue, prefix);
1265 asyncResp->res.jsonValue[prop.first]["@odata.id"] =
1266 *strValue;
1267 continue;
1268 }
1269 }
1270
1271 // If we added links to a previously unsuccessful (non-200) response
1272 // then we need to make sure the response contains the bare minimum
1273 // amount of additional information that we'd expect to have been
1274 // populated.
1275 if (addedLinks && (asyncResp->res.resultInt() != 200))
1276 {
1277 // This resource didn't locally exist or an error
1278 // occurred while generating the response. Remove any
1279 // error messages and update the error code.
1280 asyncResp->res.jsonValue.erase(
1281 asyncResp->res.jsonValue.find("error"));
1282 asyncResp->res.result(resp.result());
1283
1284 const auto& it1 = object->find("@odata.id");
1285 if (it1 != object->end())
1286 {
1287 asyncResp->res.jsonValue["@odata.id"] = (it1->second);
1288 }
1289 const auto& it2 = object->find("@odata.type");
1290 if (it2 != object->end())
1291 {
1292 asyncResp->res.jsonValue["@odata.type"] = (it2->second);
1293 }
1294 const auto& it3 = object->find("Id");
1295 if (it3 != object->end())
1296 {
1297 asyncResp->res.jsonValue["Id"] = (it3->second);
1298 }
1299 const auto& it4 = object->find("Name");
1300 if (it4 != object->end())
1301 {
1302 asyncResp->res.jsonValue["Name"] = (it4->second);
1303 }
1304 }
1305 }
1306 else
1307 {
1308 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"",
1309 prefix);
1310 // We received as response that was not a json
1311 // Notify the user only if we did not receive any valid responses,
1312 // and if the resource does not already exist on the aggregating BMC
1313 if (asyncResp->res.resultInt() != 200)
1314 {
1315 messages::operationFailed(asyncResp->res);
1316 }
1317 }
1318 }
1319
1320 // Entry point to Redfish Aggregation
1321 // Returns Result stating whether or not we still need to locally handle the
1322 // request
beginAggregation(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)1323 Result beginAggregation(const crow::Request& thisReq,
1324 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
1325 {
1326 using crow::utility::OrMorePaths;
1327 using crow::utility::readUrlSegments;
1328 boost::urls::url_view url = thisReq.url();
1329
1330 // We don't need to aggregate JsonSchemas due to potential issues such
1331 // as version mismatches between aggregator and satellite BMCs. For
1332 // now assume that the aggregator has all the schemas and versions that
1333 // the aggregated server has.
1334 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
1335 crow::utility::OrMorePaths()))
1336 {
1337 return Result::LocalHandle;
1338 }
1339
1340 // The first two segments should be "/redfish/v1". We need to check
1341 // that before we can search topCollections
1342 if (!crow::utility::readUrlSegments(url, "redfish", "v1",
1343 crow::utility::OrMorePaths()))
1344 {
1345 return Result::LocalHandle;
1346 }
1347
1348 // Parse the URI to see if it begins with a known top level collection
1349 // such as:
1350 // /redfish/v1/Chassis
1351 // /redfish/v1/UpdateService/FirmwareInventory
1352 const boost::urls::segments_view urlSegments = url.segments();
1353 boost::urls::url currentUrl("/");
1354 boost::urls::segments_view::const_iterator it = urlSegments.begin();
1355 boost::urls::segments_view::const_iterator end = urlSegments.end();
1356
1357 // Skip past the leading "/redfish/v1"
1358 it++;
1359 it++;
1360 for (; it != end; it++)
1361 {
1362 const std::string& collectionItem = *it;
1363 if (std::binary_search(topCollections.begin(), topCollections.end(),
1364 currentUrl.buffer()))
1365 {
1366 // We've matched a resource collection so this current segment
1367 // might contain an aggregation prefix
1368 if (segmentHasPrefix(collectionItem))
1369 {
1370 BMCWEB_LOG_DEBUG("Need to forward a request");
1371
1372 // Extract the prefix from the request's URI, retrieve the
1373 // associated satellite config information, and then forward
1374 // the request to that satellite.
1375 startAggregation(AggregationType::Resource, thisReq,
1376 asyncResp);
1377 return Result::NoLocalHandle;
1378 }
1379
1380 // Handle collection URI with a trailing backslash
1381 // e.g. /redfish/v1/Chassis/
1382 it++;
1383 if ((it == end) && collectionItem.empty())
1384 {
1385 startAggregation(AggregationType::Collection, thisReq,
1386 asyncResp);
1387 }
1388
1389 // We didn't recognize the prefix or it's a collection with a
1390 // trailing "/". In both cases we still want to locally handle
1391 // the request
1392 return Result::LocalHandle;
1393 }
1394
1395 currentUrl.segments().push_back(collectionItem);
1396 }
1397
1398 // If we made it here then currentUrl could contain a top level
1399 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
1400 if (std::binary_search(topCollections.begin(), topCollections.end(),
1401 currentUrl.buffer()))
1402 {
1403 startAggregation(AggregationType::Collection, thisReq, asyncResp);
1404 return Result::LocalHandle;
1405 }
1406
1407 // If nothing else then the request could be for a resource which has a
1408 // top level collection as a subordinate
1409 if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate))
1410 {
1411 startAggregation(AggregationType::ContainsSubordinate, thisReq,
1412 asyncResp);
1413 return Result::LocalHandle;
1414 }
1415
1416 BMCWEB_LOG_DEBUG("Aggregation not required for {}", url.buffer());
1417 return Result::LocalHandle;
1418 }
1419
1420 // Check if the given URL segment matches with any satellite prefix
1421 // Assumes the given segment starts with <prefix>_
segmentHasPrefix(const std::string & urlSegment) const1422 bool segmentHasPrefix(const std::string& urlSegment) const
1423 {
1424 // TODO: handle this better
1425 // For now 5B247A_ wont be in the aggregationSources map so
1426 // check explicitly for now
1427 if (urlSegment.starts_with("5B247A_"))
1428 {
1429 return true;
1430 }
1431
1432 // Find the first underscore
1433 std::size_t underscorePos = urlSegment.find('_');
1434 if (underscorePos == std::string::npos)
1435 {
1436 return false; // No underscore, can't be a satellite prefix
1437 }
1438
1439 // Extract the prefix
1440 std::string prefix = urlSegment.substr(0, underscorePos);
1441
1442 // Check if this prefix exists
1443 return aggregationSources.contains(prefix);
1444 }
1445 };
1446
1447 } // namespace redfish
1448