blob: aaf368e27921e4031d5331fd11d6f37c2356852e [file] [log] [blame]
// Copyright (c) 2012 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/cloud/device_management_service.h"
#include <utility>
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/message_loop/message_loop.h"
#include "base/message_loop/message_loop_proxy.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/cookies/cookie_monster.h"
#include "net/dns/host_resolver.h"
#include "net/http/http_network_layer.h"
#include "net/http/http_response_headers.h"
#include "net/proxy/proxy_service.h"
#include "net/ssl/ssl_config_service_defaults.h"
#include "net/url_request/static_http_user_agent_settings.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_status.h"
#include "url/gurl.h"
namespace em = enterprise_management;
namespace policy {
namespace {
const char kPostContentType[] = "application/protobuf";
const char kServiceTokenAuthHeader[] = "Authorization: GoogleLogin auth=";
const char kDMTokenAuthHeader[] = "Authorization: GoogleDMToken token=";
// Number of times to retry on ERR_NETWORK_CHANGED errors.
const int kMaxNetworkChangedRetries = 3;
// HTTP Error Codes of the DM Server with their concrete meanings in the context
// of the DM Server communication.
const int kSuccess = 200;
const int kInvalidArgument = 400;
const int kInvalidAuthCookieOrDMToken = 401;
const int kMissingLicenses = 402;
const int kDeviceManagementNotAllowed = 403;
const int kInvalidURL = 404; // This error is not coming from the GFE.
const int kInvalidSerialNumber = 405;
const int kDeviceIdConflict = 409;
const int kDeviceNotFound = 410;
const int kPendingApproval = 412;
const int kInternalServerError = 500;
const int kServiceUnavailable = 503;
const int kPolicyNotFound = 902; // This error is not sent as HTTP status code.
bool IsProxyError(const net::URLRequestStatus status) {
switch (status.error()) {
case net::ERR_PROXY_CONNECTION_FAILED:
case net::ERR_TUNNEL_CONNECTION_FAILED:
case net::ERR_PROXY_AUTH_UNSUPPORTED:
case net::ERR_HTTPS_PROXY_TUNNEL_RESPONSE:
case net::ERR_MANDATORY_PROXY_CONFIGURATION_FAILED:
case net::ERR_PROXY_CERTIFICATE_INVALID:
case net::ERR_SOCKS_CONNECTION_FAILED:
case net::ERR_SOCKS_CONNECTION_HOST_UNREACHABLE:
return true;
}
return false;
}
bool IsProtobufMimeType(const net::URLFetcher* fetcher) {
return fetcher->GetResponseHeaders()->HasHeaderValue(
"content-type", "application/x-protobuffer");
}
bool FailedWithProxy(const net::URLFetcher* fetcher) {
if ((fetcher->GetLoadFlags() & net::LOAD_BYPASS_PROXY) != 0) {
// The request didn't use a proxy.
return false;
}
if (!fetcher->GetStatus().is_success() &&
IsProxyError(fetcher->GetStatus())) {
LOG(WARNING) << "Proxy failed while contacting dmserver.";
return true;
}
if (fetcher->GetStatus().is_success() &&
fetcher->GetResponseCode() == kSuccess &&
fetcher->WasFetchedViaProxy() &&
!IsProtobufMimeType(fetcher)) {
// The proxy server can be misconfigured but pointing to an existing
// server that replies to requests. Try to recover if a successful
// request that went through a proxy returns an unexpected mime type.
LOG(WARNING) << "Got bad mime-type in response from dmserver that was "
<< "fetched via a proxy.";
return true;
}
return false;
}
const char* UserAffiliationToString(UserAffiliation affiliation) {
switch (affiliation) {
case USER_AFFILIATION_MANAGED:
return dm_protocol::kValueUserAffiliationManaged;
case USER_AFFILIATION_NONE:
return dm_protocol::kValueUserAffiliationNone;
}
NOTREACHED() << "Invalid user affiliation " << affiliation;
return dm_protocol::kValueUserAffiliationNone;
}
const char* JobTypeToRequestType(DeviceManagementRequestJob::JobType type) {
switch (type) {
case DeviceManagementRequestJob::TYPE_AUTO_ENROLLMENT:
return dm_protocol::kValueRequestAutoEnrollment;
case DeviceManagementRequestJob::TYPE_REGISTRATION:
return dm_protocol::kValueRequestRegister;
case DeviceManagementRequestJob::TYPE_POLICY_FETCH:
return dm_protocol::kValueRequestPolicy;
case DeviceManagementRequestJob::TYPE_API_AUTH_CODE_FETCH:
return dm_protocol::kValueRequestApiAuthorization;
case DeviceManagementRequestJob::TYPE_UNREGISTRATION:
return dm_protocol::kValueRequestUnregister;
case DeviceManagementRequestJob::TYPE_UPLOAD_CERTIFICATE:
return dm_protocol::kValueRequestUploadCertificate;
}
NOTREACHED() << "Invalid job type " << type;
return "";
}
// Custom request context implementation that allows to override the user agent,
// amongst others. Wraps a baseline request context from which we reuse the
// networking components.
class DeviceManagementRequestContext : public net::URLRequestContext {
public:
DeviceManagementRequestContext(net::URLRequestContext* base_context,
const std::string& user_agent);
virtual ~DeviceManagementRequestContext();
private:
net::StaticHttpUserAgentSettings http_user_agent_settings_;
};
DeviceManagementRequestContext::DeviceManagementRequestContext(
net::URLRequestContext* base_context,
const std::string& user_agent)
: http_user_agent_settings_("*", user_agent) {
// Share resolver, proxy service and ssl bits with the baseline context. This
// is important so we don't make redundant requests (e.g. when resolving proxy
// auto configuration).
set_net_log(base_context->net_log());
set_host_resolver(base_context->host_resolver());
set_proxy_service(base_context->proxy_service());
set_ssl_config_service(base_context->ssl_config_service());
// Share the http session.
set_http_transaction_factory(
new net::HttpNetworkLayer(
base_context->http_transaction_factory()->GetSession()));
// No cookies, please.
set_cookie_store(new net::CookieMonster(NULL, NULL));
set_http_user_agent_settings(&http_user_agent_settings_);
}
DeviceManagementRequestContext::~DeviceManagementRequestContext() {
delete http_transaction_factory();
}
// Request context holder.
class DeviceManagementRequestContextGetter
: public net::URLRequestContextGetter {
public:
DeviceManagementRequestContextGetter(
scoped_refptr<net::URLRequestContextGetter> base_context_getter,
const std::string& user_agent)
: base_context_getter_(base_context_getter),
user_agent_(user_agent) {}
// Overridden from net::URLRequestContextGetter:
virtual net::URLRequestContext* GetURLRequestContext() OVERRIDE;
virtual scoped_refptr<base::SingleThreadTaskRunner>
GetNetworkTaskRunner() const OVERRIDE;
protected:
virtual ~DeviceManagementRequestContextGetter() {}
private:
scoped_refptr<net::URLRequestContextGetter> base_context_getter_;
scoped_ptr<net::URLRequestContext> context_;
const std::string user_agent_;
};
net::URLRequestContext*
DeviceManagementRequestContextGetter::GetURLRequestContext() {
DCHECK(GetNetworkTaskRunner()->RunsTasksOnCurrentThread());
if (!context_.get()) {
context_.reset(new DeviceManagementRequestContext(
base_context_getter_->GetURLRequestContext(), user_agent_));
}
return context_.get();
}
scoped_refptr<base::SingleThreadTaskRunner>
DeviceManagementRequestContextGetter::GetNetworkTaskRunner() const {
return base_context_getter_->GetNetworkTaskRunner();
}
} // namespace
// Request job implementation used with DeviceManagementService.
class DeviceManagementRequestJobImpl : public DeviceManagementRequestJob {
public:
DeviceManagementRequestJobImpl(JobType type,
const std::string& agent_parameter,
const std::string& platform_parameter,
DeviceManagementService* service);
virtual ~DeviceManagementRequestJobImpl();
// Handles the URL request response.
void HandleResponse(const net::URLRequestStatus& status,
int response_code,
const net::ResponseCookies& cookies,
const std::string& data);
// Gets the URL to contact.
GURL GetURL(const std::string& server_url);
// Configures the fetcher, setting up payload and headers.
void ConfigureRequest(net::URLFetcher* fetcher);
// Returns true if this job should be retried. |fetcher| has just completed,
// and can be inspected to determine if the request failed and should be
// retried.
bool ShouldRetry(const net::URLFetcher* fetcher);
// Invoked right before retrying this job.
void PrepareRetry();
protected:
// DeviceManagementRequestJob:
virtual void Run() OVERRIDE;
private:
// Invokes the callback with the given error code.
void ReportError(DeviceManagementStatus code);
// Pointer to the service this job is associated with.
DeviceManagementService* service_;
// Whether the BYPASS_PROXY flag should be set by ConfigureRequest().
bool bypass_proxy_;
// Number of times that this job has been retried due to ERR_NETWORK_CHANGED.
int retries_count_;
DISALLOW_COPY_AND_ASSIGN(DeviceManagementRequestJobImpl);
};
DeviceManagementRequestJobImpl::DeviceManagementRequestJobImpl(
JobType type,
const std::string& agent_parameter,
const std::string& platform_parameter,
DeviceManagementService* service)
: DeviceManagementRequestJob(type, agent_parameter, platform_parameter),
service_(service),
bypass_proxy_(false),
retries_count_(0) {}
DeviceManagementRequestJobImpl::~DeviceManagementRequestJobImpl() {
service_->RemoveJob(this);
}
void DeviceManagementRequestJobImpl::Run() {
service_->AddJob(this);
}
void DeviceManagementRequestJobImpl::HandleResponse(
const net::URLRequestStatus& status,
int response_code,
const net::ResponseCookies& cookies,
const std::string& data) {
if (status.status() != net::URLRequestStatus::SUCCESS) {
LOG(WARNING) << "DMServer request failed, status: " << status.status()
<< ", error: " << status.error();
em::DeviceManagementResponse dummy_response;
callback_.Run(DM_STATUS_REQUEST_FAILED, status.error(), dummy_response);
return;
}
if (response_code != kSuccess)
LOG(WARNING) << "DMServer sent an error response: " << response_code;
switch (response_code) {
case kSuccess: {
em::DeviceManagementResponse response;
if (!response.ParseFromString(data)) {
ReportError(DM_STATUS_RESPONSE_DECODING_ERROR);
return;
}
callback_.Run(DM_STATUS_SUCCESS, net::OK, response);
return;
}
case kInvalidArgument:
ReportError(DM_STATUS_REQUEST_INVALID);
return;
case kInvalidAuthCookieOrDMToken:
ReportError(DM_STATUS_SERVICE_MANAGEMENT_TOKEN_INVALID);
return;
case kMissingLicenses:
ReportError(DM_STATUS_SERVICE_MISSING_LICENSES);
return;
case kDeviceManagementNotAllowed:
ReportError(DM_STATUS_SERVICE_MANAGEMENT_NOT_SUPPORTED);
return;
case kPendingApproval:
ReportError(DM_STATUS_SERVICE_ACTIVATION_PENDING);
return;
case kInvalidURL:
case kInternalServerError:
case kServiceUnavailable:
ReportError(DM_STATUS_TEMPORARY_UNAVAILABLE);
return;
case kDeviceNotFound:
ReportError(DM_STATUS_SERVICE_DEVICE_NOT_FOUND);
return;
case kPolicyNotFound:
ReportError(DM_STATUS_SERVICE_POLICY_NOT_FOUND);
return;
case kInvalidSerialNumber:
ReportError(DM_STATUS_SERVICE_INVALID_SERIAL_NUMBER);
return;
case kDeviceIdConflict:
ReportError(DM_STATUS_SERVICE_DEVICE_ID_CONFLICT);
return;
default:
// 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)
ReportError(DM_STATUS_TEMPORARY_UNAVAILABLE);
else
ReportError(DM_STATUS_HTTP_STATUS_ERROR);
return;
}
}
GURL DeviceManagementRequestJobImpl::GetURL(
const std::string& server_url) {
std::string result(server_url);
result += '?';
for (ParameterMap::const_iterator entry(query_params_.begin());
entry != query_params_.end();
++entry) {
if (entry != query_params_.begin())
result += '&';
result += net::EscapeQueryParamValue(entry->first, true);
result += '=';
result += net::EscapeQueryParamValue(entry->second, true);
}
return GURL(result);
}
void DeviceManagementRequestJobImpl::ConfigureRequest(
net::URLFetcher* fetcher) {
fetcher->SetLoadFlags(net::LOAD_DO_NOT_SEND_COOKIES |
net::LOAD_DO_NOT_SAVE_COOKIES |
net::LOAD_DISABLE_CACHE |
(bypass_proxy_ ? net::LOAD_BYPASS_PROXY : 0));
std::string payload;
CHECK(request_.SerializeToString(&payload));
fetcher->SetUploadData(kPostContentType, payload);
std::string extra_headers;
if (!gaia_token_.empty())
extra_headers += kServiceTokenAuthHeader + gaia_token_ + "\n";
if (!dm_token_.empty())
extra_headers += kDMTokenAuthHeader + dm_token_ + "\n";
fetcher->SetExtraRequestHeaders(extra_headers);
}
bool DeviceManagementRequestJobImpl::ShouldRetry(
const net::URLFetcher* fetcher) {
if (FailedWithProxy(fetcher) && !bypass_proxy_) {
// Retry the job if it failed due to a broken proxy, by bypassing the
// proxy on the next try.
bypass_proxy_ = true;
return true;
}
// Early device policy fetches on ChromeOS and Auto-Enrollment checks are
// often interrupted during ChromeOS startup when network change notifications
// are sent. Allowing the fetcher to retry once after that is enough to
// recover; allow it to retry up to 3 times just in case.
if (fetcher->GetStatus().error() == net::ERR_NETWORK_CHANGED &&
retries_count_ < kMaxNetworkChangedRetries) {
++retries_count_;
return true;
}
// The request didn't fail, or the limit of retry attempts has been reached;
// forward the result to the job owner.
return false;
}
void DeviceManagementRequestJobImpl::PrepareRetry() {
if (!retry_callback_.is_null())
retry_callback_.Run(this);
}
void DeviceManagementRequestJobImpl::ReportError(DeviceManagementStatus code) {
em::DeviceManagementResponse dummy_response;
callback_.Run(code, net::OK, dummy_response);
}
DeviceManagementRequestJob::~DeviceManagementRequestJob() {}
void DeviceManagementRequestJob::SetGaiaToken(const std::string& gaia_token) {
gaia_token_ = gaia_token;
}
void DeviceManagementRequestJob::SetOAuthToken(const std::string& oauth_token) {
AddParameter(dm_protocol::kParamOAuthToken, oauth_token);
}
void DeviceManagementRequestJob::SetUserAffiliation(
UserAffiliation user_affiliation) {
AddParameter(dm_protocol::kParamUserAffiliation,
UserAffiliationToString(user_affiliation));
}
void DeviceManagementRequestJob::SetDMToken(const std::string& dm_token) {
dm_token_ = dm_token;
}
void DeviceManagementRequestJob::SetClientID(const std::string& client_id) {
AddParameter(dm_protocol::kParamDeviceID, client_id);
}
em::DeviceManagementRequest* DeviceManagementRequestJob::GetRequest() {
return &request_;
}
DeviceManagementRequestJob::DeviceManagementRequestJob(
JobType type,
const std::string& agent_parameter,
const std::string& platform_parameter) {
AddParameter(dm_protocol::kParamRequest, JobTypeToRequestType(type));
AddParameter(dm_protocol::kParamDeviceType, dm_protocol::kValueDeviceType);
AddParameter(dm_protocol::kParamAppType, dm_protocol::kValueAppType);
AddParameter(dm_protocol::kParamAgent, agent_parameter);
AddParameter(dm_protocol::kParamPlatform, platform_parameter);
}
void DeviceManagementRequestJob::SetRetryCallback(
const RetryCallback& retry_callback) {
retry_callback_ = retry_callback;
}
void DeviceManagementRequestJob::Start(const Callback& callback) {
callback_ = callback;
Run();
}
void DeviceManagementRequestJob::AddParameter(const std::string& name,
const std::string& value) {
query_params_.push_back(std::make_pair(name, value));
}
// A random value that other fetchers won't likely use.
const int DeviceManagementService::kURLFetcherID = 0xde71ce1d;
DeviceManagementService::~DeviceManagementService() {
// All running jobs should have been cancelled by now.
DCHECK(pending_jobs_.empty());
DCHECK(queued_jobs_.empty());
}
DeviceManagementRequestJob* DeviceManagementService::CreateJob(
DeviceManagementRequestJob::JobType type) {
return new DeviceManagementRequestJobImpl(
type,
configuration_->GetAgentParameter(),
configuration_->GetPlatformParameter(),
this);
}
void DeviceManagementService::ScheduleInitialization(int64 delay_milliseconds) {
if (initialized_)
return;
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE,
base::Bind(&DeviceManagementService::Initialize,
weak_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(delay_milliseconds));
}
void DeviceManagementService::Initialize() {
if (initialized_)
return;
DCHECK(!request_context_getter_.get());
request_context_getter_ = new DeviceManagementRequestContextGetter(
request_context_, configuration_->GetUserAgent());
initialized_ = true;
while (!queued_jobs_.empty()) {
StartJob(queued_jobs_.front());
queued_jobs_.pop_front();
}
}
void DeviceManagementService::Shutdown() {
for (JobFetcherMap::iterator job(pending_jobs_.begin());
job != pending_jobs_.end();
++job) {
delete job->first;
queued_jobs_.push_back(job->second);
}
pending_jobs_.clear();
}
DeviceManagementService::DeviceManagementService(
scoped_ptr<Configuration> configuration,
scoped_refptr<net::URLRequestContextGetter> request_context)
: configuration_(configuration.Pass()),
request_context_(request_context),
initialized_(false),
weak_ptr_factory_(this) {
DCHECK(configuration_);
}
void DeviceManagementService::StartJob(DeviceManagementRequestJobImpl* job) {
std::string server_url = configuration_->GetServerUrl();
net::URLFetcher* fetcher = net::URLFetcher::Create(
kURLFetcherID, job->GetURL(server_url), net::URLFetcher::POST, this);
fetcher->SetRequestContext(request_context_getter_.get());
job->ConfigureRequest(fetcher);
pending_jobs_[fetcher] = job;
fetcher->Start();
}
void DeviceManagementService::OnURLFetchComplete(
const net::URLFetcher* source) {
JobFetcherMap::iterator entry(pending_jobs_.find(source));
if (entry == pending_jobs_.end()) {
NOTREACHED() << "Callback from foreign URL fetcher";
return;
}
DeviceManagementRequestJobImpl* job = entry->second;
pending_jobs_.erase(entry);
if (job->ShouldRetry(source)) {
VLOG(1) << "Retrying dmserver request.";
job->PrepareRetry();
StartJob(job);
} else {
std::string data;
source->GetResponseAsString(&data);
job->HandleResponse(source->GetStatus(), source->GetResponseCode(),
source->GetCookies(), data);
}
delete source;
}
void DeviceManagementService::AddJob(DeviceManagementRequestJobImpl* job) {
if (initialized_)
StartJob(job);
else
queued_jobs_.push_back(job);
}
void DeviceManagementService::RemoveJob(DeviceManagementRequestJobImpl* job) {
for (JobFetcherMap::iterator entry(pending_jobs_.begin());
entry != pending_jobs_.end();
++entry) {
if (entry->second == job) {
delete entry->first;
pending_jobs_.erase(entry);
return;
}
}
const JobQueue::iterator elem =
std::find(queued_jobs_.begin(), queued_jobs_.end(), job);
if (elem != queued_jobs_.end())
queued_jobs_.erase(elem);
}
} // namespace policy