1 // Copyright (c) 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/chromeos/policy/upload_job_impl.h"
9 #include "base/logging.h"
10 #include "base/rand_util.h"
11 #include "base/strings/stringprintf.h"
12 #include "google_apis/gaia/gaia_constants.h"
13 #include "google_apis/gaia/google_service_auth_error.h"
14 #include "net/http/http_status_code.h"
15 #include "net/url_request/url_request_status.h"
21 // Defines the characters that might appear in strings generated by
22 // GenerateRandomString().
23 const char kAlphaNum
[] =
24 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
26 // Format for bearer tokens in HTTP requests to access OAuth 2.0 protected
28 const char kAuthorizationHeaderFormat
[] = "Authorization: Bearer %s";
30 // Prefix added to a randomly generated string when choosing the MIME boundary.
31 const char kMultipartBoundaryPrefix
[] = "----**--";
33 // Postfix added to a randomly generated string when choosing the MIME boundary.
34 const char kMultipartBoundaryPostfix
[] = "--**----";
36 // Value the "Content-Type" field will be set to in the POST request.
37 const char kUploadContentType
[] = "multipart/form-data";
39 // Number of retries when randomly generating a MIME boundary.
40 const int kMimeBoundaryRetries
= 3;
42 // Length of the random string for the MIME boundary.
43 const int kMimeBoundarySize
= 32;
45 // Number of upload retries.
46 const int kMaxRetries
= 1;
48 // Generates a random alphanumeric string of length |length|.
49 std::string
GenerateRandomString(size_t length
) {
51 random
.reserve(length
);
52 for (size_t i
= 0; i
< length
; i
++)
53 random
.push_back(kAlphaNum
[base::RandGenerator(sizeof(kAlphaNum
) - 1)]);
59 UploadJobImpl::Delegate::~Delegate() {
62 UploadJobImpl::MimeBoundaryGenerator::~MimeBoundaryGenerator() {
65 UploadJobImpl::RandomMimeBoundaryGenerator::~RandomMimeBoundaryGenerator() {
68 // multipart/form-data POST request to upload the data. A DataSegment
69 // corresponds to one "Content-Disposition" in the "multipart" request.
72 DataSegment(const std::string
& name
,
73 const std::string
& filename
,
74 scoped_ptr
<std::string
> data
,
75 const std::map
<std::string
, std::string
>& header_entries
);
77 // Returns the header entries for this DataSegment.
78 const std::map
<std::string
, std::string
>& GetHeaderEntries() const;
80 // Returns the string that will be assigned to the |name| field in the header.
81 // |name| must be unique throughout the multipart message. This is enforced in
83 const std::string
& GetName() const;
85 // Returns the string that will be assigned to the |filename| field in the
86 // header. If the |filename| is the empty string, the header field will be
88 const std::string
& GetFilename() const;
90 // Returns the data contained in this DataSegment. Ownership is passed.
91 scoped_ptr
<std::string
> GetData();
93 // Returns the size in bytes of the blob in |data_|.
94 size_t GetDataSize() const;
96 // Helper method that performs a substring match of |chunk| in |data_|.
97 // Returns |true| if |chunk| matches a substring, |false| otherwise.
98 bool CheckIfDataContains(const std::string
& chunk
);
101 const std::string name_
;
102 const std::string filename_
;
103 scoped_ptr
<std::string
> data_
;
104 std::map
<std::string
, std::string
> header_entries_
;
106 DISALLOW_COPY_AND_ASSIGN(DataSegment
);
109 DataSegment::DataSegment(
110 const std::string
& name
,
111 const std::string
& filename
,
112 scoped_ptr
<std::string
> data
,
113 const std::map
<std::string
, std::string
>& header_entries
)
117 header_entries_(header_entries
) {
121 const std::map
<std::string
, std::string
>& DataSegment::GetHeaderEntries()
123 return header_entries_
;
126 const std::string
& DataSegment::GetName() const {
130 const std::string
& DataSegment::GetFilename() const {
134 scoped_ptr
<std::string
> DataSegment::GetData() {
138 bool DataSegment::CheckIfDataContains(const std::string
& chunk
) {
140 return data_
->find(chunk
) != std::string::npos
;
143 size_t DataSegment::GetDataSize() const {
145 return data_
->size();
148 std::string
UploadJobImpl::RandomMimeBoundaryGenerator::GenerateBoundary(
149 size_t length
) const {
150 std::string boundary
;
151 boundary
.reserve(length
);
152 DCHECK_GT(length
, sizeof(kMultipartBoundaryPrefix
) +
153 sizeof(kMultipartBoundaryPostfix
));
154 const size_t random_part_length
= length
- sizeof(kMultipartBoundaryPrefix
) -
155 sizeof(kMultipartBoundaryPostfix
);
156 boundary
.append(kMultipartBoundaryPrefix
);
157 boundary
.append(GenerateRandomString(random_part_length
));
158 boundary
.append(kMultipartBoundaryPostfix
);
162 UploadJobImpl::UploadJobImpl(
163 const GURL
& upload_url
,
164 const std::string
& account_id
,
165 OAuth2TokenService
* token_service
,
166 scoped_refptr
<net::URLRequestContextGetter
> url_context_getter
,
168 scoped_ptr
<MimeBoundaryGenerator
> boundary_generator
)
169 : OAuth2TokenService::Consumer("cros_upload_job"),
170 upload_url_(upload_url
),
171 account_id_(account_id
),
172 token_service_(token_service
),
173 url_context_getter_(url_context_getter
),
175 boundary_generator_(boundary_generator
.Pass()),
178 DCHECK(token_service_
);
179 DCHECK(url_context_getter_
);
181 if (!upload_url_
.is_valid()) {
183 NOTREACHED() << upload_url_
<< " is not a valid URL.";
187 UploadJobImpl::~UploadJobImpl() {
190 void UploadJobImpl::AddDataSegment(
191 const std::string
& name
,
192 const std::string
& filename
,
193 const std::map
<std::string
, std::string
>& header_entries
,
194 scoped_ptr
<std::string
> data
) {
195 // Cannot add data to busy or failed instance.
196 DCHECK_EQ(IDLE
, state_
);
200 scoped_ptr
<DataSegment
> data_segment(
201 new DataSegment(name
, filename
, data
.Pass(), header_entries
));
202 data_segments_
.push_back(data_segment
.Pass());
205 void UploadJobImpl::Start() {
206 // Cannot start an upload on a busy or failed instance.
207 DCHECK_EQ(IDLE
, state_
);
210 RequestAccessToken();
213 void UploadJobImpl::RequestAccessToken() {
214 state_
= ACQUIRING_TOKEN
;
216 OAuth2TokenService::ScopeSet scope_set
;
217 scope_set
.insert(GaiaConstants::kDeviceManagementServiceOAuth
);
218 access_token_request_
=
219 token_service_
->StartRequest(account_id_
, scope_set
, this);
222 bool UploadJobImpl::SetUpMultipart() {
223 DCHECK_EQ(ACQUIRING_TOKEN
, state_
);
224 state_
= PREPARING_CONTENT
;
226 if (mime_boundary_
&& post_data_
)
229 std::set
<std::string
> used_names
;
231 // Check uniqueness of header field names.
232 for (const auto& data_segment
: data_segments_
) {
233 if (!used_names
.insert(data_segment
->GetName()).second
)
237 // Generates random MIME boundaries and tests if they appear in any of the
238 // data segments. Tries up to |kMimeBoundaryRetries| times to find a MIME
239 // boundary that does not appear within any data segment.
244 mime_boundary_
.reset(new std::string(
245 boundary_generator_
->GenerateBoundary(kMimeBoundarySize
)));
246 for (const auto& data_segment
: data_segments_
) {
247 if (data_segment
->CheckIfDataContains(*mime_boundary_
)) {
253 } while (!found
&& retry
<= kMimeBoundaryRetries
);
255 // Notify the delegate that content encoding failed.
257 delegate_
->OnFailure(CONTENT_ENCODING_ERROR
);
258 mime_boundary_
.reset();
262 // Estimate an upper bound for the total message size to make memory
263 // allocation more efficient. It is not an error if this turns out to be too
264 // small as std::string will take care of the realloc.
266 for (const auto& data_segment
: data_segments_
) {
267 for (const auto& entry
: data_segment
->GetHeaderEntries())
268 size
+= entry
.first
.size() + entry
.second
.size();
269 size
+= kMimeBoundarySize
+ data_segment
->GetName().size() +
270 data_segment
->GetFilename().size() + data_segment
->GetDataSize();
271 // Add some extra space for all the constants and control characters.
275 // Allocate memory of the expected size.
276 post_data_
.reset(new std::string
);
277 post_data_
->reserve(size
);
279 for (const auto& data_segment
: data_segments_
) {
280 post_data_
->append("--" + *mime_boundary_
.get() + "\r\n");
281 post_data_
->append("Content-Disposition: form-data; name=\"" +
282 data_segment
->GetName() + "\"");
283 if (!data_segment
->GetFilename().empty()) {
284 post_data_
->append("; filename=\"" + data_segment
->GetFilename() + "\"");
286 post_data_
->append("\r\n");
288 // Add custom header fields.
289 for (const auto& entry
: data_segment
->GetHeaderEntries()) {
290 post_data_
->append(entry
.first
+ ": " + entry
.second
+ "\r\n");
292 scoped_ptr
<std::string
> data
= data_segment
->GetData();
293 post_data_
->append("\r\n" + *data
+ "\r\n");
295 post_data_
->append("--" + *mime_boundary_
.get() + "--\r\n");
297 // Issues a warning if our buffer size estimate was too small.
298 if (post_data_
->size() > size
) {
300 << "Reallocation needed in POST data buffer. Expected maximum size "
301 << size
<< " bytes, actual size " << post_data_
->size() << " bytes.";
304 // Discard the data segments as they are not needed anymore from here on.
305 data_segments_
.clear();
310 void UploadJobImpl::CreateAndStartURLFetcher(const std::string
& access_token
) {
311 // Ensure that the content has been prepared and the upload url is valid.
312 DCHECK_EQ(PREPARING_CONTENT
, state_
);
314 std::string content_type
= kUploadContentType
;
315 content_type
.append("; boundary=");
316 content_type
.append(*mime_boundary_
.get());
319 net::URLFetcher::Create(upload_url_
, net::URLFetcher::POST
, this);
320 upload_fetcher_
->SetRequestContext(url_context_getter_
.get());
321 upload_fetcher_
->SetUploadData(content_type
, *post_data_
);
322 upload_fetcher_
->AddExtraRequestHeader(
323 base::StringPrintf(kAuthorizationHeaderFormat
, access_token
.c_str()));
324 upload_fetcher_
->Start();
327 void UploadJobImpl::StartUpload(const std::string
& access_token
) {
328 if (!SetUpMultipart()) {
329 LOG(ERROR
) << "Multipart message assembly failed.";
333 CreateAndStartURLFetcher(access_token
);
337 void UploadJobImpl::OnGetTokenSuccess(
338 const OAuth2TokenService::Request
* request
,
339 const std::string
& access_token
,
340 const base::Time
& expiration_time
) {
341 DCHECK_EQ(ACQUIRING_TOKEN
, state_
);
342 DCHECK_EQ(access_token_request_
.get(), request
);
343 access_token_request_
.reset();
345 // Also cache the token locally, so that we can revoke it later if necessary.
346 access_token_
= access_token
;
347 StartUpload(access_token
);
350 void UploadJobImpl::OnGetTokenFailure(
351 const OAuth2TokenService::Request
* request
,
352 const GoogleServiceAuthError
& error
) {
353 DCHECK_EQ(ACQUIRING_TOKEN
, state_
);
354 DCHECK_EQ(access_token_request_
.get(), request
);
355 access_token_request_
.reset();
356 LOG(ERROR
) << "Token request failed: " << error
.ToString();
358 delegate_
->OnFailure(AUTHENTICATION_ERROR
);
361 void UploadJobImpl::OnURLFetchComplete(const net::URLFetcher
* source
) {
362 DCHECK_EQ(upload_fetcher_
.get(), source
);
363 const net::URLRequestStatus
& status
= source
->GetStatus();
364 if (!status
.is_success()) {
365 LOG(ERROR
) << "URLRequestStatus error " << status
.error();
366 upload_fetcher_
.reset();
369 delegate_
->OnFailure(NETWORK_ERROR
);
373 const int response_code
= source
->GetResponseCode();
374 const bool success
= response_code
== net::HTTP_OK
;
376 LOG(ERROR
) << "POST request failed with HTTP status code " << response_code
;
378 if (response_code
== net::HTTP_UNAUTHORIZED
) {
379 if (retry_
>= kMaxRetries
) {
380 upload_fetcher_
.reset();
381 LOG(ERROR
) << "Unauthorized request.";
384 delegate_
->OnFailure(AUTHENTICATION_ERROR
);
388 upload_fetcher_
.reset();
389 OAuth2TokenService::ScopeSet scope_set
;
390 scope_set
.insert(GaiaConstants::kDeviceManagementServiceOAuth
);
391 token_service_
->InvalidateAccessToken(account_id_
, scope_set
,
393 access_token_
.clear();
394 RequestAccessToken();
398 upload_fetcher_
.reset();
399 access_token_
.clear();
400 upload_fetcher_
.reset();
404 delegate_
->OnSuccess();
407 delegate_
->OnFailure(SERVER_ERROR
);
411 } // namespace policy