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