| // Copyright 2019 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 <string> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/files/file_util.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/values_test_util.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/values.h" |
| #include "chrome/browser/devtools/protocol/devtools_protocol_test_support.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/unpacked_installer.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/ssl_status.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/test_extension_registry_observer.h" |
| #include "extensions/common/manifest_handlers/background_info.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/ssl/ssl_cipher_suite_names.h" |
| #include "net/ssl/ssl_connection_status_flags.h" |
| #include "printing/buildflags/buildflags.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/boringssl/src/include/openssl/ssl.h" |
| |
| using DevToolsProtocolTest = DevToolsProtocolTestBase; |
| using testing::Eq; |
| |
| namespace { |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, |
| VisibleSecurityStateChangedNeutralState) { |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), GURL("about:blank"))); |
| EXPECT_TRUE(content::WaitForLoadStop(web_contents())); |
| |
| Attach(); |
| SendCommand("Security.enable"); |
| base::Value params = |
| WaitForNotification("Security.visibleSecurityStateChanged"); |
| |
| std::string* security_state = |
| params.FindStringPath("visibleSecurityState.securityState"); |
| ASSERT_TRUE(security_state); |
| ASSERT_EQ(std::string("neutral"), *security_state); |
| ASSERT_FALSE( |
| params.FindPath("visibleSecurityState.certificateSecurityState")); |
| ASSERT_FALSE(params.FindPath("visibleSecurityState.safetyTipInfo")); |
| const base::Value* security_state_issue_ids = |
| params.FindListPath("visibleSecurityState.securityStateIssueIds"); |
| ASSERT_TRUE(std::find(security_state_issue_ids->GetList().begin(), |
| security_state_issue_ids->GetList().end(), |
| base::Value("scheme-is-not-cryptographic")) != |
| security_state_issue_ids->GetList().end()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, CreateDeleteContext) { |
| AttachToBrowser(); |
| for (int i = 0; i < 2; i++) { |
| SendCommandSync("Target.createBrowserContext"); |
| std::string* context_id_value = result_.FindStringPath("browserContextId"); |
| ASSERT_TRUE(context_id_value); |
| std::string context_id = *context_id_value; |
| |
| base::DictionaryValue params; |
| params.SetStringPath("url", "about:blank"); |
| params.SetStringPath("browserContextId", context_id); |
| SendCommandSync("Target.createTarget", std::move(params)); |
| |
| params = base::DictionaryValue(); |
| params.SetStringPath("browserContextId", context_id); |
| SendCommandSync("Target.disposeBrowserContext", std::move(params)); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, |
| NewTabPageInCreatedContextDoesNotCrash) { |
| AttachToBrowser(); |
| SendCommandSync("Target.createBrowserContext"); |
| std::string* context_id_value = result_.FindStringPath("browserContextId"); |
| ASSERT_TRUE(context_id_value); |
| std::string context_id = *context_id_value; |
| |
| base::DictionaryValue params; |
| params.SetStringPath("url", chrome::kChromeUINewTabURL); |
| params.SetStringPath("browserContextId", context_id); |
| content::WebContentsAddedObserver observer; |
| SendCommandSync("Target.createTarget", std::move(params)); |
| content::WebContents* wc = observer.GetWebContents(); |
| ASSERT_TRUE(content::WaitForLoadStop(wc)); |
| EXPECT_EQ(chrome::kChromeUINewTabURL, wc->GetLastCommittedURL().spec()); |
| |
| // Should not crash by this point. |
| } |
| |
| class DevToolsProtocolTest_AppId : public DevToolsProtocolTest { |
| public: |
| DevToolsProtocolTest_AppId() { |
| scoped_feature_list_.InitAndEnableFeature( |
| blink::features::kWebAppEnableManifestId); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, ReturnsManifestAppId) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL url(embedded_test_server()->GetURL( |
| "/banners/manifest_test_page.html?manifest=manifest_with_id.json")); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| Attach(); |
| |
| SendCommandSync("Page.getAppId"); |
| EXPECT_EQ(*result_.FindStringPath("appId"), |
| embedded_test_server()->GetURL("/some_id")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, |
| ReturnsStartUrlAsManifestAppIdIfNotSet) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL url( |
| embedded_test_server()->GetURL("/web_apps/no_service_worker.html")); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| Attach(); |
| |
| SendCommandSync("Page.getAppId"); |
| EXPECT_EQ(*result_.FindStringPath("appId"), |
| embedded_test_server()->GetURL("/web_apps/no_service_worker.html")); |
| EXPECT_EQ(*result_.FindStringPath("recommendedId"), |
| "/web_apps/no_service_worker.html"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest_AppId, ReturnsNoAppIdIfNoManifest) { |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| const GURL url(embedded_test_server()->GetURL("/empty.html")); |
| ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url)); |
| Attach(); |
| |
| SendCommandSync("Page.getAppId"); |
| ASSERT_TRUE(result_.FindPath("appId") == nullptr); |
| ASSERT_TRUE(result_.FindPath("recommendedId") == nullptr); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(DevToolsProtocolTest, VisibleSecurityStateSecureState) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.ServeFilesFromSourceDirectory(GetChromeTestDataDir()); |
| ASSERT_TRUE(https_server.Start()); |
| |
| ui_test_utils::NavigateToURLBlockUntilNavigationsComplete( |
| browser(), https_server.GetURL("/title1.html"), 1); |
| content::NavigationEntry* entry = |
| web_contents()->GetController().GetLastCommittedEntry(); |
| ASSERT_TRUE(entry); |
| |
| // Extract SSL status data from the navigation entry. |
| scoped_refptr<net::X509Certificate> page_cert = entry->GetSSL().certificate; |
| ASSERT_TRUE(page_cert); |
| |
| int ssl_version = |
| net::SSLConnectionStatusToVersion(entry->GetSSL().connection_status); |
| const char* page_protocol; |
| net::SSLVersionToString(&page_protocol, ssl_version); |
| |
| const char* page_key_exchange_str; |
| const char* page_cipher; |
| const char* page_mac; |
| bool is_aead; |
| bool is_tls13; |
| uint16_t page_cipher_suite = |
| net::SSLConnectionStatusToCipherSuite(entry->GetSSL().connection_status); |
| net::SSLCipherSuiteToStrings(&page_key_exchange_str, &page_cipher, &page_mac, |
| &is_aead, &is_tls13, page_cipher_suite); |
| std::string page_key_exchange; |
| if (page_key_exchange_str) |
| page_key_exchange = page_key_exchange_str; |
| |
| const char* page_key_exchange_group = |
| SSL_get_curve_name(entry->GetSSL().key_exchange_group); |
| |
| std::string page_subject_name; |
| std::string page_issuer_name; |
| double page_valid_from = 0.0; |
| double page_valid_to = 0.0; |
| if (entry->GetSSL().certificate) { |
| page_subject_name = entry->GetSSL().certificate->subject().common_name; |
| page_issuer_name = entry->GetSSL().certificate->issuer().common_name; |
| page_valid_from = entry->GetSSL().certificate->valid_start().ToDoubleT(); |
| page_valid_to = entry->GetSSL().certificate->valid_expiry().ToDoubleT(); |
| } |
| |
| std::string page_certificate_network_error; |
| if (net::IsCertStatusError(entry->GetSSL().cert_status)) { |
| page_certificate_network_error = net::ErrorToString( |
| net::MapCertStatusToNetError(entry->GetSSL().cert_status)); |
| } |
| |
| bool page_certificate_has_weak_signature = |
| (entry->GetSSL().cert_status & net::CERT_STATUS_WEAK_SIGNATURE_ALGORITHM); |
| |
| bool page_certificate_has_sha1_signature_present = |
| (entry->GetSSL().cert_status & net::CERT_STATUS_SHA1_SIGNATURE_PRESENT); |
| |
| int status = net::ObsoleteSSLStatus(entry->GetSSL().connection_status, |
| entry->GetSSL().peer_signature_algorithm); |
| bool page_modern_ssl = status == net::OBSOLETE_SSL_NONE; |
| bool page_obsolete_ssl_protocol = status & net::OBSOLETE_SSL_MASK_PROTOCOL; |
| bool page_obsolete_ssl_key_exchange = |
| status & net::OBSOLETE_SSL_MASK_KEY_EXCHANGE; |
| bool page_obsolete_ssl_cipher = status & net::OBSOLETE_SSL_MASK_CIPHER; |
| bool page_obsolete_ssl_signature = status & net::OBSOLETE_SSL_MASK_SIGNATURE; |
| |
| Attach(); |
| SendCommand("Security.enable"); |
| auto has_certificate = [](const base::Value& params) { |
| return params.FindListPath( |
| "visibleSecurityState.certificateSecurityState.certificate") != |
| nullptr; |
| }; |
| base::Value params = |
| WaitForMatchingNotification("Security.visibleSecurityStateChanged", |
| base::BindRepeating(has_certificate)); |
| |
| // Verify that the visibleSecurityState payload matches the SSL status data. |
| std::string* security_state = |
| params.FindStringPath("visibleSecurityState.securityState"); |
| ASSERT_TRUE(security_state); |
| ASSERT_EQ(std::string("secure"), *security_state); |
| |
| base::Value* certificate_security_state = |
| params.FindPath("visibleSecurityState.certificateSecurityState"); |
| ASSERT_TRUE(certificate_security_state); |
| |
| std::string* protocol = |
| certificate_security_state->FindStringPath("protocol"); |
| ASSERT_TRUE(protocol); |
| ASSERT_EQ(*protocol, page_protocol); |
| |
| std::string* key_exchange = |
| certificate_security_state->FindStringPath("keyExchange"); |
| ASSERT_TRUE(key_exchange); |
| ASSERT_EQ(*key_exchange, page_key_exchange); |
| |
| std::string* key_exchange_group = |
| certificate_security_state->FindStringPath("keyExchangeGroup"); |
| if (key_exchange_group) { |
| ASSERT_EQ(*key_exchange_group, page_key_exchange_group); |
| } |
| |
| std::string* mac = certificate_security_state->FindStringPath("mac"); |
| if (mac) { |
| ASSERT_EQ(*mac, page_mac); |
| } |
| |
| std::string* cipher = certificate_security_state->FindStringPath("cipher"); |
| ASSERT_TRUE(cipher); |
| ASSERT_EQ(*cipher, page_cipher); |
| |
| std::string* subject_name = |
| certificate_security_state->FindStringPath("subjectName"); |
| ASSERT_TRUE(subject_name); |
| ASSERT_EQ(*subject_name, page_subject_name); |
| |
| std::string* issuer = certificate_security_state->FindStringPath("issuer"); |
| ASSERT_TRUE(issuer); |
| ASSERT_EQ(*issuer, page_issuer_name); |
| |
| auto valid_from = certificate_security_state->FindDoublePath("validFrom"); |
| ASSERT_TRUE(valid_from); |
| ASSERT_EQ(*valid_from, page_valid_from); |
| |
| auto valid_to = certificate_security_state->FindDoublePath("validTo"); |
| ASSERT_TRUE(valid_to); |
| ASSERT_EQ(*valid_to, page_valid_to); |
| |
| std::string* certificate_network_error = |
| certificate_security_state->FindStringPath("certificateNetworkError"); |
| if (certificate_network_error) { |
| ASSERT_EQ(*certificate_network_error, page_certificate_network_error); |
| } |
| |
| auto certificate_has_weak_signature = |
| certificate_security_state->FindBoolPath("certificateHasWeakSignature"); |
| ASSERT_TRUE(certificate_has_weak_signature); |
| ASSERT_EQ(*certificate_has_weak_signature, |
| page_certificate_has_weak_signature); |
| |
| auto certificate_has_sha1_signature_present = |
| certificate_security_state->FindBoolPath("certificateHasSha1Signature"); |
| ASSERT_TRUE(certificate_has_sha1_signature_present); |
| ASSERT_EQ(*certificate_has_sha1_signature_present, |
| page_certificate_has_sha1_signature_present); |
| |
| auto modern_ssl = certificate_security_state->FindBoolPath("modernSSL"); |
| ASSERT_TRUE(modern_ssl); |
| ASSERT_EQ(*modern_ssl, page_modern_ssl); |
| |
| auto obsolete_ssl_protocol = |
| certificate_security_state->FindBoolPath("obsoleteSslProtocol"); |
| ASSERT_TRUE(obsolete_ssl_protocol); |
| ASSERT_EQ(*obsolete_ssl_protocol, page_obsolete_ssl_protocol); |
| |
| auto obsolete_ssl_key_exchange = |
| certificate_security_state->FindBoolPath("obsoleteSslKeyExchange"); |
| ASSERT_TRUE(obsolete_ssl_key_exchange); |
| ASSERT_EQ(*obsolete_ssl_key_exchange, page_obsolete_ssl_key_exchange); |
| |
| auto obsolete_ssl_cipher = |
| certificate_security_state->FindBoolPath("obsoleteSslCipher"); |
| ASSERT_TRUE(obsolete_ssl_cipher); |
| ASSERT_EQ(*obsolete_ssl_cipher, page_obsolete_ssl_cipher); |
| |
| auto obsolete_ssl_signature = |
| certificate_security_state->FindBoolPath("obsoleteSslSignature"); |
| ASSERT_TRUE(obsolete_ssl_signature); |
| ASSERT_EQ(*obsolete_ssl_signature, page_obsolete_ssl_signature); |
| |
| const base::Value* certificate_value = |
| certificate_security_state->FindListPath("certificate"); |
| std::vector<std::string> der_certs; |
| for (const auto& cert : certificate_value->GetList()) { |
| std::string decoded; |
| ASSERT_TRUE(base::Base64Decode(cert.GetString(), &decoded)); |
| der_certs.push_back(decoded); |
| } |
| std::vector<base::StringPiece> cert_string_piece; |
| for (const auto& str : der_certs) { |
| cert_string_piece.push_back(str); |
| } |
| |
| // Check that the certificateSecurityState.certificate matches. |
| net::SHA256HashValue page_cert_chain_fingerprint = |
| page_cert->CalculateChainFingerprint256(); |
| scoped_refptr<net::X509Certificate> certificate = |
| net::X509Certificate::CreateFromDERCertChain(cert_string_piece); |
| ASSERT_TRUE(certificate); |
| EXPECT_EQ(page_cert_chain_fingerprint, |
| certificate->CalculateChainFingerprint256()); |
| const base::Value* security_state_issue_ids = |
| params.FindListPath("visibleSecurityState.securityStateIssueIds"); |
| EXPECT_EQ(security_state_issue_ids->GetList().size(), 0u); |
| |
| ASSERT_FALSE(params.FindPath("visibleSecurityState.safetyTipInfo")); |
| } |
| |
| class ExtensionProtocolTest : public DevToolsProtocolTest { |
| protected: |
| void SetUpOnMainThread() override { |
| DevToolsProtocolTest::SetUpOnMainThread(); |
| Profile* profile = browser()->profile(); |
| extension_service_ = |
| extensions::ExtensionSystem::Get(profile)->extension_service(); |
| extension_registry_ = extensions::ExtensionRegistry::Get(profile); |
| } |
| |
| content::WebContents* web_contents() override { |
| return background_web_contents_; |
| } |
| |
| const extensions::Extension* LoadExtension(base::FilePath extension_path) { |
| extensions::TestExtensionRegistryObserver observer(extension_registry_); |
| ExtensionTestMessageListener activated_listener("WORKER_ACTIVATED", false); |
| extensions::UnpackedInstaller::Create(extension_service_) |
| ->Load(extension_path); |
| observer.WaitForExtensionLoaded(); |
| const extensions::Extension* extension = nullptr; |
| for (const auto& enabled_extension : |
| extension_registry_->enabled_extensions()) { |
| if (enabled_extension->path() == extension_path) { |
| extension = enabled_extension.get(); |
| break; |
| } |
| } |
| CHECK(extension) << "Failed to find loaded extension " << extension_path; |
| auto* process_manager = |
| extensions::ProcessManager::Get(browser()->profile()); |
| if (extensions::BackgroundInfo::IsServiceWorkerBased(extension)) { |
| EXPECT_TRUE(activated_listener.WaitUntilSatisfied()); |
| auto worker_ids = |
| process_manager->GetServiceWorkersForExtension(extension->id()); |
| CHECK_EQ(1lu, worker_ids.size()); |
| } else { |
| extensions::ExtensionHost* host = |
| process_manager->GetBackgroundHostForExtension(extension->id()); |
| background_web_contents_ = host->host_contents(); |
| } |
| |
| return extension; |
| } |
| |
| void ReloadExtension(const std::string extension_id) { |
| extensions::TestExtensionRegistryObserver observer(extension_registry_); |
| extension_service_->ReloadExtension(extension_id); |
| observer.WaitForExtensionLoaded(); |
| } |
| |
| private: |
| extensions::ExtensionService* extension_service_; |
| extensions::ExtensionRegistry* extension_registry_; |
| content::WebContents* background_web_contents_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionProtocolTest, ReloadTracedExtension) { |
| base::FilePath extension_path = |
| base::PathService::CheckedGet(chrome::DIR_TEST_DATA) |
| .AppendASCII("devtools") |
| .AppendASCII("extensions") |
| .AppendASCII("simple_background_page"); |
| auto* extension = LoadExtension(extension_path); |
| ASSERT_TRUE(extension); |
| Attach(); |
| ReloadExtension(extension->id()); |
| base::DictionaryValue params; |
| params.SetStringPath("categories", "-*"); |
| SendCommandSync("Tracing.start", std::move(params)); |
| SendCommand("Tracing.end"); |
| base::Value tracing_complete = WaitForNotification("Tracing.tracingComplete"); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionProtocolTest, ReloadServiceWorkerExtension) { |
| base::FilePath extension_path = |
| base::PathService::CheckedGet(chrome::DIR_TEST_DATA) |
| .AppendASCII("devtools") |
| .AppendASCII("extensions") |
| .AppendASCII("service_worker"); |
| std::string extension_id; |
| { |
| // `extension` is stale after reload. |
| auto* extension = LoadExtension(extension_path); |
| ASSERT_THAT(extension, testing::NotNull()); |
| extension_id = extension->id(); |
| } |
| AttachToBrowser(); |
| SendCommandSync("Target.getTargets"); |
| |
| std::string target_id; |
| base::Value ext_target; |
| for (auto& target : result_.FindListKey("targetInfos")->GetList()) { |
| if (*target.FindStringKey("type") == "service_worker") { |
| ext_target = target.Clone(); |
| break; |
| } |
| } |
| { |
| base::Value params(base::Value::Type::DICTIONARY); |
| params.SetStringKey("targetId", *ext_target.FindStringKey("targetId")); |
| params.SetBoolKey("waitForDebuggerOnStart", false); |
| SendCommandSync("Target.autoAttachRelated", std::move(params)); |
| } |
| ReloadExtension(extension_id); |
| auto attached = WaitForNotification("Target.attachedToTarget"); |
| base::Value* targetInfo = attached.FindDictKey("targetInfo"); |
| ASSERT_THAT(targetInfo, testing::NotNull()); |
| EXPECT_THAT(*targetInfo, base::test::DictionaryHasValue( |
| "type", base::Value("service_worker"))); |
| EXPECT_THAT(*targetInfo, base::test::DictionaryHasValue( |
| "url", *ext_target.FindKey("url"))); |
| EXPECT_THAT(attached, base::test::DictionaryHasValue("waitingForDebugger", |
| base::Value(false))); |
| |
| { |
| base::Value params(base::Value::Type::DICTIONARY); |
| params.SetStringKey("targetId", *targetInfo->FindStringKey("targetId")); |
| params.SetBoolKey("waitForDebuggerOnStart", false); |
| SendCommandSync("Target.autoAttachRelated", std::move(params)); |
| } |
| auto detached = WaitForNotification("Target.detachedFromTarget"); |
| EXPECT_THAT(detached, base::test::DictionaryHasValue( |
| "sessionId", |
| base::Value(*attached.FindStringKey("sessionId")))); |
| } |
| |
| } // namespace |