/* Copyright (c) 2018 Intel Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #pragma once #include "error_messages.hpp" #include "http_connection.hpp" #include "http_request.hpp" #include "http_response.hpp" #include "human_sort.hpp" #include "logging.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // IWYU pragma: no_forward_declare crow::Request namespace redfish { namespace json_util { /** * @brief Processes request to extract JSON from its body. If it fails, adds * MalformedJSON message to response and ends it. * * @param[io] res Response object * @param[in] req Request object * @param[out] reqJson JSON object extracted from request's body * * @return true if JSON is valid, false when JSON is invalid and response has * been filled with message and ended. */ bool processJsonFromRequest(crow::Response& res, const crow::Request& req, nlohmann::json& reqJson); namespace details { template struct IsOptional : std::false_type {}; template struct IsOptional> : std::true_type {}; template struct IsVector : std::false_type {}; template struct IsVector> : std::true_type {}; template struct IsStdArray : std::false_type {}; template struct IsStdArray> : std::true_type {}; template struct IsVariant : std::false_type {}; template struct IsVariant> : std::true_type {}; enum class UnpackErrorCode { success, invalidType, outOfRange }; template bool checkRange(const FromType& from [[maybe_unused]], std::string_view key [[maybe_unused]]) { if constexpr (std::is_floating_point_v) { if (std::isnan(from)) { BMCWEB_LOG_DEBUG("Value for key {} was NAN", key); return false; } } if constexpr (std::numeric_limits::max() < std::numeric_limits::max()) { if (from > std::numeric_limits::max()) { BMCWEB_LOG_DEBUG("Value for key {} was greater than max {}", key, std::numeric_limits::max()); return false; } } if constexpr (std::numeric_limits::lowest() > std::numeric_limits::lowest()) { if (from < std::numeric_limits::lowest()) { BMCWEB_LOG_DEBUG("Value for key {} was less than min {}", key, std::numeric_limits::lowest()); return false; } } return true; } template UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue, std::string_view key, Type& value); template UnpackErrorCode unpackValueVariant(nlohmann::json& j, std::string_view key, std::variant& v) { if constexpr (Index < std::variant_size_v>) { std::variant_alternative_t> type{}; UnpackErrorCode unpack = unpackValueWithErrorCode(j, key, type); if (unpack == UnpackErrorCode::success) { v = std::move(type); return unpack; } return unpackValueVariant(j, key, v); } return UnpackErrorCode::invalidType; } template UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue, std::string_view key, Type& value) { UnpackErrorCode ret = UnpackErrorCode::success; if constexpr (std::is_floating_point_v) { double helper = 0; double* jsonPtr = jsonValue.get_ptr(); if (jsonPtr == nullptr) { int64_t* intPtr = jsonValue.get_ptr(); if (intPtr != nullptr) { helper = static_cast(*intPtr); jsonPtr = &helper; } } if (jsonPtr == nullptr) { return UnpackErrorCode::invalidType; } if (!checkRange(*jsonPtr, key)) { return UnpackErrorCode::outOfRange; } value = static_cast(*jsonPtr); } else if constexpr (std::is_signed_v) { int64_t* jsonPtr = jsonValue.get_ptr(); if (jsonPtr == nullptr) { return UnpackErrorCode::invalidType; } if (!checkRange(*jsonPtr, key)) { return UnpackErrorCode::outOfRange; } value = static_cast(*jsonPtr); } else if constexpr ((std::is_unsigned_v) && (!std::is_same_v)) { uint64_t* jsonPtr = jsonValue.get_ptr(); if (jsonPtr == nullptr) { return UnpackErrorCode::invalidType; } if (!checkRange(*jsonPtr, key)) { return UnpackErrorCode::outOfRange; } value = static_cast(*jsonPtr); } else if constexpr (std::is_same_v) { value = std::move(jsonValue); } else if constexpr (std::is_same_v) { if (!jsonValue.is_null()) { return UnpackErrorCode::invalidType; } } else if constexpr (IsVector::value) { nlohmann::json::object_t* obj = jsonValue.get_ptr(); if (obj == nullptr) { return UnpackErrorCode::invalidType; } for (const auto& val : *obj) { value.emplace_back(); ret = unpackValueWithErrorCode( val, key, value.back()) && ret; } } else { using JsonType = std::add_const_t>; JsonType jsonPtr = jsonValue.get_ptr(); if (jsonPtr == nullptr) { BMCWEB_LOG_DEBUG("Value for key {} was incorrect type: {}", key, jsonValue.type_name()); return UnpackErrorCode::invalidType; } value = std::move(*jsonPtr); } return ret; } template bool unpackValue(nlohmann::json& jsonValue, std::string_view key, crow::Response& res, Type& value) { bool ret = true; if constexpr (IsOptional::value) { value.emplace(); ret = unpackValue(jsonValue, key, res, *value) && ret; } else if constexpr (IsStdArray::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr(); if (arr == nullptr) { messages::propertyValueTypeError(res, res.jsonValue, key); return false; } if (jsonValue.size() != value.size()) { messages::propertyValueTypeError(res, res.jsonValue, key); return false; } size_t index = 0; for (auto& val : *arr) { ret = unpackValue(val, key, res, value[index++]) && ret; } } else if constexpr (IsVector::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr(); if (arr == nullptr) { messages::propertyValueTypeError(res, res.jsonValue, key); return false; } for (auto& val : *arr) { value.emplace_back(); ret = unpackValue(val, key, res, value.back()) && ret; } } else if constexpr (IsVariant::value) { UnpackErrorCode ec = unpackValueVariant(jsonValue, key, value); if (ec != UnpackErrorCode::success) { if (ec == UnpackErrorCode::invalidType) { messages::propertyValueTypeError(res, jsonValue, key); } else if (ec == UnpackErrorCode::outOfRange) { messages::propertyValueNotInList(res, jsonValue, key); } return false; } } else { UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value); if (ec != UnpackErrorCode::success) { if (ec == UnpackErrorCode::invalidType) { messages::propertyValueTypeError(res, jsonValue, key); } else if (ec == UnpackErrorCode::outOfRange) { messages::propertyValueNotInList(res, jsonValue, key); } return false; } } return ret; } template bool unpackValue(nlohmann::json& jsonValue, std::string_view key, Type& value) { bool ret = true; if constexpr (IsOptional::value) { value.emplace(); ret = unpackValue(jsonValue, key, *value) && ret; } else if constexpr (IsStdArray::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr(); if (arr == nullptr) { return false; } if (jsonValue.size() != value.size()) { return false; } size_t index = 0; for (const auto& val : *arr) { ret = unpackValue(val, key, value[index++]) && ret; } } else if constexpr (IsVector::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr(); if (arr == nullptr) { return false; } for (const auto& val : *arr) { value.emplace_back(); ret = unpackValue(val, key, value.back()) && ret; } } else { UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value); if (ec != UnpackErrorCode::success) { return false; } } return ret; } } // namespace details // clang-format off using UnpackVariant = std::variant< uint8_t*, uint16_t*, int16_t*, uint32_t*, int32_t*, uint64_t*, int64_t*, bool*, double*, std::string*, nlohmann::json::object_t*, std::variant*, std::variant*, std::variant*, std::variant*, std::variant*, std::variant*, std::variant*, std::variant*, std::variant*, std::variant*, std::vector*, std::vector*, std::vector*, std::vector*, std::vector*, std::vector*, std::vector*, //std::vector*, std::vector*, std::vector*, std::vector*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, //std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>*, std::optional>>*, std::optional>>*, // Note, these types are kept for historical completeness, but should not be used, // As they do not provide object type safety. Instead, rely on nlohmann::json::object_t // Will be removed Q2 2025 nlohmann::json*, std::optional>*, std::vector*, std::optional* >; // clang-format on struct PerUnpack { std::string_view key; UnpackVariant value; bool complete = false; }; inline bool readJsonHelper(nlohmann::json& jsonRequest, crow::Response& res, std::span toUnpack); inline bool readJsonHelperObject(nlohmann::json::object_t& obj, crow::Response& res, std::span toUnpack) { bool result = true; for (auto& item : obj) { size_t unpackIndex = 0; for (; unpackIndex < toUnpack.size(); unpackIndex++) { PerUnpack& unpackSpec = toUnpack[unpackIndex]; std::string_view key = unpackSpec.key; size_t keysplitIndex = key.find('/'); std::string_view leftover; if (keysplitIndex != std::string_view::npos) { leftover = key.substr(keysplitIndex + 1); key = key.substr(0, keysplitIndex); } if (key != item.first || unpackSpec.complete) { continue; } // Sublevel key if (!leftover.empty()) { // Include the slash in the key so we can compare later key = unpackSpec.key.substr(0, keysplitIndex + 1); nlohmann::json j; result = details::unpackValue(item.second, key, res, j) && result; if (!result) { return result; } std::vector nextLevel; for (PerUnpack& p : toUnpack) { if (!p.key.starts_with(key)) { continue; } std::string_view thisLeftover = p.key.substr(key.size()); nextLevel.push_back({thisLeftover, p.value, false}); p.complete = true; } result = readJsonHelper(j, res, nextLevel) && result; break; } result = std::visit( [&item, &unpackSpec, &res](auto& val) { using ContainedT = std::remove_pointer_t>; return details::unpackValue( item.second, unpackSpec.key, res, *val); }, unpackSpec.value) && result; unpackSpec.complete = true; break; } if (unpackIndex == toUnpack.size()) { messages::propertyUnknown(res, item.first); result = false; } } for (PerUnpack& perUnpack : toUnpack) { if (!perUnpack.complete) { bool isOptional = std::visit( [](auto& val) { using ContainedType = std::remove_pointer_t>; return details::IsOptional::value; }, perUnpack.value); if (isOptional) { continue; } messages::propertyMissing(res, perUnpack.key); result = false; } } return result; } inline bool readJsonHelper(nlohmann::json& jsonRequest, crow::Response& res, std::span toUnpack) { nlohmann::json::object_t* obj = jsonRequest.get_ptr(); if (obj == nullptr) { BMCWEB_LOG_DEBUG("Json value is not an object"); messages::unrecognizedRequestBody(res); return false; } return readJsonHelperObject(*obj, res, toUnpack); } inline void packVariant(std::span /*toPack*/) {} template void packVariant(std::span toPack, std::string_view key, FirstType&& first, UnpackTypes&&... in) { if (toPack.empty()) { return; } toPack[0].key = key; toPack[0].value = &first; // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) packVariant(toPack.subspan(1), std::forward(in)...); } template bool readJsonObject(nlohmann::json::object_t& jsonRequest, crow::Response& res, std::string_view key, FirstType&& first, UnpackTypes&&... in) { const std::size_t n = sizeof...(UnpackTypes) + 2; std::array toUnpack2; packVariant(toUnpack2, key, std::forward(first), std::forward(in)...); return readJsonHelperObject(jsonRequest, res, toUnpack2); } template bool readJson(nlohmann::json& jsonRequest, crow::Response& res, std::string_view key, FirstType&& first, UnpackTypes&&... in) { nlohmann::json::object_t* obj = jsonRequest.get_ptr(); if (obj == nullptr) { BMCWEB_LOG_DEBUG("Json value is not an object"); messages::unrecognizedRequestBody(res); return false; } return readJsonObject(*obj, res, key, std::forward(first), std::forward(in)...); } inline std::optional readJsonPatchHelper(const crow::Request& req, crow::Response& res) { nlohmann::json jsonRequest; if (!json_util::processJsonFromRequest(res, req, jsonRequest)) { BMCWEB_LOG_DEBUG("Json value not readable"); return std::nullopt; } nlohmann::json::object_t* object = jsonRequest.get_ptr(); if (object == nullptr || object->empty()) { BMCWEB_LOG_DEBUG("Json value is empty"); messages::emptyJSON(res); return std::nullopt; } std::erase_if(*object, [](const std::pair& item) { return item.first.starts_with("@odata."); }); if (object->empty()) { // If the update request only contains OData annotations, the service // should return the HTTP 400 Bad Request status code with the // NoOperation message from the Base Message Registry, ... messages::noOperation(res); return std::nullopt; } return {std::move(*object)}; } template bool readJsonPatch(const crow::Request& req, crow::Response& res, std::string_view key, UnpackTypes&&... in) { std::optional jsonRequest = readJsonPatchHelper(req, res); if (!jsonRequest) { return false; } if (jsonRequest->empty()) { messages::emptyJSON(res); return false; } return readJsonObject(*jsonRequest, res, key, std::forward(in)...); } template bool readJsonAction(const crow::Request& req, crow::Response& res, const char* key, UnpackTypes&&... in) { nlohmann::json jsonRequest; if (!json_util::processJsonFromRequest(res, req, jsonRequest)) { BMCWEB_LOG_DEBUG("Json value not readable"); return false; } nlohmann::json::object_t* object = jsonRequest.get_ptr(); if (object == nullptr) { BMCWEB_LOG_DEBUG("Json value is empty"); messages::emptyJSON(res); return false; } return readJsonObject(*object, res, key, std::forward(in)...); } // Determines if two json objects are less, based on the presence of the // @odata.id key inline int objectKeyCmp(std::string_view key, const nlohmann::json& a, const nlohmann::json& b) { using object_t = nlohmann::json::object_t; const object_t* aObj = a.get_ptr(); const object_t* bObj = b.get_ptr(); if (aObj == nullptr) { if (bObj == nullptr) { return 0; } return -1; } if (bObj == nullptr) { return 1; } object_t::const_iterator aIt = aObj->find(key); object_t::const_iterator bIt = bObj->find(key); // If either object doesn't have the key, they get "sorted" to the // beginning. if (aIt == aObj->end()) { if (bIt == bObj->end()) { return 0; } return -1; } if (bIt == bObj->end()) { return 1; } const nlohmann::json::string_t* nameA = aIt->second.get_ptr(); const nlohmann::json::string_t* nameB = bIt->second.get_ptr(); // If either object doesn't have a string as the key, they get "sorted" to // the beginning. if (nameA == nullptr) { if (nameB == nullptr) { return 0; } return -1; } if (nameB == nullptr) { return 1; } boost::urls::url_view aUrl(*nameA); boost::urls::url_view bUrl(*nameB); auto segmentsAIt = aUrl.segments().begin(); auto segmentsBIt = bUrl.segments().begin(); while (true) { if (segmentsAIt == aUrl.segments().end()) { if (segmentsBIt == bUrl.segments().end()) { return 0; } return -1; } if (segmentsBIt == bUrl.segments().end()) { return 1; } int res = alphanumComp(*segmentsAIt, *segmentsBIt); if (res != 0) { return res; } segmentsAIt++; segmentsBIt++; } }; // kept for backward compatibility inline int odataObjectCmp(const nlohmann::json& left, const nlohmann::json& right) { return objectKeyCmp("@odata.id", left, right); } struct ODataObjectLess { std::string_view key; explicit ODataObjectLess(std::string_view keyIn) : key(keyIn) {} bool operator()(const nlohmann::json& left, const nlohmann::json& right) const { return objectKeyCmp(key, left, right) < 0; } }; // Sort the JSON array by |element[key]|. // Elements without |key| or type of |element[key]| is not string are smaller // those whose |element[key]| is string. inline void sortJsonArrayByKey(nlohmann::json::array_t& array, std::string_view key) { std::ranges::sort(array, ODataObjectLess(key)); } // Sort the JSON array by |element[key]|. // Elements without |key| or type of |element[key]| is not string are smaller // those whose |element[key]| is string. inline void sortJsonArrayByOData(nlohmann::json::array_t& array) { std::ranges::sort(array, ODataObjectLess("@odata.id")); } // Returns the estimated size of the JSON value // The implementation walks through every key and every value, accumulates the // total size of keys and values. // Ideally, we should use a custom allocator that nlohmann JSON supports. // Assumption made: // 1. number: 8 characters // 2. boolean: 5 characters (False) // 3. string: len(str) + 2 characters (quote) // 4. bytes: len(bytes) characters // 5. null: 4 characters (null) uint64_t getEstimatedJsonSize(const nlohmann::json& root); } // namespace json_util } // namespace redfish