#include "StdAfx.h" #include "FormElement.h" namespace FSecure::C3::Linter { namespace { /// Validate input length against argument rules /// @param argument definition /// @returns input to validate /// @throws std::invalid_argument if validation fails inline void CheckLengthConstraint(json const& definition, std::string_view input) { if (definition.contains("min") && input.length() < definition["min"].get()) throw std::invalid_argument("Input \""s + std::string(input) + "\" too short for argument " + definition.at("name").get() + ". (min = " + std::to_string(definition["min"].get()) + ", actual = " + std::to_string(input.length()) + ')'); if (definition.contains("max") && input.length() > definition["max"].get()) throw std::invalid_argument("Input \""s + std::string(input) + "\" too long for argument " + definition.at("name").get() + ". (max = " + std::to_string(definition["max"].get()) + ", actual = " + std::to_string(input.length()) + ')'); } /// Validate numeric value against argument rules /// @param argument definition /// @returns value to validate /// @throws std::invalid_argument if validation fails template void CheckValueConstraint(json const& definition, Numeric value) { if (definition.contains("min") && value < definition["min"]) throw std::invalid_argument("Value too low for argument " + definition.at("name").get() + ". (min = " + std::to_string(definition["min"].get()) + ", actual = " + std::to_string(value) + ')'); if (definition.contains("max") && value > definition["max"]) throw std::invalid_argument("Value too high for argument " + definition.at("name").get() + ". (min = " + std::to_string(definition["min"].get()) + ", actual = " + std::to_string(value) + ')'); }; /// Concrete implementation for boolean argument type class BooleanFormElement : public FormElement { public: BooleanFormElement(json& element) : FormElement(element) { } /// Destructor virtual ~BooleanFormElement() = default; /// Set boolean value from input accepted values interpreted as true are "true", "yes", "y", "1" /// @param input to validate void ValidateAndSet(std::string_view input) override { std::string lowercase(input); std::transform(lowercase.begin(), lowercase.end(), lowercase.begin(), ::tolower); auto trueOptions = { "true", "yes", "y", "1" }; m_Definition["value"] = std::any_of(begin(trueOptions), end(trueOptions), [&lowercase](auto&& trueOption) {return lowercase == trueOption; }); } }; /// Concrete implementation for string argument type class StringFormElement : public FormElement { public: StringFormElement(json& element) : FormElement(element) { } /// Destructor virtual ~StringFormElement() = default; /// Validate and set string value from input /// @param input to validate /// @throws std::invalid_argument if validation fails void ValidateAndSet(std::string_view input) override { CheckLengthConstraint(m_Definition, input); m_Definition["value"] = input; } }; /// Concrete implementation for ip argument type class IpFormElement : public FormElement { public: IpFormElement(json& element) : FormElement(element) { } /// Destructor virtual ~IpFormElement() = default; /// Validate and set IP value from input /// @param input to validate /// @throws std::invalid_argument if validation fails void ValidateAndSet(std::string_view input) override { if (!IsIpV4(std::string(input))) throw std::invalid_argument("Failed to IPv4 value from input ("s + std::string(input) + ')'); m_Definition["value"] = input; } /// Determine if input is an IPv4 /// @param input to validate /// @returns true if input is in IPv4 dotted notation static bool IsIpV4(std::string const& input) { sockaddr_in client; client.sin_family = AF_INET; switch (InetPtonA(AF_INET, input.c_str(), &client.sin_addr.s_addr)) { case 1: return true; default: return false; } } }; /// Concrete implementation for binary argument type class BinaryFormElement : public FormElement { public: BinaryFormElement(json& element) : FormElement(element) { } /// Destructor virtual ~BinaryFormElement() = default; /// Validate and set binary value from input (in base64 string) /// @param input to validate /// @throws std::invalid_argument if validation fails void ValidateAndSet(std::string_view input) override { try { auto decoded = base64::decode(input); CheckLengthConstraint(m_Definition, decoded); m_Definition["value"] = input; } catch (std::domain_error&) { throw std::invalid_argument("Failed to decode base64 encoded input \""s + std::string(input) + '"'); } } }; /// Concrete implementation for binary argument type /// @tparam Numeric - type of numeric argument // TODO constrain Numeric template parameter to numeric types only template class NumericFormElement : public FormElement { public: NumericFormElement(json& element) : FormElement(element) { } /// Destructor virtual ~NumericFormElement() = default; /// Validate and set numeric value from input /// @param input to validate /// @throws std::invalid_argument if validation fails void ValidateAndSet(std::string_view input) override { Numeric value; auto x = std::from_chars(input.data(), input.data() + input.size(), value); if (x.ptr != input.data() + input.size()) throw std::invalid_argument("Failed to read numeric value from input \""s + std::string(input) + '"'); CheckValueConstraint(m_Definition, value); m_Definition["value"] = value; } }; } FormElement::FormElement(json& definition) : m_Definition(definition) { } /// Generate FormElement::Type to_json and from_json fuctions to enable serialization NLOHMANN_JSON_SERIALIZE_ENUM ( FormElement::Type, { {FormElement::Type::Unknown, nullptr}, {FormElement::Type::Uint8, "uint8"}, {FormElement::Type::Uint16, "uint16"}, {FormElement::Type::Uint32, "uint32"}, {FormElement::Type::Uint64, "uint64"}, {FormElement::Type::Int8, "int8"}, {FormElement::Type::Int16, "int16"}, {FormElement::Type::Int32, "int32"}, {FormElement::Type::Int64, "int64"}, {FormElement::Type::Float, "float"}, {FormElement::Type::Boolean, "boolean"}, {FormElement::Type::String, "string"}, {FormElement::Type::Ip, "ip"}, {FormElement::Type::Binary, "binary"}, } ); std::unique_ptr MakeFormElement(json& element) { if (!element.is_object()) throw std::invalid_argument { "Form element must be a json object." }; if (!element.contains("name")) throw std::invalid_argument{ "Form element must contain 'name' property. \nInvalid element:\n" + element.dump(4) }; if (!element.contains("type")) throw std::invalid_argument{ "Form element '" + element["name"].get() + "' must contain 'type' property." }; switch (element["type"].get()) { case FormElement::Type::Uint8: return std::make_unique>(element); case FormElement::Type::Uint16: return std::make_unique>(element); case FormElement::Type::Uint32: return std::make_unique>(element); case FormElement::Type::Uint64: return std::make_unique>(element); case FormElement::Type::Int8: return std::make_unique>(element); case FormElement::Type::Int16: return std::make_unique>(element); case FormElement::Type::Int32: return std::make_unique>(element); case FormElement::Type::Int64: return std::make_unique>(element); case FormElement::Type::Float: return std::make_unique>(element); case FormElement::Type::Boolean: return std::make_unique(element); case FormElement::Type::String: return std::make_unique(element); case FormElement::Type::Ip: return std::make_unique(element); case FormElement::Type::Binary: return std::make_unique(element); default: throw std::runtime_error("Unknown form argument type: \"" + element["type"].get() + '"'); } } }