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