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