// Copyright (c) 2013 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 "chrome/test/chromedriver/chrome_launcher.h" #include <algorithm> #include <vector> #include "base/base64.h" #include "base/basictypes.h" #include "base/command_line.h" #include "base/file_util.h" #include "base/files/file_path.h" #include "base/format_macros.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/logging.h" #include "base/process/kill.h" #include "base/process/launch.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/platform_thread.h" #include "base/time/time.h" #include "base/values.h" #include "chrome/test/chromedriver/chrome/chrome_android_impl.h" #include "chrome/test/chromedriver/chrome/chrome_desktop_impl.h" #include "chrome/test/chromedriver/chrome/chrome_existing_impl.h" #include "chrome/test/chromedriver/chrome/chrome_finder.h" #include "chrome/test/chromedriver/chrome/device_manager.h" #include "chrome/test/chromedriver/chrome/devtools_http_client.h" #include "chrome/test/chromedriver/chrome/embedded_automation_extension.h" #include "chrome/test/chromedriver/chrome/status.h" #include "chrome/test/chromedriver/chrome/user_data_dir.h" #include "chrome/test/chromedriver/chrome/version.h" #include "chrome/test/chromedriver/chrome/web_view.h" #include "chrome/test/chromedriver/chrome/zip.h" #include "chrome/test/chromedriver/net/port_server.h" #include "chrome/test/chromedriver/net/url_request_context_getter.h" #include "crypto/sha2.h" #if defined(OS_POSIX) #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #endif namespace { const char* kCommonSwitches[] = { "ignore-certificate-errors", "metrics-recording-only"}; #if defined(OS_LINUX) const char* kEnableCrashReport = "enable-crash-reporter-for-testing"; #endif Status UnpackAutomationExtension(const base::FilePath& temp_dir, base::FilePath* automation_extension) { std::string decoded_extension; if (!base::Base64Decode(kAutomationExtension, &decoded_extension)) return Status(kUnknownError, "failed to base64decode automation extension"); base::FilePath extension_zip = temp_dir.AppendASCII("internal.zip"); int size = static_cast<int>(decoded_extension.length()); if (file_util::WriteFile(extension_zip, decoded_extension.c_str(), size) != size) { return Status(kUnknownError, "failed to write automation extension zip"); } base::FilePath extension_dir = temp_dir.AppendASCII("internal"); if (!zip::Unzip(extension_zip, extension_dir)) return Status(kUnknownError, "failed to unzip automation extension"); *automation_extension = extension_dir; return Status(kOk); } Status PrepareCommandLine(int port, const Capabilities& capabilities, CommandLine* prepared_command, base::ScopedTempDir* user_data_dir, base::ScopedTempDir* extension_dir, std::vector<std::string>* extension_bg_pages) { base::FilePath program = capabilities.binary; if (program.empty()) { if (!FindChrome(&program)) return Status(kUnknownError, "cannot find Chrome binary"); } else if (!base::PathExists(program)) { return Status(kUnknownError, base::StringPrintf("no chrome binary at %" PRFilePath, program.value().c_str())); } CommandLine command(program); Switches switches; // TODO(chrisgao): Add "disable-sync" when chrome 30- is not supported. // For chrome 30-, it leads to crash when opening chrome://settings. for (size_t i = 0; i < arraysize(kCommonSwitches); ++i) switches.SetSwitch(kCommonSwitches[i]); switches.SetSwitch("disable-hang-monitor"); switches.SetSwitch("disable-prompt-on-repost"); switches.SetSwitch("full-memory-crash-report"); switches.SetSwitch("no-first-run"); switches.SetSwitch("disable-background-networking"); switches.SetSwitch("disable-web-resources"); switches.SetSwitch("safebrowsing-disable-auto-update"); switches.SetSwitch("safebrowsing-disable-download-protection"); switches.SetSwitch("disable-client-side-phishing-detection"); switches.SetSwitch("disable-component-update"); switches.SetSwitch("disable-default-apps"); switches.SetSwitch("enable-logging"); switches.SetSwitch("logging-level", "1"); switches.SetSwitch("password-store", "basic"); switches.SetSwitch("use-mock-keychain"); switches.SetSwitch("remote-debugging-port", base::IntToString(port)); for (std::set<std::string>::const_iterator iter = capabilities.exclude_switches.begin(); iter != capabilities.exclude_switches.end(); ++iter) { switches.RemoveSwitch(*iter); } switches.SetFromSwitches(capabilities.switches); if (!switches.HasSwitch("user-data-dir")) { command.AppendArg("data:,"); if (!user_data_dir->CreateUniqueTempDir()) return Status(kUnknownError, "cannot create temp dir for user data dir"); switches.SetSwitch("user-data-dir", user_data_dir->path().value()); Status status = internal::PrepareUserDataDir( user_data_dir->path(), capabilities.prefs.get(), capabilities.local_state.get()); if (status.IsError()) return status; } if (!extension_dir->CreateUniqueTempDir()) { return Status(kUnknownError, "cannot create temp dir for unpacking extensions"); } Status status = internal::ProcessExtensions(capabilities.extensions, extension_dir->path(), true, &switches, extension_bg_pages); if (status.IsError()) return status; switches.AppendToCommandLine(&command); *prepared_command = command; return Status(kOk); } Status WaitForDevToolsAndCheckVersion( const NetAddress& address, URLRequestContextGetter* context_getter, const SyncWebSocketFactory& socket_factory, scoped_ptr<DevToolsHttpClient>* user_client) { scoped_ptr<DevToolsHttpClient> client(new DevToolsHttpClient( address, context_getter, socket_factory)); base::TimeTicks deadline = base::TimeTicks::Now() + base::TimeDelta::FromSeconds(20); Status status = client->Init(deadline - base::TimeTicks::Now()); if (status.IsError()) return status; if (client->build_no() < kMinimumSupportedChromeBuildNo) { return Status(kUnknownError, "Chrome version must be >= " + GetMinimumSupportedChromeVersion()); } while (base::TimeTicks::Now() < deadline) { WebViewsInfo views_info; client->GetWebViewsInfo(&views_info); for (size_t i = 0; i < views_info.GetSize(); ++i) { if (views_info.Get(i).type == WebViewInfo::kPage) { *user_client = client.Pass(); return Status(kOk); } } base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(50)); } return Status(kUnknownError, "unable to discover open pages"); } Status LaunchExistingChromeSession( URLRequestContextGetter* context_getter, const SyncWebSocketFactory& socket_factory, const Capabilities& capabilities, ScopedVector<DevToolsEventListener>& devtools_event_listeners, scoped_ptr<Chrome>* chrome) { Status status(kOk); scoped_ptr<DevToolsHttpClient> devtools_client; status = WaitForDevToolsAndCheckVersion( capabilities.debugger_address, context_getter, socket_factory, &devtools_client); if (status.IsError()) { return Status(kUnknownError, "cannot connect to chrome at " + capabilities.debugger_address.ToString(), status); } chrome->reset(new ChromeExistingImpl(devtools_client.Pass(), devtools_event_listeners)); return Status(kOk); } Status LaunchDesktopChrome( URLRequestContextGetter* context_getter, int port, scoped_ptr<PortReservation> port_reservation, const SyncWebSocketFactory& socket_factory, const Capabilities& capabilities, ScopedVector<DevToolsEventListener>& devtools_event_listeners, scoped_ptr<Chrome>* chrome) { CommandLine command(CommandLine::NO_PROGRAM); base::ScopedTempDir user_data_dir; base::ScopedTempDir extension_dir; std::vector<std::string> extension_bg_pages; Status status = PrepareCommandLine(port, capabilities, &command, &user_data_dir, &extension_dir, &extension_bg_pages); if (status.IsError()) return status; base::LaunchOptions options; #if defined(OS_LINUX) // If minidump path is set in the capability, enable minidump for crashes. if (!capabilities.minidump_path.empty()) { VLOG(0) << "Minidump generation specified. Will save dumps to: " << capabilities.minidump_path; options.environ["CHROME_HEADLESS"] = 1; options.environ["BREAKPAD_DUMP_LOCATION"] = capabilities.minidump_path; if (!command.HasSwitch(kEnableCrashReport)) command.AppendSwitch(kEnableCrashReport); } #endif #if !defined(OS_WIN) if (!capabilities.log_path.empty()) options.environ["CHROME_LOG_FILE"] = capabilities.log_path; if (capabilities.detach) options.new_process_group = true; #endif #if defined(OS_POSIX) base::FileHandleMappingVector no_stderr; int devnull = -1; file_util::ScopedFD scoped_devnull(&devnull); if (!CommandLine::ForCurrentProcess()->HasSwitch("verbose")) { // Redirect stderr to /dev/null, so that Chrome log spew doesn't confuse // users. devnull = open("/dev/null", O_WRONLY); if (devnull == -1) return Status(kUnknownError, "couldn't open /dev/null"); no_stderr.push_back(std::make_pair(devnull, STDERR_FILENO)); options.fds_to_remap = &no_stderr; } #endif #if defined(OS_WIN) std::string command_string = base::WideToUTF8(command.GetCommandLineString()); #else std::string command_string = command.GetCommandLineString(); #endif VLOG(0) << "Launching chrome: " << command_string; base::ProcessHandle process; if (!base::LaunchProcess(command, options, &process)) return Status(kUnknownError, "chrome failed to start"); scoped_ptr<DevToolsHttpClient> devtools_client; status = WaitForDevToolsAndCheckVersion( NetAddress(port), context_getter, socket_factory, &devtools_client); if (status.IsError()) { int exit_code; base::TerminationStatus chrome_status = base::GetTerminationStatus(process, &exit_code); if (chrome_status != base::TERMINATION_STATUS_STILL_RUNNING) { std::string termination_reason; switch (chrome_status) { case base::TERMINATION_STATUS_NORMAL_TERMINATION: termination_reason = "exited normally"; break; case base::TERMINATION_STATUS_ABNORMAL_TERMINATION: termination_reason = "exited abnormally"; break; case base::TERMINATION_STATUS_PROCESS_WAS_KILLED: termination_reason = "was killed"; break; case base::TERMINATION_STATUS_PROCESS_CRASHED: termination_reason = "crashed"; break; default: termination_reason = "unknown"; break; } return Status(kUnknownError, "Chrome failed to start: " + termination_reason); } if (!base::KillProcess(process, 0, true)) { int exit_code; if (base::GetTerminationStatus(process, &exit_code) == base::TERMINATION_STATUS_STILL_RUNNING) return Status(kUnknownError, "cannot kill Chrome", status); } return status; } scoped_ptr<ChromeDesktopImpl> chrome_desktop( new ChromeDesktopImpl(devtools_client.Pass(), devtools_event_listeners, port_reservation.Pass(), process, command, &user_data_dir, &extension_dir)); for (size_t i = 0; i < extension_bg_pages.size(); ++i) { VLOG(0) << "Waiting for extension bg page load: " << extension_bg_pages[i]; scoped_ptr<WebView> web_view; Status status = chrome_desktop->WaitForPageToLoad( extension_bg_pages[i], base::TimeDelta::FromSeconds(10), &web_view); if (status.IsError()) { return Status(kUnknownError, "failed to wait for extension background page to load: " + extension_bg_pages[i], status); } } *chrome = chrome_desktop.Pass(); return Status(kOk); } Status LaunchAndroidChrome( URLRequestContextGetter* context_getter, int port, scoped_ptr<PortReservation> port_reservation, const SyncWebSocketFactory& socket_factory, const Capabilities& capabilities, ScopedVector<DevToolsEventListener>& devtools_event_listeners, DeviceManager* device_manager, scoped_ptr<Chrome>* chrome) { Status status(kOk); scoped_ptr<Device> device; if (capabilities.android_device_serial.empty()) { status = device_manager->AcquireDevice(&device); } else { status = device_manager->AcquireSpecificDevice( capabilities.android_device_serial, &device); } if (!status.IsOk()) return status; Switches switches(capabilities.switches); for (size_t i = 0; i < arraysize(kCommonSwitches); ++i) switches.SetSwitch(kCommonSwitches[i]); switches.SetSwitch("disable-fre"); switches.SetSwitch("enable-remote-debugging"); status = device->SetUp(capabilities.android_package, capabilities.android_activity, capabilities.android_process, switches.ToString(), capabilities.android_use_running_app, port); if (!status.IsOk()) { device->TearDown(); return status; } scoped_ptr<DevToolsHttpClient> devtools_client; status = WaitForDevToolsAndCheckVersion(NetAddress(port), context_getter, socket_factory, &devtools_client); if (status.IsError()) return status; chrome->reset(new ChromeAndroidImpl(devtools_client.Pass(), devtools_event_listeners, port_reservation.Pass(), device.Pass())); return Status(kOk); } } // namespace Status LaunchChrome( URLRequestContextGetter* context_getter, const SyncWebSocketFactory& socket_factory, DeviceManager* device_manager, PortServer* port_server, PortManager* port_manager, const Capabilities& capabilities, ScopedVector<DevToolsEventListener>& devtools_event_listeners, scoped_ptr<Chrome>* chrome) { if (capabilities.IsExistingBrowser()) { return LaunchExistingChromeSession( context_getter, socket_factory, capabilities, devtools_event_listeners, chrome); } int port = 0; scoped_ptr<PortReservation> port_reservation; Status port_status(kOk); if (port_server) port_status = port_server->ReservePort(&port, &port_reservation); else port_status = port_manager->ReservePort(&port, &port_reservation); if (port_status.IsError()) return Status(kUnknownError, "cannot reserve port for Chrome", port_status); if (capabilities.IsAndroid()) { return LaunchAndroidChrome(context_getter, port, port_reservation.Pass(), socket_factory, capabilities, devtools_event_listeners, device_manager, chrome); } else { return LaunchDesktopChrome(context_getter, port, port_reservation.Pass(), socket_factory, capabilities, devtools_event_listeners, chrome); } } namespace internal { void ConvertHexadecimalToIDAlphabet(std::string* id) { for (size_t i = 0; i < id->size(); ++i) { int val; if (base::HexStringToInt(base::StringPiece(id->begin() + i, id->begin() + i + 1), &val)) { (*id)[i] = val + 'a'; } else { (*id)[i] = 'a'; } } } std::string GenerateExtensionId(const std::string& input) { uint8 hash[16]; crypto::SHA256HashString(input, hash, sizeof(hash)); std::string output = StringToLowerASCII(base::HexEncode(hash, sizeof(hash))); ConvertHexadecimalToIDAlphabet(&output); return output; } Status GetExtensionBackgroundPage(const base::DictionaryValue* manifest, const std::string& id, std::string* bg_page) { std::string bg_page_name; bool persistent = true; manifest->GetBoolean("background.persistent", &persistent); const base::Value* unused_value; if (manifest->Get("background.scripts", &unused_value)) bg_page_name = "_generated_background_page.html"; manifest->GetString("background.page", &bg_page_name); manifest->GetString("background_page", &bg_page_name); if (bg_page_name.empty() || !persistent) return Status(kOk); *bg_page = "chrome-extension://" + id + "/" + bg_page_name; return Status(kOk); } Status ProcessExtension(const std::string& extension, const base::FilePath& temp_dir, base::FilePath* path, std::string* bg_page) { // Decodes extension string. // Some WebDriver client base64 encoders follow RFC 1521, which require that // 'encoded lines be no more than 76 characters long'. Just remove any // newlines. std::string extension_base64; base::RemoveChars(extension, "\n", &extension_base64); std::string decoded_extension; if (!base::Base64Decode(extension_base64, &decoded_extension)) return Status(kUnknownError, "cannot base64 decode"); // Get extension's ID from public key in crx file. // Assumes crx v2. See http://developer.chrome.com/extensions/crx.html. std::string key_len_str = decoded_extension.substr(8, 4); if (key_len_str.size() != 4) return Status(kUnknownError, "cannot extract public key length"); uint32 key_len = *reinterpret_cast<const uint32*>(key_len_str.c_str()); std::string public_key = decoded_extension.substr(16, key_len); if (key_len != public_key.size()) return Status(kUnknownError, "invalid public key length"); std::string public_key_base64; base::Base64Encode(public_key, &public_key_base64); std::string id = GenerateExtensionId(public_key); // Unzip the crx file. base::ScopedTempDir temp_crx_dir; if (!temp_crx_dir.CreateUniqueTempDir()) return Status(kUnknownError, "cannot create temp dir"); base::FilePath extension_crx = temp_crx_dir.path().AppendASCII("temp.crx"); int size = static_cast<int>(decoded_extension.length()); if (file_util::WriteFile(extension_crx, decoded_extension.c_str(), size) != size) { return Status(kUnknownError, "cannot write file"); } base::FilePath extension_dir = temp_dir.AppendASCII("extension_" + id); if (!zip::Unzip(extension_crx, extension_dir)) return Status(kUnknownError, "cannot unzip"); // Parse the manifest and set the 'key' if not already present. base::FilePath manifest_path(extension_dir.AppendASCII("manifest.json")); std::string manifest_data; if (!base::ReadFileToString(manifest_path, &manifest_data)) return Status(kUnknownError, "cannot read manifest"); scoped_ptr<base::Value> manifest_value(base::JSONReader::Read(manifest_data)); base::DictionaryValue* manifest; if (!manifest_value || !manifest_value->GetAsDictionary(&manifest)) return Status(kUnknownError, "invalid manifest"); std::string manifest_key_base64; if (manifest->GetString("key", &manifest_key_base64)) { // If there is a key in both the header and the manifest, use the key in the // manifest. This allows chromedriver users users who generate dummy crxs // to set the manifest key and have a consistent ID. std::string manifest_key; if (!base::Base64Decode(manifest_key_base64, &manifest_key)) return Status(kUnknownError, "'key' in manifest is not base64 encoded"); std::string manifest_id = GenerateExtensionId(manifest_key); if (id != manifest_id) { LOG(WARNING) << "Public key in crx header is different from key in manifest" << std::endl << "key from header: " << public_key_base64 << std::endl << "key from manifest: " << manifest_key_base64 << std::endl << "generated extension id from header key: " << id << std::endl << "generated extension id from manifest key: " << manifest_id; id = manifest_id; } } else { manifest->SetString("key", public_key_base64); base::JSONWriter::Write(manifest, &manifest_data); if (file_util::WriteFile( manifest_path, manifest_data.c_str(), manifest_data.size()) != static_cast<int>(manifest_data.size())) { return Status(kUnknownError, "cannot add 'key' to manifest"); } } // Get extension's background page URL, if there is one. std::string bg_page_tmp; Status status = GetExtensionBackgroundPage(manifest, id, &bg_page_tmp); if (status.IsError()) return status; *path = extension_dir; if (bg_page_tmp.size()) *bg_page = bg_page_tmp; return Status(kOk); } void UpdateExtensionSwitch(Switches* switches, const char name[], const base::FilePath::StringType& extension) { base::FilePath::StringType value = switches->GetSwitchValueNative(name); if (value.length()) value += FILE_PATH_LITERAL(","); value += extension; switches->SetSwitch(name, value); } Status ProcessExtensions(const std::vector<std::string>& extensions, const base::FilePath& temp_dir, bool include_automation_extension, Switches* switches, std::vector<std::string>* bg_pages) { std::vector<std::string> bg_pages_tmp; std::vector<base::FilePath::StringType> extension_paths; for (size_t i = 0; i < extensions.size(); ++i) { base::FilePath path; std::string bg_page; Status status = ProcessExtension(extensions[i], temp_dir, &path, &bg_page); if (status.IsError()) { return Status( kUnknownError, base::StringPrintf("cannot process extension #%" PRIuS, i + 1), status); } extension_paths.push_back(path.value()); if (bg_page.length()) bg_pages_tmp.push_back(bg_page); } if (include_automation_extension) { base::FilePath automation_extension; Status status = UnpackAutomationExtension(temp_dir, &automation_extension); if (status.IsError()) return status; if (switches->HasSwitch("disable-extensions")) { UpdateExtensionSwitch(switches, "load-component-extension", automation_extension.value()); } else { extension_paths.push_back(automation_extension.value()); } } if (extension_paths.size()) { base::FilePath::StringType extension_paths_value = JoinString( extension_paths, FILE_PATH_LITERAL(',')); UpdateExtensionSwitch(switches, "load-extension", extension_paths_value); } bg_pages->swap(bg_pages_tmp); return Status(kOk); } Status WritePrefsFile( const std::string& template_string, const base::DictionaryValue* custom_prefs, const base::FilePath& path) { int code; std::string error_msg; scoped_ptr<base::Value> template_value(base::JSONReader::ReadAndReturnError( template_string, 0, &code, &error_msg)); base::DictionaryValue* prefs; if (!template_value || !template_value->GetAsDictionary(&prefs)) { return Status(kUnknownError, "cannot parse internal JSON template: " + error_msg); } if (custom_prefs) { for (base::DictionaryValue::Iterator it(*custom_prefs); !it.IsAtEnd(); it.Advance()) { prefs->Set(it.key(), it.value().DeepCopy()); } } std::string prefs_str; base::JSONWriter::Write(prefs, &prefs_str); VLOG(0) << "Populating " << path.BaseName().value() << " file: " << PrettyPrintValue(*prefs); if (static_cast<int>(prefs_str.length()) != file_util::WriteFile( path, prefs_str.c_str(), prefs_str.length())) { return Status(kUnknownError, "failed to write prefs file"); } return Status(kOk); } Status PrepareUserDataDir( const base::FilePath& user_data_dir, const base::DictionaryValue* custom_prefs, const base::DictionaryValue* custom_local_state) { base::FilePath default_dir = user_data_dir.AppendASCII("Default"); if (!base::CreateDirectory(default_dir)) return Status(kUnknownError, "cannot create default profile directory"); Status status = WritePrefsFile( kPreferences, custom_prefs, default_dir.AppendASCII("Preferences")); if (status.IsError()) return status; status = WritePrefsFile( kLocalState, custom_local_state, user_data_dir.AppendASCII("Local State")); if (status.IsError()) return status; // Write empty "First Run" file, otherwise Chrome will wipe the default // profile that was written. if (file_util::WriteFile( user_data_dir.AppendASCII("First Run"), "", 0) != 0) { return Status(kUnknownError, "failed to write first run file"); } return Status(kOk); } } // namespace internal