diff --git a/browser/brave_content_browser_client.cc b/browser/brave_content_browser_client.cc index 1c9bac3821c3..4d09840013dc 100644 --- a/browser/brave_content_browser_client.cc +++ b/browser/brave_content_browser_client.cc @@ -84,6 +84,8 @@ void BraveContentBrowserClient::BrowserURLHandlerCreated( // before anything else can. handler->AddHandlerPair(&webtorrent::HandleMagnetURLRewrite, content::BrowserURLHandler::null_handler()); + handler->AddHandlerPair(&webtorrent::HandleTorrentURLRewrite, + &webtorrent::HandleTorrentURLReverseRewrite); handler->AddHandlerPair(&HandleURLRewrite, &HandleURLReverseRewrite); ChromeContentBrowserClient::BrowserURLHandlerCreated(handler); diff --git a/browser/brave_content_browser_client_browsertest.cc b/browser/brave_content_browser_client_browsertest.cc index 6e0c01d694d4..18b3674640ed 100644 --- a/browser/brave_content_browser_client_browsertest.cc +++ b/browser/brave_content_browser_client_browsertest.cc @@ -35,9 +35,11 @@ class BraveContentBrowserClientTest : public InProcessBrowserTest { ASSERT_TRUE(embedded_test_server()->Start()); - url_ = embedded_test_server()->GetURL("a.com", "/magnet.html"); + magnet_html_url_ = embedded_test_server()->GetURL("a.com", "/magnet.html"); magnet_url_ = GURL("magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fbig-buck-bunny.torrent"); extension_url_ = GURL("chrome-extension://lgjmpdmojkpocjcopdikifhejkkjglho/extension/brave_webtorrent.html?magnet%3A%3Fxt%3Durn%3Abtih%3Add8255ecdc7ca55fb0bbf81323d87062db1f6d1c%26dn%3DBig%2BBuck%2BBunny%26tr%3Dudp%253A%252F%252Fexplodie.org%253A6969%26tr%3Dudp%253A%252F%252Ftracker.coppersurfer.tk%253A6969%26tr%3Dudp%253A%252F%252Ftracker.empire-js.us%253A1337%26tr%3Dudp%253A%252F%252Ftracker.leechers-paradise.org%253A6969%26tr%3Dudp%253A%252F%252Ftracker.opentrackr.org%253A1337%26tr%3Dwss%253A%252F%252Ftracker.btorrent.xyz%26tr%3Dwss%253A%252F%252Ftracker.fastcast.nz%26tr%3Dwss%253A%252F%252Ftracker.openwebtorrent.com%26ws%3Dhttps%253A%252F%252Fwebtorrent.io%252Ftorrents%252F%26xs%3Dhttps%253A%252F%252Fwebtorrent.io%252Ftorrents%252Fbig-buck-bunny.torrent"); + torrent_url_ = GURL("https://webtorrent.io/torrents/sintel.torrent#ix=5"); + torrent_extension_url_ = GURL("chrome-extension://lgjmpdmojkpocjcopdikifhejkkjglho/extension/brave_webtorrent.html?https://webtorrent.io/torrents/sintel.torrent#ix=5"); } void TearDown() override { @@ -45,14 +47,18 @@ class BraveContentBrowserClientTest : public InProcessBrowserTest { content_client_.reset(); } - const GURL& url() { return url_; } + const GURL& magnet_html_url() { return magnet_html_url_; } const GURL& magnet_url() { return magnet_url_; } const GURL& extension_url() { return extension_url_; } + const GURL& torrent_url() { return torrent_url_; } + const GURL& torrent_extension_url() { return torrent_extension_url_; } private: - GURL url_; + GURL magnet_html_url_; GURL magnet_url_; GURL extension_url_; + GURL torrent_url_; + GURL torrent_extension_url_; ContentSettingsPattern top_level_page_pattern_; ContentSettingsPattern empty_pattern_; std::unique_ptr content_client_; @@ -104,7 +110,7 @@ IN_PROC_BROWSER_TEST_F(BraveContentBrowserClientTest, RewriteMagnetURLURLBar) { IN_PROC_BROWSER_TEST_F(BraveContentBrowserClientTest, RewriteMagnetURLLink) { content::WebContents* contents = browser()->tab_strip_model()->GetActiveWebContents(); - ui_test_utils::NavigateToURL(browser(), url()); + ui_test_utils::NavigateToURL(browser(), magnet_html_url()); ASSERT_TRUE(WaitForLoadStop(contents)); bool value; EXPECT_TRUE(ExecuteScriptAndExtractBool(contents, "clickMagnetLink();", @@ -118,3 +124,15 @@ IN_PROC_BROWSER_TEST_F(BraveContentBrowserClientTest, RewriteMagnetURLLink) { EXPECT_STREQ(entry->GetURL().spec().c_str(), extension_url().spec().c_str()) << "Real URL should be extension URL"; } + +IN_PROC_BROWSER_TEST_F(BraveContentBrowserClientTest, ReverseRewriteTorrentURL) { + content::WebContents* contents = browser()->tab_strip_model()->GetActiveWebContents(); + ui_test_utils::NavigateToURL(browser(), torrent_extension_url()); + ASSERT_TRUE(WaitForLoadStop(contents)); + + EXPECT_STREQ(contents->GetLastCommittedURL().spec().c_str(), + torrent_url().spec().c_str()) << "URL visible to users should stay as the torrent URL"; + content::NavigationEntry* entry = contents->GetController().GetLastCommittedEntry(); + EXPECT_STREQ(entry->GetURL().spec().c_str(), + torrent_extension_url().spec().c_str()) << "Real URL should be extension URL"; +} diff --git a/browser/net/BUILD.gn b/browser/net/BUILD.gn index e4c5e3870423..c685dd3ce40a 100644 --- a/browser/net/BUILD.gn +++ b/browser/net/BUILD.gn @@ -18,10 +18,11 @@ source_set("net") { "brave_system_network_delegate.cc", "brave_system_network_delegate.h", "url_context.cc", - "url_context.h" + "url_context.h", ] deps = [ "//brave/browser/safebrowsing", + "//brave/components/brave_webtorrent/browser/net", "//chrome/browser", "//content/public/browser", "//net", diff --git a/browser/net/brave_network_delegate_base.cc b/browser/net/brave_network_delegate_base.cc index 4ebfc1f86104..99093ad0b39d 100644 --- a/browser/net/brave_network_delegate_base.cc +++ b/browser/net/brave_network_delegate_base.cc @@ -53,6 +53,36 @@ int BraveNetworkDelegateBase::OnBeforeStartTransaction(net::URLRequest* request, return net::ERR_IO_PENDING; } +int BraveNetworkDelegateBase::OnHeadersReceived(net::URLRequest* request, + net::CompletionOnceCallback callback, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers, + GURL* allowed_unsafe_redirect_url) { + if (headers_received_callbacks_.empty() || !request) { + return ChromeNetworkDelegate::OnHeadersReceived(request, + std::move(callback), original_response_headers, + override_response_headers, allowed_unsafe_redirect_url); + } + + std::shared_ptr ctx( + new brave::BraveRequestInfo()); + callbacks_[request->identifier()] = std::move(callback); + ctx->request_identifier = request->identifier(); + ctx->event_type = brave::kOnHeadersReceived; + ctx->original_response_headers = original_response_headers; + ctx->override_response_headers = override_response_headers; + ctx->allowed_unsafe_redirect_url = allowed_unsafe_redirect_url; + + // Return ERR_IO_PENDING and run callbacks later by posting a task. + // URLRequestHttpJob::awaiting_callback_ will be set to true after we + // return net::ERR_IO_PENDING here, callbacks need to be run later than this + // to set awaiting_callback_ back to false. + BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, + base::Bind(&BraveNetworkDelegateBase::RunNextCallback, + base::Unretained(this), request, nullptr, ctx)); + return net::ERR_IO_PENDING; +} + void BraveNetworkDelegateBase::RunCallbackForRequestIdentifier(uint64_t request_identifier, int rv) { std::map::iterator it = callbacks_.find(request_identifier); @@ -106,6 +136,23 @@ void BraveNetworkDelegateBase::RunNextCallback( break; } } + } else if (ctx->event_type == brave::kOnHeadersReceived) { + while(headers_received_callbacks_.size() != ctx->next_url_request_index) { + brave::OnHeadersReceivedCallback callback = + headers_received_callbacks_[ctx->next_url_request_index++]; + brave::ResponseCallback next_callback = + base::Bind(&BraveNetworkDelegateBase::RunNextCallback, + base::Unretained(this), request, new_url, ctx); + rv = callback.Run(request, ctx->original_response_headers, + ctx->override_response_headers, ctx->allowed_unsafe_redirect_url, + next_callback, ctx); + if (rv == net::ERR_IO_PENDING) { + return; + } + if (rv == net::ERR_ABORTED) { + break; + } + } } std::map::iterator it = @@ -124,6 +171,10 @@ void BraveNetworkDelegateBase::RunNextCallback( } else if (ctx->event_type == brave::kOnBeforeStartTransaction) { rv = ChromeNetworkDelegate::OnBeforeStartTransaction(request, std::move(wrapped_callback), ctx->headers); + } else if (ctx->event_type == brave::kOnHeadersReceived) { + rv = ChromeNetworkDelegate::OnHeadersReceived(request, + std::move(wrapped_callback), ctx->original_response_headers, + ctx->override_response_headers, ctx->allowed_unsafe_redirect_url); } // ChromeNetworkDelegate returns net::ERR_IO_PENDING if an extension is diff --git a/browser/net/brave_network_delegate_base.h b/browser/net/brave_network_delegate_base.h index b2a12ba7782f..a7dd170136fd 100644 --- a/browser/net/brave_network_delegate_base.h +++ b/browser/net/brave_network_delegate_base.h @@ -36,6 +36,13 @@ class BraveNetworkDelegateBase : public ChromeNetworkDelegate { int OnBeforeStartTransaction(net::URLRequest* request, net::CompletionOnceCallback callback, net::HttpRequestHeaders* headers) override; + int OnHeadersReceived( + net::URLRequest* request, + net::CompletionOnceCallback callback, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers, + GURL* allowed_unsafe_redirect_url) override; + void OnURLRequestDestroyed(net::URLRequest* request) override; void RunCallbackForRequestIdentifier(uint64_t request_identifier, int rv); @@ -48,6 +55,8 @@ class BraveNetworkDelegateBase : public ChromeNetworkDelegate { before_url_request_callbacks_; std::vector before_start_transaction_callbacks_; + std::vector + headers_received_callbacks_; private: std::map callbacks_; diff --git a/browser/net/brave_profile_network_delegate.cc b/browser/net/brave_profile_network_delegate.cc index 284c348c4c76..8ba4faba4ba2 100644 --- a/browser/net/brave_profile_network_delegate.cc +++ b/browser/net/brave_profile_network_delegate.cc @@ -8,6 +8,7 @@ #include "brave/browser/net/brave_httpse_network_delegate_helper.h" #include "brave/browser/net/brave_site_hacks_network_delegate_helper.h" #include "brave/components/brave_rewards/browser/buildflags/buildflags.h" +#include "brave/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.h" #if BUILDFLAG(BRAVE_REWARDS_ENABLED) #include "brave/components/brave_rewards/browser/net/network_delegate_helper.h" @@ -37,6 +38,11 @@ BraveProfileNetworkDelegate::BraveProfileNetworkDelegate( brave::OnBeforeStartTransactionCallback start_transactions_callback = base::Bind(brave::OnBeforeStartTransaction_SiteHacksWork); before_start_transaction_callbacks_.push_back(start_transactions_callback); + + brave::OnHeadersReceivedCallback headers_received_callback = + base::Bind( + webtorrent::OnHeadersReceived_TorrentRedirectWork); + headers_received_callbacks_.push_back(headers_received_callback); } BraveProfileNetworkDelegate::~BraveProfileNetworkDelegate() { diff --git a/browser/net/url_context.h b/browser/net/url_context.h index 97e392e2febf..0ab116b4537c 100644 --- a/browser/net/url_context.h +++ b/browser/net/url_context.h @@ -16,6 +16,7 @@ namespace brave { enum BraveNetworkDelegateEventType { kOnBeforeRequest, kOnBeforeStartTransaction, + kOnHeadersReceived, kUnknownEventType }; @@ -27,6 +28,9 @@ struct BraveRequestInfo { uint64_t request_identifier = 0; size_t next_url_request_index = 0; net::HttpRequestHeaders* headers = nullptr; + const net::HttpResponseHeaders* original_response_headers = nullptr; + scoped_refptr* override_response_headers = nullptr; + GURL* allowed_unsafe_redirect_url = nullptr; BraveNetworkDelegateEventType event_type = kUnknownEventType; DISALLOW_COPY_AND_ASSIGN(BraveRequestInfo); }; @@ -44,6 +48,14 @@ using OnBeforeStartTransactionCallback = net::HttpRequestHeaders* headers, const ResponseCallback& next_callback, std::shared_ptr ctx)>; +using OnHeadersReceivedCallback = + base::Callback* override_response_headers, + GURL* allowed_unsafe_redirect_url, + const ResponseCallback& next_callback, + std::shared_ptr ctx)>; + } // namespace brave diff --git a/common/network_constants.cc b/common/network_constants.cc index 0d97b8465d3a..eec85688476e 100644 --- a/common/network_constants.cc +++ b/common/network_constants.cc @@ -20,3 +20,6 @@ const char kCookieHeader[] = "Cookie"; // Intentional misspelling on referrer to match HTTP spec const char kRefererHeader[] = "Referer"; const char kUserAgentHeader[] = "User-Agent"; + +const char kBittorrentMimeType[] = "application/x-bittorrent"; +const char kOctetStreamMimeType[] = "application/octet-stream"; diff --git a/common/network_constants.h b/common/network_constants.h index 193c58fdf050..9a0ee5617a8d 100644 --- a/common/network_constants.h +++ b/common/network_constants.h @@ -19,5 +19,7 @@ extern const char kCookieHeader[]; extern const char kRefererHeader[]; extern const char kUserAgentHeader[]; +extern const char kBittorrentMimeType[]; +extern const char kOctetStreamMimeType[]; #endif // BRAVE_COMMON_NETWORK_CONSTANTS_H_ diff --git a/components/brave_webtorrent/browser/content_browser_client_helper.h b/components/brave_webtorrent/browser/content_browser_client_helper.h index b7de17e33091..a04ee06ee2e0 100644 --- a/components/brave_webtorrent/browser/content_browser_client_helper.h +++ b/components/brave_webtorrent/browser/content_browser_client_helper.h @@ -29,6 +29,44 @@ static GURL TranslateMagnetURL(const GURL& url) { return GURL(translatedSpec); } +static GURL TranslateTorrentUIURLReversed(const GURL& url) { + GURL translatedURL(net::UnescapeURLComponent( + url.query(), net::UnescapeRule::URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS | + net::UnescapeRule::PATH_SEPARATORS)); + GURL::Replacements replacements; + replacements.SetRefStr(url.ref_piece()); + return translatedURL.ReplaceComponents(replacements); +} + +static bool HandleTorrentURLReverseRewrite(GURL* url, + content::BrowserContext* browser_context) { + if (url->SchemeIs(extensions::kExtensionScheme) && + url->host() == brave_webtorrent_extension_id && + url->ExtractFileName() == "brave_webtorrent.html") { + *url = TranslateTorrentUIURLReversed(*url); + return true; + } + + return false; +} + +static bool HandleTorrentURLRewrite(GURL* url, + content::BrowserContext* browser_context) { + // The HTTP/HTTPS URL could be modified later by the network delegate if the + // mime type matches or .torrent is in the path. + // Handle http and https here for making reverse_on_redirect to be true in + // BrowserURLHandlerImpl::RewriteURLIfNecessary to trigger ReverseURLRewrite + // for updating the virtual URL. + if (url->SchemeIsHTTPOrHTTPS() || + (url->SchemeIs(extensions::kExtensionScheme) && + url->host() == brave_webtorrent_extension_id && + url->ExtractFileName() == "brave_webtorrent.html")) { + return true; + } + + return false; +} + static bool IsWebtorrentInstalled(content::BrowserContext* browser_context) { extensions::ExtensionRegistry* registry = extensions::ExtensionRegistry::Get(browser_context); diff --git a/components/brave_webtorrent/browser/net/BUILD.gn b/components/brave_webtorrent/browser/net/BUILD.gn new file mode 100644 index 000000000000..dc327bd5fa03 --- /dev/null +++ b/components/brave_webtorrent/browser/net/BUILD.gn @@ -0,0 +1,12 @@ +source_set("net") { + sources = [ + "brave_torrent_redirect_network_delegate_helper.cc", + "brave_torrent_redirect_network_delegate_helper.h", + ] + + deps = [ + "//base", + "//brave/common", + "//net", + ] +} diff --git a/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.cc b/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.cc new file mode 100644 index 000000000000..194ca9bcb6c7 --- /dev/null +++ b/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.cc @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.h" + +#include "base/strings/strcat.h" +#include "base/strings/string_util.h" +#include "brave/common/network_constants.h" +#include "brave/common/extensions/extension_constants.h" +#include "extensions/common/constants.h" +#include "net/http/http_content_disposition.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request.h" + +namespace { + +bool FileNameMatched(const net::HttpResponseHeaders* headers) { + std::string disposition; + if (!headers->GetNormalizedHeader("Content-Disposition", &disposition)) { + return false; + } + + net::HttpContentDisposition cd_headers(disposition, std::string()); + if (base::EndsWith(cd_headers.filename(), ".torrent", + base::CompareCase::INSENSITIVE_ASCII) || + base::EndsWith(cd_headers.filename(), ".torrent\"", + base::CompareCase::INSENSITIVE_ASCII)) { + return true; + } + + return false; +} + +bool URLMatched(net::URLRequest* request) { + return base::EndsWith(request->url().spec(), ".torrent", + base::CompareCase::INSENSITIVE_ASCII); +} + +bool IsTorrentFile(net::URLRequest* request, + const net::HttpResponseHeaders* headers) { + std::string mimeType; + if (!headers->GetMimeType(&mimeType)) { + return false; + } + + if (mimeType == kBittorrentMimeType) { + return true; + } + + if (mimeType == kOctetStreamMimeType && + (URLMatched(request) || FileNameMatched(headers))) { + return true; + } + + return false; +} + +bool IsWebtorrentInitiated(net::URLRequest* request) { + return request->initiator().has_value() && + request->initiator()->GetURL().spec() == + base::StrCat({extensions::kExtensionScheme, "://", + brave_webtorrent_extension_id, "/"}); +} + +} // namespace + +namespace webtorrent { + +int OnHeadersReceived_TorrentRedirectWork( + net::URLRequest* request, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers, + GURL* allowed_unsafe_redirect_url, + const brave::ResponseCallback& next_callback, + std::shared_ptr ctx) { + + if (!request || !original_response_headers || + IsWebtorrentInitiated(request) || // download .torrent, do not redirect + !IsTorrentFile(request, original_response_headers)) { + return net::OK; + } + + *override_response_headers = + new net::HttpResponseHeaders(original_response_headers->raw_headers()); + (*override_response_headers)->ReplaceStatusLine("HTTP/1.1 307 Temporary Redirect"); + (*override_response_headers)->RemoveHeader("Location"); + GURL url( + base::StrCat({extensions::kExtensionScheme, "://", + brave_webtorrent_extension_id, + "/extension/brave_webtorrent.html?", + request->url().spec()})); + (*override_response_headers)->AddHeader( + "Location: " + url.spec()); + *allowed_unsafe_redirect_url = url; + return net::OK; +} + +} // namespace webtorrent diff --git a/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.h b/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.h new file mode 100644 index 000000000000..255f8c9d3644 --- /dev/null +++ b/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_COMPONENTS_BRAVE_WEBTORRENT_BROWSER_NET_BRAVE_TORRENT_REDIRECT_NETWORK_DELEGATE_HELPER_H_ +#define BRAVE_COMPONENTS_BRAVE_WEBTORRENT_BROWSER_NET_BRAVE_TORRENT_REDIRECT_NETWORK_DELEGATE_HELPER_H_ + +#include "chrome/browser/net/chrome_network_delegate.h" +#include "brave/browser/net/url_context.h" + +namespace brave { +struct BraveRequestInfo; +} + +namespace net { +class URLRequest; +} + +namespace webtorrent { + +int OnHeadersReceived_TorrentRedirectWork( + net::URLRequest* request, + const net::HttpResponseHeaders* original_response_headers, + scoped_refptr* override_response_headers, + GURL* allowed_unsafe_redirect_url, + const brave::ResponseCallback& next_callback, + std::shared_ptr ctx); + +} // namespace webtorrent + +#endif // BRAVE_COMPONENTS_BRAVE_WEBTORRENT_BROWSER_NET_BRAVE_TORRENT_REDIRECT_NETWORK_DELEGATE_HELPER_H_ diff --git a/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper_unittest.cc b/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper_unittest.cc new file mode 100644 index 000000000000..2441ccd22f21 --- /dev/null +++ b/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper_unittest.cc @@ -0,0 +1,286 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "brave/components/brave_webtorrent/browser/net/brave_torrent_redirect_network_delegate_helper.h" + +#include "base/strings/strcat.h" +#include "brave/browser/net/url_context.h" +#include "brave/common/network_constants.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class BraveTorrentRedirectNetworkDelegateHelperTest: public testing::Test { + public: + BraveTorrentRedirectNetworkDelegateHelperTest() + : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP), + context_(new net::TestURLRequestContext(true)) { + } + + ~BraveTorrentRedirectNetworkDelegateHelperTest() override {} + + void SetUp() override { + context_->Init(); + torrent_url_ = GURL("https://webtorrent.io/torrents/sintel.torrent"); + non_torrent_url_ = GURL("https://webtorrent.io/torrents/sintel"); + extension_url_ = GURL("chrome-extension://lgjmpdmojkpocjcopdikifhejkkjglho/extension/brave_webtorrent.html?https://webtorrent.io/torrents/sintel.torrent"); + non_torrent_extension_url_ = GURL("chrome-extension://lgjmpdmojkpocjcopdikifhejkkjglho/extension/brave_webtorrent.html?https://webtorrent.io/torrents/sintel"); + } + + net::TestURLRequestContext* context() { return context_.get(); } + + const GURL& torrent_url() { + return torrent_url_; + } + + const GURL& non_torrent_url() { + return non_torrent_url_; + } + + const GURL& extension_url() { + return extension_url_; + } + + const GURL& non_torrent_extension_url() { + return non_torrent_extension_url_; + } + + private: + GURL torrent_url_; + GURL non_torrent_url_; + GURL extension_url_; + GURL non_torrent_extension_url_; + content::TestBrowserThreadBundle thread_bundle_; + std::unique_ptr context_; +}; + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, NoRedirectWithoutMimeType) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.0 200 OK"); + std::string location; + EXPECT_FALSE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(allowed_unsafe_redirect_url, GURL::EmptyGURL()); + EXPECT_EQ(ret, net::OK); +} + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, BittorrentMimeTypeRedirect) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + orig_response_headers->AddHeader( + base::StrCat({"Content-Type: ", kBittorrentMimeType})); + std::string mimeType; + ASSERT_TRUE(orig_response_headers->GetMimeType(&mimeType)); + ASSERT_EQ(mimeType, kBittorrentMimeType); + + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.1 307 Temporary Redirect"); + std::string location; + EXPECT_TRUE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(location, extension_url().spec()); + EXPECT_EQ(allowed_unsafe_redirect_url.spec(), extension_url().spec()); + EXPECT_EQ(ret, net::OK); +} + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, OctetStreamMimeTypeRedirectWithTorrentURL) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + orig_response_headers->AddHeader( + base::StrCat({"Content-Type: ", kOctetStreamMimeType})); + std::string mimeType; + ASSERT_TRUE(orig_response_headers->GetMimeType(&mimeType)); + ASSERT_EQ(mimeType, kOctetStreamMimeType); + + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.1 307 Temporary Redirect"); + std::string location; + EXPECT_TRUE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(location, extension_url().spec()); + EXPECT_EQ(allowed_unsafe_redirect_url.spec(), extension_url().spec()); + EXPECT_EQ(ret, net::OK); +} + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, OctetStreamMimeTypeRedirectWithTorrentFileName) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(non_torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + orig_response_headers->AddHeader( + base::StrCat({"Content-Type: ", kOctetStreamMimeType})); + std::string mimeType; + ASSERT_TRUE(orig_response_headers->GetMimeType(&mimeType)); + ASSERT_EQ(mimeType, kOctetStreamMimeType); + orig_response_headers->AddHeader("Content-Disposition: filename=\"sintel.torrent\""); + std::string disposition; + ASSERT_TRUE(orig_response_headers->GetNormalizedHeader( + "Content-Disposition", &disposition)); + + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.1 307 Temporary Redirect"); + std::string location; + EXPECT_TRUE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(location, non_torrent_extension_url().spec()); + EXPECT_EQ(allowed_unsafe_redirect_url.spec(), non_torrent_extension_url().spec()); + EXPECT_EQ(ret, net::OK); +} + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, OctetStreamMimeTypeNoRedirect) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(non_torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + orig_response_headers->AddHeader( + base::StrCat({"Content-Type: ", kOctetStreamMimeType})); + std::string mimeType; + ASSERT_TRUE(orig_response_headers->GetMimeType(&mimeType)); + ASSERT_EQ(mimeType, kOctetStreamMimeType); + + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.0 200 OK"); + std::string location; + EXPECT_FALSE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(allowed_unsafe_redirect_url, GURL::EmptyGURL()); + EXPECT_EQ(ret, net::OK); +} + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, MimeTypeNoRedirect) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + orig_response_headers->AddHeader("Content-Type: text/html"); + std::string mimeType; + ASSERT_TRUE(orig_response_headers->GetMimeType(&mimeType)); + ASSERT_EQ(mimeType, "text/html"); + + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.0 200 OK"); + std::string location; + EXPECT_FALSE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(allowed_unsafe_redirect_url, GURL::EmptyGURL()); + EXPECT_EQ(ret, net::OK); +} + +TEST_F(BraveTorrentRedirectNetworkDelegateHelperTest, WebtorrentInitiatedNoRedirect) { + net::TestDelegate test_delegate; + std::unique_ptr request = + context()->CreateRequest(torrent_url(), net::IDLE, &test_delegate, + TRAFFIC_ANNOTATION_FOR_TESTS); + + request->set_initiator(url::Origin::Create(extension_url().GetOrigin())); + scoped_refptr orig_response_headers = + new net::HttpResponseHeaders(std::string()); + orig_response_headers->AddHeader( + base::StrCat({"Content-Type: ", kBittorrentMimeType})); + std::string mimeType; + ASSERT_TRUE(orig_response_headers->GetMimeType(&mimeType)); + ASSERT_EQ(mimeType, kBittorrentMimeType); + + scoped_refptr overwrite_response_headers = + new net::HttpResponseHeaders(std::string()); + GURL allowed_unsafe_redirect_url = GURL::EmptyGURL(); + std::shared_ptr + brave_request_info(new brave::BraveRequestInfo()); + brave::ResponseCallback callback; + + int ret = webtorrent::OnHeadersReceived_TorrentRedirectWork(request.get(), + orig_response_headers.get(), &overwrite_response_headers, + &allowed_unsafe_redirect_url, callback, brave_request_info); + + EXPECT_EQ(overwrite_response_headers->GetStatusLine(), "HTTP/1.0 200 OK"); + std::string location; + EXPECT_FALSE(overwrite_response_headers->EnumerateHeader(nullptr, "Location", &location)); + EXPECT_EQ(allowed_unsafe_redirect_url, GURL::EmptyGURL()); + EXPECT_EQ(ret, net::OK); +} + +} // namespace diff --git a/components/brave_webtorrent/extension/BUILD.gn b/components/brave_webtorrent/extension/BUILD.gn index e43f4d4ccb9a..76ed90d3cd54 100644 --- a/components/brave_webtorrent/extension/BUILD.gn +++ b/components/brave_webtorrent/extension/BUILD.gn @@ -14,6 +14,7 @@ transpile_web_ui("brave_webtorrent") { "background/actions/webtorrentActions.ts", "background/actions/windowActions.ts", "background/api/tabs_api.ts", + "background/api/torrent_api.ts", "background/events/tabsEvents.ts", "background/events/torrentEvents.ts", "background/events/webtorrentEvents.ts", diff --git a/components/brave_webtorrent/extension/actions/webtorrent_actions.ts b/components/brave_webtorrent/extension/actions/webtorrent_actions.ts index d2f558515c45..86ec9c679f94 100644 --- a/components/brave_webtorrent/extension/actions/webtorrent_actions.ts +++ b/components/brave_webtorrent/extension/actions/webtorrent_actions.ts @@ -4,6 +4,7 @@ import { action } from 'typesafe-actions' import { Torrent } from 'webtorrent' +import { Instance } from 'parse-torrent' // Constants import { types } from '../constants/webtorrent_types' @@ -14,3 +15,4 @@ export const serverUpdated = (torrent: Torrent, serverURL: string) => action(types.WEBTORRENT_SERVER_UPDATED, { torrent, serverURL }) export const startTorrent = (torrentId: string, tabId: number) => action(types.WEBTORRENT_START_TORRENT, { torrentId, tabId }) export const stopDownload = (tabId: number) => action(types.WEBTORRENT_STOP_DOWNLOAD, { tabId }) +export const torrentParsed = (torrentId: string, tabId: number, infoHash: string | undefined, errorMsg: string | undefined, parsedTorrent?: Instance) => action(types.WEBTORRENT_TORRENT_PARSED, { torrentId, tabId, infoHash, errorMsg, parsedTorrent }) diff --git a/components/brave_webtorrent/extension/background/api/torrent_api.ts b/components/brave_webtorrent/extension/background/api/torrent_api.ts new file mode 100644 index 000000000000..f226e81715e1 --- /dev/null +++ b/components/brave_webtorrent/extension/background/api/torrent_api.ts @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as ParseTorrent from 'parse-torrent' + +export const parseTorrentRemote = (torrentId: string, tabId: number) => { + ParseTorrent.remote(torrentId, (err: Error, parsedTorrent?: ParseTorrent.Instance) => { + const webtorrentActions = require('../actions/webtorrentActions').default + const errMsg = err ? err.message : undefined + const infoHash = parsedTorrent ? parsedTorrent.infoHash : undefined + webtorrentActions.torrentParsed(torrentId, tabId, infoHash, errMsg, parsedTorrent) + }) +} diff --git a/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts b/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts index 8390732d2736..3f6c695e0e73 100644 --- a/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts +++ b/components/brave_webtorrent/extension/background/reducers/webtorrent_reducer.ts @@ -4,6 +4,7 @@ import * as ParseTorrent from 'parse-torrent' import { Torrent } from 'webtorrent' +import { parse } from 'querystring' // Constants import * as tabTypes from '../../constants/tab_types' @@ -14,6 +15,7 @@ import { File, TorrentState, TorrentsState } from '../../constants/webtorrentSta // Utils import { addTorrent, delTorrent, findTorrent } from '../webtorrent' import { getTabData } from '../api/tabs_api' +import { parseTorrentRemote } from '../api/torrent_api' const focusedWindowChanged = (windowId: number, state: TorrentsState) => { return { ...state, currentWindowId: windowId } @@ -55,16 +57,13 @@ const tabUpdated = (tabId: number, url: string, state: TorrentsState) => { const { torrentStateMap, torrentObjMap } = state const origTorrentState: TorrentState = torrentStateMap[tabId] const origInfoHash = origTorrentState ? origTorrentState.infoHash : undefined - let newTorrentState - let newInfoHash - - // delete old torrent state - delete torrentStateMap[tabId] // delete old torrent state + let newTorrentState: TorrentState | undefined + let newInfoHash: string | undefined // create new torrent state const parsedURL = new window.URL(url) + const torrentId = parsedURL.href if (parsedURL.protocol === 'magnet:') { // parse torrent - const torrentId = parsedURL.href try { const { name, infoHash, ix } = ParseTorrent(torrentId) newInfoHash = infoHash @@ -72,8 +71,28 @@ const tabUpdated = (tabId: number, url: string, state: TorrentsState) => { } catch (error) { newTorrentState = { tabId, torrentId, errorMsg: error.message } } + } else if (parsedURL.protocol === 'https:' || parsedURL.protocol === 'http:') { + const name = parsedURL.pathname.substr(parsedURL.pathname.lastIndexOf('/') + 1) + // for .torrent case, ix (index of file) for selecting a specific file in + // the file list is given in url like #ix=5 + let ix: number | undefined = Number(parse(parsedURL.hash.slice(1)).ix) + ix = Number.isNaN(ix) ? undefined : ix + + // Use an existing infoHash if it's the same torrentId + const torrentUrl = parsedURL.origin + parsedURL.pathname + const key = Object.keys(torrentStateMap).find( + key => torrentStateMap[key].infoHash && + torrentStateMap[key].torrentId === torrentUrl) + newInfoHash = key + ? torrentStateMap[key].infoHash + : undefined + + newTorrentState = { tabId, torrentId, name, ix, infoHash: newInfoHash } } + // delete old torrent state + delete torrentStateMap[tabId] + // unsubscribe old torrent if not the same const isSameTorrent = newInfoHash && origInfoHash === newInfoHash if (origInfoHash && torrentObjMap[origInfoHash] && !isSameTorrent) { @@ -117,9 +136,17 @@ const startTorrent = (torrentId: string, tabId: number, state: TorrentsState) => const { torrentStateMap, torrentObjMap } = state const torrentState = torrentStateMap[tabId] + // infoHash might not be available yet for .torrent case + if (torrentState && !torrentState.errorMsg && !torrentState.infoHash) { + parseTorrentRemote(torrentId, tabId) + } + if (torrentState && torrentState.infoHash && !findTorrent(torrentState.infoHash)) { addTorrent(torrentId) // objectMap will be updated when info event is emitted + } else if (torrentState && torrentState.infoHash && + torrentObjMap[torrentState.infoHash]) { + torrentObjMap[torrentState.infoHash].tabClients.add(tabId) } return { ...state, torrentObjMap } @@ -184,6 +211,19 @@ const updateServer = (state: TorrentsState, torrent: Torrent, serverURL: string) return { ...state, torrentObjMap } } +const torrentParsed = (torrentId: string, tabId: number, infoHash: string | undefined, errorMsg: string | undefined, parsedTorrent: ParseTorrent.Instance | undefined, state: TorrentsState) => { + const { torrentObjMap, torrentStateMap } = state + torrentStateMap[tabId] = { ...torrentStateMap[tabId], infoHash, errorMsg } + + if (infoHash && !findTorrent(infoHash) && parsedTorrent) { + addTorrent(parsedTorrent) // objectMap will be updated when info event is emitted + } else if (infoHash && torrentObjMap[infoHash]) { + torrentObjMap[infoHash].tabClients.add(tabId) + } + + return { ...state, torrentObjMap, torrentStateMap } +} + const defaultState: TorrentsState = { currentWindowId: -1, activeTabIds: {}, torrentStateMap: {}, torrentObjMap: {} } export const webtorrentReducer = (state: TorrentsState = defaultState, action: any) => { // TODO: modify any to be actual action type const payload = action.payload @@ -245,6 +285,10 @@ export const webtorrentReducer = (state: TorrentsState = defaultState, action: a case torrentTypes.types.WEBTORRENT_STOP_DOWNLOAD: state = stopDownload(payload.tabId, state) break + case torrentTypes.types.WEBTORRENT_TORRENT_PARSED: + state = torrentParsed(payload.torrentId, payload.tabId, payload.infoHash, + payload.errorMsg, payload.parsedTorrent, state) + break } return state diff --git a/components/brave_webtorrent/extension/background/webtorrent.ts b/components/brave_webtorrent/extension/background/webtorrent.ts index 855d9c552f39..739896ce2aeb 100644 --- a/components/brave_webtorrent/extension/background/webtorrent.ts +++ b/components/brave_webtorrent/extension/background/webtorrent.ts @@ -6,6 +6,7 @@ import * as WebTorrent from 'webtorrent' import { addTorrentEvents } from './events/torrentEvents' import { addWebtorrentEvents } from './events/webtorrentEvents' import { AddressInfo } from 'net' +import { Instance } from 'parse-torrent' let webTorrent: WebTorrent.Instance let servers: { [key: string]: any } = { } @@ -38,7 +39,7 @@ export const createServer = (torrent: WebTorrent.Torrent, cb: (serverURL: string } } -export const addTorrent = (torrentId: string) => { +export const addTorrent = (torrentId: string | Instance) => { const torrentObj = webTorrent.add(torrentId) addTorrentEvents(torrentObj) } diff --git a/components/brave_webtorrent/extension/components/app.tsx b/components/brave_webtorrent/extension/components/app.tsx index 35855c26a0af..e0525f00aa5c 100644 --- a/components/brave_webtorrent/extension/components/app.tsx +++ b/components/brave_webtorrent/extension/components/app.tsx @@ -30,7 +30,10 @@ interface Props { export class BraveWebtorrentPage extends React.Component { render () { const { actions, torrentState, torrentObj } = this.props - const torrentId = decodeURIComponent(window.location.search.substring(1)) + let torrentId = decodeURIComponent(window.location.search.substring(1)) + torrentId = window.location.hash + ? torrentId + window.location.hash + : torrentId // The active tab change might not be propagated here yet, so we might get // the old active tabId here which might be a different torrent page or a diff --git a/components/brave_webtorrent/extension/components/torrentFileList.tsx b/components/brave_webtorrent/extension/components/torrentFileList.tsx index 643bf6715acf..6316d06aa2cd 100644 --- a/components/brave_webtorrent/extension/components/torrentFileList.tsx +++ b/components/brave_webtorrent/extension/components/torrentFileList.tsx @@ -52,7 +52,12 @@ export default class TorrentFileList extends React.PureComponent { return (
) // No download links until the server is ready } } else { - const href = torrentId + '&ix=' + ix + // use # for .torrent links, since query params might cause the remote + // server to return 404 + const suffix = /^https?:/.test(torrentId) + ? '#ix=' + ix + : '&ix=' + ix + const href = torrentId + suffix return ( {file.name} ) } } diff --git a/components/brave_webtorrent/extension/components/torrentViewerHeader.tsx b/components/brave_webtorrent/extension/components/torrentViewerHeader.tsx index 3815575e3c8b..eb020bb557b1 100644 --- a/components/brave_webtorrent/extension/components/torrentViewerHeader.tsx +++ b/components/brave_webtorrent/extension/components/torrentViewerHeader.tsx @@ -31,7 +31,14 @@ export default class TorrentViewerHeader extends React.PureComponent } onCopyClick = () => { - clipboardCopy(this.props.torrentId) + if (this.props.torrentId.startsWith('magnet:')) { + clipboardCopy(this.props.torrentId) + } else { + let a = document.createElement('a') + a.download = '' + a.href = this.props.torrentId + a.click() + } } render () { @@ -45,6 +52,9 @@ export default class TorrentViewerHeader extends React.PureComponent ? `Start Torrenting ${name}?` : 'Loading torrent information...' const mainButtonText = torrent ? 'Stop Download' : 'Start Torrent' + const copyButtonText = this.props.torrentId.startsWith('magnet:') + ? 'Copy Magnet Link' + : 'Save Torrent File' return ( @@ -62,7 +72,7 @@ export default class TorrentViewerHeader extends React.PureComponent