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 if (!inLinks) 393 { 394 // Check if this is a links node 395 inLinks = element.first == "Links"; 396 } 397 // Only traverse the parts of the tree the user asked for 398 // Per section 7.3 of the redfish specification 399 if (inLinks && eType == ExpandType::NotLinks) 400 { 401 continue; 402 } 403 if (!inLinks && eType == ExpandType::Links) 404 { 405 continue; 406 } 407 nlohmann::json::json_pointer newPtr = p / element.first; 408 BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr; 409 410 findNavigationReferencesRecursive(eType, element.second, newPtr, 411 inLinks, out); 412 } 413 } 414 415 inline std::vector<ExpandNode> 416 findNavigationReferences(ExpandType eType, nlohmann::json& jsonResponse, 417 const nlohmann::json::json_pointer& root) 418 { 419 std::vector<ExpandNode> ret; 420 findNavigationReferencesRecursive(eType, jsonResponse, root, false, ret); 421 return ret; 422 } 423 424 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> 425 { 426 public: 427 // This object takes a single asyncResp object as the "final" one, then 428 // allows callers to attach sub-responses within the json tree that need 429 // to be executed and filled into their appropriate locations. This 430 // class manages the final "merge" of the json resources. 431 MultiAsyncResp(crow::App& app, 432 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 433 app(app), 434 finalRes(std::move(finalResIn)) 435 {} 436 437 void addAwaitingResponse( 438 Query query, std::shared_ptr<bmcweb::AsyncResp>& res, 439 const nlohmann::json::json_pointer& finalExpandLocation) 440 { 441 res->res.setCompleteRequestHandler(std::bind_front( 442 onEndStatic, shared_from_this(), query, finalExpandLocation)); 443 } 444 445 void onEnd(Query query, const nlohmann::json::json_pointer& locationToPlace, 446 crow::Response& res) 447 { 448 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; 449 finalObj = std::move(res.jsonValue); 450 451 if (query.expandLevel <= 0) 452 { 453 // Last level to expand, no need to go deeper 454 return; 455 } 456 // Now decrease the depth by one to account for the tree node we 457 // just resolved 458 query.expandLevel--; 459 460 std::vector<ExpandNode> nodes = findNavigationReferences( 461 query.expandType, finalObj, locationToPlace); 462 BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; 463 for (const ExpandNode& node : nodes) 464 { 465 BMCWEB_LOG_DEBUG << "Expanding " << locationToPlace; 466 std::error_code ec; 467 crow::Request newReq({boost::beast::http::verb::get, node.uri, 11}, 468 ec); 469 if (ec) 470 { 471 messages::internalError(res); 472 return; 473 } 474 475 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 476 BMCWEB_LOG_DEBUG << "setting completion handler on " 477 << &asyncResp->res; 478 addAwaitingResponse(query, asyncResp, node.location); 479 app.handle(newReq, asyncResp); 480 } 481 } 482 483 private: 484 static void onEndStatic(const std::shared_ptr<MultiAsyncResp>& multi, 485 Query query, 486 const nlohmann::json::json_pointer& locationToPlace, 487 crow::Response& res) 488 { 489 multi->onEnd(query, locationToPlace, res); 490 } 491 492 crow::App& app; 493 std::shared_ptr<bmcweb::AsyncResp> finalRes; 494 }; 495 496 inline void 497 processAllParams(crow::App& app, const Query query, 498 499 std::function<void(crow::Response&)>& completionHandler, 500 crow::Response& intermediateResponse) 501 { 502 if (!completionHandler) 503 { 504 BMCWEB_LOG_DEBUG << "Function was invalid?"; 505 return; 506 } 507 508 BMCWEB_LOG_DEBUG << "Processing query params"; 509 // If the request failed, there's no reason to even try to run query 510 // params. 511 if (intermediateResponse.resultInt() < 200 || 512 intermediateResponse.resultInt() >= 400) 513 { 514 completionHandler(intermediateResponse); 515 return; 516 } 517 if (query.isOnly) 518 { 519 processOnly(app, intermediateResponse, completionHandler); 520 return; 521 } 522 if (query.expandType != ExpandType::None) 523 { 524 BMCWEB_LOG_DEBUG << "Executing expand query"; 525 // TODO(ed) this is a copy of the response object. Admittedly, 526 // we're inherently doing something inefficient, but we shouldn't 527 // have to do a full copy 528 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 529 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 530 asyncResp->res.jsonValue = std::move(intermediateResponse.jsonValue); 531 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 532 533 // Start the chain by "ending" the root response 534 multi->onEnd(query, nlohmann::json::json_pointer(""), asyncResp->res); 535 return; 536 } 537 completionHandler(intermediateResponse); 538 } 539 540 } // namespace query_param 541 } // namespace redfish 542