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