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