// Copyright (c) 2015 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/policy/upload_job_impl.h" #include #include "base/logging.h" #include "base/rand_util.h" #include "base/strings/stringprintf.h" #include "google_apis/gaia/gaia_constants.h" #include "google_apis/gaia/google_service_auth_error.h" #include "net/http/http_status_code.h" #include "net/url_request/url_request_status.h" namespace policy { namespace { // Defines the characters that might appear in strings generated by // GenerateRandomString(). const char kAlphaNum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // Format for bearer tokens in HTTP requests to access OAuth 2.0 protected // resources. const char kAuthorizationHeaderFormat[] = "Authorization: Bearer %s"; // Prefix added to a randomly generated string when choosing the MIME boundary. const char kMultipartBoundaryPrefix[] = "----**--"; // Postfix added to a randomly generated string when choosing the MIME boundary. const char kMultipartBoundaryPostfix[] = "--**----"; // Value the "Content-Type" field will be set to in the POST request. const char kUploadContentType[] = "multipart/form-data"; // Number of retries when randomly generating a MIME boundary. const int kMimeBoundaryRetries = 3; // Length of the random string for the MIME boundary. const int kMimeBoundarySize = 32; // Number of upload retries. const int kMaxRetries = 1; // Generates a random alphanumeric string of length |length|. std::string GenerateRandomString(size_t length) { std::string random; random.reserve(length); for (size_t i = 0; i < length; i++) random.push_back(kAlphaNum[base::RandGenerator(sizeof(kAlphaNum) - 1)]); return random; } } // namespace UploadJobImpl::Delegate::~Delegate() { } UploadJobImpl::MimeBoundaryGenerator::~MimeBoundaryGenerator() { } UploadJobImpl::RandomMimeBoundaryGenerator::~RandomMimeBoundaryGenerator() { } // multipart/form-data POST request to upload the data. A DataSegment // corresponds to one "Content-Disposition" in the "multipart" request. class DataSegment { public: DataSegment(const std::string& name, const std::string& filename, scoped_ptr data, const std::map& header_entries); // Returns the header entries for this DataSegment. const std::map& GetHeaderEntries() const; // Returns the string that will be assigned to the |name| field in the header. // |name| must be unique throughout the multipart message. This is enforced in // SetUpMultipart(). const std::string& GetName() const; // Returns the string that will be assigned to the |filename| field in the // header. If the |filename| is the empty string, the header field will be // omitted. const std::string& GetFilename() const; // Returns the data contained in this DataSegment. Ownership is passed. scoped_ptr GetData(); // Returns the size in bytes of the blob in |data_|. size_t GetDataSize() const; // Helper method that performs a substring match of |chunk| in |data_|. // Returns |true| if |chunk| matches a substring, |false| otherwise. bool CheckIfDataContains(const std::string& chunk); private: const std::string name_; const std::string filename_; scoped_ptr data_; std::map header_entries_; DISALLOW_COPY_AND_ASSIGN(DataSegment); }; DataSegment::DataSegment( const std::string& name, const std::string& filename, scoped_ptr data, const std::map& header_entries) : name_(name), filename_(filename), data_(data.Pass()), header_entries_(header_entries) { DCHECK(data_); } const std::map& DataSegment::GetHeaderEntries() const { return header_entries_; } const std::string& DataSegment::GetName() const { return name_; } const std::string& DataSegment::GetFilename() const { return filename_; } scoped_ptr DataSegment::GetData() { return data_.Pass(); } bool DataSegment::CheckIfDataContains(const std::string& chunk) { DCHECK(data_); return data_->find(chunk) != std::string::npos; } size_t DataSegment::GetDataSize() const { DCHECK(data_); return data_->size(); } std::string UploadJobImpl::RandomMimeBoundaryGenerator::GenerateBoundary( size_t length) const { std::string boundary; boundary.reserve(length); DCHECK_GT(length, sizeof(kMultipartBoundaryPrefix) + sizeof(kMultipartBoundaryPostfix)); const size_t random_part_length = length - sizeof(kMultipartBoundaryPrefix) - sizeof(kMultipartBoundaryPostfix); boundary.append(kMultipartBoundaryPrefix); boundary.append(GenerateRandomString(random_part_length)); boundary.append(kMultipartBoundaryPostfix); return boundary; } UploadJobImpl::UploadJobImpl( const GURL& upload_url, const std::string& account_id, OAuth2TokenService* token_service, scoped_refptr url_context_getter, Delegate* delegate, scoped_ptr boundary_generator) : OAuth2TokenService::Consumer("cros_upload_job"), upload_url_(upload_url), account_id_(account_id), token_service_(token_service), url_context_getter_(url_context_getter), delegate_(delegate), boundary_generator_(boundary_generator.Pass()), state_(IDLE), retry_(0) { DCHECK(token_service_); DCHECK(url_context_getter_); DCHECK(delegate_); if (!upload_url_.is_valid()) { state_ = ERROR; NOTREACHED() << upload_url_ << " is not a valid URL."; } } UploadJobImpl::~UploadJobImpl() { } void UploadJobImpl::AddDataSegment( const std::string& name, const std::string& filename, const std::map& header_entries, scoped_ptr data) { // Cannot add data to busy or failed instance. DCHECK_EQ(IDLE, state_); if (state_ != IDLE) return; scoped_ptr data_segment( new DataSegment(name, filename, data.Pass(), header_entries)); data_segments_.push_back(data_segment.Pass()); } void UploadJobImpl::Start() { // Cannot start an upload on a busy or failed instance. DCHECK_EQ(IDLE, state_); if (state_ != IDLE) return; RequestAccessToken(); } void UploadJobImpl::RequestAccessToken() { state_ = ACQUIRING_TOKEN; OAuth2TokenService::ScopeSet scope_set; scope_set.insert(GaiaConstants::kDeviceManagementServiceOAuth); access_token_request_ = token_service_->StartRequest(account_id_, scope_set, this); } bool UploadJobImpl::SetUpMultipart() { DCHECK_EQ(ACQUIRING_TOKEN, state_); state_ = PREPARING_CONTENT; if (mime_boundary_ && post_data_) return true; std::set used_names; // Check uniqueness of header field names. for (const auto& data_segment : data_segments_) { if (!used_names.insert(data_segment->GetName()).second) return false; } // Generates random MIME boundaries and tests if they appear in any of the // data segments. Tries up to |kMimeBoundaryRetries| times to find a MIME // boundary that does not appear within any data segment. bool found = false; int retry = 0; do { found = true; mime_boundary_.reset(new std::string( boundary_generator_->GenerateBoundary(kMimeBoundarySize))); for (const auto& data_segment : data_segments_) { if (data_segment->CheckIfDataContains(*mime_boundary_)) { found = false; break; } } ++retry; } while (!found && retry <= kMimeBoundaryRetries); // Notify the delegate that content encoding failed. if (!found) { delegate_->OnFailure(CONTENT_ENCODING_ERROR); mime_boundary_.reset(); return false; } // Estimate an upper bound for the total message size to make memory // allocation more efficient. It is not an error if this turns out to be too // small as std::string will take care of the realloc. size_t size = 0; for (const auto& data_segment : data_segments_) { for (const auto& entry : data_segment->GetHeaderEntries()) size += entry.first.size() + entry.second.size(); size += kMimeBoundarySize + data_segment->GetName().size() + data_segment->GetFilename().size() + data_segment->GetDataSize(); // Add some extra space for all the constants and control characters. size += 128; } // Allocate memory of the expected size. post_data_.reset(new std::string); post_data_->reserve(size); for (const auto& data_segment : data_segments_) { post_data_->append("--" + *mime_boundary_.get() + "\r\n"); post_data_->append("Content-Disposition: form-data; name=\"" + data_segment->GetName() + "\""); if (!data_segment->GetFilename().empty()) { post_data_->append("; filename=\"" + data_segment->GetFilename() + "\""); } post_data_->append("\r\n"); // Add custom header fields. for (const auto& entry : data_segment->GetHeaderEntries()) { post_data_->append(entry.first + ": " + entry.second + "\r\n"); } scoped_ptr data = data_segment->GetData(); post_data_->append("\r\n" + *data + "\r\n"); } post_data_->append("--" + *mime_boundary_.get() + "--\r\n"); // Issues a warning if our buffer size estimate was too small. if (post_data_->size() > size) { LOG(WARNING) << "Reallocation needed in POST data buffer. Expected maximum size " << size << " bytes, actual size " << post_data_->size() << " bytes."; } // Discard the data segments as they are not needed anymore from here on. data_segments_.clear(); return true; } void UploadJobImpl::CreateAndStartURLFetcher(const std::string& access_token) { // Ensure that the content has been prepared and the upload url is valid. DCHECK_EQ(PREPARING_CONTENT, state_); std::string content_type = kUploadContentType; content_type.append("; boundary="); content_type.append(*mime_boundary_.get()); upload_fetcher_ = net::URLFetcher::Create(upload_url_, net::URLFetcher::POST, this); upload_fetcher_->SetRequestContext(url_context_getter_.get()); upload_fetcher_->SetUploadData(content_type, *post_data_); upload_fetcher_->AddExtraRequestHeader( base::StringPrintf(kAuthorizationHeaderFormat, access_token.c_str())); upload_fetcher_->Start(); } void UploadJobImpl::StartUpload(const std::string& access_token) { if (!SetUpMultipart()) { LOG(ERROR) << "Multipart message assembly failed."; state_ = ERROR; return; } CreateAndStartURLFetcher(access_token); state_ = UPLOADING; } void UploadJobImpl::OnGetTokenSuccess( const OAuth2TokenService::Request* request, const std::string& access_token, const base::Time& expiration_time) { DCHECK_EQ(ACQUIRING_TOKEN, state_); DCHECK_EQ(access_token_request_.get(), request); access_token_request_.reset(); // Also cache the token locally, so that we can revoke it later if necessary. access_token_ = access_token; StartUpload(access_token); } void UploadJobImpl::OnGetTokenFailure( const OAuth2TokenService::Request* request, const GoogleServiceAuthError& error) { DCHECK_EQ(ACQUIRING_TOKEN, state_); DCHECK_EQ(access_token_request_.get(), request); access_token_request_.reset(); LOG(ERROR) << "Token request failed: " << error.ToString(); state_ = ERROR; delegate_->OnFailure(AUTHENTICATION_ERROR); } void UploadJobImpl::OnURLFetchComplete(const net::URLFetcher* source) { DCHECK_EQ(upload_fetcher_.get(), source); const net::URLRequestStatus& status = source->GetStatus(); if (!status.is_success()) { LOG(ERROR) << "URLRequestStatus error " << status.error(); upload_fetcher_.reset(); state_ = ERROR; post_data_.reset(); delegate_->OnFailure(NETWORK_ERROR); return; } const int response_code = source->GetResponseCode(); const bool success = response_code == net::HTTP_OK; if (!success) LOG(ERROR) << "POST request failed with HTTP status code " << response_code; if (response_code == net::HTTP_UNAUTHORIZED) { if (retry_ >= kMaxRetries) { upload_fetcher_.reset(); LOG(ERROR) << "Unauthorized request."; state_ = ERROR; post_data_.reset(); delegate_->OnFailure(AUTHENTICATION_ERROR); return; } retry_++; upload_fetcher_.reset(); OAuth2TokenService::ScopeSet scope_set; scope_set.insert(GaiaConstants::kDeviceManagementServiceOAuth); token_service_->InvalidateToken(account_id_, scope_set, access_token_); access_token_.clear(); RequestAccessToken(); return; } upload_fetcher_.reset(); access_token_.clear(); upload_fetcher_.reset(); post_data_.reset(); if (success) { state_ = SUCCESS; delegate_->OnSuccess(); } else { state_ = ERROR; delegate_->OnFailure(SERVER_ERROR); } } } // namespace policy