Commit 31d0dd65 authored by John Z Wu's avatar John Z Wu Committed by Commit Bot

Fix CSP translate issues once and for all.

CSP has a directive 'connect-src' that can prevent xhr requests. We proxy these requests to the
native iOS app to circumvent CSP. The native apps then takes the response and returns it to the
original xhr object. This fixes all remaining CSP issues with translate on iOS.

Cq-Include-Trybots: luci.chromium.try:ios-simulator-cronet;luci.chromium.try:ios-simulator-full-configs
Change-Id: I53e633bffbf6fea4b025651ad1c0efe8f367aaf7
Bug: 686364
Reviewed-on: https://chromium-review.googlesource.com/c/1285513
Commit-Queue: John Wu <jzw@chromium.org>
Reviewed-by: default avatarDavid Roger <droger@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604786}
parent 9c779cb8
...@@ -121,17 +121,6 @@ cr.googleTranslate = (function() { ...@@ -121,17 +121,6 @@ cr.googleTranslate = (function() {
*/ */
var loadJavascriptCallback; var loadJavascriptCallback;
/**
* Listens to security policy violations to set |errorCode|.
*/
document.addEventListener('securitypolicyviolation', function(event) {
if (securityOrigin.startsWith(event.blockedURI) &&
event.effectiveDirective == 'script-src') {
errorCode = ERROR['BAD_ORIGIN'];
invokeReadyCallback();
}
});
function checkLibReady() { function checkLibReady() {
if (lib.isAvailable()) { if (lib.isAvailable()) {
readyTime = performance.now(); readyTime = performance.now();
......
...@@ -164,9 +164,9 @@ void TranslateScript::OnScriptFetchComplete(bool success, ...@@ -164,9 +164,9 @@ void TranslateScript::OnScriptFetchComplete(bool success,
#if defined(OS_IOS) #if defined(OS_IOS)
// Append snippet to install callbacks on translate.js if available. // Append snippet to install callbacks on translate.js if available.
const char* install_callbacks = const char* install_callbacks =
"if (installTranslateCallbacks) {" "try {"
" installTranslateCallbacks();" " __gCrWeb.translate.installCallbacks();"
"}"; "} catch (error) {};";
base::StringPiece(install_callbacks).AppendToString(&data_); base::StringPiece(install_callbacks).AppendToString(&data_);
#endif // defined(OS_IOS) #endif // defined(OS_IOS)
......
...@@ -3,46 +3,150 @@ ...@@ -3,46 +3,150 @@
// found in the LICENSE file. // found in the LICENSE file.
/** /**
* @fileoverview Installs iOS Translate callbacks on cr.googleTranslate. * @fileoverview Translate script for iOS that is needed in addition to the
* cross platform script translate.js.
* *
* TODO(crbug.com/659442): Enable checkTypes, checkVars errors for this file. * TODO(crbug.com/659442): Enable checkTypes, checkVars errors for this file.
* @suppress {checkTypes, checkVars} * @suppress {checkTypes, checkVars}
*/ */
goog.require('__crWeb.base');
/**
* Namespace for this module.
*/
__gCrWeb.translate = {};
// Store message namespace object in a global __gCrWeb object referenced by a
// string, so it does not get renamed by closure compiler during the
// minification.
__gCrWeb['translate'] = __gCrWeb.translate;
/* Beginning of anonymous object. */
(function() {
/** /**
* Defines function to install callbacks on cr.googleTranslate. * Defines function to install callbacks on cr.googleTranslate.
* See translate_script.cc for usage. * See translate_script.cc for usage.
*/ */
var installTranslateCallbacks = function() { __gCrWeb.translate['installCallbacks'] = function() {
/** /**
* Sets a callback to inform host of the ready state of the translate element. * Sets a callback to inform host of the ready state of the translate element.
*/ */
cr.googleTranslate.readyCallback = function() { cr.googleTranslate.readyCallback = function() {
__gCrWeb.message.invokeOnHost({ __gCrWeb.message.invokeOnHost({
'command': 'translate.ready', 'command': 'translate.ready',
'errorCode': cr.googleTranslate.errorCode, 'errorCode': cr.googleTranslate.errorCode,
'loadTime': cr.googleTranslate.loadTime, 'loadTime': cr.googleTranslate.loadTime,
'readyTime': cr.googleTranslate.readyTime}); 'readyTime': cr.googleTranslate.readyTime});
} }
/** /**
* Sets a callback to inform host of the result of translation. * Sets a callback to inform host of the result of translation.
*/ */
cr.googleTranslate.resultCallback = function() { cr.googleTranslate.resultCallback = function() {
__gCrWeb.message.invokeOnHost({ __gCrWeb.message.invokeOnHost({
'command': 'translate.status', 'command': 'translate.status',
'errorCode': cr.googleTranslate.errorCode, 'errorCode': cr.googleTranslate.errorCode,
'originalPageLanguage': cr.googleTranslate.sourceLang, 'originalPageLanguage': cr.googleTranslate.sourceLang,
'translationTime': cr.googleTranslate.translationTime}); 'translationTime': cr.googleTranslate.translationTime});
} }
/** /**
* Sets a callback to inform host to download javascript. * Sets a callback to inform host to download javascript.
*/ */
cr.googleTranslate.loadJavascriptCallback = function(url) { cr.googleTranslate.loadJavascriptCallback = function(url) {
__gCrWeb.message.invokeOnHost({ __gCrWeb.message.invokeOnHost({
'command': 'translate.loadjavascript', 'command': 'translate.loadjavascript',
'url': url}); 'url': url});
}
};
/**
* Redefine XMLHttpRequest's open to capture request configurations.
*/
XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
this.savedMethod = method;
this.savedUrl = url;
this.savedAsync = async;
this.savedUser = user;
this.savedPassword = password;
this.realOpen(method, url, async, user, password);
} }
} // installTranslateCallbacks /**
* XMLHttpRequests still outstanding.
* @type {Array<XMLHttpRequest>}
*/
var xhrs = [];
/**
* Redefine XMLHttpRequest's send to call into the browser if it matches the
* predefined translate security origin.
*/
XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
// If this is a translate request, save this xhr and proxy the request to the
// browser. Else, pass it through to the original implementation.
// |securityOrigin| is predefined by translate_script.cc.
if (this.savedUrl.startsWith(securityOrigin)) {
var length = xhrs.push(this);
__gCrWeb.message.invokeOnHost({
'command': 'translate.sendrequest',
'method': this.savedMethod,
'url': this.savedUrl,
'body': body,
'requestID': length - 1});
} else {
this.realSend(body);
}
}
/**
* Receives the response the browser got for the proxied xhr request and
* configures the xhr object as it would have been had it been sent normally.
* @param {string} url The original url which initiated the request.
* @param {number} requestID The index of the xhr request in |xhrs|.
* @param {number} status HTTP response code.
* @param {string} statusText HTTP response status text.
* @param {string} responseURL The url which the response was returned from.
* @param {string} responseText The text received from the server.
*/
__gCrWeb.translate['handleResponse'] = function(url, requestID, status,
statusText, responseURL,
responseText) {
// Retrive xhr object that's waiting for the response.
xhr = xhrs[requestID];
// Configure xhr as it would have been if it was sent.
Object.defineProperties(xhr, {
responseText: {
value: responseText
},
response: {
value: responseText
},
readyState: {
value: XMLHttpRequest.DONE
},
status: {
value: status
},
statusText : {
value: statusText
},
responseType: {
value: "text"
},
responseURL: {
value: responseURL
},
});
xhr.onreadystatechange();
// Clean it up
delete xhrs[requestID];
};
}()); // End of anonymous function.
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
#ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_ #ifndef COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_
#define COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_ #define COMPONENTS_TRANSLATE_IOS_BROWSER_TRANSLATE_CONTROLLER_H_
#include <iterator>
#include <memory> #include <memory>
#include <set>
#include <string> #include <string>
#include "base/gtest_prod_util.h" #include "base/gtest_prod_util.h"
...@@ -83,6 +85,7 @@ class TranslateController : public web::WebStateObserver { ...@@ -83,6 +85,7 @@ class TranslateController : public web::WebStateObserver {
FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationSuccess); FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationSuccess);
FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationFailure); FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, TranslationFailure);
FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, OnTranslateLoadJavascript); FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, OnTranslateLoadJavascript);
FRIEND_TEST_ALL_PREFIXES(TranslateControllerTest, OnTranslateSendRequest);
// Called when a JavaScript command is received. // Called when a JavaScript command is received.
bool OnJavascriptCommandReceived(const base::DictionaryValue& command, bool OnJavascriptCommandReceived(const base::DictionaryValue& command,
...@@ -95,11 +98,16 @@ class TranslateController : public web::WebStateObserver { ...@@ -95,11 +98,16 @@ class TranslateController : public web::WebStateObserver {
bool OnTranslateReady(const base::DictionaryValue& command); bool OnTranslateReady(const base::DictionaryValue& command);
bool OnTranslateComplete(const base::DictionaryValue& command); bool OnTranslateComplete(const base::DictionaryValue& command);
bool OnTranslateLoadJavaScript(const base::DictionaryValue& command); bool OnTranslateLoadJavaScript(const base::DictionaryValue& command);
bool OnTranslateSendRequest(const base::DictionaryValue& command);
// Use to fetch additional scripts needed for translate.
void FetchScript(const std::string& url);
// The callback when the script is fetched or a server error occurred. // The callback when the script is fetched or a server error occurred.
void OnScriptFetchComplete(std::unique_ptr<std::string> response_body); void OnScriptFetchComplete(std::unique_ptr<std::string> response_body);
// The callback when translate requests have completed.
void OnRequestFetchComplete(
std::set<std::unique_ptr<network::SimpleURLLoader>>::iterator it,
std::string url,
int request_id,
std::unique_ptr<std::string> response_body);
// web::WebStateObserver implementation: // web::WebStateObserver implementation:
void WebStateDestroyed(web::WebState* web_state) override; void WebStateDestroyed(web::WebState* web_state) override;
...@@ -110,6 +118,8 @@ class TranslateController : public web::WebStateObserver { ...@@ -110,6 +118,8 @@ class TranslateController : public web::WebStateObserver {
// WebStateDestroyed has been called. // WebStateDestroyed has been called.
web::WebState* web_state_ = nullptr; web::WebState* web_state_ = nullptr;
// Used to fetch translate requests. There may be multiple requests in flight.
std::set<std::unique_ptr<network::SimpleURLLoader>> request_fetchers_;
// Used to fetch additional scripts needed for translate. // Used to fetch additional scripts needed for translate.
std::unique_ptr<network::SimpleURLLoader> script_fetcher_; std::unique_ptr<network::SimpleURLLoader> script_fetcher_;
......
...@@ -4,10 +4,14 @@ ...@@ -4,10 +4,14 @@
#import "components/translate/ios/browser/translate_controller.h" #import "components/translate/ios/browser/translate_controller.h"
#include <utility>
#include "base/bind.h" #include "base/bind.h"
#include "base/bind_helpers.h" #include "base/bind_helpers.h"
#include "base/json/string_escape.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/strings/string16.h" #include "base/strings/string16.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h" #include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "base/values.h" #include "base/values.h"
...@@ -16,8 +20,11 @@ ...@@ -16,8 +20,11 @@
#include "ios/web/public/browser_state.h" #include "ios/web/public/browser_state.h"
#include "ios/web/public/web_state/navigation_context.h" #include "ios/web/public/web_state/navigation_context.h"
#include "ios/web/public/web_state/web_state.h" #include "ios/web/public/web_state/web_state.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/traffic_annotation/network_traffic_annotation.h" #include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h" #include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_response.h"
#include "url/gurl.h" #include "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
...@@ -98,6 +105,8 @@ bool TranslateController::OnJavascriptCommandReceived( ...@@ -98,6 +105,8 @@ bool TranslateController::OnJavascriptCommandReceived(
return OnTranslateComplete(command); return OnTranslateComplete(command);
if (out_string == "translate.loadjavascript") if (out_string == "translate.loadjavascript")
return OnTranslateLoadJavaScript(command); return OnTranslateLoadJavaScript(command);
if (out_string == "translate.sendrequest")
return OnTranslateSendRequest(command);
return false; return false;
} }
...@@ -166,15 +175,9 @@ bool TranslateController::OnTranslateLoadJavaScript( ...@@ -166,15 +175,9 @@ bool TranslateController::OnTranslateLoadJavaScript(
return false; return false;
} }
FetchScript(url);
return true;
}
void TranslateController::FetchScript(const std::string& url) {
GURL security_origin = translate::GetTranslateSecurityOrigin(); GURL security_origin = translate::GetTranslateSecurityOrigin();
if (url.find(security_origin.spec()) || script_fetcher_) { if (url.find(security_origin.spec()) || script_fetcher_) {
return; return false;
} }
auto resource_request = std::make_unique<network::ResourceRequest>(); auto resource_request = std::make_unique<network::ResourceRequest>();
...@@ -186,6 +189,51 @@ void TranslateController::FetchScript(const std::string& url) { ...@@ -186,6 +189,51 @@ void TranslateController::FetchScript(const std::string& url) {
web_state_->GetBrowserState()->GetURLLoaderFactory(), web_state_->GetBrowserState()->GetURLLoaderFactory(),
base::BindOnce(&TranslateController::OnScriptFetchComplete, base::BindOnce(&TranslateController::OnScriptFetchComplete,
base::Unretained(this))); base::Unretained(this)));
return true;
}
bool TranslateController::OnTranslateSendRequest(
const base::DictionaryValue& command) {
std::string method;
if (!command.HasKey("method") || !command.GetString("method", &method)) {
return false;
}
std::string url;
if (!command.HasKey("url") || !command.GetString("url", &url)) {
return false;
}
std::string body;
if (!command.HasKey("body") || !command.GetString("body", &body)) {
return false;
}
double request_id;
if (!command.HasKey("requestID") ||
!command.GetDouble("requestID", &request_id)) {
return false;
}
GURL security_origin = translate::GetTranslateSecurityOrigin();
if (url.find(security_origin.spec())) {
return false;
}
auto request = std::make_unique<network::ResourceRequest>();
request->method = method;
request->url = GURL(url);
request->load_flags =
net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES;
auto fetcher = network::SimpleURLLoader::Create(std::move(request),
NO_TRAFFIC_ANNOTATION_YET);
fetcher->AttachStringForUpload(body, "application/x-www-form-urlencoded");
auto* raw_fetcher = fetcher.get();
auto pair = request_fetchers_.insert(std::move(fetcher));
raw_fetcher->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
web_state_->GetBrowserState()->GetURLLoaderFactory(),
base::BindOnce(&TranslateController::OnRequestFetchComplete,
base::Unretained(this), pair.first, url,
static_cast<int>(request_id)));
return true;
} }
void TranslateController::OnScriptFetchComplete( void TranslateController::OnScriptFetchComplete(
...@@ -196,6 +244,34 @@ void TranslateController::OnScriptFetchComplete( ...@@ -196,6 +244,34 @@ void TranslateController::OnScriptFetchComplete(
script_fetcher_.reset(); script_fetcher_.reset();
} }
void TranslateController::OnRequestFetchComplete(
std::set<std::unique_ptr<network::SimpleURLLoader>>::iterator it,
std::string url,
int request_id,
std::unique_ptr<std::string> response_body) {
const std::unique_ptr<network::SimpleURLLoader>& url_loader = *it;
std::string response_url = url_loader->GetFinalURL().spec();
net::HttpResponseHeaders* headers = url_loader->ResponseInfo()->headers.get();
int response_code = headers->response_code();
const std::string& status_text = headers->GetStatusText();
// Escape the returned string so it can be parsed by JSON.parse.
std::string response_text = response_body ? *response_body : "";
std::string escaped_response_text;
base::EscapeJSONString(response_text, /*put_in_quotes=*/false,
&escaped_response_text);
// Return the response details to function defined in translate_ios.js.
std::string script = base::StringPrintf(
"__gCrWeb.translate.handleResponse('%s', %d, %d, '%s', '%s', '%s')",
url.c_str(), request_id, response_code, status_text.c_str(),
response_url.c_str(), escaped_response_text.c_str());
web_state_->ExecuteJavaScript(base::UTF8ToUTF16(script));
request_fetchers_.erase(it);
}
// web::WebStateObserver implementation. // web::WebStateObserver implementation.
void TranslateController::WebStateDestroyed(web::WebState* web_state) { void TranslateController::WebStateDestroyed(web::WebState* web_state) {
...@@ -204,6 +280,7 @@ void TranslateController::WebStateDestroyed(web::WebState* web_state) { ...@@ -204,6 +280,7 @@ void TranslateController::WebStateDestroyed(web::WebState* web_state) {
web_state_->RemoveObserver(this); web_state_->RemoveObserver(this);
web_state_ = nullptr; web_state_ = nullptr;
request_fetchers_.clear();
script_fetcher_.reset(); script_fetcher_.reset();
} }
...@@ -211,6 +288,7 @@ void TranslateController::DidStartNavigation( ...@@ -211,6 +288,7 @@ void TranslateController::DidStartNavigation(
web::WebState* web_state, web::WebState* web_state,
web::NavigationContext* navigation_context) { web::NavigationContext* navigation_context) {
if (!navigation_context->IsSameDocument()) { if (!navigation_context->IsSameDocument()) {
request_fetchers_.clear();
script_fetcher_.reset(); script_fetcher_.reset();
} }
} }
......
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
#include "base/values.h" #include "base/values.h"
#import "components/translate/ios/browser/js_translate_manager.h" #import "components/translate/ios/browser/js_translate_manager.h"
#include "ios/web/public/test/fakes/test_browser_state.h"
#import "ios/web/public/test/fakes/test_web_state.h" #import "ios/web/public/test/fakes/test_web_state.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#include "testing/platform_test.h" #include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h" #import "third_party/ocmock/OCMock/OCMock.h"
#include "url/gurl.h" #include "url/gurl.h"
...@@ -24,12 +26,14 @@ class TranslateControllerTest : public PlatformTest, ...@@ -24,12 +26,14 @@ class TranslateControllerTest : public PlatformTest,
protected: protected:
TranslateControllerTest() TranslateControllerTest()
: test_web_state_(new web::TestWebState), : test_web_state_(new web::TestWebState),
test_browser_state_(new web::TestBrowserState),
error_type_(TranslateErrors::Type::NONE), error_type_(TranslateErrors::Type::NONE),
ready_time_(0), ready_time_(0),
load_time_(0), load_time_(0),
translation_time_(0), translation_time_(0),
on_script_ready_called_(false), on_script_ready_called_(false),
on_translate_complete_called_(false) { on_translate_complete_called_(false) {
test_web_state_->SetBrowserState(test_browser_state_.get());
mock_js_translate_manager_ = mock_js_translate_manager_ =
[OCMockObject niceMockForClass:[JsTranslateManager class]]; [OCMockObject niceMockForClass:[JsTranslateManager class]];
translate_controller_ = std::make_unique<TranslateController>( translate_controller_ = std::make_unique<TranslateController>(
...@@ -56,7 +60,9 @@ class TranslateControllerTest : public PlatformTest, ...@@ -56,7 +60,9 @@ class TranslateControllerTest : public PlatformTest,
translation_time_ = translation_time; translation_time_ = translation_time;
} }
web::TestWebThreadBundle web_thread_bundle_;
std::unique_ptr<web::TestWebState> test_web_state_; std::unique_ptr<web::TestWebState> test_web_state_;
std::unique_ptr<web::TestBrowserState> test_browser_state_;
id mock_js_translate_manager_; id mock_js_translate_manager_;
std::unique_ptr<TranslateController> translate_controller_; std::unique_ptr<TranslateController> translate_controller_;
TranslateErrors::Type error_type_; TranslateErrors::Type error_type_;
...@@ -167,7 +173,22 @@ TEST_F(TranslateControllerTest, TranslationFailure) { ...@@ -167,7 +173,22 @@ TEST_F(TranslateControllerTest, TranslationFailure) {
TEST_F(TranslateControllerTest, OnTranslateLoadJavascript) { TEST_F(TranslateControllerTest, OnTranslateLoadJavascript) {
base::DictionaryValue command; base::DictionaryValue command;
command.SetString("command", "translate.loadjavascript"); command.SetString("command", "translate.loadjavascript");
command.SetString("url", "https:///translate.googleapis.com/javascript.js"); command.SetString("url", "https://translate.googleapis.com/javascript.js");
EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived(
command, GURL("http://google.com"), /*interacting=*/false,
/*is_main_frame=*/true, /*sender_frame=*/nullptr));
}
// Tests that OnTranslateSendRequest() is called with the right paramters
// when a |translate.sendrequest| message is received from the JS side.
TEST_F(TranslateControllerTest, OnTranslateSendRequest) {
base::DictionaryValue command;
command.SetString("command", "translate.sendrequest");
command.SetString("method", "POST");
command.SetString("url",
"https://translate.googleapis.com/translate?key=abcd");
command.SetString("body", "helloworld");
command.SetDouble("requestID", 0);
EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived( EXPECT_TRUE(translate_controller_->OnJavascriptCommandReceived(
command, GURL("http://google.com"), /*interacting=*/false, command, GURL("http://google.com"), /*interacting=*/false,
/*is_main_frame=*/true, /*sender_frame=*/nullptr)); /*is_main_frame=*/true, /*sender_frame=*/nullptr));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment