xref: /openbmc/bmcweb/features/redfish/include/utils/query_param.hpp (revision a6b9125ff91500afed34dc923e9bafb90da75ec4)
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 
37 // The struct defines how resource handlers in redfish-core/lib/ can handle
38 // query parameters themselves, so that the default Redfish route will delegate
39 // the processing.
40 struct QueryCapabilities
41 {
42     bool canDelegateOnly = false;
43     uint8_t canDelegateExpandLevel = 0;
44 };
45 
46 // Delegates query parameters according to the given |queryCapabilities|
47 // This function doesn't check query parameter conflicts since the parse
48 // function will take care of it.
49 // Returns a delegated query object which can be used by individual resource
50 // handlers so that handlers don't need to query again.
51 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
52 {
53     Query delegated;
54     // delegate only
55     if (query.isOnly && queryCapabilities.canDelegateOnly)
56     {
57         delegated.isOnly = true;
58         query.isOnly = false;
59     }
60     // delegate expand as much as we can
61     if (query.expandType != ExpandType::None)
62     {
63         delegated.expandType = query.expandType;
64         if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
65         {
66             query.expandType = ExpandType::None;
67             delegated.expandLevel = query.expandLevel;
68             query.expandLevel = 0;
69         }
70         else
71         {
72             query.expandLevel -= queryCapabilities.canDelegateExpandLevel;
73             delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
74         }
75     }
76     return delegated;
77 }
78 
79 inline bool getExpandType(std::string_view value, Query& query)
80 {
81     if (value.empty())
82     {
83         return false;
84     }
85     switch (value[0])
86     {
87         case '*':
88             query.expandType = ExpandType::Both;
89             break;
90         case '.':
91             query.expandType = ExpandType::NotLinks;
92             break;
93         case '~':
94             query.expandType = ExpandType::Links;
95             break;
96         default:
97             return false;
98 
99             break;
100     }
101     value.remove_prefix(1);
102     if (value.empty())
103     {
104         query.expandLevel = 1;
105         return true;
106     }
107     constexpr std::string_view levels = "($levels=";
108     if (!value.starts_with(levels))
109     {
110         return false;
111     }
112     value.remove_prefix(levels.size());
113 
114     auto it = std::from_chars(value.data(), value.data() + value.size(),
115                               query.expandLevel);
116     if (it.ec != std::errc())
117     {
118         return false;
119     }
120     value.remove_prefix(static_cast<size_t>(it.ptr - value.data()));
121     return value == ")";
122 }
123 
124 inline std::optional<Query>
125     parseParameters(const boost::urls::params_view& urlParams,
126                     crow::Response& res)
127 {
128     Query ret;
129     for (const boost::urls::params_view::value_type& it : urlParams)
130     {
131         std::string_view key(it.key.data(), it.key.size());
132         std::string_view value(it.value.data(), it.value.size());
133         if (key == "only")
134         {
135             if (!it.value.empty())
136             {
137                 messages::queryParameterValueFormatError(res, value, key);
138                 return std::nullopt;
139             }
140             ret.isOnly = true;
141         }
142         else if (key == "$expand")
143         {
144             if (!getExpandType(value, ret))
145             {
146                 messages::queryParameterValueFormatError(res, value, key);
147                 return std::nullopt;
148             }
149         }
150         else
151         {
152             // Intentionally ignore other errors Redfish spec, 7.3.1
153             if (key.starts_with("$"))
154             {
155                 // Services shall return... The HTTP 501 Not Implemented
156                 // status code for any unsupported query parameters that
157                 // start with $ .
158                 messages::queryParameterValueFormatError(res, value, key);
159                 res.result(boost::beast::http::status::not_implemented);
160                 return std::nullopt;
161             }
162             // "Shall ignore unknown or unsupported query parameters that do
163             // not begin with $ ."
164         }
165     }
166 
167     return ret;
168 }
169 
170 inline bool processOnly(crow::App& app, crow::Response& res,
171                         std::function<void(crow::Response&)>& completionHandler)
172 {
173     BMCWEB_LOG_DEBUG << "Processing only query param";
174     auto itMembers = res.jsonValue.find("Members");
175     if (itMembers == res.jsonValue.end())
176     {
177         messages::queryNotSupportedOnResource(res);
178         completionHandler(res);
179         return false;
180     }
181     auto itMemBegin = itMembers->begin();
182     if (itMemBegin == itMembers->end() || itMembers->size() != 1)
183     {
184         BMCWEB_LOG_DEBUG << "Members contains " << itMembers->size()
185                          << " element, returning full collection.";
186         completionHandler(res);
187         return false;
188     }
189 
190     auto itUrl = itMemBegin->find("@odata.id");
191     if (itUrl == itMemBegin->end())
192     {
193         BMCWEB_LOG_DEBUG << "No found odata.id";
194         messages::internalError(res);
195         completionHandler(res);
196         return false;
197     }
198     const std::string* url = itUrl->get_ptr<const std::string*>();
199     if (url == nullptr)
200     {
201         BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????";
202         messages::internalError(res);
203         completionHandler(res);
204         return false;
205     }
206     // TODO(Ed) copy request headers?
207     // newReq.session = req.session;
208     std::error_code ec;
209     crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec);
210     if (ec)
211     {
212         messages::internalError(res);
213         completionHandler(res);
214         return false;
215     }
216 
217     auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
218     BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res;
219     asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
220     asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper());
221     app.handle(newReq, asyncResp);
222     return true;
223 }
224 
225 struct ExpandNode
226 {
227     nlohmann::json::json_pointer location;
228     std::string uri;
229 
230     inline bool operator==(const ExpandNode& other) const
231     {
232         return location == other.location && uri == other.uri;
233     }
234 };
235 
236 // Walks a json object looking for Redfish NavigationReference entries that
237 // might need resolved.  It recursively walks the jsonResponse object, looking
238 // for links at every level, and returns a list (out) of locations within the
239 // tree that need to be expanded.  The current json pointer location p is passed
240 // in to reference the current node that's being expanded, so it can be combined
241 // with the keys from the jsonResponse object
242 inline void findNavigationReferencesRecursive(
243     ExpandType eType, nlohmann::json& jsonResponse,
244     const nlohmann::json::json_pointer& p, bool inLinks,
245     std::vector<ExpandNode>& out)
246 {
247     // If no expand is needed, return early
248     if (eType == ExpandType::None)
249     {
250         return;
251     }
252     nlohmann::json::array_t* array =
253         jsonResponse.get_ptr<nlohmann::json::array_t*>();
254     if (array != nullptr)
255     {
256         size_t index = 0;
257         // For arrays, walk every element in the array
258         for (auto& element : *array)
259         {
260             nlohmann::json::json_pointer newPtr = p / index;
261             BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string();
262             findNavigationReferencesRecursive(eType, element, newPtr, inLinks,
263                                               out);
264             index++;
265         }
266     }
267     nlohmann::json::object_t* obj =
268         jsonResponse.get_ptr<nlohmann::json::object_t*>();
269     if (obj == nullptr)
270     {
271         return;
272     }
273     // Navigation References only ever have a single element
274     if (obj->size() == 1)
275     {
276         if (obj->begin()->first == "@odata.id")
277         {
278             const std::string* uri =
279                 obj->begin()->second.get_ptr<const std::string*>();
280             if (uri != nullptr)
281             {
282                 BMCWEB_LOG_DEBUG << "Found element at " << p.to_string();
283                 out.push_back({p, *uri});
284             }
285         }
286     }
287     // Loop the object and look for links
288     for (auto& element : *obj)
289     {
290         if (!inLinks)
291         {
292             // Check if this is a links node
293             inLinks = element.first == "Links";
294         }
295         // Only traverse the parts of the tree the user asked for
296         // Per section 7.3 of the redfish specification
297         if (inLinks && eType == ExpandType::NotLinks)
298         {
299             continue;
300         }
301         if (!inLinks && eType == ExpandType::Links)
302         {
303             continue;
304         }
305         nlohmann::json::json_pointer newPtr = p / element.first;
306         BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr;
307 
308         findNavigationReferencesRecursive(eType, element.second, newPtr,
309                                           inLinks, out);
310     }
311 }
312 
313 inline std::vector<ExpandNode>
314     findNavigationReferences(ExpandType eType, nlohmann::json& jsonResponse,
315                              const nlohmann::json::json_pointer& root)
316 {
317     std::vector<ExpandNode> ret;
318     findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret);
319     return ret;
320 }
321 
322 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
323 {
324   public:
325     // This object takes a single asyncResp object as the "final" one, then
326     // allows callers to attach sub-responses within the json tree that need
327     // to be executed and filled into their appropriate locations.  This
328     // class manages the final "merge" of the json resources.
329     MultiAsyncResp(crow::App& app,
330                    std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
331         app(app),
332         finalRes(std::move(finalResIn))
333     {}
334 
335     void addAwaitingResponse(
336         Query query, std::shared_ptr<bmcweb::AsyncResp>& res,
337         const nlohmann::json::json_pointer& finalExpandLocation)
338     {
339         res->res.setCompleteRequestHandler(std::bind_front(
340             onEndStatic, shared_from_this(), query, finalExpandLocation));
341     }
342 
343     void onEnd(Query query, const nlohmann::json::json_pointer& locationToPlace,
344                crow::Response& res)
345     {
346         nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
347         finalObj = std::move(res.jsonValue);
348 
349         if (query.expandLevel <= 0)
350         {
351             // Last level to expand, no need to go deeper
352             return;
353         }
354         // Now decrease the depth by one to account for the tree node we
355         // just resolved
356         query.expandLevel--;
357 
358         std::vector<ExpandNode> nodes = findNavigationReferences(
359             query.expandType, finalObj, locationToPlace);
360         BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse";
361         for (const ExpandNode& node : nodes)
362         {
363             BMCWEB_LOG_DEBUG << "Expanding " << locationToPlace;
364             std::error_code ec;
365             crow::Request newReq({boost::beast::http::verb::get, node.uri, 11},
366                                  ec);
367             if (ec)
368             {
369                 messages::internalError(res);
370                 return;
371             }
372 
373             auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
374             BMCWEB_LOG_DEBUG << "setting completion handler on "
375                              << &asyncResp->res;
376             addAwaitingResponse(query, asyncResp, node.location);
377             app.handle(newReq, asyncResp);
378         }
379     }
380 
381   private:
382     static void onEndStatic(const std::shared_ptr<MultiAsyncResp>& multi,
383                             Query query,
384                             const nlohmann::json::json_pointer& locationToPlace,
385                             crow::Response& res)
386     {
387         multi->onEnd(query, locationToPlace, res);
388     }
389 
390     crow::App& app;
391     std::shared_ptr<bmcweb::AsyncResp> finalRes;
392 };
393 
394 inline void
395     processAllParams(crow::App& app, const Query query,
396 
397                      std::function<void(crow::Response&)>& completionHandler,
398                      crow::Response& intermediateResponse)
399 {
400     if (!completionHandler)
401     {
402         BMCWEB_LOG_DEBUG << "Function was invalid?";
403         return;
404     }
405 
406     BMCWEB_LOG_DEBUG << "Processing query params";
407     // If the request failed, there's no reason to even try to run query
408     // params.
409     if (intermediateResponse.resultInt() < 200 ||
410         intermediateResponse.resultInt() >= 400)
411     {
412         completionHandler(intermediateResponse);
413         return;
414     }
415     if (query.isOnly)
416     {
417         processOnly(app, intermediateResponse, completionHandler);
418         return;
419     }
420     if (query.expandType != ExpandType::None)
421     {
422         BMCWEB_LOG_DEBUG << "Executing expand query";
423         // TODO(ed) this is a copy of the response object.  Admittedly,
424         // we're inherently doing something inefficient, but we shouldn't
425         // have to do a full copy
426         auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
427         asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
428         asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue);
429         auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
430 
431         // Start the chain by "ending" the root response
432         multi->onEnd(query, nlohmann::json::json_pointer(""), asyncResp->res);
433         return;
434     }
435     completionHandler(intermediateResponse);
436 }
437 
438 } // namespace query_param
439 } // namespace redfish
440