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

#include "components/domain_reliability/header.h"

#include <stdint.h>

#include <memory>
#include <string>

#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "components/domain_reliability/config.h"
#include "content/public/common/origin_util.h"

namespace {

// Parses directives in the format ("foo; bar=value for bar; baz; quux=123")
// used by NEL.
class DirectiveHeaderValueParser {
 public:
  enum State {
    BEFORE_NAME,
    AFTER_NAME,
    BEFORE_VALUE,
    AFTER_DIRECTIVE,
    SYNTAX_ERROR
  };

  DirectiveHeaderValueParser(base::StringPiece value)
      : value_(value.data()),
        tokenizer_(value_.begin(), value_.end(), ";= "),
        stopped_with_error_(false) {
    tokenizer_.set_options(base::StringTokenizer::RETURN_DELIMS);
    tokenizer_.set_quote_chars("\"'");
  }

  // Gets the next directive, if there is one. Returns whether there was one.
  bool GetNext() {
    if (stopped_with_error_)
      return false;

    directive_name_ = base::StringPiece();
    directive_has_value_ = false;
    directive_values_.clear();

    State state = BEFORE_NAME;
    while (state != AFTER_DIRECTIVE && state != SYNTAX_ERROR
           && tokenizer_.GetNext()) {
      if (*tokenizer_.token_begin() == ' ')
        continue;

      switch (state) {
        case BEFORE_NAME:
          state = DoBeforeName();
          break;
        case AFTER_NAME:
          state = DoAfterName();
          break;
        case BEFORE_VALUE:
          state = DoBeforeValue();
          break;
        case AFTER_DIRECTIVE:
        case SYNTAX_ERROR:
          NOTREACHED();
          break;
      }
    }

    switch (state) {
      // If the parser just read the last directive, it may be in one of these
      // states, so return true to yield that directive.
      case AFTER_NAME:
      case BEFORE_VALUE:
      case AFTER_DIRECTIVE:
        return true;

      // If the parser never found a name, return false, since it doesn't have
      // a new directive for the caller.
      case BEFORE_NAME:
        return false;

      case SYNTAX_ERROR:
        stopped_with_error_ = true;
        return false;

      default:
        NOTREACHED();
        return false;
    }
  }


  base::StringPiece directive_name() const { return directive_name_; }
  bool directive_has_value() const { return directive_has_value_; }
  const std::vector<base::StringPiece>& directive_values() const {
    return directive_values_;
  }
  bool stopped_with_error() const { return stopped_with_error_; }

 private:
  State DoBeforeName() {
    if (tokenizer_.token_is_delim())
      return SYNTAX_ERROR;

    directive_name_ = tokenizer_.token_piece();
    return AFTER_NAME;
  }

  State DoAfterName() {
    if (tokenizer_.token_is_delim()) {
      char token_begin = *tokenizer_.token_begin();
      // Name can be followed by =value, ;, or just EOF.
      if (token_begin == '=') {
        directive_has_value_ = true;
        return BEFORE_VALUE;
      }
      if (token_begin == ';')
        return AFTER_DIRECTIVE;
    }
    return SYNTAX_ERROR;
  }

  State DoBeforeValue() {
    if (tokenizer_.token_is_delim()) {
      char token_begin = *tokenizer_.token_begin();
      if (token_begin == ';')
        return AFTER_DIRECTIVE;
      return SYNTAX_ERROR;
    }

    directive_values_.push_back(tokenizer_.token_piece());
    return BEFORE_VALUE;
  }

  std::string value_;
  base::StringTokenizer tokenizer_;

  base::StringPiece directive_name_;
  bool directive_has_value_;
  std::vector<base::StringPiece> directive_values_;
  bool stopped_with_error_;
};

bool Unquote(const std::string& in, std::string* out) {
  char first = in[0];
  char last = in[in.length() - 1];

  if (((first == '"') ^ (last == '"')) || ((first == '<') ^ (last == '>')))
    return false;

  if ((first == '"') || (first == '<'))
    *out = in.substr(1, in.length() - 2);
  else
    *out = in;
  return true;
}

bool ParseReportUri(const std::vector<base::StringPiece> in,
                    std::vector<std::unique_ptr<GURL>>* out) {
  if (in.size() < 1u)
    return false;

  out->clear();
  for (const auto& in_token : in) {
    std::string unquoted;
    if (!Unquote(in_token.as_string(), &unquoted))
      return false;
    GURL url(unquoted);
    if (!url.is_valid() || !content::IsOriginSecure(url))
      return false;
    out->push_back(std::make_unique<GURL>(url));
  }

  return true;
}

bool ParseMaxAge(const std::vector<base::StringPiece> in,
                 base::TimeDelta* out) {
  if (in.size() != 1u)
    return false;

  int64_t seconds;
  if (!base::StringToInt64(in[0], &seconds))
    return false;

  if (seconds < 0)
    return false;

  *out = base::TimeDelta::FromSeconds(seconds);
  return true;
}

}  // namespace

namespace domain_reliability {

DomainReliabilityHeader::~DomainReliabilityHeader() {}

// static
std::unique_ptr<DomainReliabilityHeader> DomainReliabilityHeader::Parse(
    base::StringPiece value) {
  std::vector<std::unique_ptr<GURL>> report_uri;
  base::TimeDelta max_age;
  bool include_subdomains = false;

  bool got_report_uri = false;
  bool got_max_age = false;
  bool got_include_subdomains = false;

  DirectiveHeaderValueParser parser(value);
  while (parser.GetNext()) {
    base::StringPiece name = parser.directive_name();
    if (name == "report-uri") {
      if (got_report_uri
          || !parser.directive_has_value()
          || !ParseReportUri(parser.directive_values(), &report_uri)) {
        return base::WrapUnique(new DomainReliabilityHeader(PARSE_ERROR));
      }
      got_report_uri = true;
    } else if (name == "max-age") {
      if (got_max_age
          || !parser.directive_has_value()
          || !ParseMaxAge(parser.directive_values(), &max_age)) {
        return base::WrapUnique(new DomainReliabilityHeader(PARSE_ERROR));
      }
      got_max_age = true;
    } else if (name == "includeSubdomains") {
      if (got_include_subdomains ||
          parser.directive_has_value()) {
        return base::WrapUnique(new DomainReliabilityHeader(PARSE_ERROR));
      }
      include_subdomains = true;
      got_include_subdomains = true;
    } else {
      LOG(WARNING) << "Ignoring unknown NEL header directive " << name << ".";
    }
  }

  if (parser.stopped_with_error() || !got_max_age)
    return base::WrapUnique(new DomainReliabilityHeader(PARSE_ERROR));

  if (max_age == base::TimeDelta::FromMicroseconds(0))
    return base::WrapUnique(new DomainReliabilityHeader(PARSE_CLEAR_CONFIG));

  if (!got_report_uri)
    return base::WrapUnique(new DomainReliabilityHeader(PARSE_ERROR));

  std::unique_ptr<DomainReliabilityConfig> config(
      new DomainReliabilityConfig());
  config->include_subdomains = include_subdomains;
  config->collectors.clear();
  config->collectors.swap(report_uri);
  config->success_sample_rate = 0.0;
  config->failure_sample_rate = 1.0;
  config->path_prefixes.clear();
  return base::WrapUnique(new DomainReliabilityHeader(
      PARSE_SET_CONFIG, std::move(config), max_age));
}

const DomainReliabilityConfig& DomainReliabilityHeader::config() const {
  DCHECK_EQ(PARSE_SET_CONFIG, status_);
  return *config_;
}

base::TimeDelta DomainReliabilityHeader::max_age() const {
  DCHECK_EQ(PARSE_SET_CONFIG, status_);
  return max_age_;
}

std::unique_ptr<DomainReliabilityConfig>
DomainReliabilityHeader::ReleaseConfig() {
  DCHECK_EQ(PARSE_SET_CONFIG, status_);
  status_ = PARSE_ERROR;
  return std::move(config_);
}

std::string DomainReliabilityHeader::ToString() const {
  std::string string;
  int64_t max_age_s = max_age_.InSeconds();

  if (config_->collectors.empty()) {
    DCHECK_EQ(0, max_age_s);
  } else {
    string += "report-uri=";
    for (const auto& uri : config_->collectors)
      string += uri->spec() + " ";
    // Remove trailing space.
    string.erase(string.length() - 1, 1);
    string += "; ";
  }

  string += "max-age=" + base::Int64ToString(max_age_s) + "; ";

  if (config_->include_subdomains)
    string += "includeSubdomains; ";

  // Remove trailing "; ".
  string.erase(string.length() - 2, 2);

  return string;
}

DomainReliabilityHeader::DomainReliabilityHeader(ParseStatus status)
    : status_(status) {
  DCHECK_NE(PARSE_SET_CONFIG, status_);
}

DomainReliabilityHeader::DomainReliabilityHeader(
    ParseStatus status,
    std::unique_ptr<DomainReliabilityConfig> config,
    base::TimeDelta max_age)
    : status_(status), config_(std::move(config)), max_age_(max_age) {
  DCHECK_EQ(PARSE_SET_CONFIG, status_);
  DCHECK(config_);
  DCHECK_NE(0, max_age_.InMicroseconds());
}

}  // namespace domain_reliability
