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 const nlohmann::json::json_pointer& root) 419 { 420 std::vector<ExpandNode> ret; 421 findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret); 422 return ret; 423 } 424 425 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> 426 { 427 public: 428 // This object takes a single asyncResp object as the "final" one, then 429 // allows callers to attach sub-responses within the json tree that need 430 // to be executed and filled into their appropriate locations. This 431 // class manages the final "merge" of the json resources. 432 MultiAsyncResp(crow::App& app, 433 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 434 app(app), 435 finalRes(std::move(finalResIn)) 436 {} 437 438 void addAwaitingResponse( 439 Query query, std::shared_ptr<bmcweb::AsyncResp>& res, 440 const nlohmann::json::json_pointer& finalExpandLocation) 441 { 442 res->res.setCompleteRequestHandler(std::bind_front( 443 onEndStatic, shared_from_this(), query, finalExpandLocation)); 444 } 445 446 void onEnd(Query query, const nlohmann::json::json_pointer& locationToPlace, 447 crow::Response& res) 448 { 449 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; 450 finalObj = std::move(res.jsonValue); 451 452 if (query.expandLevel <= 0) 453 { 454 // Last level to expand, no need to go deeper 455 return; 456 } 457 // Now decrease the depth by one to account for the tree node we 458 // just resolved 459 query.expandLevel--; 460 461 std::vector<ExpandNode> nodes = findNavigationReferences( 462 query.expandType, finalObj, locationToPlace); 463 BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; 464 for (const ExpandNode& node : nodes) 465 { 466 BMCWEB_LOG_DEBUG << "Expanding " << locationToPlace; 467 std::error_code ec; 468 crow::Request newReq({boost::beast::http::verb::get, node.uri, 11}, 469 ec); 470 if (ec) 471 { 472 messages::internalError(res); 473 return; 474 } 475 476 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 477 BMCWEB_LOG_DEBUG << "setting completion handler on " 478 << &asyncResp->res; 479 addAwaitingResponse(query, asyncResp, node.location); 480 app.handle(newReq, asyncResp); 481 } 482 } 483 484 private: 485 static void onEndStatic(const std::shared_ptr<MultiAsyncResp>& multi, 486 Query query, 487 const nlohmann::json::json_pointer& locationToPlace, 488 crow::Response& res) 489 { 490 multi->onEnd(query, locationToPlace, res); 491 } 492 493 crow::App& app; 494 std::shared_ptr<bmcweb::AsyncResp> finalRes; 495 }; 496 497 inline void processTopAndSkip(const Query& query, crow::Response& res) 498 { 499 nlohmann::json::object_t* obj = 500 res.jsonValue.get_ptr<nlohmann::json::object_t*>(); 501 if (obj == nullptr) 502 { 503 // Shouldn't be possible. All responses should be objects. 504 messages::internalError(res); 505 return; 506 } 507 508 BMCWEB_LOG_DEBUG << "Handling top/skip"; 509 nlohmann::json::object_t::iterator members = obj->find("Members"); 510 if (members == obj->end()) 511 { 512 // From the Redfish specification 7.3.1 513 // ... the HTTP 400 Bad Request status code with the 514 // QueryNotSupportedOnResource message from the Base Message Registry 515 // for any supported query parameters that apply only to resource 516 // collections but are used on singular resources. 517 messages::queryNotSupportedOnResource(res); 518 return; 519 } 520 521 nlohmann::json::array_t* arr = 522 members->second.get_ptr<nlohmann::json::array_t*>(); 523 if (arr == nullptr) 524 { 525 messages::internalError(res); 526 return; 527 } 528 529 // Per section 7.3.1 of the Redfish specification, $skip is run before $top 530 // Can only skip as many values as we have 531 size_t skip = std::min(arr->size(), query.skip); 532 arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); 533 534 size_t top = std::min(arr->size(), query.top); 535 arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); 536 } 537 538 inline void 539 processAllParams(crow::App& app, const Query query, 540 std::function<void(crow::Response&)>& completionHandler, 541 crow::Response& intermediateResponse) 542 { 543 if (!completionHandler) 544 { 545 BMCWEB_LOG_DEBUG << "Function was invalid?"; 546 return; 547 } 548 549 BMCWEB_LOG_DEBUG << "Processing query params"; 550 // If the request failed, there's no reason to even try to run query 551 // params. 552 if (intermediateResponse.resultInt() < 200 || 553 intermediateResponse.resultInt() >= 400) 554 { 555 completionHandler(intermediateResponse); 556 return; 557 } 558 if (query.isOnly) 559 { 560 processOnly(app, intermediateResponse, completionHandler); 561 return; 562 } 563 564 if (query.top != std::numeric_limits<size_t>::max() || query.skip != 0) 565 { 566 processTopAndSkip(query, intermediateResponse); 567 } 568 569 if (query.expandType != ExpandType::None) 570 { 571 BMCWEB_LOG_DEBUG << "Executing expand query"; 572 // TODO(ed) this is a copy of the response object. Admittedly, 573 // we're inherently doing something inefficient, but we shouldn't 574 // have to do a full copy 575 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 576 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 577 asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue); 578 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 579 580 // Start the chain by "ending" the root response 581 multi->onEnd(query, nlohmann::json::json_pointer(""), asyncResp->res); 582 return; 583 } 584 completionHandler(intermediateResponse); 585 } 586 587 } // namespace query_param 588 } // namespace redfish 589