1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors
3 #pragma once
4 #include "bmcweb_config.h"
5
6 #include "app.hpp"
7 #include "async_resp.hpp"
8 #include "error_message_utils.hpp"
9 #include "error_messages.hpp"
10 #include "filter_expr_executor.hpp"
11 #include "filter_expr_printer.hpp"
12 #include "http_request.hpp"
13 #include "http_response.hpp"
14 #include "json_formatters.hpp"
15 #include "logging.hpp"
16 #include "str_utility.hpp"
17
18 #include <sys/types.h>
19
20 #include <boost/beast/http/message.hpp>
21 #include <boost/beast/http/status.hpp>
22 #include <boost/beast/http/verb.hpp>
23 #include <boost/url/params_view.hpp>
24 #include <nlohmann/json.hpp>
25
26 #include <algorithm>
27 #include <array>
28 #include <cctype>
29 #include <charconv>
30 #include <compare>
31 #include <cstdint>
32 #include <functional>
33 #include <iterator>
34 #include <limits>
35 #include <map>
36 #include <memory>
37 #include <optional>
38 #include <ranges>
39 #include <string>
40 #include <string_view>
41 #include <system_error>
42 #include <utility>
43 #include <vector>
44
45 namespace redfish
46 {
47 namespace query_param
48 {
49
50 enum class ExpandType : uint8_t
51 {
52 None,
53 Links,
54 NotLinks,
55 Both,
56 };
57
58 // A simple implementation of Trie to help |recursiveSelect|.
59 class SelectTrieNode
60 {
61 public:
62 SelectTrieNode() = default;
63
find(const std::string & jsonKey) const64 const SelectTrieNode* find(const std::string& jsonKey) const
65 {
66 auto it = children.find(jsonKey);
67 if (it == children.end())
68 {
69 return nullptr;
70 }
71 return &it->second;
72 }
73
74 // Creates a new node if the key doesn't exist, returns the reference to the
75 // newly created node; otherwise, return the reference to the existing node
emplace(std::string_view jsonKey)76 SelectTrieNode* emplace(std::string_view jsonKey)
77 {
78 auto [it, _] = children.emplace(jsonKey, SelectTrieNode{});
79 return &it->second;
80 }
81
empty() const82 bool empty() const
83 {
84 return children.empty();
85 }
86
clear()87 void clear()
88 {
89 children.clear();
90 }
91
setToSelected()92 void setToSelected()
93 {
94 selected = true;
95 }
96
isSelected() const97 bool isSelected() const
98 {
99 return selected;
100 }
101
102 private:
103 std::map<std::string, SelectTrieNode, std::less<>> children;
104 bool selected = false;
105 };
106
107 // Validates the property in the $select parameter. Every character is among
108 // [a-zA-Z0-9#@_.] (taken from Redfish spec, section 9.6 Properties)
isSelectedPropertyAllowed(std::string_view property)109 inline bool isSelectedPropertyAllowed(std::string_view property)
110 {
111 // These a magic number, but with it it's less likely that this code
112 // introduces CVE; e.g., too large properties crash the service.
113 constexpr int maxPropertyLength = 60;
114 if (property.empty() || property.size() > maxPropertyLength)
115 {
116 return false;
117 }
118 for (char ch : property)
119 {
120 if (std::isalnum(static_cast<unsigned char>(ch)) == 0 && ch != '#' &&
121 ch != '@' && ch != '.')
122 {
123 return false;
124 }
125 }
126 return true;
127 }
128
129 struct SelectTrie
130 {
131 SelectTrie() = default;
132
133 // Inserts a $select value; returns false if the nestedProperty is illegal.
insertNoderedfish::query_param::SelectTrie134 bool insertNode(std::string_view nestedProperty)
135 {
136 if (nestedProperty.empty())
137 {
138 return false;
139 }
140 SelectTrieNode* currNode = &root;
141 size_t index = nestedProperty.find_first_of('/');
142 while (!nestedProperty.empty())
143 {
144 std::string_view property = nestedProperty.substr(0, index);
145 if (!isSelectedPropertyAllowed(property))
146 {
147 return false;
148 }
149 currNode = currNode->emplace(property);
150 if (index == std::string::npos)
151 {
152 break;
153 }
154 nestedProperty.remove_prefix(index + 1);
155 index = nestedProperty.find_first_of('/');
156 }
157 currNode->setToSelected();
158 return true;
159 }
160
161 SelectTrieNode root;
162 };
163
164 // The struct stores the parsed query parameters of the default Redfish route.
165 struct Query
166 {
167 // Only
168 bool isOnly = false;
169
170 // Expand
171 uint8_t expandLevel = 0;
172 ExpandType expandType = ExpandType::None;
173
174 // Skip
175 std::optional<size_t> skip = std::nullopt;
176
177 // Top
178 static constexpr size_t maxTop = 1000; // Max entries a response contain
179 std::optional<size_t> top = std::nullopt;
180
181 // Filter
182 std::optional<filter_ast::LogicalAnd> filter = std::nullopt;
183
184 // Select
185 // Unclear how to make this use structured initialization without this.
186 // Might be a tidy bug? Ignore for now
187 // NOLINTNEXTLINE(readability-redundant-member-init)
188 SelectTrie selectTrie{};
189 };
190
191 // The struct defines how resource handlers in redfish-core/lib/ can handle
192 // query parameters themselves, so that the default Redfish route will delegate
193 // the processing.
194 struct QueryCapabilities
195 {
196 bool canDelegateOnly = false;
197 bool canDelegateTop = false;
198 bool canDelegateSkip = false;
199 uint8_t canDelegateExpandLevel = 0;
200 bool canDelegateSelect = false;
201 };
202
203 // Delegates query parameters according to the given |queryCapabilities|
204 // This function doesn't check query parameter conflicts since the parse
205 // function will take care of it.
206 // Returns a delegated query object which can be used by individual resource
207 // handlers so that handlers don't need to query again.
delegate(const QueryCapabilities & queryCapabilities,Query & query)208 inline Query delegate(const QueryCapabilities& queryCapabilities, Query& query)
209 {
210 Query delegated{};
211 // delegate only
212 if (query.isOnly && queryCapabilities.canDelegateOnly)
213 {
214 delegated.isOnly = true;
215 query.isOnly = false;
216 }
217 // delegate expand as much as we can
218 if (query.expandType != ExpandType::None)
219 {
220 delegated.expandType = query.expandType;
221 if (query.expandLevel <= queryCapabilities.canDelegateExpandLevel)
222 {
223 query.expandType = ExpandType::None;
224 delegated.expandLevel = query.expandLevel;
225 query.expandLevel = 0;
226 }
227 else
228 {
229 delegated.expandLevel = queryCapabilities.canDelegateExpandLevel;
230 }
231 }
232
233 // delegate top
234 if (query.top && queryCapabilities.canDelegateTop)
235 {
236 delegated.top = query.top;
237 query.top = std::nullopt;
238 }
239
240 // delegate skip
241 if (query.skip && queryCapabilities.canDelegateSkip)
242 {
243 delegated.skip = query.skip;
244 query.skip = 0;
245 }
246
247 // delegate select
248 if (!query.selectTrie.root.empty() && queryCapabilities.canDelegateSelect)
249 {
250 delegated.selectTrie = std::move(query.selectTrie);
251 query.selectTrie.root.clear();
252 }
253 return delegated;
254 }
255
getExpandType(std::string_view value,Query & query)256 inline bool getExpandType(std::string_view value, Query& query)
257 {
258 if (value.empty())
259 {
260 return false;
261 }
262 switch (value[0])
263 {
264 case '*':
265 query.expandType = ExpandType::Both;
266 break;
267 case '.':
268 query.expandType = ExpandType::NotLinks;
269 break;
270 case '~':
271 query.expandType = ExpandType::Links;
272 break;
273 default:
274 return false;
275 }
276 value.remove_prefix(1);
277 if (value.empty())
278 {
279 query.expandLevel = 1;
280 return true;
281 }
282 constexpr std::string_view levels = "($levels=";
283 if (!value.starts_with(levels))
284 {
285 return false;
286 }
287 value.remove_prefix(levels.size());
288
289 auto it = std::from_chars(value.begin(), value.end(), query.expandLevel);
290 if (it.ec != std::errc())
291 {
292 return false;
293 }
294 value.remove_prefix(
295 static_cast<size_t>(std::distance(value.begin(), it.ptr)));
296 return value == ")";
297 }
298
299 enum class QueryError
300 {
301 Ok,
302 OutOfRange,
303 ValueFormat,
304 };
305
getNumericParam(std::string_view value,size_t & param)306 inline QueryError getNumericParam(std::string_view value, size_t& param)
307 {
308 std::from_chars_result r =
309 std::from_chars(value.begin(), value.end(), param);
310
311 // If the number wasn't representable in the type, it's out of range
312 if (r.ec == std::errc::result_out_of_range)
313 {
314 return QueryError::OutOfRange;
315 }
316 // All other errors are value format
317 if (r.ec != std::errc())
318 {
319 return QueryError::ValueFormat;
320 }
321 return QueryError::Ok;
322 }
323
getSkipParam(std::string_view value,Query & query)324 inline QueryError getSkipParam(std::string_view value, Query& query)
325 {
326 return getNumericParam(value, query.skip.emplace());
327 }
328
getTopParam(std::string_view value,Query & query)329 inline QueryError getTopParam(std::string_view value, Query& query)
330 {
331 QueryError ret = getNumericParam(value, query.top.emplace());
332 if (ret != QueryError::Ok)
333 {
334 return ret;
335 }
336
337 // Range check for sanity.
338 if (query.top > Query::maxTop)
339 {
340 return QueryError::OutOfRange;
341 }
342
343 return QueryError::Ok;
344 }
345
346 // Parses and validates the $select parameter.
347 // As per OData URL Conventions and Redfish Spec, the $select values shall be
348 // comma separated Resource Path
349 // Ref:
350 // 1. https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
351 // 2.
352 // https://docs.oasis-open.org/odata/odata/v4.01/os/abnf/odata-abnf-construction-rules.txt
getSelectParam(std::string_view value,Query & query)353 inline bool getSelectParam(std::string_view value, Query& query)
354 {
355 std::vector<std::string> properties;
356 bmcweb::split(properties, value, ',');
357 if (properties.empty())
358 {
359 return false;
360 }
361 // These a magic number, but with it it's less likely that this code
362 // introduces CVE; e.g., too large properties crash the service.
363 constexpr int maxNumProperties = 10;
364 if (properties.size() > maxNumProperties)
365 {
366 return false;
367 }
368 for (const auto& property : properties)
369 {
370 if (!query.selectTrie.insertNode(property))
371 {
372 return false;
373 }
374 }
375 return true;
376 }
377
378 // Parses and validates the $filter parameter.
getFilterParam(std::string_view value,Query & query)379 inline bool getFilterParam(std::string_view value, Query& query)
380 {
381 query.filter = parseFilter(value);
382 return query.filter.has_value();
383 }
384
385 inline std::optional<Query>
parseParameters(boost::urls::params_view urlParams,crow::Response & res)386 parseParameters(boost::urls::params_view urlParams, crow::Response& res)
387 {
388 Query ret{};
389 for (const boost::urls::params_view::value_type& it : urlParams)
390 {
391 if (it.key == "only")
392 {
393 if (!it.value.empty())
394 {
395 messages::queryParameterValueFormatError(res, it.value, it.key);
396 return std::nullopt;
397 }
398 ret.isOnly = true;
399 }
400 else if (it.key == "$expand" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
401 {
402 if (!getExpandType(it.value, ret))
403 {
404 messages::queryParameterValueFormatError(res, it.value, it.key);
405 return std::nullopt;
406 }
407 }
408 else if (it.key == "$top")
409 {
410 QueryError topRet = getTopParam(it.value, ret);
411 if (topRet == QueryError::ValueFormat)
412 {
413 messages::queryParameterValueFormatError(res, it.value, it.key);
414 return std::nullopt;
415 }
416 if (topRet == QueryError::OutOfRange)
417 {
418 messages::queryParameterOutOfRange(
419 res, it.value, "$top",
420 "0-" + std::to_string(Query::maxTop));
421 return std::nullopt;
422 }
423 }
424 else if (it.key == "$skip")
425 {
426 QueryError topRet = getSkipParam(it.value, ret);
427 if (topRet == QueryError::ValueFormat)
428 {
429 messages::queryParameterValueFormatError(res, it.value, it.key);
430 return std::nullopt;
431 }
432 if (topRet == QueryError::OutOfRange)
433 {
434 messages::queryParameterOutOfRange(
435 res, it.value, it.key,
436 "0-" + std::to_string(std::numeric_limits<size_t>::max()));
437 return std::nullopt;
438 }
439 }
440 else if (it.key == "$select")
441 {
442 if (!getSelectParam(it.value, ret))
443 {
444 messages::queryParameterValueFormatError(res, it.value, it.key);
445 return std::nullopt;
446 }
447 }
448 else if (it.key == "$filter" && BMCWEB_INSECURE_ENABLE_REDFISH_QUERY)
449 {
450 if (!getFilterParam(it.value, ret))
451 {
452 messages::queryParameterValueFormatError(res, it.value, it.key);
453 return std::nullopt;
454 }
455 }
456 else
457 {
458 // Intentionally ignore other errors Redfish spec, 7.3.1
459 if (it.key.starts_with("$"))
460 {
461 // Services shall return... The HTTP 501 Not Implemented
462 // status code for any unsupported query parameters that
463 // start with $ .
464 messages::queryParameterValueFormatError(res, it.value, it.key);
465 res.result(boost::beast::http::status::not_implemented);
466 return std::nullopt;
467 }
468 // "Shall ignore unknown or unsupported query parameters that do
469 // not begin with $ ."
470 }
471 }
472
473 if (ret.expandType != ExpandType::None && !ret.selectTrie.root.empty())
474 {
475 messages::queryCombinationInvalid(res);
476 return std::nullopt;
477 }
478
479 return ret;
480 }
481
processOnly(crow::App & app,crow::Response & res,std::function<void (crow::Response &)> & completionHandler)482 inline bool processOnly(crow::App& app, crow::Response& res,
483 std::function<void(crow::Response&)>& completionHandler)
484 {
485 BMCWEB_LOG_DEBUG("Processing only query param");
486 auto itMembers = res.jsonValue.find("Members");
487 if (itMembers == res.jsonValue.end())
488 {
489 messages::queryNotSupportedOnResource(res);
490 completionHandler(res);
491 return false;
492 }
493 auto itMemBegin = itMembers->begin();
494 if (itMemBegin == itMembers->end() || itMembers->size() != 1)
495 {
496 BMCWEB_LOG_DEBUG(
497 "Members contains {} element, returning full collection.",
498 itMembers->size());
499 completionHandler(res);
500 return false;
501 }
502
503 auto itUrl = itMemBegin->find("@odata.id");
504 if (itUrl == itMemBegin->end())
505 {
506 BMCWEB_LOG_DEBUG("No found odata.id");
507 messages::internalError(res);
508 completionHandler(res);
509 return false;
510 }
511 const std::string* url = itUrl->get_ptr<const std::string*>();
512 if (url == nullptr)
513 {
514 BMCWEB_LOG_DEBUG("@odata.id wasn't a string????");
515 messages::internalError(res);
516 completionHandler(res);
517 return false;
518 }
519 // TODO(Ed) copy request headers?
520 // newReq.session = req.session;
521 std::error_code ec;
522 auto newReq = std::make_shared<crow::Request>(
523 crow::Request::Body{boost::beast::http::verb::get, *url, 11}, ec);
524 if (ec)
525 {
526 messages::internalError(res);
527 completionHandler(res);
528 return false;
529 }
530
531 auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
532 BMCWEB_LOG_DEBUG("setting completion handler on {}",
533 logPtr(&asyncResp->res));
534 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
535 app.handle(newReq, asyncResp);
536 return true;
537 }
538
539 struct ExpandNode
540 {
541 nlohmann::json::json_pointer location;
542 std::string uri;
543
operator ==redfish::query_param::ExpandNode544 bool operator==(const ExpandNode& other) const
545 {
546 return location == other.location && uri == other.uri;
547 }
548 };
549
550 inline void findNavigationReferencesInArrayRecursive(
551 ExpandType eType, nlohmann::json::array_t& array,
552 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
553 bool inLinks, std::vector<ExpandNode>& out);
554
555 inline void findNavigationReferencesInObjectRecursive(
556 ExpandType eType, nlohmann::json::object_t& obj,
557 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
558 bool inLinks, std::vector<ExpandNode>& out);
559
560 // Walks a json object looking for Redfish NavigationReference entries that
561 // might need resolved. It recursively walks the jsonResponse object, looking
562 // for links at every level, and returns a list (out) of locations within the
563 // tree that need to be expanded. The current json pointer location p is passed
564 // in to reference the current node that's being expanded, so it can be combined
565 // with the keys from the jsonResponse object
findNavigationReferencesRecursive(ExpandType eType,nlohmann::json & jsonResponse,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)566 inline void findNavigationReferencesRecursive(
567 ExpandType eType, nlohmann::json& jsonResponse,
568 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
569 bool inLinks, std::vector<ExpandNode>& out)
570 {
571 // If no expand is needed, return early
572 if (eType == ExpandType::None)
573 {
574 return;
575 }
576
577 nlohmann::json::array_t* array =
578 jsonResponse.get_ptr<nlohmann::json::array_t*>();
579 if (array != nullptr)
580 {
581 findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth,
582 skipDepth, inLinks, out);
583 }
584 nlohmann::json::object_t* obj =
585 jsonResponse.get_ptr<nlohmann::json::object_t*>();
586 if (obj == nullptr)
587 {
588 return;
589 }
590 findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth,
591 skipDepth, inLinks, out);
592 }
593
findNavigationReferencesInArrayRecursive(ExpandType eType,nlohmann::json::array_t & array,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)594 inline void findNavigationReferencesInArrayRecursive(
595 ExpandType eType, nlohmann::json::array_t& array,
596 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
597 bool inLinks, std::vector<ExpandNode>& out)
598 {
599 size_t index = 0;
600 // For arrays, walk every element in the array
601 for (auto& element : array)
602 {
603 nlohmann::json::json_pointer newPtr = jsonPtr / index;
604 BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr.to_string());
605 findNavigationReferencesRecursive(eType, element, newPtr, depth,
606 skipDepth, inLinks, out);
607 index++;
608 }
609 }
610
findNavigationReferencesInObjectRecursive(ExpandType eType,nlohmann::json::object_t & obj,const nlohmann::json::json_pointer & jsonPtr,int depth,int skipDepth,bool inLinks,std::vector<ExpandNode> & out)611 inline void findNavigationReferencesInObjectRecursive(
612 ExpandType eType, nlohmann::json::object_t& obj,
613 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth,
614 bool inLinks, std::vector<ExpandNode>& out)
615 {
616 // Navigation References only ever have a single element
617 if (obj.size() == 1)
618 {
619 if (obj.begin()->first == "@odata.id")
620 {
621 const std::string* uri =
622 obj.begin()->second.get_ptr<const std::string*>();
623 if (uri != nullptr)
624 {
625 BMCWEB_LOG_DEBUG("Found {} at {}", *uri, jsonPtr.to_string());
626 if (skipDepth == 0)
627 {
628 out.push_back({jsonPtr, *uri});
629 }
630 return;
631 }
632 }
633 }
634
635 int newDepth = depth;
636 auto odataId = obj.find("@odata.id");
637 if (odataId != obj.end())
638 {
639 // The Redfish spec requires all resources to include the resource
640 // identifier. If the object has multiple elements and one of them is
641 // "@odata.id" then that means we have entered a new level / expanded
642 // resource. We need to stop traversing if we're already at the desired
643 // depth
644 if (obj.size() > 1)
645 {
646 if (depth == 0)
647 {
648 return;
649 }
650 if (skipDepth > 0)
651 {
652 skipDepth--;
653 }
654 }
655
656 if (skipDepth == 0)
657 {
658 newDepth--;
659 }
660 }
661
662 // Loop the object and look for links
663 for (auto& element : obj)
664 {
665 bool localInLinks = inLinks;
666 if (!localInLinks)
667 {
668 // Check if this is a links node
669 localInLinks = element.first == "Links";
670 }
671 // Only traverse the parts of the tree the user asked for
672 // Per section 7.3 of the redfish specification
673 if (localInLinks && eType == ExpandType::NotLinks)
674 {
675 continue;
676 }
677 if (!localInLinks && eType == ExpandType::Links)
678 {
679 continue;
680 }
681 nlohmann::json::json_pointer newPtr = jsonPtr / element.first;
682 BMCWEB_LOG_DEBUG("Traversing response at {}", newPtr);
683
684 findNavigationReferencesRecursive(eType, element.second, newPtr,
685 newDepth, skipDepth, localInLinks,
686 out);
687 }
688 }
689
690 // TODO: When aggregation is enabled and we receive a partially expanded
691 // response we may need need additional handling when the original URI was
692 // up tree from a top level collection.
693 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556
694 // lands. May want to avoid forwarding query params when request is uptree from
695 // a top level collection.
findNavigationReferences(ExpandType eType,int depth,int skipDepth,nlohmann::json & jsonResponse)696 inline std::vector<ExpandNode> findNavigationReferences(
697 ExpandType eType, int depth, int skipDepth, nlohmann::json& jsonResponse)
698 {
699 std::vector<ExpandNode> ret;
700 const nlohmann::json::json_pointer root = nlohmann::json::json_pointer("");
701 // SkipDepth +1 since we are skipping the root by default.
702 findNavigationReferencesRecursive(eType, jsonResponse, root, depth,
703 skipDepth + 1, false, ret);
704 return ret;
705 }
706
707 // Formats a query parameter string for the sub-query.
708 // Returns std::nullopt on failures.
709 // This function shall handle $select when it is added.
710 // There is no need to handle parameters that's not compatible with $expand,
711 // e.g., $only, since this function will only be called in side $expand handlers
formatQueryForExpand(const Query & query)712 inline std::optional<std::string> formatQueryForExpand(const Query& query)
713 {
714 // query.expandLevel<=1: no need to do subqueries
715 if (query.expandLevel <= 1)
716 {
717 return "";
718 }
719 std::string str = "?$expand=";
720 switch (query.expandType)
721 {
722 case ExpandType::Links:
723 str += '~';
724 break;
725 case ExpandType::NotLinks:
726 str += '.';
727 break;
728 case ExpandType::Both:
729 str += '*';
730 break;
731 case ExpandType::None:
732 return "";
733 default:
734 return std::nullopt;
735 }
736 str += "($levels=";
737 str += std::to_string(query.expandLevel - 1);
738 str += ')';
739 return str;
740 }
741
742 // Propagates the worst error code to the final response.
743 // The order of error code is (from high to low)
744 // 500 Internal Server Error
745 // 511 Network Authentication Required
746 // 510 Not Extended
747 // 508 Loop Detected
748 // 507 Insufficient Storage
749 // 506 Variant Also Negotiates
750 // 505 HTTP Version Not Supported
751 // 504 Gateway Timeout
752 // 503 Service Unavailable
753 // 502 Bad Gateway
754 // 501 Not Implemented
755 // 401 Unauthorized
756 // 451 - 409 Error codes (not listed explicitly)
757 // 408 Request Timeout
758 // 407 Proxy Authentication Required
759 // 406 Not Acceptable
760 // 405 Method Not Allowed
761 // 404 Not Found
762 // 403 Forbidden
763 // 402 Payment Required
764 // 400 Bad Request
propogateErrorCode(unsigned finalCode,unsigned subResponseCode)765 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode)
766 {
767 // We keep a explicit list for error codes that this project often uses
768 // Higher priority codes are in lower indexes
769 constexpr std::array<unsigned, 13> orderedCodes = {
770 500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400};
771 size_t finalCodeIndex = std::numeric_limits<size_t>::max();
772 size_t subResponseCodeIndex = std::numeric_limits<size_t>::max();
773 for (size_t i = 0; i < orderedCodes.size(); ++i)
774 {
775 if (orderedCodes[i] == finalCode)
776 {
777 finalCodeIndex = i;
778 }
779 if (orderedCodes[i] == subResponseCode)
780 {
781 subResponseCodeIndex = i;
782 }
783 }
784 if (finalCodeIndex != std::numeric_limits<size_t>::max() &&
785 subResponseCodeIndex != std::numeric_limits<size_t>::max())
786 {
787 return finalCodeIndex <= subResponseCodeIndex
788 ? finalCode
789 : subResponseCode;
790 }
791 if (subResponseCode == 500 || finalCode == 500)
792 {
793 return 500;
794 }
795 if (subResponseCode > 500 || finalCode > 500)
796 {
797 return std::max(finalCode, subResponseCode);
798 }
799 if (subResponseCode == 401)
800 {
801 return subResponseCode;
802 }
803 return std::max(finalCode, subResponseCode);
804 }
805
806 // Propagates all error messages into |finalResponse|
propogateError(crow::Response & finalResponse,crow::Response & subResponse)807 inline void propogateError(crow::Response& finalResponse,
808 crow::Response& subResponse)
809 {
810 // no errors
811 if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400)
812 {
813 return;
814 }
815 messages::moveErrorsToErrorJson(finalResponse.jsonValue,
816 subResponse.jsonValue);
817 finalResponse.result(
818 propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt()));
819 }
820
821 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp>
822 {
823 public:
824 // This object takes a single asyncResp object as the "final" one, then
825 // allows callers to attach sub-responses within the json tree that need
826 // to be executed and filled into their appropriate locations. This
827 // class manages the final "merge" of the json resources.
MultiAsyncResp(crow::App & appIn,std::shared_ptr<bmcweb::AsyncResp> finalResIn)828 MultiAsyncResp(crow::App& appIn,
829 std::shared_ptr<bmcweb::AsyncResp> finalResIn) :
830 app(appIn), finalRes(std::move(finalResIn))
831 {}
832
addAwaitingResponse(const std::shared_ptr<bmcweb::AsyncResp> & res,const nlohmann::json::json_pointer & finalExpandLocation)833 void addAwaitingResponse(
834 const std::shared_ptr<bmcweb::AsyncResp>& res,
835 const nlohmann::json::json_pointer& finalExpandLocation)
836 {
837 res->res.setCompleteRequestHandler(std::bind_front(
838 placeResultStatic, shared_from_this(), finalExpandLocation));
839 }
840
placeResult(const nlohmann::json::json_pointer & locationToPlace,crow::Response & res)841 void placeResult(const nlohmann::json::json_pointer& locationToPlace,
842 crow::Response& res)
843 {
844 BMCWEB_LOG_DEBUG("placeResult for {}", locationToPlace);
845 propogateError(finalRes->res, res);
846 if (!res.jsonValue.is_object() || res.jsonValue.empty())
847 {
848 return;
849 }
850 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace];
851 finalObj = std::move(res.jsonValue);
852 }
853
854 // Handles the very first level of Expand, and starts a chain of sub-queries
855 // for deeper levels.
startQuery(const Query & query,const Query & delegated)856 void startQuery(const Query& query, const Query& delegated)
857 {
858 std::vector<ExpandNode> nodes = findNavigationReferences(
859 query.expandType, query.expandLevel, delegated.expandLevel,
860 finalRes->res.jsonValue);
861 BMCWEB_LOG_DEBUG("{} nodes to traverse", nodes.size());
862 const std::optional<std::string> queryStr = formatQueryForExpand(query);
863 if (!queryStr)
864 {
865 messages::internalError(finalRes->res);
866 return;
867 }
868 for (const ExpandNode& node : nodes)
869 {
870 const std::string subQuery = node.uri + *queryStr;
871 BMCWEB_LOG_DEBUG("URL of subquery: {}", subQuery);
872 std::error_code ec;
873 auto newReq = std::make_shared<crow::Request>(
874 crow::Request::Body{boost::beast::http::verb::get, subQuery,
875 11},
876 ec);
877 if (ec)
878 {
879 messages::internalError(finalRes->res);
880 return;
881 }
882
883 auto asyncResp = std::make_shared<bmcweb::AsyncResp>();
884 BMCWEB_LOG_DEBUG("setting completion handler on {}",
885 logPtr(&asyncResp->res));
886
887 addAwaitingResponse(asyncResp, node.location);
888 app.handle(newReq, asyncResp);
889 }
890 }
891
892 private:
893 static void
placeResultStatic(const std::shared_ptr<MultiAsyncResp> & multi,const nlohmann::json::json_pointer & locationToPlace,crow::Response & res)894 placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi,
895 const nlohmann::json::json_pointer& locationToPlace,
896 crow::Response& res)
897 {
898 multi->placeResult(locationToPlace, res);
899 }
900
901 crow::App& app;
902 std::shared_ptr<bmcweb::AsyncResp> finalRes;
903 };
904
processTopAndSkip(const Query & query,crow::Response & res)905 inline void processTopAndSkip(const Query& query, crow::Response& res)
906 {
907 if (!query.skip && !query.top)
908 {
909 // No work to do.
910 return;
911 }
912 nlohmann::json::object_t* obj =
913 res.jsonValue.get_ptr<nlohmann::json::object_t*>();
914 if (obj == nullptr)
915 {
916 // Shouldn't be possible. All responses should be objects.
917 messages::internalError(res);
918 return;
919 }
920
921 BMCWEB_LOG_DEBUG("Handling top/skip");
922 nlohmann::json::object_t::iterator members = obj->find("Members");
923 if (members == obj->end())
924 {
925 // From the Redfish specification 7.3.1
926 // ... the HTTP 400 Bad Request status code with the
927 // QueryNotSupportedOnResource message from the Base Message Registry
928 // for any supported query parameters that apply only to resource
929 // collections but are used on singular resources.
930 messages::queryNotSupportedOnResource(res);
931 return;
932 }
933
934 nlohmann::json::array_t* arr =
935 members->second.get_ptr<nlohmann::json::array_t*>();
936 if (arr == nullptr)
937 {
938 messages::internalError(res);
939 return;
940 }
941
942 if (query.skip)
943 {
944 // Per section 7.3.1 of the Redfish specification, $skip is run before
945 // $top Can only skip as many values as we have
946 size_t skip = std::min(arr->size(), *query.skip);
947 arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip));
948 }
949 if (query.top)
950 {
951 size_t top = std::min(arr->size(), *query.top);
952 arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end());
953 }
954 }
955
956 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are
957 // not in the |currNode| Trie node.
recursiveSelect(nlohmann::json & currRoot,const SelectTrieNode & currNode)958 inline void recursiveSelect(nlohmann::json& currRoot,
959 const SelectTrieNode& currNode)
960 {
961 nlohmann::json::object_t* object =
962 currRoot.get_ptr<nlohmann::json::object_t*>();
963 if (object != nullptr)
964 {
965 BMCWEB_LOG_DEBUG("Current JSON is an object");
966 auto it = currRoot.begin();
967 while (it != currRoot.end())
968 {
969 auto nextIt = std::next(it);
970 BMCWEB_LOG_DEBUG("key={}", it.key());
971 const SelectTrieNode* nextNode = currNode.find(it.key());
972 // Per the Redfish spec section 7.3.3, the service shall select
973 // certain properties as if $select was omitted. This applies to
974 // every TrieNode that contains leaves and the root.
975 constexpr std::array<std::string_view, 5> reservedProperties = {
976 "@odata.id", "@odata.type", "@odata.context", "@odata.etag",
977 "error"};
978 bool reserved = std::ranges::find(reservedProperties, it.key()) !=
979 reservedProperties.end();
980 if (reserved || (nextNode != nullptr && nextNode->isSelected()))
981 {
982 it = nextIt;
983 continue;
984 }
985 if (nextNode != nullptr)
986 {
987 BMCWEB_LOG_DEBUG("Recursively select: {}", it.key());
988 recursiveSelect(*it, *nextNode);
989 it = nextIt;
990 continue;
991 }
992 BMCWEB_LOG_DEBUG("{} is getting removed!", it.key());
993 it = currRoot.erase(it);
994 }
995 }
996 nlohmann::json::array_t* array =
997 currRoot.get_ptr<nlohmann::json::array_t*>();
998 if (array != nullptr)
999 {
1000 BMCWEB_LOG_DEBUG("Current JSON is an array");
1001 // Array index is omitted, so reuse the same Trie node
1002 for (nlohmann::json& nextRoot : *array)
1003 {
1004 recursiveSelect(nextRoot, currNode);
1005 }
1006 }
1007 }
1008
1009 // The current implementation of $select still has the following TODOs due to
1010 // ambiguity and/or complexity.
1011 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was
1012 // created for clarification.
1013 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*),
1014 // etc.
processSelect(crow::Response & intermediateResponse,const SelectTrieNode & trieRoot)1015 inline void processSelect(crow::Response& intermediateResponse,
1016 const SelectTrieNode& trieRoot)
1017 {
1018 BMCWEB_LOG_DEBUG("Process $select quary parameter");
1019 recursiveSelect(intermediateResponse.jsonValue, trieRoot);
1020 }
1021
1022 inline void
processAllParams(crow::App & app,const Query & query,const Query & delegated,std::function<void (crow::Response &)> & completionHandler,crow::Response & intermediateResponse)1023 processAllParams(crow::App& app, const Query& query, const Query& delegated,
1024 std::function<void(crow::Response&)>& completionHandler,
1025 crow::Response& intermediateResponse)
1026 {
1027 if (!completionHandler)
1028 {
1029 BMCWEB_LOG_DEBUG("Function was invalid?");
1030 return;
1031 }
1032
1033 BMCWEB_LOG_DEBUG("Processing query params");
1034 // If the request failed, there's no reason to even try to run query
1035 // params.
1036 if (intermediateResponse.resultInt() < 200 ||
1037 intermediateResponse.resultInt() >= 400)
1038 {
1039 completionHandler(intermediateResponse);
1040 return;
1041 }
1042 if (query.isOnly)
1043 {
1044 processOnly(app, intermediateResponse, completionHandler);
1045 return;
1046 }
1047
1048 if (query.top || query.skip)
1049 {
1050 processTopAndSkip(query, intermediateResponse);
1051 }
1052
1053 if (query.expandType != ExpandType::None)
1054 {
1055 BMCWEB_LOG_DEBUG("Executing expand query");
1056 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(
1057 std::move(intermediateResponse));
1058
1059 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler));
1060 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp);
1061 multi->startQuery(query, delegated);
1062 return;
1063 }
1064
1065 if (query.filter)
1066 {
1067 applyFilterToCollection(intermediateResponse.jsonValue, *query.filter);
1068 }
1069
1070 // According to Redfish Spec Section 7.3.1, $select is the last parameter to
1071 // to process
1072 if (!query.selectTrie.root.empty())
1073 {
1074 processSelect(intermediateResponse, query.selectTrie.root);
1075 }
1076
1077 completionHandler(intermediateResponse);
1078 }
1079
1080 } // namespace query_param
1081 } // namespace redfish
1082