// Copyright (c) 2011 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 "chrome/browser/chromeos/proxy_config_service_impl.h"

#include <ostream>

#include "base/logging.h"
#include "base/string_util.h"
#include "base/task.h"
#include "chrome/browser/chromeos/cros/cros_library.h"
#include "chrome/browser/chromeos/cros_settings_names.h"
#include "chrome/browser/policy/proto/chrome_device_policy.pb.h"
#include "chrome/browser/prefs/proxy_prefs.h"
#include "content/browser/browser_thread.h"

namespace em = enterprise_management;

namespace chromeos {

namespace {

const char* SourceToString(ProxyConfigServiceImpl::ProxyConfig::Source source) {
  switch (source) {
    case ProxyConfigServiceImpl::ProxyConfig::SOURCE_NONE:
      return "SOURCE_NONE";
    case ProxyConfigServiceImpl::ProxyConfig::SOURCE_POLICY:
      return "SOURCE_POLICY";
    case ProxyConfigServiceImpl::ProxyConfig::SOURCE_OWNER:
      return "SOURCE_OWNER";
  }
  NOTREACHED() << "Unrecognized source type";
  return "";
}

std::ostream& operator<<(std::ostream& out,
    const ProxyConfigServiceImpl::ProxyConfig::ManualProxy& proxy) {
  out << "  " << SourceToString(proxy.source) << "\n"
      << "  server: " << (proxy.server.is_valid() ? proxy.server.ToURI() : "")
      << "\n";
  return out;
}

std::ostream& operator<<(std::ostream& out,
    const ProxyConfigServiceImpl::ProxyConfig& config) {
  switch (config.mode) {
    case ProxyConfigServiceImpl::ProxyConfig::MODE_DIRECT:
      out << "Direct connection:\n  "
          << SourceToString(config.automatic_proxy.source) << "\n";
      break;
    case ProxyConfigServiceImpl::ProxyConfig::MODE_AUTO_DETECT:
      out << "Auto detection:\n  "
          << SourceToString(config.automatic_proxy.source) << "\n";
      break;
    case ProxyConfigServiceImpl::ProxyConfig::MODE_PAC_SCRIPT:
      out << "Custom PAC script:\n  "
          << SourceToString(config.automatic_proxy.source)
          << "\n  PAC: " << config.automatic_proxy.pac_url << "\n";
      break;
    case ProxyConfigServiceImpl::ProxyConfig::MODE_SINGLE_PROXY:
      out << "Single proxy:\n" << config.single_proxy;
      break;
    case ProxyConfigServiceImpl::ProxyConfig::MODE_PROXY_PER_SCHEME:
      out << "HTTP proxy: " << config.http_proxy;
      out << "HTTPS proxy: " << config.https_proxy;
      out << "FTP proxy: " << config.ftp_proxy;
      out << "SOCKS proxy: " << config.socks_proxy;
      break;
    default:
      NOTREACHED() << "Unrecognized proxy config mode";
      break;
  }
  if (config.mode == ProxyConfigServiceImpl::ProxyConfig::MODE_SINGLE_PROXY ||
      config.mode ==
          ProxyConfigServiceImpl::ProxyConfig::MODE_PROXY_PER_SCHEME) {
    out << "Bypass list: ";
    if (config.bypass_rules.rules().empty()) {
      out << "[None]";
    } else {
      const net::ProxyBypassRules& bypass_rules = config.bypass_rules;
      net::ProxyBypassRules::RuleList::const_iterator it;
      for (it = bypass_rules.rules().begin();
           it != bypass_rules.rules().end(); ++it) {
        out << "\n    " << (*it)->ToString();
      }
    }
  }
  return out;
}

std::string ProxyConfigToString(
    const ProxyConfigServiceImpl::ProxyConfig& proxy_config) {
  std::ostringstream stream;
  stream << proxy_config;
  return stream.str();
}

}  // namespace

//---------- ProxyConfigServiceImpl::ProxyConfig::Setting methods --------------

bool ProxyConfigServiceImpl::ProxyConfig::Setting::CanBeWrittenByUser(
    bool user_is_owner) {
  // Setting can only be written by user if user is owner and setting is not
  // from policy.
  return user_is_owner && source != ProxyConfig::SOURCE_POLICY;
}

//----------- ProxyConfigServiceImpl::ProxyConfig: public methods --------------

void ProxyConfigServiceImpl::ProxyConfig::ToNetProxyConfig(
    net::ProxyConfig* net_config) {
  switch (mode) {
    case MODE_DIRECT:
      *net_config = net::ProxyConfig::CreateDirect();
      break;
    case MODE_AUTO_DETECT:
      *net_config = net::ProxyConfig::CreateAutoDetect();
      break;
    case MODE_PAC_SCRIPT:
      *net_config = net::ProxyConfig::CreateFromCustomPacURL(
          automatic_proxy.pac_url);
      break;
    case MODE_SINGLE_PROXY:
      *net_config = net::ProxyConfig();
      net_config->proxy_rules().type =
             net::ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY;
      net_config->proxy_rules().single_proxy = single_proxy.server;
      net_config->proxy_rules().bypass_rules = bypass_rules;
      break;
    case MODE_PROXY_PER_SCHEME:
      *net_config = net::ProxyConfig();
      net_config->proxy_rules().type =
          net::ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME;
      net_config->proxy_rules().proxy_for_http = http_proxy.server;
      net_config->proxy_rules().proxy_for_https = https_proxy.server;
      net_config->proxy_rules().proxy_for_ftp = ftp_proxy.server;
      net_config->proxy_rules().fallback_proxy = socks_proxy.server;
      net_config->proxy_rules().bypass_rules = bypass_rules;
      break;
    default:
      NOTREACHED() << "Unrecognized proxy config mode";
      break;
  }
}

bool ProxyConfigServiceImpl::ProxyConfig::CanBeWrittenByUser(
    bool user_is_owner, const std::string& scheme) {
  // Setting can only be written by user if user is owner and setting is not
  // from policy.
  Setting* setting = NULL;
  switch (mode) {
    case MODE_DIRECT:
    case MODE_AUTO_DETECT:
    case MODE_PAC_SCRIPT:
      setting = &automatic_proxy;
      break;
    case MODE_SINGLE_PROXY:
      setting = &single_proxy;
      break;
    case MODE_PROXY_PER_SCHEME:
      setting = MapSchemeToProxy(scheme);
      break;
    default:
      break;
  }
  if (!setting) {
    NOTREACHED() << "Unrecognized proxy config mode";
    return false;
  }
  return setting->CanBeWrittenByUser(user_is_owner);
}

ProxyConfigServiceImpl::ProxyConfig::ManualProxy*
    ProxyConfigServiceImpl::ProxyConfig::MapSchemeToProxy(
        const std::string& scheme) {
  if (scheme == "http")
    return &http_proxy;
  if (scheme == "https")
    return &https_proxy;
  if (scheme == "ftp")
    return &ftp_proxy;
  if (scheme == "socks")
    return &socks_proxy;
  NOTREACHED() << "Invalid scheme: " << scheme;
  return NULL;
}

bool ProxyConfigServiceImpl::ProxyConfig::Serialize(std::string* output) {
  em::DeviceProxySettingsProto proxy_proto;
  switch (mode) {
    case MODE_DIRECT: {
      proxy_proto.set_proxy_mode(ProxyPrefs::kDirectProxyModeName);
      break;
    }
    case MODE_AUTO_DETECT: {
      proxy_proto.set_proxy_mode(ProxyPrefs::kAutoDetectProxyModeName);
      break;
    }
    case MODE_PAC_SCRIPT: {
      proxy_proto.set_proxy_mode(ProxyPrefs::kPacScriptProxyModeName);
      if (!automatic_proxy.pac_url.is_empty())
        proxy_proto.set_proxy_pac_url(automatic_proxy.pac_url.spec());
      break;
    }
    case MODE_SINGLE_PROXY: {
      proxy_proto.set_proxy_mode(ProxyPrefs::kFixedServersProxyModeName);
      if (single_proxy.server.is_valid())
        proxy_proto.set_proxy_server(single_proxy.server.ToURI());
      break;
    }
    case MODE_PROXY_PER_SCHEME: {
      proxy_proto.set_proxy_mode(ProxyPrefs::kFixedServersProxyModeName);
      std::string spec;
      EncodeAndAppendProxyServer("http", http_proxy.server, &spec);
      EncodeAndAppendProxyServer("https", https_proxy.server, &spec);
      EncodeAndAppendProxyServer("ftp", ftp_proxy.server, &spec);
      EncodeAndAppendProxyServer("socks", socks_proxy.server, &spec);
      if (!spec.empty())
        proxy_proto.set_proxy_server(spec);
      break;
    }
    default: {
      NOTREACHED() << "Unrecognized proxy config mode";
      break;
    }
  }
  proxy_proto.set_proxy_bypass_list(bypass_rules.ToString());
  return proxy_proto.SerializeToString(output);
}

bool ProxyConfigServiceImpl::ProxyConfig::Deserialize(
    const std::string& input) {
  em::DeviceProxySettingsProto proxy_proto;
  if (!proxy_proto.ParseFromString(input))
    return false;

  const std::string& mode_string(proxy_proto.proxy_mode());
  if (mode_string == ProxyPrefs::kDirectProxyModeName) {
    mode = MODE_DIRECT;
  } else if (mode_string == ProxyPrefs::kAutoDetectProxyModeName) {
    mode = MODE_AUTO_DETECT;
  } else if (mode_string == ProxyPrefs::kPacScriptProxyModeName) {
    mode = MODE_PAC_SCRIPT;
    if (proxy_proto.has_proxy_pac_url())
      automatic_proxy.pac_url = GURL(proxy_proto.proxy_pac_url());
  } else if (mode_string == ProxyPrefs::kFixedServersProxyModeName) {
    net::ProxyConfig::ProxyRules rules;
    rules.ParseFromString(proxy_proto.proxy_server());
    switch (rules.type) {
      case net::ProxyConfig::ProxyRules::TYPE_NO_RULES:
        return false;
      case net::ProxyConfig::ProxyRules::TYPE_SINGLE_PROXY:
        if (!rules.single_proxy.is_valid())
          return false;
        mode = MODE_SINGLE_PROXY;
        single_proxy.server = rules.single_proxy;
        break;
      case net::ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME:
        // Make sure we have valid server for at least one of the protocols.
        if (!rules.proxy_for_http.is_valid() &&
            !rules.proxy_for_https.is_valid() &&
            !rules.proxy_for_ftp.is_valid() &&
            !rules.fallback_proxy.is_valid()) {
          return false;
        }
        mode = MODE_PROXY_PER_SCHEME;
        if (rules.proxy_for_http.is_valid())
          http_proxy.server = rules.proxy_for_http;
        if (rules.proxy_for_https.is_valid())
          https_proxy.server = rules.proxy_for_https;
        if (rules.proxy_for_ftp.is_valid())
          ftp_proxy.server = rules.proxy_for_ftp;
        if (rules.fallback_proxy.is_valid())
          socks_proxy.server = rules.fallback_proxy;
        break;
    }
  } else {
    NOTREACHED() << "Unrecognized proxy config mode";
    return false;
  }

  if (proxy_proto.has_proxy_bypass_list())
    bypass_rules.ParseFromString(proxy_proto.proxy_bypass_list());

  return true;
}

std::string ProxyConfigServiceImpl::ProxyConfig::ToString() const {
  return ProxyConfigToString(*this);
}

//----------- ProxyConfigServiceImpl::ProxyConfig: private methods -------------

// static
void ProxyConfigServiceImpl::ProxyConfig::EncodeAndAppendProxyServer(
    const std::string& scheme,
    const net::ProxyServer& server,
    std::string* spec) {
  if (!server.is_valid())
    return;

  if (!spec->empty())
    *spec += ';';

  if (!scheme.empty()) {
    *spec += scheme;
    *spec += "=";
  }
  *spec += server.ToURI();
}

//------------------- ProxyConfigServiceImpl: public methods -------------------

ProxyConfigServiceImpl::ProxyConfigServiceImpl()
    : can_post_task_(false),
      config_availability_(net::ProxyConfigService::CONFIG_PENDING),
      persist_to_device_(true),
      persist_to_device_pending_(false) {
  // Start async fetch of proxy config from settings persisted on device.
  // TODO(kuan): retrieve config from policy and owner and merge them
  bool use_default = true;
  if (CrosLibrary::Get()->EnsureLoaded()) {
    retrieve_property_op_ = SignedSettings::CreateRetrievePropertyOp(
        kSettingProxyEverywhere, this);
    if (retrieve_property_op_) {
      retrieve_property_op_->Execute();
      VLOG(1) << "Start retrieving proxy setting from device";
      use_default = false;
    } else {
      VLOG(1) << "Fail to retrieve proxy setting from device";
    }
  }
  if (use_default)
    config_availability_ = net::ProxyConfigService::CONFIG_UNSET;
  can_post_task_ = true;
}

ProxyConfigServiceImpl::ProxyConfigServiceImpl(const ProxyConfig& init_config)
    : can_post_task_(true),
      config_availability_(net::ProxyConfigService::CONFIG_VALID),
      persist_to_device_(false),
      persist_to_device_pending_(false) {
  reference_config_ = init_config;
  // Update the IO-accessible copy in |cached_config_| as well.
  cached_config_ = reference_config_;
}

ProxyConfigServiceImpl::~ProxyConfigServiceImpl() {
}

void ProxyConfigServiceImpl::UIGetProxyConfig(ProxyConfig* config) {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  // Simply returns the copy on the UI thread.
  *config = reference_config_;
}

bool ProxyConfigServiceImpl::UISetProxyConfigToDirect() {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  reference_config_.mode = ProxyConfig::MODE_DIRECT;
  OnUISetProxyConfig(persist_to_device_);
  return true;
}

bool ProxyConfigServiceImpl::UISetProxyConfigToAutoDetect() {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  reference_config_.mode = ProxyConfig::MODE_AUTO_DETECT;
  OnUISetProxyConfig(persist_to_device_);
  return true;
}

bool ProxyConfigServiceImpl::UISetProxyConfigToPACScript(const GURL& pac_url) {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  reference_config_.mode = ProxyConfig::MODE_PAC_SCRIPT;
  reference_config_.automatic_proxy.pac_url = pac_url;
  OnUISetProxyConfig(persist_to_device_);
  return true;
}

bool ProxyConfigServiceImpl::UISetProxyConfigToSingleProxy(
    const net::ProxyServer& server) {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  reference_config_.mode = ProxyConfig::MODE_SINGLE_PROXY;
  reference_config_.single_proxy.server = server;
  OnUISetProxyConfig(persist_to_device_);
  return true;
}

bool ProxyConfigServiceImpl::UISetProxyConfigToProxyPerScheme(
    const std::string& scheme, const net::ProxyServer& server) {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  ProxyConfig::ManualProxy* proxy = reference_config_.MapSchemeToProxy(scheme);
  if (!proxy) {
    NOTREACHED() << "Cannot set proxy: invalid scheme [" << scheme << "]";
    return false;
  }
  reference_config_.mode = ProxyConfig::MODE_PROXY_PER_SCHEME;
  proxy->server = server;
  OnUISetProxyConfig(persist_to_device_);
  return true;
}

bool ProxyConfigServiceImpl::UISetProxyConfigBypassRules(
    const net::ProxyBypassRules& bypass_rules) {
  // Should be called from UI thread.
  CheckCurrentlyOnUIThread();
  DCHECK(reference_config_.mode == ProxyConfig::MODE_SINGLE_PROXY ||
         reference_config_.mode == ProxyConfig::MODE_PROXY_PER_SCHEME);
  if (reference_config_.mode != ProxyConfig::MODE_SINGLE_PROXY &&
      reference_config_.mode != ProxyConfig::MODE_PROXY_PER_SCHEME) {
    VLOG(1) << "Cannot set bypass rules for proxy mode ["
             << reference_config_.mode << "]";
    return false;
  }
  reference_config_.bypass_rules = bypass_rules;
  OnUISetProxyConfig(persist_to_device_);
  return true;
}

void ProxyConfigServiceImpl::AddObserver(
    net::ProxyConfigService::Observer* observer) {
  // Should be called from IO thread.
  CheckCurrentlyOnIOThread();
  observers_.AddObserver(observer);
}

void ProxyConfigServiceImpl::RemoveObserver(
    net::ProxyConfigService::Observer* observer) {
  // Should be called from IO thread.
  CheckCurrentlyOnIOThread();
  observers_.RemoveObserver(observer);
}

net::ProxyConfigService::ConfigAvailability
    ProxyConfigServiceImpl::IOGetProxyConfig(net::ProxyConfig* net_config) {
  // Should be called from IO thread.
  CheckCurrentlyOnIOThread();
  if (config_availability_ == net::ProxyConfigService::CONFIG_VALID)
    cached_config_.ToNetProxyConfig(net_config);

  return config_availability_;
}

void ProxyConfigServiceImpl::OnSettingsOpCompleted(
    SignedSettings::ReturnCode code,
    bool value) {
  if (SignedSettings::SUCCESS == code)
    VLOG(1) << "Stored proxy setting to device";
  else
    LOG(WARNING) << "Error storing proxy setting to device";
  store_property_op_ = NULL;
  if (persist_to_device_pending_)
    PersistConfigToDevice();
}

void ProxyConfigServiceImpl::OnSettingsOpCompleted(
    SignedSettings::ReturnCode code,
    std::string value) {
  retrieve_property_op_ = NULL;
  if (SignedSettings::SUCCESS == code) {
    VLOG(1) << "Retrieved proxy setting from device, value=[" << value << "]";
    if (reference_config_.Deserialize(value)) {
      IOSetProxyConfig(reference_config_,
                       net::ProxyConfigService::CONFIG_VALID);
      return;
    } else {
      LOG(WARNING) << "Error deserializing device's proxy setting";
    }
  } else {
    LOG(WARNING) << "Error retrieving proxy setting from device";
  }

  // Update the configuration state on the IO thread.
  IOSetProxyConfig(reference_config_, net::ProxyConfigService::CONFIG_UNSET);
}

//------------------ ProxyConfigServiceImpl: private methods -------------------

void ProxyConfigServiceImpl::PersistConfigToDevice() {
  DCHECK(!store_property_op_);
  persist_to_device_pending_ = false;
  std::string value;
  if (!reference_config_.Serialize(&value)) {
    LOG(WARNING) << "Error serializing proxy config";
    return;
  }
  store_property_op_ = SignedSettings::CreateStorePropertyOp(
      kSettingProxyEverywhere, value, this);
  store_property_op_->Execute();
  VLOG(1) << "Start storing proxy setting to device, value=" << value;
}

void ProxyConfigServiceImpl::OnUISetProxyConfig(bool persist_to_device) {
  IOSetProxyConfig(reference_config_, net::ProxyConfigService::CONFIG_VALID);
  if (persist_to_device && CrosLibrary::Get()->EnsureLoaded()) {
    if (store_property_op_) {
      persist_to_device_pending_ = true;
      VLOG(1) << "Pending persisting proxy setting to device";
    } else {
      PersistConfigToDevice();
    }
  }
}

void ProxyConfigServiceImpl::IOSetProxyConfig(
    const ProxyConfig& new_config,
    net::ProxyConfigService::ConfigAvailability new_availability) {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO) && can_post_task_) {
    // Posts a task to IO thread with the new config, so it can update
    // |cached_config_|.
    Task* task = NewRunnableMethod(this,
                                   &ProxyConfigServiceImpl::IOSetProxyConfig,
                                   new_config,
                                   new_availability);
    if (!BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, task))
      VLOG(1) << "Couldn't post task to IO thread to set new proxy config";
    return;
  }

  // Now guaranteed to be on the correct thread.
  VLOG(1) << "Proxy configuration changed";
  cached_config_ = new_config;
  config_availability_ = new_availability;
  // Notify observers of new proxy config.
  net::ProxyConfig net_config;
  cached_config_.ToNetProxyConfig(&net_config);
  FOR_EACH_OBSERVER(net::ProxyConfigService::Observer, observers_,
                    OnProxyConfigChanged(net_config, config_availability_));
}

void ProxyConfigServiceImpl::CheckCurrentlyOnIOThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
}

void ProxyConfigServiceImpl::CheckCurrentlyOnUIThread() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}

}  // namespace chromeos