| // Copyright 2016 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 "net/reporting/reporting_uploader.h" | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "base/callback_helpers.h" | |
| #include "base/metrics/histogram_functions.h" | |
| #include "base/metrics/histogram_macros.h" | |
| #include "base/strings/string_split.h" | |
| #include "base/strings/string_util.h" | |
| #include "net/base/elements_upload_data_stream.h" | |
| #include "net/base/load_flags.h" | |
| #include "net/base/upload_bytes_element_reader.h" | |
| #include "net/http/http_response_headers.h" | |
| #include "net/traffic_annotation/network_traffic_annotation.h" | |
| #include "net/url_request/redirect_info.h" | |
| #include "net/url_request/url_request_context.h" | |
| #include "url/gurl.h" | |
| #include "url/origin.h" | |
| namespace net { | |
| namespace { | |
| constexpr char kUploadContentType[] = "application/reports+json"; | |
| constexpr net::NetworkTrafficAnnotationTag kReportUploadTrafficAnnotation = | |
| net::DefineNetworkTrafficAnnotation("reporting", R"( | |
| semantics { | |
| sender: "Reporting API" | |
| description: | |
| "The Reporting API reports various issues back to website owners " | |
| "to help them detect and fix problems." | |
| trigger: | |
| "Encountering issues. Examples of these issues are Content " | |
| "Security Policy violations and Interventions/Deprecations " | |
| "encountered. See draft of reporting spec here: " | |
| "https://wicg.github.io/reporting." | |
| data: "Details of the issue, depending on issue type." | |
| destination: OTHER | |
| } | |
| policy { | |
| cookies_allowed: NO | |
| setting: "This feature cannot be disabled by settings." | |
| policy_exception_justification: "Not implemented." | |
| })"); | |
| class UploadUserData : public base::SupportsUserData::Data { | |
| public: | |
| static const void* const kUserDataKey; | |
| UploadUserData(int depth) : depth(depth) {} | |
| int depth; | |
| }; | |
| // Returns true if |request| contains any of the |allowed_values| in a response | |
| // header field named |header|. |allowed_values| are expected to be lower-case | |
| // and the check is case-insensitive. | |
| bool HasHeaderValues(URLRequest* request, | |
| const std::string& header, | |
| const std::set<std::string>& allowed_values) { | |
| std::string response_headers; | |
| request->GetResponseHeaderByName(header, &response_headers); | |
| const std::vector<std::string> response_values = | |
| base::SplitString(base::ToLowerASCII(response_headers), ",", | |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | |
| for (const auto& value : response_values) { | |
| if (allowed_values.find(value) != allowed_values.end()) | |
| return true; | |
| } | |
| return false; | |
| } | |
| // SetUserData needs a unique const void* to serve as the key, so create a const | |
| // void* and use its own address as the unique pointer. | |
| const void* const UploadUserData::kUserDataKey = &UploadUserData::kUserDataKey; | |
| ReportingUploader::Outcome ResponseCodeToOutcome(int response_code) { | |
| if (response_code >= 200 && response_code <= 299) | |
| return ReportingUploader::Outcome::SUCCESS; | |
| if (response_code == 410) | |
| return ReportingUploader::Outcome::REMOVE_ENDPOINT; | |
| return ReportingUploader::Outcome::FAILURE; | |
| } | |
| enum class UploadOutcome { | |
| CANCELED_REDIRECT_TO_INSECURE_URL = 0, | |
| CANCELED_AUTH_REQUIRED = 1, | |
| CANCELED_CERTIFICATE_REQUESTED = 2, | |
| CANCELED_SSL_CERTIFICATE_ERROR = 3, | |
| CANCELED_REPORTING_SHUTDOWN = 4, | |
| FAILED = 5, // See Net.Reporting.UploadError for breakdown. | |
| SUCCEEDED_SUCCESS = 6, | |
| SUCCEEDED_REMOVE_ENDPOINT = 7, | |
| CORS_PREFLIGHT_ERROR = 8, | |
| MAX | |
| }; | |
| void RecordUploadOutcome(UploadOutcome outcome) { | |
| UMA_HISTOGRAM_ENUMERATION("Net.Reporting.UploadOutcome", outcome, | |
| UploadOutcome::MAX); | |
| } | |
| // TODO: Record net and HTTP error. | |
| struct PendingUpload { | |
| enum State { CREATED, SENDING_PREFLIGHT, SENDING_PAYLOAD }; | |
| PendingUpload(const url::Origin& report_origin, | |
| const GURL& url, | |
| const std::string& json, | |
| int max_depth, | |
| ReportingUploader::UploadCallback callback) | |
| : state(CREATED), | |
| report_origin(report_origin), | |
| url(url), | |
| payload_reader(UploadOwnedBytesElementReader::CreateWithString(json)), | |
| max_depth(max_depth), | |
| callback(std::move(callback)) {} | |
| void RunCallback(ReportingUploader::Outcome outcome) { | |
| std::move(callback).Run(outcome); | |
| } | |
| State state; | |
| const url::Origin report_origin; | |
| const GURL url; | |
| std::unique_ptr<UploadElementReader> payload_reader; | |
| int max_depth; | |
| ReportingUploader::UploadCallback callback; | |
| std::unique_ptr<URLRequest> request; | |
| }; | |
| class ReportingUploaderImpl : public ReportingUploader, URLRequest::Delegate { | |
| public: | |
| ReportingUploaderImpl(const URLRequestContext* context) : context_(context) { | |
| DCHECK(context_); | |
| } | |
| ~ReportingUploaderImpl() override { | |
| for (auto& request_and_upload : uploads_) { | |
| auto& upload = request_and_upload.second; | |
| upload->RunCallback(Outcome::FAILURE); | |
| } | |
| } | |
| void StartUpload(const url::Origin& report_origin, | |
| const GURL& url, | |
| const std::string& json, | |
| int max_depth, | |
| UploadCallback callback) override { | |
| auto upload = std::make_unique<PendingUpload>( | |
| report_origin, url, json, max_depth, std::move(callback)); | |
| auto collector_origin = url::Origin::Create(url); | |
| if (collector_origin == report_origin) { | |
| // Skip the preflight check if the reports are being sent to the same | |
| // origin as the requests they describe. | |
| StartPayloadRequest(std::move(upload)); | |
| } else { | |
| StartPreflightRequest(std::move(upload)); | |
| } | |
| } | |
| void StartPreflightRequest(std::unique_ptr<PendingUpload> upload) { | |
| DCHECK(upload->state == PendingUpload::CREATED); | |
| upload->state = PendingUpload::SENDING_PREFLIGHT; | |
| upload->request = context_->CreateRequest(upload->url, IDLE, this, | |
| kReportUploadTrafficAnnotation); | |
| upload->request->set_method("OPTIONS"); | |
| upload->request->SetLoadFlags(LOAD_DISABLE_CACHE | | |
| LOAD_DO_NOT_SAVE_COOKIES | | |
| LOAD_DO_NOT_SEND_COOKIES); | |
| upload->request->SetExtraRequestHeaderByName( | |
| HttpRequestHeaders::kOrigin, upload->report_origin.Serialize(), true); | |
| upload->request->SetExtraRequestHeaderByName( | |
| "Access-Control-Request-Method", "POST", true); | |
| upload->request->SetExtraRequestHeaderByName( | |
| "Access-Control-Request-Headers", "content-type", true); | |
| // Set the max_depth for this request, to cap how deep a stack of "reports | |
| // about reports" can get. (Without this, a Reporting policy that uploads | |
| // reports to the same origin can cause an infinite stack of reports about | |
| // reports.) | |
| upload->request->SetUserData( | |
| UploadUserData::kUserDataKey, | |
| std::make_unique<UploadUserData>(upload->max_depth)); | |
| URLRequest* raw_request = upload->request.get(); | |
| uploads_[raw_request] = std::move(upload); | |
| raw_request->Start(); | |
| } | |
| void StartPayloadRequest(std::unique_ptr<PendingUpload> upload) { | |
| DCHECK(upload->state == PendingUpload::CREATED || | |
| upload->state == PendingUpload::SENDING_PREFLIGHT); | |
| upload->state = PendingUpload::SENDING_PAYLOAD; | |
| upload->request = context_->CreateRequest(upload->url, IDLE, this, | |
| kReportUploadTrafficAnnotation); | |
| upload->request->set_method("POST"); | |
| upload->request->SetLoadFlags(LOAD_DISABLE_CACHE | | |
| LOAD_DO_NOT_SAVE_COOKIES | | |
| LOAD_DO_NOT_SEND_COOKIES); | |
| upload->request->SetExtraRequestHeaderByName( | |
| HttpRequestHeaders::kContentType, kUploadContentType, true); | |
| upload->request->set_upload(ElementsUploadDataStream::CreateWithReader( | |
| std::move(upload->payload_reader), 0)); | |
| // Set the max_depth for this request, to cap how deep a stack of "reports | |
| // about reports" can get. (Without this, a Reporting policy that uploads | |
| // reports to the same origin can cause an infinite stack of reports about | |
| // reports.) | |
| upload->request->SetUserData( | |
| UploadUserData::kUserDataKey, | |
| std::make_unique<UploadUserData>(upload->max_depth)); | |
| URLRequest* raw_request = upload->request.get(); | |
| uploads_[raw_request] = std::move(upload); | |
| raw_request->Start(); | |
| } | |
| int GetUploadDepth(const net::URLRequest& request) override { | |
| UploadUserData* data = static_cast<UploadUserData*>( | |
| request.GetUserData(UploadUserData::kUserDataKey)); | |
| return data ? data->depth + 1 : 0; | |
| } | |
| // URLRequest::Delegate implementation: | |
| void OnReceivedRedirect(URLRequest* request, | |
| const RedirectInfo& redirect_info, | |
| bool* defer_redirect) override { | |
| if (!redirect_info.new_url.SchemeIsCryptographic()) { | |
| request->Cancel(); | |
| return; | |
| } | |
| } | |
| void OnAuthRequired(URLRequest* request, | |
| AuthChallengeInfo* auth_info) override { | |
| request->Cancel(); | |
| } | |
| void OnCertificateRequested(URLRequest* request, | |
| SSLCertRequestInfo* cert_request_info) override { | |
| request->Cancel(); | |
| } | |
| void OnSSLCertificateError(URLRequest* request, | |
| const SSLInfo& ssl_info, | |
| bool fatal) override { | |
| request->Cancel(); | |
| } | |
| void OnResponseStarted(URLRequest* request, int net_error) override { | |
| // Grab Upload from map, and hold on to it in a local unique_ptr so it's | |
| // removed at the end of the method. | |
| auto it = uploads_.find(request); | |
| DCHECK(it != uploads_.end()); | |
| std::unique_ptr<PendingUpload> upload = std::move(it->second); | |
| uploads_.erase(it); | |
| if (net_error != OK) { | |
| RecordUploadOutcome(UploadOutcome::FAILED); | |
| base::UmaHistogramSparse("Net.Reporting.UploadError", net_error); | |
| upload->RunCallback(ReportingUploader::Outcome::FAILURE); | |
| return; | |
| } | |
| // request->GetResponseCode() should work, but doesn't in the cases above | |
| // where the request was canceled, so get the response code by hand. | |
| // TODO(juliatuttle): Check if mmenke fixed this yet. | |
| HttpResponseHeaders* headers = request->response_headers(); | |
| int response_code = headers ? headers->response_code() : 0; | |
| switch (upload->state) { | |
| case PendingUpload::SENDING_PREFLIGHT: | |
| HandlePreflightResponse(std::move(upload), response_code); | |
| break; | |
| case PendingUpload::SENDING_PAYLOAD: | |
| HandlePayloadResponse(std::move(upload), response_code); | |
| break; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| void HandlePreflightResponse(std::unique_ptr<PendingUpload> upload, | |
| int response_code) { | |
| // Check that the preflight succeeded: it must have an HTTP OK status code, | |
| // with the following headers: | |
| // - Access-Control-Allow-Origin: * or the report origin | |
| // - Access-Control-Allow-Methods: POST | |
| // - Access-Control-Allow-Headers: Content-Type | |
| URLRequest* request = upload->request.get(); | |
| bool preflight_succeeded = | |
| (response_code >= 200 && response_code <= 299) && | |
| HasHeaderValues( | |
| request, "Access-Control-Allow-Origin", | |
| {"*", base::ToLowerASCII(upload->report_origin.Serialize())}) && | |
| HasHeaderValues(request, "Access-Control-Allow-Methods", {"post"}) && | |
| HasHeaderValues(request, "Access-Control-Allow-Headers", | |
| {"content-type"}); | |
| if (!preflight_succeeded) { | |
| RecordUploadOutcome(UploadOutcome::CORS_PREFLIGHT_ERROR); | |
| upload->RunCallback(ReportingUploader::Outcome::FAILURE); | |
| return; | |
| } | |
| StartPayloadRequest(std::move(upload)); | |
| } | |
| void HandlePayloadResponse(std::unique_ptr<PendingUpload> upload, | |
| int response_code) { | |
| if (response_code >= 200 && response_code <= 299) { | |
| RecordUploadOutcome(UploadOutcome::SUCCEEDED_SUCCESS); | |
| } else if (response_code == 410) { | |
| RecordUploadOutcome(UploadOutcome::SUCCEEDED_REMOVE_ENDPOINT); | |
| } else { | |
| RecordUploadOutcome(UploadOutcome::FAILED); | |
| base::UmaHistogramSparse("Net.Reporting.UploadError", response_code); | |
| } | |
| upload->RunCallback(ResponseCodeToOutcome(response_code)); | |
| } | |
| void OnReadCompleted(URLRequest* request, int bytes_read) override { | |
| // Reporting doesn't need anything in the body of the response, so it | |
| // doesn't read it, so it should never get OnReadCompleted calls. | |
| NOTREACHED(); | |
| } | |
| private: | |
| const URLRequestContext* context_; | |
| std::map<const URLRequest*, std::unique_ptr<PendingUpload>> uploads_; | |
| }; | |
| } // namespace | |
| ReportingUploader::~ReportingUploader() = default; | |
| // static | |
| std::unique_ptr<ReportingUploader> ReportingUploader::Create( | |
| const URLRequestContext* context) { | |
| return std::make_unique<ReportingUploaderImpl>(context); | |
| } | |
| } // namespace net |