1 #pragma once
2 #include "bmcweb_config.h"
3 
4 #include "app.hpp"
5 #include "async_resp.hpp"
6 #include "error_messages.hpp"
7 #include "http_request.hpp"
8 #include "http_response.hpp"
9 #include "logging.hpp"
10 
11 #include <sys/types.h>
12 
13 #include <boost/beast/http/message.hpp> // IWYU pragma: keep
14 #include <boost/beast/http/status.hpp>
15 #include <boost/beast/http/verb.hpp>
16 #include <boost/url/params_view.hpp>
17 #include <boost/url/string.hpp>
18 #include <nlohmann/json.hpp>
19 
20 #include <algorithm>
21 #include <charconv>
22 #include <cstdint>
23 #include <functional>
24 #include <limits>
25 #include <map>
26 #include <memory>
27 #include <optional>
28 #include <string>
29 #include <string_view>
30 #include <system_error>
31 #include <utility>
32 #include <vector>
33 
34 // IWYU pragma: no_include <boost/url/impl/params_view.hpp>
35 // IWYU pragma: no_include <boost/beast/http/impl/message.hpp>
36 // IWYU pragma: no_include <boost/intrusive/detail/list_iterator.hpp>
37 // IWYU pragma: no_include <stdint.h>
38 
39 namespace redfish
40 {
41 namespace query_param
42 {
43 inline constexpr size_t maxEntriesPerPage = 1000;
44 
45 enum class ExpandType : uint8_t
46 {
47     None,
48     Links,
49     NotLinks,
50     Both,
51 };
52 
53 // The struct stores the parsed query parameters of the default Redfish route.
54 struct Query
55 {
56     // Only
57     bool isOnly = false;
58     // Expand
59     uint8_t expandLevel = 0;
60     ExpandType expandType = ExpandType::None;
61 
62     // Skip
63     std::optional<size_t> skip = std::nullopt;
64 
65     // Top
66     std::optional<size_t> top = std::nullopt;
67 };
68 
69 // The struct defines how resource handlers in redfish-core/lib/ can handle
70 // query parameters themselves, so that the default Redfish route will delegate
71 // the processing.
72 struct QueryCapabilities
73 {
74     bool canDelegateOnly = false;
75     bool canDelegateTop = false;
76     bool canDelegateSkip = false;
77     uint8_t canDelegateExpandLevel = 0;
78 };
79 
80 // Delegates query parameters according to the given |queryCapabilities|
81 // This function doesn't check query parameter conflicts since the parse
82 // function will take care of it.
83 // Returns a delegated query object which can be used by individual resource
84 // handlers so that handlers don't need to query again.
85 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
86 {
87     Query delegated;
88     // delegate only
89     if (query.isOnly && queryCapabilities.canDelegateOnly)
90     {
91         delegated.isOnly = true;
92         query.isOnly = false;
93     }
94     // delegate expand as much as we can
95     if (query.expandType != ExpandType::None)
96     {
97         delegated.expandType = query.expandType;
98         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
99         {
100             query.expandType = ExpandType::None;
101             delegated.expandLevel = query.expandLevel;
102             query.expandLevel = 0;
103         }
104         else
105         {
106             query.expandLevel -= queryCapabilities.canDelegateExpandLevel;
107             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
108         }
109     }
110 
111     // delegate top
112     if (query.top && queryCapabilities.canDelegateTop)
113     {
114         delegated.top = query.top;
115         query.top = std::nullopt;
116     }
117 
118     // delegate skip
119     if (query.skip && queryCapabilities.canDelegateSkip)
120     {
121         delegated.skip = query.skip;
122         query.skip = 0;
123     }
124     return delegated;
125 }
126 
127 inline bool getExpandType(std::string_view value, Query& query)
128 {
129     if (value.empty())
130     {
131         return false;
132     }
133     switch (value[0])
134     {
135         case '*':
136             query.expandType = ExpandType::Both;
137             break;
138         case '.':
139             query.expandType = ExpandType::NotLinks;
140             break;
141         case '~':
142             query.expandType = ExpandType::Links;
143             break;
144         default:
145             return false;
146 
147             break;
148     }
149     value.remove_prefix(1);
150     if (value.empty())
151     {
152         query.expandLevel = 1;
153         return true;
154     }
155     constexpr std::string_view levels = "($levels=";
156     if (!value.starts_with(levels))
157     {
158         return false;
159     }
160     value.remove_prefix(levels.size());
161 
162     auto it = std::from_chars(value.data(), value.data() + value.size(),
163                               query.expandLevel);
164     if (it.ec != std::errc())
165     {
166         return false;
167     }
168     value.remove_prefix(static_cast<size_t>(it.ptr - value.data()));
169     return value == ")";
170 }
171 
172 enum class QueryError
173 {
174     Ok,
175     OutOfRange,
176     ValueFormat,
177 };
178 
179 inline QueryError getNumericParam(std::string_view value, size_t& param)
180 {
181     std::from_chars_result r =
182         std::from_chars(value.data(), value.data() + value.size(), param);
183 
184     // If the number wasn't representable in the type, it's out of range
185     if (r.ec == std::errc::result_out_of_range)
186     {
187         return QueryError::OutOfRange;
188     }
189     // All other errors are value format
190     if (r.ec != std::errc())
191     {
192         return QueryError::ValueFormat;
193     }
194     return QueryError::Ok;
195 }
196 
197 inline QueryError getSkipParam(std::string_view value, Query& query)
198 {
199     return getNumericParam(value, query.skip.emplace());
200 }
201 
202 inline QueryError getTopParam(std::string_view value, Query& query)
203 {
204     QueryError ret = getNumericParam(value, query.top.emplace());
205     if (ret != QueryError::Ok)
206     {
207         return ret;
208     }
209 
210     // Range check for sanity.
211     if (query.top > maxEntriesPerPage)
212     {
213         return QueryError::OutOfRange;
214     }
215 
216     return QueryError::Ok;
217 }
218 
219 inline std::optional<Query>
220     parseParameters(const boost::urls::params_view& urlParams,
221                     crow::Response& res)
222 {
223     Query ret;
224     for (const boost::urls::params_view::value_type& it : urlParams)
225     {
226         std::string_view key(it.key.data(), it.key.size());
227         std::string_view value(it.value.data(), it.value.size());
228         if (key == "only")
229         {
230             if (!it.value.empty())
231             {
232                 messages::queryParameterValueFormatError(res, value, key);
233                 return std::nullopt;
234             }
235             ret.isOnly = true;
236         }
237         else if (key == "$expand" && bmcwebInsecureEnableQueryParams)
238         {
239             if (!getExpandType(value, ret))
240             {
241                 messages::queryParameterValueFormatError(res, value, key);
242                 return std::nullopt;
243             }
244         }
245         else if (key == "$top")
246         {
247             QueryError topRet = getTopParam(value, ret);
248             if (topRet == QueryError::ValueFormat)
249             {
250                 messages::queryParameterValueFormatError(res, value, key);
251                 return std::nullopt;
252             }
253             if (topRet == QueryError::OutOfRange)
254             {
255                 messages::queryParameterOutOfRange(
256                     res, value, "$top",
257                     "0-" + std::to_string(maxEntriesPerPage));
258                 return std::nullopt;
259             }
260         }
261         else if (key == "$skip")
262         {
263             QueryError topRet = getSkipParam(value, ret);
264             if (topRet == QueryError::ValueFormat)
265             {
266                 messages::queryParameterValueFormatError(res, value, key);
267                 return std::nullopt;
268             }
269             if (topRet == QueryError::OutOfRange)
270             {
271                 messages::queryParameterOutOfRange(
272                     res, value, key,
273                     "0-" + std::to_string(std::numeric_limits<size_t>::max()));
274                 return std::nullopt;
275             }
276         }
277         else
278         {
279             // Intentionally ignore other errors Redfish spec, 7.3.1
280             if (key.starts_with("$"))
281             {
282                 // Services shall return... The HTTP 501 Not Implemented
283                 // status code for any unsupported query parameters that
284                 // start with $ .
285                 messages::queryParameterValueFormatError(res, value, key);
286                 res.result(boost::beast::http::status::not_implemented);
287                 return std::nullopt;
288             }
289             // "Shall ignore unknown or unsupported query parameters that do
290             // not begin with $ ."
291         }
292     }
293 
294     return ret;
295 }
296 
297 inline bool processOnly(crow::App& app, crow::Response& res,
298                         std::function<void(crow::Response&)>& completionHandler)
299 {
300     BMCWEB_LOG_DEBUG << "Processing only query param";
301     auto itMembers = res.jsonValue.find("Members");
302     if (itMembers == res.jsonValue.end())
303     {
304         messages::queryNotSupportedOnResource(res);
305         completionHandler(res);
306         return false;
307     }
308     auto itMemBegin = itMembers->begin();
309     if (itMemBegin == itMembers->end() || itMembers->size() != 1)
310     {
311         BMCWEB_LOG_DEBUG << "Members contains " << itMembers->size()
312                          << " element, returning full collection.";
313         completionHandler(res);
314         return false;
315     }
316 
317     auto itUrl = itMemBegin->find("@odata.id");
318     if (itUrl == itMemBegin->end())
319     {
320         BMCWEB_LOG_DEBUG << "No found odata.id";
321         messages::internalError(res);
322         completionHandler(res);
323         return false;
324     }
325     const std::string* url = itUrl->get_ptr<const std::string*>();
326     if (url == nullptr)
327     {
328         BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????";
329         messages::internalError(res);
330         completionHandler(res);
331         return false;
332     }
333     // TODO(Ed) copy request headers?
334     // newReq.session = req.session;
335     std::error_code ec;
336     crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec);
337     if (ec)
338     {
339         messages::internalError(res);
340         completionHandler(res);
341         return false;
342     }
343 
344     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
345     BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res;
346     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
347     asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper());
348     app.handle(newReq, asyncResp);
349     return true;
350 }
351 
352 struct ExpandNode
353 {
354     nlohmann::json::json_pointer location;
355     std::string uri;
356 
357     inline bool operator==(const ExpandNode& other) const
358     {
359         return location == other.location && uri == other.uri;
360     }
361 };
362 
363 // Walks a json object looking for Redfish NavigationReference entries that
364 // might need resolved.  It recursively walks the jsonResponse object, looking
365 // for links at every level, and returns a list (out) of locations within the
366 // tree that need to be expanded.  The current json pointer location p is passed
367 // in to reference the current node that's being expanded, so it can be combined
368 // with the keys from the jsonResponse object
369 inline void findNavigationReferencesRecursive(
370     ExpandType eType, nlohmann::json& jsonResponse,
371     const nlohmann::json::json_pointer& p, bool inLinks,
372     std::vector<ExpandNode>& out)
373 {
374     // If no expand is needed, return early
375     if (eType == ExpandType::None)
376     {
377         return;
378     }
379     nlohmann::json::array_t* array =
380         jsonResponse.get_ptr<nlohmann::json::array_t*>();
381     if (array != nullptr)
382     {
383         size_t index = 0;
384         // For arrays, walk every element in the array
385         for (auto& element : *array)
386         {
387             nlohmann::json::json_pointer newPtr = p / index;
388             BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string();
389             findNavigationReferencesRecursive(eType, element, newPtr, inLinks,
390                                               out);
391             index++;
392         }
393     }
394     nlohmann::json::object_t* obj =
395         jsonResponse.get_ptr<nlohmann::json::object_t*>();
396     if (obj == nullptr)
397     {
398         return;
399     }
400     // Navigation References only ever have a single element
401     if (obj->size() == 1)
402     {
403         if (obj->begin()->first == "@odata.id")
404         {
405             const std::string* uri =
406                 obj->begin()->second.get_ptr<const std::string*>();
407             if (uri != nullptr)
408             {
409                 BMCWEB_LOG_DEBUG << "Found element at " << p.to_string();
410                 out.push_back({p, *uri});
411             }
412         }
413     }
414     // Loop the object and look for links
415     for (auto& element : *obj)
416     {
417         bool localInLinks = inLinks;
418         if (!localInLinks)
419         {
420             // Check if this is a links node
421             localInLinks = element.first == "Links";
422         }
423         // Only traverse the parts of the tree the user asked for
424         // Per section 7.3 of the redfish specification
425         if (localInLinks && eType == ExpandType::NotLinks)
426         {
427             continue;
428         }
429         if (!localInLinks && eType == ExpandType::Links)
430         {
431             continue;
432         }
433         nlohmann::json::json_pointer newPtr = p / element.first;
434         BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr;
435 
436         findNavigationReferencesRecursive(eType, element.second, newPtr,
437                                           localInLinks, out);
438     }
439 }
440 
441 inline std::vector<ExpandNode>
442     findNavigationReferences(ExpandType eType, nlohmann::json& jsonResponse)
443 {
444     std::vector<ExpandNode> ret;
445     const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
446     findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret);
447     return ret;
448 }
449 
450 // Formats a query parameter string for the sub-query.
451 // Returns std::nullopt on failures.
452 // This function shall handle $select when it is added.
453 // There is no need to handle parameters that's not campatible with $expand,
454 // e.g., $only, since this function will only be called in side $expand handlers
455 inline std::optional<std::string> formatQueryForExpand(const Query& query)
456 {
457     // query.expandLevel<=1: no need to do subqueries
458     if (query.expandLevel <= 1)
459     {
460         return "";
461     }
462     std::string str = "?$expand=";
463     bool queryTypeExpected = false;
464     switch (query.expandType)
465     {
466         case ExpandType::None:
467             return "";
468         case ExpandType::Links:
469             queryTypeExpected = true;
470             str += '~';
471             break;
472         case ExpandType::NotLinks:
473             queryTypeExpected = true;
474             str += '.';
475             break;
476         case ExpandType::Both:
477             queryTypeExpected = true;
478             str += '*';
479             break;
480     }
481     if (!queryTypeExpected)
482     {
483         return std::nullopt;
484     }
485     str += "($levels=";
486     str += std::to_string(query.expandLevel - 1);
487     str += ')';
488     return str;
489 }
490 
491 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
492 {
493   public:
494     // This object takes a single asyncResp object as the "final" one, then
495     // allows callers to attach sub-responses within the json tree that need
496     // to be executed and filled into their appropriate locations.  This
497     // class manages the final "merge" of the json resources.
498     MultiAsyncResp(crow::App& appIn,
499                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
500         app(appIn),
501         finalRes(std::move(finalResIn))
502     {}
503 
504     void addAwaitingResponse(
505         const std::shared_ptr<bmcweb::AsyncResp>& res,
506         const nlohmann::json::json_pointer& finalExpandLocation)
507     {
508         res->res.setCompleteRequestHandler(std::bind_front(
509             placeResultStatic, shared_from_this(), finalExpandLocation));
510     }
511 
512     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
513                      crow::Response& res)
514     {
515         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
516         finalObj = std::move(res.jsonValue);
517     }
518 
519     // Handles the very first level of Expand, and starts a chain of sub-queries
520     // for deeper levels.
521     void startQuery(const Query& query)
522     {
523         std::vector<ExpandNode> nodes =
524             findNavigationReferences(query.expandType, finalRes->res.jsonValue);
525         BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse";
526         const std::optional<std::string> queryStr = formatQueryForExpand(query);
527         if (!queryStr)
528         {
529             messages::internalError(finalRes->res);
530             return;
531         }
532         for (const ExpandNode& node : nodes)
533         {
534             const std::string subQuery = node.uri + *queryStr;
535             BMCWEB_LOG_DEBUG << "URL of subquery:  " << subQuery;
536             std::error_code ec;
537             crow::Request newReq({boost::beast::http::verb::get, subQuery, 11},
538                                  ec);
539             if (ec)
540             {
541                 messages::internalError(finalRes->res);
542                 return;
543             }
544 
545             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
546             BMCWEB_LOG_DEBUG << "setting completion handler on "
547                              << &asyncResp->res;
548 
549             addAwaitingResponse(asyncResp, node.location);
550             app.handle(newReq, asyncResp);
551         }
552     }
553 
554   private:
555     static void
556         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
557                           const nlohmann::json::json_pointer& locationToPlace,
558                           crow::Response& res)
559     {
560         multi->placeResult(locationToPlace, res);
561     }
562 
563     crow::App& app;
564     std::shared_ptr<bmcweb::AsyncResp> finalRes;
565 };
566 
567 inline void processTopAndSkip(const Query& query, crow::Response& res)
568 {
569     if (!query.skip && !query.top)
570     {
571         // No work to do.
572         return;
573     }
574     nlohmann::json::object_t* obj =
575         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
576     if (obj == nullptr)
577     {
578         // Shouldn't be possible.  All responses should be objects.
579         messages::internalError(res);
580         return;
581     }
582 
583     BMCWEB_LOG_DEBUG << "Handling top/skip";
584     nlohmann::json::object_t::iterator members = obj->find("Members");
585     if (members == obj->end())
586     {
587         // From the Redfish specification 7.3.1
588         // ... the HTTP 400 Bad Request status code with the
589         // QueryNotSupportedOnResource message from the Base Message Registry
590         // for any supported query parameters that apply only to resource
591         // collections but are used on singular resources.
592         messages::queryNotSupportedOnResource(res);
593         return;
594     }
595 
596     nlohmann::json::array_t* arr =
597         members->second.get_ptr<nlohmann::json::array_t*>();
598     if (arr == nullptr)
599     {
600         messages::internalError(res);
601         return;
602     }
603 
604     if (query.skip)
605     {
606         // Per section 7.3.1 of the Redfish specification, $skip is run before
607         // $top Can only skip as many values as we have
608         size_t skip = std::min(arr->size(), *query.skip);
609         arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
610     }
611     if (query.top)
612     {
613         size_t top = std::min(arr->size(), *query.top);
614         arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
615     }
616 }
617 
618 inline void
619     processAllParams(crow::App& app, const Query& query,
620                      std::function<void(crow::Response&)>& completionHandler,
621                      crow::Response& intermediateResponse)
622 {
623     if (!completionHandler)
624     {
625         BMCWEB_LOG_DEBUG << "Function was invalid?";
626         return;
627     }
628 
629     BMCWEB_LOG_DEBUG << "Processing query params";
630     // If the request failed, there's no reason to even try to run query
631     // params.
632     if (intermediateResponse.resultInt() < 200 ||
633         intermediateResponse.resultInt() >= 400)
634     {
635         completionHandler(intermediateResponse);
636         return;
637     }
638     if (query.isOnly)
639     {
640         processOnly(app, intermediateResponse, completionHandler);
641         return;
642     }
643 
644     if (query.top || query.skip)
645     {
646         processTopAndSkip(query, intermediateResponse);
647     }
648 
649     if (query.expandType != ExpandType::None)
650     {
651         BMCWEB_LOG_DEBUG << "Executing expand query";
652         // TODO(ed) this is a copy of the response object.  Admittedly,
653         // we're inherently doing something inefficient, but we shouldn't
654         // have to do a full copy
655         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
656         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
657         asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue);
658         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
659 
660         multi->startQuery(query);
661         return;
662     }
663     completionHandler(intermediateResponse);
664 }
665 
666 } // namespace query_param
667 } // namespace redfish
668