// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/net/secure_dns_policy_handler.h"

#include <string>
#include <string_view>

#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "chrome/browser/net/secure_dns_config.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/common/pref_names.h"
#include "components/policy/core/browser/policy_error_map.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_value_map.h"
#include "components/strings/grit/components_strings.h"
#include "net/dns/public/dns_over_https_config.h"
#include "net/dns/public/util.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#endif

namespace {

#if BUILDFLAG(IS_CHROMEOS)
constexpr int kMinDohSaltSize = 8;
constexpr int kMaxDohSaltSize = 32;

// Returns true if the policy `kDnsOverHttpsSalt` is not set or if it has a
// valid value, otherwise returns false. kDnsOverHttpsSalt` is only valid if
// `kDnsOverHttpsTemplatesWithIdentifiers` is set. If an error occurs, the error
// message will be appended to `errors`.
bool CheckDnsOverHttpsSaltPolicy(const policy::PolicyMap& policies,
                                 policy::PolicyErrorMap* errors) {
  if (!policies.IsPolicySet(policy::key::kDnsOverHttpsSalt)) {
    return true;
  }
  const base::Value* salt =
      policies.GetValueUnsafe(policy::key::kDnsOverHttpsSalt);

  if (!salt->is_string()) {
    errors->AddError(policy::key::kDnsOverHttpsSalt,
                     IDS_POLICY_SECURE_DNS_SALT_INVALID_ERROR);
    return false;
  }

  // Salt is optional.
  if (salt->GetString().empty()) {
    return true;
  }
  if (salt->GetString().size() < kMinDohSaltSize ||
      salt->GetString().size() > kMaxDohSaltSize) {
    errors->AddError(policy::key::kDnsOverHttpsSalt,
                     IDS_POLICY_SECURE_DNS_SALT_INVALID_SIZE_ERROR);
    return false;
  }
  bool templates_set = false;
  if (policies.IsPolicySet(
          policy::key::kDnsOverHttpsTemplatesWithIdentifiers)) {
    const base::Value* templates =
        policies.GetValue(policy::key::kDnsOverHttpsTemplatesWithIdentifiers,
                          base::Value::Type::STRING);
    templates_set = templates && !templates->GetString().empty();
  }
  if (!templates_set) {
    errors->AddError(policy::key::kDnsOverHttpsSalt,
                     IDS_POLICY_DEPENDENCY_ERROR_ANY_VALUE,
                     policy::key::kDnsOverHttpsTemplatesWithIdentifiers);
    return false;
  }
  return true;
}
#endif

// Returns true if the policy `policy_name` has a valid template URI value,
// otherwise returns false. If an error occurs, the error message will be
// appended to `errors`.
bool CheckDnsOverHttpsTemplatePolicy(const policy::PolicyMap& policies,
                                     policy::PolicyErrorMap* errors,
                                     const std::string& policy_name,
                                     std::string_view mode) {
  // It is safe to use `GetValueUnsafe()` because type checking is performed
  // before the value is used.
  const base::Value* templates = policies.GetValueUnsafe(policy_name);

  if (!templates)
    return false;

  if (!templates->is_string()) {
    errors->AddError(policy_name, IDS_POLICY_TYPE_ERROR,
                     base::Value::GetTypeName(base::Value::Type::STRING));
    return false;
  }
  if (templates->GetString().empty())
    return false;

  const std::string& templates_str = templates->GetString();
  if (!net::DnsOverHttpsConfig::FromString(templates_str)) {
    errors->AddError(policy_name,
                     IDS_POLICY_SECURE_DNS_TEMPLATES_INVALID_ERROR);
  }
  return true;
}

}  // namespace

namespace policy {

SecureDnsPolicyHandler::SecureDnsPolicyHandler() = default;

SecureDnsPolicyHandler::~SecureDnsPolicyHandler() = default;

// Verifies if the combination of policies which set the secure DNS mode and
// the templates URI is valid. The templates URIs can be set via the cross
// platform policy "DnsOverHttpsTemplates" and, on Chrome OS only, via the
// policy "DnsOverHttpsTemplatesWithIdentifiers" which will override the cross
// platform policy if both are set.
bool SecureDnsPolicyHandler::CheckPolicySettings(const PolicyMap& policies,
                                                 PolicyErrorMap* errors) {
  bool mode_is_applicable = true;
  bool templates_is_applicable = true;
  std::string applicable_template_policy_name = key::kDnsOverHttpsTemplates;

  // It is safe to use `GetValueUnsafe()` because type checking is performed
  // before the value is used.
  const base::Value* mode = policies.GetValueUnsafe(key::kDnsOverHttpsMode);
  std::string_view mode_str;
  if (!mode) {
    mode_is_applicable = false;
  } else if (!mode->is_string()) {
    errors->AddError(key::kDnsOverHttpsMode, IDS_POLICY_TYPE_ERROR,
                     base::Value::GetTypeName(base::Value::Type::STRING));
    mode_is_applicable = false;
  } else {
    // Mode is set and is a string.
    mode_str = mode->GetString();

    if (mode_str.size() == 0) {
      errors->AddError(key::kDnsOverHttpsMode, IDS_POLICY_NOT_SPECIFIED_ERROR);
      mode_is_applicable = false;
    } else if (!SecureDnsConfig::ParseMode(mode_str)) {
      errors->AddError(key::kDnsOverHttpsMode,
                       IDS_POLICY_INVALID_SECURE_DNS_MODE_ERROR);
      mode_is_applicable = false;
    }
  }

  is_templates_policy_valid_ = CheckDnsOverHttpsTemplatePolicy(
      policies, errors, key::kDnsOverHttpsTemplates, mode_str);
  templates_is_applicable = is_templates_policy_valid_;

#if BUILDFLAG(IS_CHROMEOS)
  bool templates_valid = CheckDnsOverHttpsTemplatePolicy(
      policies, errors, key::kDnsOverHttpsTemplatesWithIdentifiers, mode_str);
  bool salt_valid = CheckDnsOverHttpsSaltPolicy(policies, errors);
  is_templates_with_identifiers_policy_valid_ = templates_valid && salt_valid;

  templates_is_applicable =
      is_templates_policy_valid_ || is_templates_with_identifiers_policy_valid_;

  if (is_templates_with_identifiers_policy_valid_) {
    applicable_template_policy_name =
        key::kDnsOverHttpsTemplatesWithIdentifiers;
  }
#endif
  if (IsTemplatesPolicyNotSpecified(templates_is_applicable, mode_str)) {
    errors->AddError(applicable_template_policy_name,
                     IDS_POLICY_SECURE_DNS_TEMPLATES_NOT_SPECIFIED_ERROR);
  }

  if (templates_is_applicable) {
    if (!mode) {
      errors->AddError(applicable_template_policy_name,
                       IDS_POLICY_SECURE_DNS_TEMPLATES_UNSET_MODE_ERROR);
    } else if (!mode_is_applicable) {
      errors->AddError(applicable_template_policy_name,
                       IDS_POLICY_SECURE_DNS_TEMPLATES_INVALID_MODE_ERROR);
    } else if (mode_str == SecureDnsConfig::kModeOff) {
      errors->AddError(applicable_template_policy_name,
                       IDS_POLICY_SECURE_DNS_TEMPLATES_IRRELEVANT_MODE_ERROR);
    }
  }
  return mode_is_applicable || templates_is_applicable;
}

void SecureDnsPolicyHandler::ApplyPolicySettings(const PolicyMap& policies,
                                                 PrefValueMap* prefs) {
  const base::Value* mode =
      policies.GetValue(key::kDnsOverHttpsMode, base::Value::Type::STRING);
  std::string_view mode_str;
  if (mode) {
    mode_str = mode->GetString();
    prefs->SetString(prefs::kDnsOverHttpsMode,
                     SecureDnsConfig::ParseMode(mode_str)
                         ? std::string(mode_str)
                         : SecureDnsConfig::kModeOff);
  }

  const base::Value* templates =
      policies.GetValue(key::kDnsOverHttpsTemplates, base::Value::Type::STRING);

  // A templates not specified error means that the pref should be set blank.
  if (IsTemplatesPolicyNotSpecified(is_templates_policy_valid_, mode_str))
    prefs->SetString(prefs::kDnsOverHttpsTemplates, std::string());
  else if (is_templates_policy_valid_)
    prefs->SetString(prefs::kDnsOverHttpsTemplates, templates->GetString());

#if BUILDFLAG(IS_CHROMEOS)
  const base::Value* templates_with_identifiers = policies.GetValue(
      key::kDnsOverHttpsTemplatesWithIdentifiers, base::Value::Type::STRING);
  const base::Value* salt =
      policies.GetValue(key::kDnsOverHttpsSalt, base::Value::Type::STRING);

  // A templates not specified error means that the pref should be set blank.
  if (IsTemplatesPolicyNotSpecified(is_templates_with_identifiers_policy_valid_,
                                    mode_str)) {
    prefs->SetString(ash::prefs::kDnsOverHttpsTemplatesWithIdentifiers,
                     std::string());
    prefs->SetString(ash::prefs::kDnsOverHttpsSalt, std::string());

  } else if (is_templates_with_identifiers_policy_valid_) {
    prefs->SetString(ash::prefs::kDnsOverHttpsTemplatesWithIdentifiers,
                     templates_with_identifiers->GetString());
    prefs->SetString(ash::prefs::kDnsOverHttpsSalt,
                     salt ? salt->GetString() : std::string());
  }
#endif
}

bool SecureDnsPolicyHandler::IsTemplatesPolicyNotSpecified(
    bool is_templates_policy_valid,
    std::string_view mode_str) {
  if (mode_str == SecureDnsConfig::kModeSecure)
    return !is_templates_policy_valid;

  return false;
}

}  // namespace policy
