// 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/policy/device_management_backend_impl.h"

#include <utility>
#include <vector>

#if defined(OS_POSIX) && !defined(OS_MACOSX)
#include <sys/utsname.h>
#endif

#include "base/stringprintf.h"
#include "base/sys_info.h"
#include "chrome/browser/policy/device_management_service.h"
#include "chrome/common/chrome_version_info.h"
#include "net/base/escape.h"
#include "net/url_request/url_request_status.h"

#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/system_access.h"
#endif

namespace policy {

// Name constants for URL query parameters.
const char DeviceManagementBackendImpl::kParamRequest[] = "request";
const char DeviceManagementBackendImpl::kParamDeviceType[] = "devicetype";
const char DeviceManagementBackendImpl::kParamAppType[] = "apptype";
const char DeviceManagementBackendImpl::kParamDeviceID[] = "deviceid";
const char DeviceManagementBackendImpl::kParamAgent[] = "agent";
const char DeviceManagementBackendImpl::kParamPlatform[] = "platform";

// String constants for the device and app type we report to the server.
const char DeviceManagementBackendImpl::kValueRequestRegister[] = "register";
const char DeviceManagementBackendImpl::kValueRequestUnregister[] =
    "unregister";
const char DeviceManagementBackendImpl::kValueRequestPolicy[] = "policy";
const char DeviceManagementBackendImpl::kValueDeviceType[] = "2";
const char DeviceManagementBackendImpl::kValueAppType[] = "Chrome";

namespace {

const char kValueAgent[] = "%s %s(%s)";
const char kValuePlatform[] = "%s|%s|%s";

const char kPostContentType[] = "application/protobuf";

const char kServiceTokenAuthHeader[] = "Authorization: GoogleLogin auth=";
const char kDMTokenAuthHeader[] = "Authorization: GoogleDMToken token=";

// HTTP Error Codes of the DM Server with their concrete meinings in the context
// of the DM Server communication.
const int kSuccess = 200;
const int kInvalidArgument = 400;
const int kInvalidAuthCookieOrDMToken = 401;
const int kDeviceManagementNotAllowed = 403;
const int kInvalidURL = 404; // This error is not coming from the GFE.
const int kPendingApproval = 491;
const int kInternalServerError = 500;
const int kServiceUnavailable = 503;
const int kDeviceNotFound = 901;
const int kPolicyNotFound = 902; // This error is not sent as HTTP status code.

#if defined(OS_CHROMEOS)
// Machine info keys.
const char kMachineInfoHWClass[] = "hardware_class";
const char kMachineInfoBoard[] = "CHROMEOS_RELEASE_BOARD";
#endif

}  // namespace

// Helper class for URL query parameter encoding/decoding.
class URLQueryParameters {
 public:
  URLQueryParameters() {}

  // Add a query parameter.
  void Put(const std::string& name, const std::string& value);

  // Produce the query string, taking care of properly encoding and assembling
  // the names and values.
  std::string Encode();

 private:
  typedef std::vector<std::pair<std::string, std::string> > ParameterMap;
  ParameterMap params_;

  DISALLOW_COPY_AND_ASSIGN(URLQueryParameters);
};

void URLQueryParameters::Put(const std::string& name,
                             const std::string& value) {
  params_.push_back(std::make_pair(name, value));
}

std::string URLQueryParameters::Encode() {
  std::string result;
  for (ParameterMap::const_iterator entry(params_.begin());
       entry != params_.end();
       ++entry) {
    if (entry != params_.begin())
      result += '&';
    result += EscapeQueryParamValue(entry->first, true);
    result += '=';
    result += EscapeQueryParamValue(entry->second, true);
  }
  return result;
}

// A base class containing the common code for the jobs created by the backend
// implementation. Subclasses provide custom code for handling actual register,
// unregister, and policy jobs.
class DeviceManagementJobBase
    : public DeviceManagementService::DeviceManagementJob {
 public:
  virtual ~DeviceManagementJobBase() {}

  // DeviceManagementJob overrides:
  virtual void HandleResponse(const net::URLRequestStatus& status,
                              int response_code,
                              const ResponseCookies& cookies,
                              const std::string& data);
  virtual GURL GetURL(const std::string& server_url);
  virtual void ConfigureRequest(URLFetcher* fetcher);

 protected:
  // Constructs a device management job running for the given backend.
  DeviceManagementJobBase(DeviceManagementBackendImpl* backend_impl,
                          const std::string& request_type,
                          const std::string& device_id)
      : backend_impl_(backend_impl) {
    query_params_.Put(DeviceManagementBackendImpl::kParamRequest, request_type);
    query_params_.Put(DeviceManagementBackendImpl::kParamDeviceType,
                      DeviceManagementBackendImpl::kValueDeviceType);
    query_params_.Put(DeviceManagementBackendImpl::kParamAppType,
                      DeviceManagementBackendImpl::kValueAppType);
    query_params_.Put(DeviceManagementBackendImpl::kParamDeviceID, device_id);
    query_params_.Put(DeviceManagementBackendImpl::kParamAgent,
                      DeviceManagementBackendImpl::GetAgentString());
    query_params_.Put(DeviceManagementBackendImpl::kParamPlatform,
                      DeviceManagementBackendImpl::GetPlatformString());
  }

  void SetQueryParam(const std::string& name, const std::string& value) {
    query_params_.Put(name, value);
  }

  void SetAuthToken(const std::string& auth_token) {
    auth_token_ = auth_token;
  }

  void SetDeviceManagementToken(const std::string& device_management_token) {
    device_management_token_ = device_management_token;
  }

  void SetPayload(const em::DeviceManagementRequest& request) {
    if (!request.SerializeToString(&payload_)) {
      NOTREACHED();
      LOG(ERROR) << "Failed to serialize request.";
    }
  }

 private:
  // Implemented by subclasses to handle decoded responses and errors.
  virtual void OnResponse(
      const em::DeviceManagementResponse& response) = 0;
  virtual void OnError(DeviceManagementBackend::ErrorCode error) = 0;

  // The backend this job is handling a request for.
  DeviceManagementBackendImpl* backend_impl_;

  // Query parameters.
  URLQueryParameters query_params_;

  // Auth token (if applicaple).
  std::string auth_token_;

  // Device management token (if applicable).
  std::string device_management_token_;

  // The payload.
  std::string payload_;

  DISALLOW_COPY_AND_ASSIGN(DeviceManagementJobBase);
};

void DeviceManagementJobBase::HandleResponse(
    const net::URLRequestStatus& status,
    int response_code,
    const ResponseCookies& cookies,
    const std::string& data) {
  // Delete ourselves when this is done.
  scoped_ptr<DeviceManagementJob> scoped_killer(this);
  backend_impl_->JobDone(this);
  backend_impl_ = NULL;

  if (status.status() != net::URLRequestStatus::SUCCESS) {
    OnError(DeviceManagementBackend::kErrorRequestFailed);
    return;
  }

  switch (response_code) {
    case kSuccess: {
      em::DeviceManagementResponse response;
      if (!response.ParseFromString(data)) {
        OnError(DeviceManagementBackend::kErrorResponseDecoding);
        return;
      }
      OnResponse(response);
      return;
    }
    case kInvalidArgument: {
      OnError(DeviceManagementBackend::kErrorRequestInvalid);
      return;
    }
    case kInvalidAuthCookieOrDMToken: {
      OnError(DeviceManagementBackend::kErrorServiceManagementTokenInvalid);
      return;
    }
    case kDeviceManagementNotAllowed: {
      OnError(DeviceManagementBackend::kErrorServiceManagementNotSupported);
      return;
    }
    case kPendingApproval: {
      OnError(DeviceManagementBackend::kErrorServiceActivationPending);
      return;
    }
    case kInvalidURL:
    case kInternalServerError:
    case kServiceUnavailable: {
      OnError(DeviceManagementBackend::kErrorTemporaryUnavailable);
      return;
    }
    case kDeviceNotFound: {
      OnError(DeviceManagementBackend::kErrorServiceDeviceNotFound);
      return;
    }
    case kPolicyNotFound: {
      OnError(DeviceManagementBackend::kErrorServicePolicyNotFound);
      break;
    }
    default: {
      VLOG(1) << "Unexpected HTTP status in response from DMServer : "
              << response_code << ".";
      // Handle all unknown 5xx HTTP error codes as temporary and any other
      // unknown error as one that needs more time to recover.
      if (response_code >= 500 && response_code <= 599)
        OnError(DeviceManagementBackend::kErrorTemporaryUnavailable);
      else
        OnError(DeviceManagementBackend::kErrorHttpStatus);
      return;
    }
  }
}

GURL DeviceManagementJobBase::GetURL(
    const std::string& server_url) {
  return GURL(server_url + '?' + query_params_.Encode());
}

void DeviceManagementJobBase::ConfigureRequest(URLFetcher* fetcher) {
  fetcher->set_upload_data(kPostContentType, payload_);
  std::string extra_headers;
  if (!auth_token_.empty())
    extra_headers += kServiceTokenAuthHeader + auth_token_ + "\n";
  if (!device_management_token_.empty())
    extra_headers += kDMTokenAuthHeader + device_management_token_ + "\n";
  fetcher->set_extra_request_headers(extra_headers);
}

// Handles device registration jobs.
class DeviceManagementRegisterJob : public DeviceManagementJobBase {
 public:
  DeviceManagementRegisterJob(
      DeviceManagementBackendImpl* backend_impl,
      const std::string& auth_token,
      const std::string& device_id,
      const em::DeviceRegisterRequest& request,
      DeviceManagementBackend::DeviceRegisterResponseDelegate* delegate)
      : DeviceManagementJobBase(
          backend_impl,
          DeviceManagementBackendImpl::kValueRequestRegister,
          device_id),
        delegate_(delegate) {
    SetAuthToken(auth_token);
    em::DeviceManagementRequest request_wrapper;
    request_wrapper.mutable_register_request()->CopyFrom(request);
    SetPayload(request_wrapper);
  }
  virtual ~DeviceManagementRegisterJob() {}

 private:
  // DeviceManagementJobBase overrides.
  virtual void OnError(DeviceManagementBackend::ErrorCode error) {
    delegate_->OnError(error);
  }
  virtual void OnResponse(const em::DeviceManagementResponse& response) {
    delegate_->HandleRegisterResponse(response.register_response());
  }

  DeviceManagementBackend::DeviceRegisterResponseDelegate* delegate_;

  DISALLOW_COPY_AND_ASSIGN(DeviceManagementRegisterJob);
};

// Handles device unregistration jobs.
class DeviceManagementUnregisterJob : public DeviceManagementJobBase {
 public:
  DeviceManagementUnregisterJob(
      DeviceManagementBackendImpl* backend_impl,
      const std::string& device_management_token,
      const std::string& device_id,
      const em::DeviceUnregisterRequest& request,
      DeviceManagementBackend::DeviceUnregisterResponseDelegate* delegate)
      : DeviceManagementJobBase(
          backend_impl,
          DeviceManagementBackendImpl::kValueRequestUnregister,
          device_id),
        delegate_(delegate) {
    SetDeviceManagementToken(device_management_token);
    em::DeviceManagementRequest request_wrapper;
    request_wrapper.mutable_unregister_request()->CopyFrom(request);
    SetPayload(request_wrapper);
  }
  virtual ~DeviceManagementUnregisterJob() {}

 private:
  // DeviceManagementJobBase overrides.
  virtual void OnError(DeviceManagementBackend::ErrorCode error) {
    delegate_->OnError(error);
  }
  virtual void OnResponse(const em::DeviceManagementResponse& response) {
    delegate_->HandleUnregisterResponse(response.unregister_response());
  }

  DeviceManagementBackend::DeviceUnregisterResponseDelegate* delegate_;

  DISALLOW_COPY_AND_ASSIGN(DeviceManagementUnregisterJob);
};

// Handles policy request jobs.
class DeviceManagementPolicyJob : public DeviceManagementJobBase {
 public:
  DeviceManagementPolicyJob(
      DeviceManagementBackendImpl* backend_impl,
      const std::string& device_management_token,
      const std::string& device_id,
      const em::DevicePolicyRequest& request,
      DeviceManagementBackend::DevicePolicyResponseDelegate* delegate)
      : DeviceManagementJobBase(
          backend_impl,
          DeviceManagementBackendImpl::kValueRequestPolicy,
          device_id),
        delegate_(delegate) {
    SetDeviceManagementToken(device_management_token);
    em::DeviceManagementRequest request_wrapper;
    request_wrapper.mutable_policy_request()->CopyFrom(request);
    SetPayload(request_wrapper);
  }
  virtual ~DeviceManagementPolicyJob() {}

 private:
  // DeviceManagementJobBase overrides.
  virtual void OnError(DeviceManagementBackend::ErrorCode error) {
    delegate_->OnError(error);
  }
  virtual void OnResponse(const em::DeviceManagementResponse& response) {
    delegate_->HandlePolicyResponse(response.policy_response());
  }

  DeviceManagementBackend::DevicePolicyResponseDelegate* delegate_;

  DISALLOW_COPY_AND_ASSIGN(DeviceManagementPolicyJob);
};

DeviceManagementBackendImpl::DeviceManagementBackendImpl(
    DeviceManagementService* service)
    : service_(service) {
}

DeviceManagementBackendImpl::~DeviceManagementBackendImpl() {
  for (JobSet::iterator job(pending_jobs_.begin());
       job != pending_jobs_.end();
       ++job) {
    service_->RemoveJob(*job);
    delete *job;
  }
  pending_jobs_.clear();
}

std::string DeviceManagementBackendImpl::GetAgentString() {
  static std::string agent;
  if (!agent.empty())
    return agent;

  chrome::VersionInfo version_info;
  agent = base::StringPrintf(kValueAgent,
                             version_info.Name().c_str(),
                             version_info.Version().c_str(),
                             version_info.LastChange().c_str());
  return agent;
}

std::string DeviceManagementBackendImpl::GetPlatformString() {
  static std::string platform;
  if (!platform.empty())
    return platform;

  std::string os_name(base::SysInfo::OperatingSystemName());
  std::string os_hardware(base::SysInfo::CPUArchitecture());

#if defined(OS_CHROMEOS)
  chromeos::SystemAccess* sys_lib = chromeos::SystemAccess::GetInstance();

  std::string hwclass;
  std::string board;
  if (!sys_lib->GetMachineStatistic(kMachineInfoHWClass, &hwclass) ||
      !sys_lib->GetMachineStatistic(kMachineInfoBoard, &board)) {
    LOG(ERROR) << "Failed to get machine information";
  }
  os_name += ",CrOS," + board;
  os_hardware += "," + hwclass;
#endif

  std::string os_version("-");
#if defined(OS_WIN) || defined(OS_MACOSX) || defined(OS_CHROMEOS)
  int32 os_major_version = 0;
  int32 os_minor_version = 0;
  int32 os_bugfix_version = 0;
  base::SysInfo::OperatingSystemVersionNumbers(&os_major_version,
                                               &os_minor_version,
                                               &os_bugfix_version);
  os_version = base::StringPrintf("%d.%d.%d",
                                  os_major_version,
                                  os_minor_version,
                                  os_bugfix_version);
#endif

  platform = base::StringPrintf(kValuePlatform,
                                os_name.c_str(),
                                os_hardware.c_str(),
                                os_version.c_str());
  return platform;
}

void DeviceManagementBackendImpl::JobDone(DeviceManagementJobBase* job) {
  pending_jobs_.erase(job);
}

void DeviceManagementBackendImpl::AddJob(DeviceManagementJobBase* job) {
  pending_jobs_.insert(job);
  service_->AddJob(job);
}

void DeviceManagementBackendImpl::ProcessRegisterRequest(
    const std::string& auth_token,
    const std::string& device_id,
    const em::DeviceRegisterRequest& request,
    DeviceRegisterResponseDelegate* delegate) {
  AddJob(new DeviceManagementRegisterJob(this, auth_token, device_id, request,
                                         delegate));
}

void DeviceManagementBackendImpl::ProcessUnregisterRequest(
    const std::string& device_management_token,
    const std::string& device_id,
    const em::DeviceUnregisterRequest& request,
    DeviceUnregisterResponseDelegate* delegate) {
  AddJob(new DeviceManagementUnregisterJob(this, device_management_token,
                                           device_id, request, delegate));
}

void DeviceManagementBackendImpl::ProcessPolicyRequest(
    const std::string& device_management_token,
    const std::string& device_id,
    const em::DevicePolicyRequest& request,
    DevicePolicyResponseDelegate* delegate) {
  AddJob(new DeviceManagementPolicyJob(this, device_management_token, device_id,
                                       request, delegate));
}

}  // namespace policy