| // 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 "components/policy/core/common/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/http/http_response_headers.h" |
| #include "net/url_request/url_fetcher.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; |
| const int kDeprovisioned = 903; |
| |
| 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 ""; |
| } |
| |
| } // 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, |
| net::URLRequestContextGetter* request_context); |
| 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_; |
| |
| // The request context to use for this job. |
| net::URLRequestContextGetter* request_context_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DeviceManagementRequestJobImpl); |
| }; |
| |
| DeviceManagementRequestJobImpl::DeviceManagementRequestJobImpl( |
| JobType type, |
| const std::string& agent_parameter, |
| const std::string& platform_parameter, |
| DeviceManagementService* service, |
| net::URLRequestContextGetter* request_context) |
| : DeviceManagementRequestJob(type, agent_parameter, platform_parameter), |
| service_(service), |
| bypass_proxy_(false), |
| retries_count_(0), |
| request_context_(request_context) {} |
| |
| 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 kDeprovisioned: |
| ReportError(DM_STATUS_SERVICE_DEPROVISIONED); |
| 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->SetRequestContext(request_context_); |
| 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, |
| net::URLRequestContextGetter* request_context) { |
| return new DeviceManagementRequestJobImpl( |
| type, |
| configuration_->GetAgentParameter(), |
| configuration_->GetPlatformParameter(), |
| this, |
| request_context); |
| } |
| |
| 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; |
| 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) |
| : configuration_(configuration.Pass()), |
| initialized_(false), |
| weak_ptr_factory_(this) { |
| DCHECK(configuration_); |
| } |
| |
| void DeviceManagementService::StartJob(DeviceManagementRequestJobImpl* job) { |
| std::string server_url = GetServerUrl(); |
| net::URLFetcher* fetcher = net::URLFetcher::Create( |
| kURLFetcherID, job->GetURL(server_url), net::URLFetcher::POST, this); |
| job->ConfigureRequest(fetcher); |
| pending_jobs_[fetcher] = job; |
| fetcher->Start(); |
| } |
| |
| std::string DeviceManagementService::GetServerUrl() { |
| return configuration_->GetServerUrl(); |
| } |
| |
| 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 |