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