Index: chrome/browser/policy/device_management_service.cc |
diff --git a/chrome/browser/policy/device_management_service.cc b/chrome/browser/policy/device_management_service.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..632c68c657ec72f11331fdb5ee69228a85708bf9 |
--- /dev/null |
+++ b/chrome/browser/policy/device_management_service.cc |
@@ -0,0 +1,619 @@ |
+// Copyright (c) 2010 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_service.h" |
+ |
+#include <utility> |
+#include <set> |
+#include <vector> |
+ |
+#include "base/command_line.h" |
+#include "base/lazy_instance.h" |
+#include "base/stl_util-inl.h" |
+#include "base/stringprintf.h" |
+#include "chrome/browser/browser_thread.h" |
+#include "chrome/common/chrome_switches.h" |
+#include "chrome/common/net/url_request_context_getter.h" |
+#include "net/base/cookie_monster.h" |
+#include "net/base/escape.h" |
+#include "net/base/host_resolver.h" |
+#include "net/base/load_flags.h" |
+#include "net/base/ssl_config_service_defaults.h" |
+#include "net/http/http_auth_handler_factory.h" |
+#include "net/http/http_network_layer.h" |
+#include "net/proxy/proxy_service.h" |
+#include "net/url_request/url_request_context.h" |
+#include "net/url_request/url_request_status.h" |
+#include "chrome/browser/browser_process.h" |
+#include "chrome/browser/io_thread.h" |
+#include "chrome/browser/net/chrome_net_log.h" |
+#include "chrome/browser/profile.h" |
+#include "chrome/common/chrome_switches.h" |
+#include "chrome/common/chrome_version_info.h" |
+ |
+namespace policy { |
+ |
+namespace { |
+ |
+// Name constants for URL query parameters. |
+const char kServiceParamRequest[] = "request"; |
+const char kServiceParamDeviceType[] = "devicetype"; |
+const char kServiceParamDeviceID[] = "deviceid"; |
+const char kServiceParamAgent[] = "agent"; |
+ |
+// String constants for the device type and agent we report to the service. |
+const char kServiceValueDeviceType[] = "Chrome"; |
+const char kServiceValueAgent[] = |
+ "%s enterprise management client version %s (%s)"; |
+ |
+const char kServiceTokenAuthHeader[] = "Authorization: GoogleLogin auth="; |
+const char kDMTokenAuthHeader[] = "Authorization: GoogleDMToken token="; |
+ |
+// 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 += EscapeUrlEncodedData(entry->first); |
+ result += '='; |
+ result += EscapeUrlEncodedData(entry->second); |
+ } |
+ return result; |
+} |
+ |
+// 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 DeviceManagementBackendRequestContext : public URLRequestContext { |
+ public: |
+ explicit DeviceManagementBackendRequestContext( |
+ URLRequestContext* base_context); |
+ virtual ~DeviceManagementBackendRequestContext(); |
+ |
+ private: |
+ virtual const std::string& GetUserAgent(const GURL& url) const; |
+ |
+ std::string user_agent_; |
+}; |
+ |
+DeviceManagementBackendRequestContext::DeviceManagementBackendRequestContext( |
+ URLRequestContext* base_context) { |
+ // 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). |
+ net_log_ = base_context->net_log(); |
+ host_resolver_ = base_context->host_resolver(); |
+ proxy_service_ = base_context->proxy_service(); |
+ ssl_config_service_ = base_context->ssl_config_service(); |
+ |
+ // Share the http session. |
+ http_transaction_factory_ = net::HttpNetworkLayer::CreateFactory( |
+ base_context->http_transaction_factory()->GetSession()); |
+ |
+ // No cookies, please. |
+ cookie_store_ = new net::CookieMonster(NULL, NULL); |
+ |
+ // Initialize these to sane values for our purposes. |
+ user_agent_ = DeviceManagementService::GetAgentString(); |
+ accept_language_ = "*"; |
+ accept_charset_ = "*"; |
+} |
+ |
+DeviceManagementBackendRequestContext |
+ ::~DeviceManagementBackendRequestContext() { |
+ delete http_transaction_factory_; |
+ delete http_auth_handler_factory_; |
+} |
+ |
+const std::string& |
+DeviceManagementBackendRequestContext::GetUserAgent(const GURL& url) const { |
+ return user_agent_; |
+} |
+ |
+// Request context holder. |
+class DeviceManagementBackendRequestContextGetter |
+ : public URLRequestContextGetter { |
+ public: |
+ DeviceManagementBackendRequestContextGetter( |
+ URLRequestContextGetter* base_context_getter) |
+ : base_context_getter_(base_context_getter) {} |
+ |
+ // URLRequestContextGetter overrides. |
+ virtual URLRequestContext* GetURLRequestContext(); |
+ virtual scoped_refptr<base::MessageLoopProxy> GetIOMessageLoopProxy() const; |
+ |
+ private: |
+ scoped_refptr<URLRequestContext> context_; |
+ scoped_refptr<URLRequestContextGetter> base_context_getter_; |
+}; |
+ |
+ |
+URLRequestContext* |
+DeviceManagementBackendRequestContextGetter::GetURLRequestContext() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ if (!context_) { |
+ context_ = new DeviceManagementBackendRequestContext( |
+ base_context_getter_->GetURLRequestContext()); |
+ } |
+ |
+ return context_.get(); |
+} |
+ |
+scoped_refptr<base::MessageLoopProxy> |
+DeviceManagementBackendRequestContextGetter::GetIOMessageLoopProxy() const { |
+ return BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO); |
+} |
+ |
+} // namespace |
+ |
+// A wrapper that implements the actual backend interface. This is separate from |
+// DeviceManagementService so the consumer can cancel all pending requests by |
+// destroying the backend object. |
+class DeviceManagementBackendProxy : public DeviceManagementBackend { |
+ public: |
+ explicit DeviceManagementBackendProxy(DeviceManagementService* service); |
+ virtual ~DeviceManagementBackendProxy(); |
+ |
+ // Called by the DeviceManagementJob dtor so we can clean up. |
+ void JobDone(DeviceManagementJob* job); |
+ |
+ private: |
+ typedef std::set<DeviceManagementJob*> JobSet; |
+ |
+ // Add a job to the pending job set and register it with the service (if |
+ // available). |
+ void AddJob(DeviceManagementJob* job); |
+ |
+ // DeviceManagementBackend overrides. |
+ virtual void ProcessRegisterRequest( |
+ const std::string& auth_token, |
+ const std::string& device_id, |
+ const em::DeviceRegisterRequest& request, |
+ DeviceRegisterResponseDelegate* response_delegate); |
+ virtual void ProcessUnregisterRequest( |
+ const std::string& device_management_token, |
+ const em::DeviceUnregisterRequest& request, |
+ DeviceUnregisterResponseDelegate* response_delegate); |
+ virtual void ProcessPolicyRequest( |
+ const std::string& device_management_token, |
+ const em::DevicePolicyRequest& request, |
+ DevicePolicyResponseDelegate* response_delegate); |
+ |
+ // Keeps track of the jobs currently in flight. |
+ JobSet pending_jobs_; |
+ |
+ DeviceManagementService* service_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DeviceManagementBackendProxy); |
+}; |
+ |
+// Represents a job run by the service. This contains the common code, |
+// subclasses provide custom code for actual register, unregister, and policy |
+// jobs. |
+class DeviceManagementJob { |
+ public: |
+ virtual ~DeviceManagementJob() { |
+ proxy_->JobDone(this); |
+ } |
+ |
+ // Handles the URL request response. |
+ void HandleResponse(const URLRequestStatus& status, |
+ int response_code, |
+ const 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(URLFetcher* fetcher); |
+ |
+ protected: |
+ // Constructs a device management job running for the given proxy. |
+ DeviceManagementJob(DeviceManagementBackendProxy* proxy, |
+ const std::string& request_type) |
+ : proxy_(proxy) { |
+ query_params_.Put(kServiceParamRequest, request_type); |
+ query_params_.Put(kServiceParamDeviceType, kServiceValueDeviceType); |
+ query_params_.Put(kServiceParamAgent, |
+ DeviceManagementService::GetAgentString()); |
+ } |
+ |
+ 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 SetDeviceID(const std::string& device_id) { |
+ query_params_.Put(kServiceParamDeviceID, device_id); |
+ } |
+ |
+ 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 ProcessResponse( |
+ const em::DeviceManagementResponse& response) = 0; |
+ virtual void ProcessError(DeviceManagementBackend::ErrorCode error) = 0; |
+ |
+ // The proxy this job is handling a request for. |
+ DeviceManagementBackendProxy* proxy_; |
+ |
+ // 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(DeviceManagementJob); |
+}; |
+ |
+void DeviceManagementJob::HandleResponse(const URLRequestStatus& status, |
+ int response_code, |
+ const ResponseCookies& cookies, |
+ const std::string& data) { |
+ // Delete ourselves when this is done. |
+ scoped_ptr<DeviceManagementJob> scoped_killer(this); |
+ |
+ if (status.status() != URLRequestStatus::SUCCESS) { |
+ ProcessError(DeviceManagementBackend::kErrorRequestFailed); |
+ return; |
+ } |
+ |
+ if (response_code != 200) { |
+ ProcessError(DeviceManagementBackend::kErrorHttpStatus); |
+ return; |
+ } |
+ |
+ em::DeviceManagementResponse response; |
+ if (!response.ParseFromString(data)) { |
+ ProcessError(DeviceManagementBackend::kErrorResponseDecoding); |
+ return; |
+ } |
+ |
+ // Check service error code. |
+ switch (response.error()) { |
+ case em::DeviceManagementResponse::SUCCESS: |
+ break; |
+ case em::DeviceManagementResponse::DEVICE_MANAGEMENT_NOT_SUPPORTED: |
+ ProcessError( |
+ DeviceManagementBackend::kErrorServiceManagementNotSupported); |
+ return; |
+ case em::DeviceManagementResponse::DEVICE_NOT_FOUND: |
+ ProcessError(DeviceManagementBackend::kErrorServiceDeviceNotFound); |
+ return; |
+ case em::DeviceManagementResponse::DEVICE_MANAGEMENT_TOKEN_INVALID: |
+ ProcessError( |
+ DeviceManagementBackend::kErrorServiceManagementTokenInvalid); |
+ return; |
+ case em::DeviceManagementResponse::ACTIVATION_PENDING: |
+ ProcessError(DeviceManagementBackend::kErrorServiceActivationPending); |
+ return; |
+ default: |
+ // This should be caught by the protobuf decoder. |
+ NOTREACHED(); |
+ ProcessError(DeviceManagementBackend::kErrorResponseDecoding); |
+ return; |
+ } |
+ |
+ ProcessResponse(response); |
+} |
+ |
+GURL DeviceManagementJob::GetURL(const std::string& server_url) { |
+ return GURL(server_url + '?' + query_params_.Encode()); |
+} |
+ |
+void DeviceManagementJob::ConfigureRequest(URLFetcher* fetcher) { |
+ fetcher->set_upload_data("application/octet-stream", 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 DeviceManagementJob { |
+ public: |
+ DeviceManagementRegisterJob( |
+ DeviceManagementBackendProxy* proxy, |
+ const std::string& auth_token, |
+ const std::string& device_id, |
+ const em::DeviceRegisterRequest& request, |
+ DeviceManagementBackend::DeviceRegisterResponseDelegate* delegate); |
+ virtual ~DeviceManagementRegisterJob() {} |
+ |
+ private: |
+ // DeviceManagementJob overrides. |
+ virtual void ProcessError(DeviceManagementBackend::ErrorCode error) { |
+ delegate_->OnError(error); |
+ } |
+ virtual void ProcessResponse(const em::DeviceManagementResponse& response) { |
+ delegate_->HandleRegisterResponse(response.register_response()); |
+ } |
+ |
+ DeviceManagementBackend::DeviceRegisterResponseDelegate* delegate_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DeviceManagementRegisterJob); |
+}; |
+ |
+DeviceManagementRegisterJob::DeviceManagementRegisterJob( |
+ DeviceManagementBackendProxy* proxy, |
+ const std::string& auth_token, |
+ const std::string& device_id, |
+ const em::DeviceRegisterRequest& request, |
+ DeviceManagementBackend::DeviceRegisterResponseDelegate* delegate) |
+ : DeviceManagementJob(proxy, "register"), |
+ delegate_(delegate) { |
+ SetDeviceID(device_id); |
+ SetAuthToken(auth_token); |
+ em::DeviceManagementRequest request_wrapper; |
+ request_wrapper.mutable_register_request()->CopyFrom(request); |
+ SetPayload(request_wrapper); |
+} |
+ |
+// Handles device unregistration jobs. |
+class DeviceManagementUnregisterJob : public DeviceManagementJob { |
+ public: |
+ DeviceManagementUnregisterJob( |
+ DeviceManagementBackendProxy* proxy, |
+ const std::string& device_management_token, |
+ const em::DeviceUnregisterRequest& request, |
+ DeviceManagementBackend::DeviceUnregisterResponseDelegate* delegate); |
+ virtual ~DeviceManagementUnregisterJob() {} |
+ |
+ private: |
+ // DeviceManagementJob overrides. |
+ virtual void ProcessError(DeviceManagementBackend::ErrorCode error) { |
+ delegate_->OnError(error); |
+ } |
+ virtual void ProcessResponse(const em::DeviceManagementResponse& response) { |
+ delegate_->HandleUnregisterResponse(response.unregister_response()); |
+ } |
+ |
+ DeviceManagementBackend::DeviceUnregisterResponseDelegate* delegate_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DeviceManagementUnregisterJob); |
+}; |
+ |
+DeviceManagementUnregisterJob::DeviceManagementUnregisterJob( |
+ DeviceManagementBackendProxy* proxy, |
+ const std::string& device_management_token, |
+ const em::DeviceUnregisterRequest& request, |
+ DeviceManagementBackend::DeviceUnregisterResponseDelegate* delegate) |
+ : DeviceManagementJob(proxy, "unregister"), |
+ delegate_(delegate) { |
+ SetDeviceManagementToken(device_management_token); |
+ em::DeviceManagementRequest request_wrapper; |
+ request_wrapper.mutable_unregister_request()->CopyFrom(request); |
+ SetPayload(request_wrapper); |
+} |
+ |
+// Handles policy request jobs. |
+class DeviceManagementPolicyJob : public DeviceManagementJob { |
+ public: |
+ DeviceManagementPolicyJob( |
+ DeviceManagementBackendProxy* proxy, |
+ const std::string& device_management_token, |
+ const em::DevicePolicyRequest& request, |
+ DeviceManagementBackend::DevicePolicyResponseDelegate* delegate); |
+ virtual ~DeviceManagementPolicyJob() {} |
+ |
+ private: |
+ // DeviceManagementJob overrides. |
+ virtual void ProcessError(DeviceManagementBackend::ErrorCode error) { |
+ delegate_->OnError(error); |
+ } |
+ virtual void ProcessResponse(const em::DeviceManagementResponse& response) { |
+ delegate_->HandlePolicyResponse(response.policy_response()); |
+ } |
+ |
+ DeviceManagementBackend::DevicePolicyResponseDelegate* delegate_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(DeviceManagementPolicyJob); |
+}; |
+ |
+DeviceManagementPolicyJob::DeviceManagementPolicyJob( |
+ DeviceManagementBackendProxy* proxy, |
+ const std::string& device_management_token, |
+ const em::DevicePolicyRequest& request, |
+ DeviceManagementBackend::DevicePolicyResponseDelegate* delegate) |
+ : DeviceManagementJob(proxy, "policy"), |
+ delegate_(delegate) { |
+ SetDeviceManagementToken(device_management_token); |
+ em::DeviceManagementRequest request_wrapper; |
+ request_wrapper.mutable_policy_request()->CopyFrom(request); |
+ SetPayload(request_wrapper); |
+} |
+ |
+DeviceManagementBackendProxy::DeviceManagementBackendProxy( |
+ DeviceManagementService* service) |
+ : service_(service) { |
+} |
+ |
+DeviceManagementBackendProxy::~DeviceManagementBackendProxy() { |
+ // Swap to a helper, so we don't interfere with the unregistration on delete. |
+ JobSet to_be_deleted; |
+ to_be_deleted.swap(pending_jobs_); |
+ for (JobSet::iterator job(to_be_deleted.begin()); |
+ job != to_be_deleted.end(); |
+ ++job) { |
+ service_->RemoveJob(*job); |
+ delete *job; |
+ } |
+} |
+ |
+void DeviceManagementBackendProxy::JobDone(DeviceManagementJob* job) { |
+ pending_jobs_.erase(job); |
+} |
+ |
+void DeviceManagementBackendProxy::AddJob(DeviceManagementJob* job) { |
+ pending_jobs_.insert(job); |
+ service_->AddJob(job); |
+} |
+ |
+void DeviceManagementBackendProxy::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 DeviceManagementBackendProxy::ProcessUnregisterRequest( |
+ const std::string& device_management_token, |
+ const em::DeviceUnregisterRequest& request, |
+ DeviceUnregisterResponseDelegate* delegate) { |
+ AddJob(new DeviceManagementUnregisterJob(this, device_management_token, |
+ request, delegate)); |
+} |
+ |
+void DeviceManagementBackendProxy::ProcessPolicyRequest( |
+ const std::string& device_management_token, |
+ const em::DevicePolicyRequest& request, |
+ DevicePolicyResponseDelegate* delegate) { |
+ AddJob(new DeviceManagementPolicyJob(this, device_management_token, request, |
+ delegate)); |
+} |
+ |
+DeviceManagementService::~DeviceManagementService() { |
+ // All running jobs should have been canceled by now. If not, there are proxy |
+ // objects still around, which is an error. |
+ DCHECK(pending_jobs_.empty()); |
+ DCHECK(queued_jobs_.empty()); |
+} |
+ |
+DeviceManagementBackend* DeviceManagementService::CreateBackend() { |
+ return new DeviceManagementBackendProxy(this); |
+} |
+ |
+void DeviceManagementService::Initialize( |
+ URLRequestContextGetter* request_context_getter) { |
+ DCHECK(!request_context_getter_); |
+ request_context_getter_ = request_context_getter; |
+ 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); |
+ } |
+} |
+ |
+// static |
+std::string DeviceManagementService::GetAgentString() { |
+ chrome::VersionInfo version_info; |
+ return base::StringPrintf(kServiceValueAgent, |
+ version_info.Name().c_str(), |
+ version_info.Version().c_str(), |
+ version_info.LastChange().c_str()); |
+} |
+ |
+DeviceManagementService::DeviceManagementService( |
+ const std::string& server_url) |
+ : server_url_(server_url) { |
+} |
+ |
+void DeviceManagementService::AddJob(DeviceManagementJob* job) { |
+ if (request_context_getter_.get()) |
+ StartJob(job); |
+ else |
+ queued_jobs_.push_back(job); |
+} |
+ |
+void DeviceManagementService::RemoveJob(DeviceManagementJob* job) { |
+ for (JobFetcherMap::iterator entry(pending_jobs_.begin()); |
+ entry != pending_jobs_.end(); |
+ ++entry) { |
+ if (entry->second == job) { |
+ delete entry->first; |
+ pending_jobs_.erase(entry); |
+ break; |
+ } |
+ } |
+} |
+ |
+void DeviceManagementService::StartJob(DeviceManagementJob* job) { |
+ URLFetcher* fetcher = URLFetcher::Create(0, job->GetURL(server_url_), |
+ URLFetcher::POST, this); |
+ fetcher->set_load_flags(net::LOAD_DO_NOT_SEND_COOKIES | |
+ net::LOAD_DO_NOT_SAVE_COOKIES | |
+ net::LOAD_DISABLE_CACHE); |
+ fetcher->set_request_context(request_context_getter_.get()); |
+ job->ConfigureRequest(fetcher); |
+ pending_jobs_[fetcher] = job; |
+ fetcher->Start(); |
+} |
+ |
+void DeviceManagementService::OnURLFetchComplete( |
+ const URLFetcher* source, |
+ const GURL& url, |
+ const URLRequestStatus& status, |
+ int response_code, |
+ const ResponseCookies& cookies, |
+ const std::string& data) { |
+ JobFetcherMap::iterator entry(pending_jobs_.find(source)); |
+ if (entry != pending_jobs_.end()) { |
+ DeviceManagementJob* job = entry->second; |
+ job->HandleResponse(status, response_code, cookies, data); |
+ pending_jobs_.erase(entry); |
+ } else { |
+ NOTREACHED() << "Callback from foreign URL fetcher"; |
+ } |
+ delete source; |
+} |
+ |
+} // namespace policy |