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