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 // Returns std::nullopt on failures.
427 // This function shall handle $select when it is added.
428 // There is no need to handle parameters that's not campatible with $expand,
429 // e.g., $only, since this function will only be called in side $expand handlers
430 inline std::optional<std::string> formatQueryForExpand(const Query& query)
431 {
432     // query.expandLevel<=1: no need to do subqueries
433     if (query.expandLevel <= 1)
434     {
435         return "";
436     }
437     std::string str = "?$expand=";
438     bool queryTypeExpected = false;
439     switch (query.expandType)
440     {
441         case ExpandType::None:
442             return "";
443         case ExpandType::Links:
444             queryTypeExpected = true;
445             str += '~';
446             break;
447         case ExpandType::NotLinks:
448             queryTypeExpected = true;
449             str += '.';
450             break;
451         case ExpandType::Both:
452             queryTypeExpected = true;
453             str += '*';
454             break;
455     }
456     if (!queryTypeExpected)
457     {
458         return std::nullopt;
459     }
460     str += "($levels=";
461     str += std::to_string(query.expandLevel - 1);
462     str += ')';
463     return str;
464 }
465 
466 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
467 {
468   public:
469     // This object takes a single asyncResp object as the "final" one, then
470     // allows callers to attach sub-responses within the json tree that need
471     // to be executed and filled into their appropriate locations.  This
472     // class manages the final "merge" of the json resources.
473     MultiAsyncResp(crow::App& appIn,
474                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
475         app(appIn),
476         finalRes(std::move(finalResIn))
477     {}
478 
479     void addAwaitingResponse(
480         std::shared_ptr<bmcweb::AsyncResp>& res,
481         const nlohmann::json::json_pointer& finalExpandLocation)
482     {
483         res->res.setCompleteRequestHandler(std::bind_front(
484             placeResultStatic, shared_from_this(), finalExpandLocation));
485     }
486 
487     void placeResult(const nlohmann::json::json_pointer& locationToPlace,
488                      crow::Response& res)
489     {
490         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
491         finalObj = std::move(res.jsonValue);
492     }
493 
494     // Handles the very first level of Expand, and starts a chain of sub-queries
495     // for deeper levels.
496     void startQuery(const Query& query)
497     {
498         std::vector<ExpandNode> nodes =
499             findNavigationReferences(query.expandType, finalRes->res.jsonValue);
500         BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse";
501         const std::optional<std::string> queryStr = formatQueryForExpand(query);
502         if (!queryStr)
503         {
504             messages::internalError(finalRes->res);
505             return;
506         }
507         for (const ExpandNode& node : nodes)
508         {
509             const std::string subQuery = node.uri + *queryStr;
510             BMCWEB_LOG_DEBUG << "URL of subquery:  " << subQuery;
511             std::error_code ec;
512             crow::Request newReq({boost::beast::http::verb::get, subQuery, 11},
513                                  ec);
514             if (ec)
515             {
516                 messages::internalError(finalRes->res);
517                 return;
518             }
519 
520             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
521             BMCWEB_LOG_DEBUG << "setting completion handler on "
522                              << &asyncResp->res;
523 
524             addAwaitingResponse(asyncResp, node.location);
525             app.handle(newReq, asyncResp);
526         }
527     }
528 
529   private:
530     static void
531         placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
532                           const nlohmann::json::json_pointer& locationToPlace,
533                           crow::Response& res)
534     {
535         multi->placeResult(locationToPlace, res);
536     }
537 
538     crow::App& app;
539     std::shared_ptr<bmcweb::AsyncResp> finalRes;
540 };
541 
542 inline void processTopAndSkip(const Query& query, crow::Response& res)
543 {
544     nlohmann::json::object_t* obj =
545         res.jsonValue.get_ptr<nlohmann::json::object_t*>();
546     if (obj == nullptr)
547     {
548         // Shouldn't be possible.  All responses should be objects.
549         messages::internalError(res);
550         return;
551     }
552 
553     BMCWEB_LOG_DEBUG << "Handling top/skip";
554     nlohmann::json::object_t::iterator members = obj->find("Members");
555     if (members == obj->end())
556     {
557         // From the Redfish specification 7.3.1
558         // ... the HTTP 400 Bad Request status code with the
559         // QueryNotSupportedOnResource message from the Base Message Registry
560         // for any supported query parameters that apply only to resource
561         // collections but are used on singular resources.
562         messages::queryNotSupportedOnResource(res);
563         return;
564     }
565 
566     nlohmann::json::array_t* arr =
567         members->second.get_ptr<nlohmann::json::array_t*>();
568     if (arr == nullptr)
569     {
570         messages::internalError(res);
571         return;
572     }
573 
574     // Per section 7.3.1 of the Redfish specification, $skip is run before $top
575     // Can only skip as many values as we have
576     size_t skip = std::min(arr->size(), query.skip);
577     arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
578 
579     size_t top = std::min(arr->size(), query.top);
580     arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
581 }
582 
583 inline void
584     processAllParams(crow::App& app, const Query& query,
585                      std::function<void(crow::Response&)>& completionHandler,
586                      crow::Response& intermediateResponse)
587 {
588     if (!completionHandler)
589     {
590         BMCWEB_LOG_DEBUG << "Function was invalid?";
591         return;
592     }
593 
594     BMCWEB_LOG_DEBUG << "Processing query params";
595     // If the request failed, there's no reason to even try to run query
596     // params.
597     if (intermediateResponse.resultInt() < 200 ||
598         intermediateResponse.resultInt() >= 400)
599     {
600         completionHandler(intermediateResponse);
601         return;
602     }
603     if (query.isOnly)
604     {
605         processOnly(app, intermediateResponse, completionHandler);
606         return;
607     }
608 
609     if (query.top != std::numeric_limits<size_t>::max() || query.skip != 0)
610     {
611         processTopAndSkip(query, intermediateResponse);
612     }
613 
614     if (query.expandType != ExpandType::None)
615     {
616         BMCWEB_LOG_DEBUG << "Executing expand query";
617         // TODO(ed) this is a copy of the response object.  Admittedly,
618         // we're inherently doing something inefficient, but we shouldn't
619         // have to do a full copy
620         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
621         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
622         asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue);
623         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
624 
625         multi->startQuery(query);
626         return;
627     }
628     completionHandler(intermediateResponse);
629 }
630 
631 } // namespace query_param
632 } // namespace redfish
633