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