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