/* // 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 <nlohmann/json.hpp> #include <algorithm> #include <array> #include <cmath> #include <cstddef> #include <cstdint> #include <limits> #include <map> #include <optional> #include <ranges> #include <span> #include <string> #include <string_view> #include <type_traits> #include <utility> #include <variant> #include <vector> // 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 <typename Type> struct IsOptional : std::false_type {}; template <typename Type> struct IsOptional<std::optional<Type>> : std::true_type {}; template <typename Type> struct IsVector : std::false_type {}; template <typename Type> struct IsVector<std::vector<Type>> : std::true_type {}; template <typename Type> struct IsStdArray : std::false_type {}; template <typename Type, std::size_t size> struct IsStdArray<std::array<Type, size>> : std::true_type {}; template <typename Type> struct IsVariant : std::false_type {}; template <typename... Types> struct IsVariant<std::variant<Types...>> : std::true_type {}; enum class UnpackErrorCode { success, invalidType, outOfRange }; template <typename ToType, typename FromType> bool checkRange(const FromType& from [[maybe_unused]], std::string_view key [[maybe_unused]]) { if constexpr (std::is_floating_point_v<ToType>) { if (std::isnan(from)) { BMCWEB_LOG_DEBUG("Value for key {} was NAN", key); return false; } } if constexpr (std::numeric_limits<ToType>::max() < std::numeric_limits<FromType>::max()) { if (from > std::numeric_limits<ToType>::max()) { BMCWEB_LOG_DEBUG("Value for key {} was greater than max {}", key, std::numeric_limits<FromType>::max()); return false; } } if constexpr (std::numeric_limits<ToType>::lowest() > std::numeric_limits<FromType>::lowest()) { if (from < std::numeric_limits<ToType>::lowest()) { BMCWEB_LOG_DEBUG("Value for key {} was less than min {}", key, std::numeric_limits<FromType>::lowest()); return false; } } return true; } template <typename Type> UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue, std::string_view key, Type& value); template <std::size_t Index = 0, typename... Args> UnpackErrorCode unpackValueVariant(nlohmann::json& j, std::string_view key, std::variant<Args...>& v) { if constexpr (Index < std::variant_size_v<std::variant<Args...>>) { std::variant_alternative_t<Index, std::variant<Args...>> type{}; UnpackErrorCode unpack = unpackValueWithErrorCode(j, key, type); if (unpack == UnpackErrorCode::success) { v = std::move(type); return unpack; } return unpackValueVariant<Index + 1, Args...>(j, key, v); } return UnpackErrorCode::invalidType; } template <typename Type> UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue, std::string_view key, Type& value) { UnpackErrorCode ret = UnpackErrorCode::success; if constexpr (std::is_floating_point_v<Type>) { double helper = 0; double* jsonPtr = jsonValue.get_ptr<double*>(); if (jsonPtr == nullptr) { int64_t* intPtr = jsonValue.get_ptr<int64_t*>(); if (intPtr != nullptr) { helper = static_cast<double>(*intPtr); jsonPtr = &helper; } } if (jsonPtr == nullptr) { return UnpackErrorCode::invalidType; } if (!checkRange<Type>(*jsonPtr, key)) { return UnpackErrorCode::outOfRange; } value = static_cast<Type>(*jsonPtr); } else if constexpr (std::is_signed_v<Type>) { int64_t* jsonPtr = jsonValue.get_ptr<int64_t*>(); if (jsonPtr == nullptr) { return UnpackErrorCode::invalidType; } if (!checkRange<Type>(*jsonPtr, key)) { return UnpackErrorCode::outOfRange; } value = static_cast<Type>(*jsonPtr); } else if constexpr ((std::is_unsigned_v<Type>) && (!std::is_same_v<bool, Type>)) { uint64_t* jsonPtr = jsonValue.get_ptr<uint64_t*>(); if (jsonPtr == nullptr) { return UnpackErrorCode::invalidType; } if (!checkRange<Type>(*jsonPtr, key)) { return UnpackErrorCode::outOfRange; } value = static_cast<Type>(*jsonPtr); } else if constexpr (std::is_same_v<nlohmann::json, Type>) { value = std::move(jsonValue); } else if constexpr (std::is_same_v<std::nullptr_t, Type>) { if (!jsonValue.is_null()) { return UnpackErrorCode::invalidType; } } else if constexpr (IsVector<Type>::value) { nlohmann::json::object_t* obj = jsonValue.get_ptr<nlohmann::json::object_t*>(); if (obj == nullptr) { return UnpackErrorCode::invalidType; } for (const auto& val : *obj) { value.emplace_back(); ret = unpackValueWithErrorCode<typename Type::value_type>( val, key, value.back()) && ret; } } else { using JsonType = std::add_const_t<std::add_pointer_t<Type>>; JsonType jsonPtr = jsonValue.get_ptr<JsonType>(); 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 <typename Type> bool unpackValue(nlohmann::json& jsonValue, std::string_view key, crow::Response& res, Type& value) { bool ret = true; if constexpr (IsOptional<Type>::value) { value.emplace(); ret = unpackValue<typename Type::value_type>(jsonValue, key, res, *value) && ret; } else if constexpr (IsStdArray<Type>::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr<nlohmann::json::array_t*>(); 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<typename Type::value_type>(val, key, res, value[index++]) && ret; } } else if constexpr (IsVector<Type>::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr<nlohmann::json::array_t*>(); if (arr == nullptr) { messages::propertyValueTypeError(res, res.jsonValue, key); return false; } for (auto& val : *arr) { value.emplace_back(); ret = unpackValue<typename Type::value_type>(val, key, res, value.back()) && ret; } } else if constexpr (IsVariant<Type>::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 <typename Type> bool unpackValue(nlohmann::json& jsonValue, std::string_view key, Type& value) { bool ret = true; if constexpr (IsOptional<Type>::value) { value.emplace(); ret = unpackValue<typename Type::value_type>(jsonValue, key, *value) && ret; } else if constexpr (IsStdArray<Type>::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr<nlohmann::json::array_t*>(); if (arr == nullptr) { return false; } if (jsonValue.size() != value.size()) { return false; } size_t index = 0; for (const auto& val : *arr) { ret = unpackValue<typename Type::value_type>(val, key, value[index++]) && ret; } } else if constexpr (IsVector<Type>::value) { nlohmann::json::array_t* arr = jsonValue.get_ptr<nlohmann::json::array_t*>(); if (arr == nullptr) { return false; } for (const auto& val : *arr) { value.emplace_back(); ret = unpackValue<typename Type::value_type>(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::string, std::nullptr_t>*, std::variant<uint8_t, std::nullptr_t>*, std::variant<int16_t, std::nullptr_t>*, std::variant<uint16_t, std::nullptr_t>*, std::variant<int32_t, std::nullptr_t>*, std::variant<uint32_t, std::nullptr_t>*, std::variant<int64_t, std::nullptr_t>*, std::variant<uint64_t, std::nullptr_t>*, std::variant<double, std::nullptr_t>*, std::variant<bool, std::nullptr_t>*, std::vector<uint8_t>*, std::vector<uint16_t>*, std::vector<int16_t>*, std::vector<uint32_t>*, std::vector<int32_t>*, std::vector<uint64_t>*, std::vector<int64_t>*, //std::vector<bool>*, std::vector<double>*, std::vector<std::string>*, std::vector<nlohmann::json::object_t>*, std::optional<uint8_t>*, std::optional<uint16_t>*, std::optional<int16_t>*, std::optional<uint32_t>*, std::optional<int32_t>*, std::optional<uint64_t>*, std::optional<int64_t>*, std::optional<bool>*, std::optional<double>*, std::optional<std::string>*, std::optional<nlohmann::json::object_t>*, std::optional<std::vector<uint8_t>>*, std::optional<std::vector<uint16_t>>*, std::optional<std::vector<int16_t>>*, std::optional<std::vector<uint32_t>>*, std::optional<std::vector<int32_t>>*, std::optional<std::vector<uint64_t>>*, std::optional<std::vector<int64_t>>*, //std::optional<std::vector<bool>>*, std::optional<std::vector<double>>*, std::optional<std::vector<std::string>>*, std::optional<std::vector<nlohmann::json::object_t>>*, std::optional<std::variant<std::string, std::nullptr_t>>*, std::optional<std::variant<uint8_t, std::nullptr_t>>*, std::optional<std::variant<int16_t, std::nullptr_t>>*, std::optional<std::variant<uint16_t, std::nullptr_t>>*, std::optional<std::variant<int32_t, std::nullptr_t>>*, std::optional<std::variant<uint32_t, std::nullptr_t>>*, std::optional<std::variant<int64_t, std::nullptr_t>>*, std::optional<std::variant<uint64_t, std::nullptr_t>>*, std::optional<std::variant<double, std::nullptr_t>>*, std::optional<std::variant<bool, std::nullptr_t>>*, std::optional<std::vector<std::variant<nlohmann::json::object_t, std::nullptr_t>>>*, std::optional<std::vector<std::variant<std::string, nlohmann::json::object_t, std::nullptr_t>>>*, // 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<nlohmann::json>>*, std::vector<nlohmann::json>*, std::optional<nlohmann::json>* >; // 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<PerUnpack> toUnpack); inline bool readJsonHelperObject(nlohmann::json::object_t& obj, crow::Response& res, std::span<PerUnpack> 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<nlohmann::json>(item.second, key, res, j) && result; if (!result) { return result; } std::vector<PerUnpack> 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<std::decay_t<decltype(val)>>; return details::unpackValue<ContainedT>( 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<std::decay_t<decltype(val)>>; return details::IsOptional<ContainedType>::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<PerUnpack> toUnpack) { nlohmann::json::object_t* obj = jsonRequest.get_ptr<nlohmann::json::object_t*>(); 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<PerUnpack> /*toPack*/) {} template <typename FirstType, typename... UnpackTypes> void packVariant(std::span<PerUnpack> 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<UnpackTypes&&>(in)...); } template <typename FirstType, typename... UnpackTypes> 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<PerUnpack, n / 2> toUnpack2; packVariant(toUnpack2, key, std::forward<FirstType>(first), std::forward<UnpackTypes&&>(in)...); return readJsonHelperObject(jsonRequest, res, toUnpack2); } template <typename FirstType, typename... UnpackTypes> bool readJson(nlohmann::json& jsonRequest, crow::Response& res, std::string_view key, FirstType&& first, UnpackTypes&&... in) { nlohmann::json::object_t* obj = jsonRequest.get_ptr<nlohmann::json::object_t*>(); if (obj == nullptr) { BMCWEB_LOG_DEBUG("Json value is not an object"); messages::unrecognizedRequestBody(res); return false; } return readJsonObject(*obj, res, key, std::forward<FirstType>(first), std::forward<UnpackTypes&&>(in)...); } inline std::optional<nlohmann::json::object_t> 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<nlohmann::json::object_t*>(); 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<std::string, nlohmann::json>& 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 <typename... UnpackTypes> bool readJsonPatch(const crow::Request& req, crow::Response& res, std::string_view key, UnpackTypes&&... in) { std::optional<nlohmann::json::object_t> jsonRequest = readJsonPatchHelper(req, res); if (!jsonRequest) { return false; } if (jsonRequest->empty()) { messages::emptyJSON(res); return false; } return readJsonObject(*jsonRequest, res, key, std::forward<UnpackTypes&&>(in)...); } template <typename... UnpackTypes> 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<nlohmann::json::object_t*>(); if (object == nullptr) { BMCWEB_LOG_DEBUG("Json value is empty"); messages::emptyJSON(res); return false; } return readJsonObject(*object, res, key, std::forward<UnpackTypes&&>(in)...); } // Determines if two json objects are less, based on the presence of the // @odata.id key inline int odataObjectCmp(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*>(); const object_t* bObj = b.get_ptr<const object_t*>(); if (aObj == nullptr) { if (bObj == nullptr) { return 0; } return -1; } if (bObj == nullptr) { return 1; } object_t::const_iterator aIt = aObj->find("@odata.id"); object_t::const_iterator bIt = bObj->find("@odata.id"); // If either object doesn't have the key, they get "sorted" to the end. 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 std::string*>(); const nlohmann::json::string_t* nameB = bIt->second.get_ptr<const std::string*>(); // If either object doesn't have a string as the key, they get "sorted" to // the end. 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++; } }; struct ODataObjectLess { bool operator()(const nlohmann::json& left, const nlohmann::json& right) const { return odataObjectCmp(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 sortJsonArrayByOData(nlohmann::json::array_t& array) { std::ranges::sort(array, ODataObjectLess()); } // 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