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         sdbusplus::message::object_path path("/xyz/openbmc_project/inventory");
760         dbus::utility::getManagedObjects(
761             "xyz.openbmc_project.EntityManager", path,
762             [handler{std::move(handler)}](
763                 const boost::system::error_code& ec,
764                 const dbus::utility::ManagedObjectType& objects) {
765             std::unordered_map<std::string, boost::urls::url> satelliteInfo;
766             if (ec)
767             {
768                 BMCWEB_LOG_ERROR << "DBUS response error " << ec.value() << ", "
769                                  << ec.message();
770                 handler(ec, satelliteInfo);
771                 return;
772             }
773 
774             // Maps a chosen alias representing a satellite BMC to a url
775             // containing the information required to create a http
776             // connection to the satellite
777             findSatelliteConfigs(objects, satelliteInfo);
778 
779             if (!satelliteInfo.empty())
780             {
781                 BMCWEB_LOG_DEBUG << "Redfish Aggregation enabled with "
782                                  << std::to_string(satelliteInfo.size())
783                                  << " satellite BMCs";
784             }
785             else
786             {
787                 BMCWEB_LOG_DEBUG
788                     << "No satellite BMCs detected.  Redfish Aggregation not enabled";
789             }
790             handler(ec, satelliteInfo);
791             });
792     }
793 
794     // Processes the response returned by a satellite BMC and loads its
795     // contents into asyncResp
796     static void
797         processResponse(std::string_view prefix,
798                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
799                         crow::Response& resp)
800     {
801         // 429 and 502 mean we didn't actually send the request so don't
802         // overwrite the response headers in that case
803         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
804             (resp.result() == boost::beast::http::status::bad_gateway))
805         {
806             asyncResp->res.result(resp.result());
807             return;
808         }
809 
810         // We want to attempt prefix fixing regardless of response code
811         // The resp will not have a json component
812         // We need to create a json from resp's stringResponse
813         std::string_view contentType = resp.getHeaderValue("Content-Type");
814         if (boost::iequals(contentType, "application/json") ||
815             boost::iequals(contentType, "application/json; charset=utf-8"))
816         {
817             nlohmann::json jsonVal = nlohmann::json::parse(resp.body(), nullptr,
818                                                            false);
819             if (jsonVal.is_discarded())
820             {
821                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
822                 messages::operationFailed(asyncResp->res);
823                 return;
824             }
825 
826             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
827 
828             addPrefixes(jsonVal, prefix);
829 
830             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
831 
832             asyncResp->res.result(resp.result());
833             asyncResp->res.jsonValue = std::move(jsonVal);
834 
835             BMCWEB_LOG_DEBUG << "Finished writing asyncResp";
836         }
837         else
838         {
839             // We allow any Content-Type that is not "application/json" now
840             asyncResp->res.result(resp.result());
841             asyncResp->res.write(resp.body());
842         }
843         addAggregatedHeaders(asyncResp->res, resp, prefix);
844     }
845 
846     // Processes the collection response returned by a satellite BMC and merges
847     // its "@odata.id" values
848     static void processCollectionResponse(
849         const std::string& prefix,
850         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
851         crow::Response& resp)
852     {
853         // 429 and 502 mean we didn't actually send the request so don't
854         // overwrite the response headers in that case
855         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
856             (resp.result() == boost::beast::http::status::bad_gateway))
857         {
858             return;
859         }
860 
861         if (resp.resultInt() != 200)
862         {
863             BMCWEB_LOG_DEBUG
864                 << "Collection resource does not exist in satellite BMC \""
865                 << prefix << "\"";
866             // Return the error if we haven't had any successes
867             if (asyncResp->res.resultInt() != 200)
868             {
869                 asyncResp->res.result(resp.result());
870                 asyncResp->res.write(resp.body());
871             }
872             return;
873         }
874 
875         // The resp will not have a json component
876         // We need to create a json from resp's stringResponse
877         std::string_view contentType = resp.getHeaderValue("Content-Type");
878         if (boost::iequals(contentType, "application/json") ||
879             boost::iequals(contentType, "application/json; charset=utf-8"))
880         {
881             nlohmann::json jsonVal = nlohmann::json::parse(resp.body(), nullptr,
882                                                            false);
883             if (jsonVal.is_discarded())
884             {
885                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
886 
887                 // Notify the user if doing so won't overwrite a valid response
888                 if (asyncResp->res.resultInt() != 200)
889                 {
890                     messages::operationFailed(asyncResp->res);
891                 }
892                 return;
893             }
894 
895             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
896 
897             // Now we need to add the prefix to the URIs contained in the
898             // response.
899             addPrefixes(jsonVal, prefix);
900 
901             BMCWEB_LOG_DEBUG << "Added prefix to parsed satellite response";
902 
903             // If this resource collection does not exist on the aggregating bmc
904             // and has not already been added from processing the response from
905             // a different satellite then we need to completely overwrite
906             // asyncResp
907             if (asyncResp->res.resultInt() != 200)
908             {
909                 // We only want to aggregate collections that contain a
910                 // "Members" array
911                 if ((!jsonVal.contains("Members")) &&
912                     (!jsonVal["Members"].is_array()))
913                 {
914                     BMCWEB_LOG_DEBUG
915                         << "Skipping aggregating unsupported resource";
916                     return;
917                 }
918 
919                 BMCWEB_LOG_DEBUG
920                     << "Collection does not exist, overwriting asyncResp";
921                 asyncResp->res.result(resp.result());
922                 asyncResp->res.jsonValue = std::move(jsonVal);
923                 asyncResp->res.addHeader("Content-Type", "application/json");
924 
925                 BMCWEB_LOG_DEBUG << "Finished overwriting asyncResp";
926             }
927             else
928             {
929                 // We only want to aggregate collections that contain a
930                 // "Members" array
931                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
932                     (!asyncResp->res.jsonValue["Members"].is_array()))
933 
934                 {
935                     BMCWEB_LOG_DEBUG
936                         << "Skipping aggregating unsupported resource";
937                     return;
938                 }
939 
940                 BMCWEB_LOG_DEBUG << "Adding aggregated resources from \""
941                                  << prefix << "\" to collection";
942 
943                 // TODO: This is a potential race condition with multiple
944                 // satellites and the aggregating bmc attempting to write to
945                 // update this array.  May need to cascade calls to the next
946                 // satellite at the end of this function.
947                 // This is presumably not a concern when there is only a single
948                 // satellite since the aggregating bmc should have completed
949                 // before the response is received from the satellite.
950 
951                 auto& members = asyncResp->res.jsonValue["Members"];
952                 auto& satMembers = jsonVal["Members"];
953                 for (auto& satMem : satMembers)
954                 {
955                     members.emplace_back(std::move(satMem));
956                 }
957                 asyncResp->res.jsonValue["Members@odata.count"] =
958                     members.size();
959 
960                 // TODO: Do we need to sort() after updating the array?
961             }
962         }
963         else
964         {
965             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
966                              << "\"";
967             // We received a response that was not a json.
968             // Notify the user only if we did not receive any valid responses
969             // and if the resource collection does not already exist on the
970             // aggregating BMC
971             if (asyncResp->res.resultInt() != 200)
972             {
973                 messages::operationFailed(asyncResp->res);
974             }
975         }
976     } // End processCollectionResponse()
977 
978     // Processes the response returned by a satellite BMC and merges any
979     // properties whose "@odata.id" value is the URI or either a top level
980     // collection or is uptree from a top level collection
981     static void processContainsSubordinateResponse(
982         const std::string& prefix,
983         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
984         crow::Response& resp)
985     {
986         // 429 and 502 mean we didn't actually send the request so don't
987         // overwrite the response headers in that case
988         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
989             (resp.result() == boost::beast::http::status::bad_gateway))
990         {
991             return;
992         }
993 
994         if (resp.resultInt() != 200)
995         {
996             BMCWEB_LOG_DEBUG
997                 << "Resource uptree from Collection does not exist in "
998                 << "satellite BMC \"" << prefix << "\"";
999             // Return the error if we haven't had any successes
1000             if (asyncResp->res.resultInt() != 200)
1001             {
1002                 asyncResp->res.result(resp.result());
1003                 asyncResp->res.write(resp.body());
1004             }
1005             return;
1006         }
1007 
1008         // The resp will not have a json component
1009         // We need to create a json from resp's stringResponse
1010         std::string_view contentType = resp.getHeaderValue("Content-Type");
1011         if (boost::iequals(contentType, "application/json") ||
1012             boost::iequals(contentType, "application/json; charset=utf-8"))
1013         {
1014             bool addedLinks = false;
1015             nlohmann::json jsonVal = nlohmann::json::parse(resp.body(), nullptr,
1016                                                            false);
1017             if (jsonVal.is_discarded())
1018             {
1019                 BMCWEB_LOG_ERROR << "Error parsing satellite response as JSON";
1020 
1021                 // Notify the user if doing so won't overwrite a valid response
1022                 if (asyncResp->res.resultInt() != 200)
1023                 {
1024                     messages::operationFailed(asyncResp->res);
1025                 }
1026                 return;
1027             }
1028 
1029             BMCWEB_LOG_DEBUG << "Successfully parsed satellite response";
1030 
1031             // Parse response and add properties missing from the AsyncResp
1032             // Valid properties will be of the form <property>.@odata.id and
1033             // @odata.id is a <URI>.  In other words, the json should contain
1034             // multiple properties such that
1035             // {"<property>":{"@odata.id": "<URI>"}}
1036             nlohmann::json::object_t* object =
1037                 jsonVal.get_ptr<nlohmann::json::object_t*>();
1038             if (object == nullptr)
1039             {
1040                 BMCWEB_LOG_ERROR << "Parsed JSON was not an object?";
1041                 return;
1042             }
1043 
1044             for (std::pair<const std::string, nlohmann::json>& prop : *object)
1045             {
1046                 if (!prop.second.contains("@odata.id"))
1047                 {
1048                     continue;
1049                 }
1050 
1051                 std::string* strValue =
1052                     prop.second["@odata.id"].get_ptr<std::string*>();
1053                 if (strValue == nullptr)
1054                 {
1055                     BMCWEB_LOG_CRITICAL << "Field wasn't a string????";
1056                     continue;
1057                 }
1058                 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon))
1059                 {
1060                     continue;
1061                 }
1062 
1063                 BMCWEB_LOG_DEBUG << "Adding link for " << *strValue
1064                                  << " from BMC " << prefix;
1065                 addedLinks = true;
1066                 if (!asyncResp->res.jsonValue.contains(prop.first))
1067                 {
1068                     // Only add the property if it did not already exist
1069                     asyncResp->res.jsonValue[prop.first]["@odata.id"] =
1070                         *strValue;
1071                     continue;
1072                 }
1073             }
1074 
1075             // If we added links to a previously unsuccessful (non-200) response
1076             // then we need to make sure the response contains the bare minimum
1077             // amount of additional information that we'd expect to have been
1078             // populated.
1079             if (addedLinks && (asyncResp->res.resultInt() != 200))
1080             {
1081                 // This resource didn't locally exist or an error
1082                 // occurred while generating the response.  Remove any
1083                 // error messages and update the error code.
1084                 asyncResp->res.jsonValue.erase(
1085                     asyncResp->res.jsonValue.find("error"));
1086                 asyncResp->res.result(resp.result());
1087 
1088                 const auto& it1 = object->find("@odata.id");
1089                 if (it1 != object->end())
1090                 {
1091                     asyncResp->res.jsonValue["@odata.id"] = (it1->second);
1092                 }
1093                 const auto& it2 = object->find("@odata.type");
1094                 if (it2 != object->end())
1095                 {
1096                     asyncResp->res.jsonValue["@odata.type"] = (it2->second);
1097                 }
1098                 const auto& it3 = object->find("Id");
1099                 if (it3 != object->end())
1100                 {
1101                     asyncResp->res.jsonValue["Id"] = (it3->second);
1102                 }
1103                 const auto& it4 = object->find("Name");
1104                 if (it4 != object->end())
1105                 {
1106                     asyncResp->res.jsonValue["Name"] = (it4->second);
1107                 }
1108             }
1109         }
1110         else
1111         {
1112             BMCWEB_LOG_ERROR << "Received unparsable response from \"" << prefix
1113                              << "\"";
1114             // We received as response that was not a json
1115             // Notify the user only if we did not receive any valid responses,
1116             // and if the resource does not already exist on the aggregating BMC
1117             if (asyncResp->res.resultInt() != 200)
1118             {
1119                 messages::operationFailed(asyncResp->res);
1120             }
1121         }
1122     }
1123 
1124     // Entry point to Redfish Aggregation
1125     // Returns Result stating whether or not we still need to locally handle the
1126     // request
1127     static Result
1128         beginAggregation(const crow::Request& thisReq,
1129                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
1130     {
1131         using crow::utility::OrMorePaths;
1132         using crow::utility::readUrlSegments;
1133         const boost::urls::url_view url = thisReq.url();
1134 
1135         // We don't need to aggregate JsonSchemas due to potential issues such
1136         // as version mismatches between aggregator and satellite BMCs.  For
1137         // now assume that the aggregator has all the schemas and versions that
1138         // the aggregated server has.
1139         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
1140                                            crow::utility::OrMorePaths()))
1141         {
1142             return Result::LocalHandle;
1143         }
1144 
1145         // The first two segments should be "/redfish/v1".  We need to check
1146         // that before we can search topCollections
1147         if (!crow::utility::readUrlSegments(url, "redfish", "v1",
1148                                             crow::utility::OrMorePaths()))
1149         {
1150             return Result::LocalHandle;
1151         }
1152 
1153         // Parse the URI to see if it begins with a known top level collection
1154         // such as:
1155         // /redfish/v1/Chassis
1156         // /redfish/v1/UpdateService/FirmwareInventory
1157         const boost::urls::segments_view urlSegments = url.segments();
1158         boost::urls::url currentUrl("/");
1159         boost::urls::segments_view::iterator it = urlSegments.begin();
1160         const boost::urls::segments_view::const_iterator end =
1161             urlSegments.end();
1162 
1163         // Skip past the leading "/redfish/v1"
1164         it++;
1165         it++;
1166         for (; it != end; it++)
1167         {
1168             const std::string& collectionItem = *it;
1169             if (std::binary_search(topCollections.begin(), topCollections.end(),
1170                                    currentUrl.buffer()))
1171             {
1172                 // We've matched a resource collection so this current segment
1173                 // might contain an aggregation prefix
1174                 // TODO: This needs to be rethought when we can support multiple
1175                 // satellites due to
1176                 // /redfish/v1/AggregationService/AggregationSources/5B247A
1177                 // being a local resource describing the satellite
1178                 if (collectionItem.starts_with("5B247A_"))
1179                 {
1180                     BMCWEB_LOG_DEBUG << "Need to forward a request";
1181 
1182                     // Extract the prefix from the request's URI, retrieve the
1183                     // associated satellite config information, and then forward
1184                     // the request to that satellite.
1185                     startAggregation(AggregationType::Resource, thisReq,
1186                                      asyncResp);
1187                     return Result::NoLocalHandle;
1188                 }
1189 
1190                 // Handle collection URI with a trailing backslash
1191                 // e.g. /redfish/v1/Chassis/
1192                 it++;
1193                 if ((it == end) && collectionItem.empty())
1194                 {
1195                     startAggregation(AggregationType::Collection, thisReq,
1196                                      asyncResp);
1197                 }
1198 
1199                 // We didn't recognize the prefix or it's a collection with a
1200                 // trailing "/".  In both cases we still want to locally handle
1201                 // the request
1202                 return Result::LocalHandle;
1203             }
1204 
1205             currentUrl.segments().push_back(collectionItem);
1206         }
1207 
1208         // If we made it here then currentUrl could contain a top level
1209         // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
1210         if (std::binary_search(topCollections.begin(), topCollections.end(),
1211                                currentUrl.buffer()))
1212         {
1213             startAggregation(AggregationType::Collection, thisReq, asyncResp);
1214             return Result::LocalHandle;
1215         }
1216 
1217         BMCWEB_LOG_DEBUG << "Aggregation not required";
1218         return Result::LocalHandle;
1219     }
1220 };
1221 
1222 } // namespace redfish
1223