// 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/capabilities.h" #include <map> #include "base/bind.h" #include "base/callback.h" #include "base/json/string_escape.h" #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" #include "base/strings/string_tokenizer.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "chrome/test/chromedriver/chrome/mobile_device.h" #include "chrome/test/chromedriver/chrome/status.h" #include "chrome/test/chromedriver/logging.h" #include "net/base/net_util.h" namespace { typedef base::Callback<Status(const base::Value&, Capabilities*)> Parser; Status ParseBoolean( bool* to_set, const base::Value& option, Capabilities* capabilities) { if (!option.GetAsBoolean(to_set)) return Status(kUnknownError, "must be a boolean"); return Status(kOk); } Status ParseString(std::string* to_set, const base::Value& option, Capabilities* capabilities) { std::string str; if (!option.GetAsString(&str)) return Status(kUnknownError, "must be a string"); if (str.empty()) return Status(kUnknownError, "cannot be empty"); *to_set = str; return Status(kOk); } Status ParseFilePath(base::FilePath* to_set, const base::Value& option, Capabilities* capabilities) { base::FilePath::StringType str; if (!option.GetAsString(&str)) return Status(kUnknownError, "must be a string"); *to_set = base::FilePath(str); return Status(kOk); } Status ParseDict(scoped_ptr<base::DictionaryValue>* to_set, const base::Value& option, Capabilities* capabilities) { const base::DictionaryValue* dict = NULL; if (!option.GetAsDictionary(&dict)) return Status(kUnknownError, "must be a dictionary"); to_set->reset(dict->DeepCopy()); return Status(kOk); } Status IgnoreDeprecatedOption( const char* option_name, const base::Value& option, Capabilities* capabilities) { LOG(WARNING) << "Deprecated chrome option is ignored: " << option_name; return Status(kOk); } Status IgnoreCapability(const base::Value& option, Capabilities* capabilities) { return Status(kOk); } Status ParseLogPath(const base::Value& option, Capabilities* capabilities) { if (!option.GetAsString(&capabilities->log_path)) return Status(kUnknownError, "must be a string"); return Status(kOk); } Status ParseDeviceName(std::string device_name, Capabilities* capabilities) { scoped_ptr<MobileDevice> device; Status status = FindMobileDevice(device_name, &device); if (status.IsError()) { return Status(kUnknownError, "'" + device_name + "' must be a valid device", status); } capabilities->device_metrics.reset(device->device_metrics.release()); capabilities->switches.SetSwitch("user-agent", device->user_agent); return Status(kOk); } Status ParseMobileEmulation(const base::Value& option, Capabilities* capabilities) { const base::DictionaryValue* mobile_emulation; if (!option.GetAsDictionary(&mobile_emulation)) return Status(kUnknownError, "'mobileEmulation' must be a dictionary"); if (mobile_emulation->HasKey("deviceName")) { // Cannot use any other options with deviceName. if (mobile_emulation->size() > 1) return Status(kUnknownError, "'deviceName' must be used alone"); std::string device_name; if (!mobile_emulation->GetString("deviceName", &device_name)) return Status(kUnknownError, "'deviceName' must be a string"); return ParseDeviceName(device_name, capabilities); } if (mobile_emulation->HasKey("deviceMetrics")) { const base::DictionaryValue* metrics; if (!mobile_emulation->GetDictionary("deviceMetrics", &metrics)) return Status(kUnknownError, "'deviceMetrics' must be a dictionary"); int width; int height; double device_scale_factor; if (!metrics->GetInteger("width", &width) || !metrics->GetInteger("height", &height) || !metrics->GetDouble("pixelRatio", &device_scale_factor)) return Status(kUnknownError, "invalid 'deviceMetrics'"); DeviceMetrics* device_metrics = new DeviceMetrics(width, height, device_scale_factor); capabilities->device_metrics = scoped_ptr<DeviceMetrics>(device_metrics); } if (mobile_emulation->HasKey("userAgent")) { std::string user_agent; if (!mobile_emulation->GetString("userAgent", &user_agent)) return Status(kUnknownError, "'userAgent' must be a string"); capabilities->switches.SetSwitch("user-agent", user_agent); } return Status(kOk); } Status ParseSwitches(const base::Value& option, Capabilities* capabilities) { const base::ListValue* switches_list = NULL; if (!option.GetAsList(&switches_list)) return Status(kUnknownError, "must be a list"); for (size_t i = 0; i < switches_list->GetSize(); ++i) { std::string arg_string; if (!switches_list->GetString(i, &arg_string)) return Status(kUnknownError, "each argument must be a string"); capabilities->switches.SetUnparsedSwitch(arg_string); } return Status(kOk); } Status ParseExtensions(const base::Value& option, Capabilities* capabilities) { const base::ListValue* extensions = NULL; if (!option.GetAsList(&extensions)) return Status(kUnknownError, "must be a list"); for (size_t i = 0; i < extensions->GetSize(); ++i) { std::string extension; if (!extensions->GetString(i, &extension)) { return Status(kUnknownError, "each extension must be a base64 encoded string"); } capabilities->extensions.push_back(extension); } return Status(kOk); } Status ParseProxy(const base::Value& option, Capabilities* capabilities) { const base::DictionaryValue* proxy_dict; if (!option.GetAsDictionary(&proxy_dict)) return Status(kUnknownError, "must be a dictionary"); std::string proxy_type; if (!proxy_dict->GetString("proxyType", &proxy_type)) return Status(kUnknownError, "'proxyType' must be a string"); proxy_type = StringToLowerASCII(proxy_type); if (proxy_type == "direct") { capabilities->switches.SetSwitch("no-proxy-server"); } else if (proxy_type == "system") { // Chrome default. } else if (proxy_type == "pac") { CommandLine::StringType proxy_pac_url; if (!proxy_dict->GetString("proxyAutoconfigUrl", &proxy_pac_url)) return Status(kUnknownError, "'proxyAutoconfigUrl' must be a string"); capabilities->switches.SetSwitch("proxy-pac-url", proxy_pac_url); } else if (proxy_type == "autodetect") { capabilities->switches.SetSwitch("proxy-auto-detect"); } else if (proxy_type == "manual") { const char* proxy_servers_options[][2] = { {"ftpProxy", "ftp"}, {"httpProxy", "http"}, {"sslProxy", "https"}}; const base::Value* option_value = NULL; std::string proxy_servers; for (size_t i = 0; i < arraysize(proxy_servers_options); ++i) { if (!proxy_dict->Get(proxy_servers_options[i][0], &option_value) || option_value->IsType(base::Value::TYPE_NULL)) { continue; } std::string value; if (!option_value->GetAsString(&value)) { return Status( kUnknownError, base::StringPrintf("'%s' must be a string", proxy_servers_options[i][0])); } // Converts into Chrome proxy scheme. // Example: "http=localhost:9000;ftp=localhost:8000". if (!proxy_servers.empty()) proxy_servers += ";"; proxy_servers += base::StringPrintf( "%s=%s", proxy_servers_options[i][1], value.c_str()); } std::string proxy_bypass_list; if (proxy_dict->Get("noProxy", &option_value) && !option_value->IsType(base::Value::TYPE_NULL)) { if (!option_value->GetAsString(&proxy_bypass_list)) return Status(kUnknownError, "'noProxy' must be a string"); } if (proxy_servers.empty() && proxy_bypass_list.empty()) { return Status(kUnknownError, "proxyType is 'manual' but no manual " "proxy capabilities were found"); } if (!proxy_servers.empty()) capabilities->switches.SetSwitch("proxy-server", proxy_servers); if (!proxy_bypass_list.empty()) { capabilities->switches.SetSwitch("proxy-bypass-list", proxy_bypass_list); } } else { return Status(kUnknownError, "unrecognized proxy type:" + proxy_type); } return Status(kOk); } Status ParseExcludeSwitches(const base::Value& option, Capabilities* capabilities) { const base::ListValue* switches = NULL; if (!option.GetAsList(&switches)) return Status(kUnknownError, "must be a list"); for (size_t i = 0; i < switches->GetSize(); ++i) { std::string switch_name; if (!switches->GetString(i, &switch_name)) { return Status(kUnknownError, "each switch to be removed must be a string"); } capabilities->exclude_switches.insert(switch_name); } return Status(kOk); } Status ParseUseRemoteBrowser(const base::Value& option, Capabilities* capabilities) { std::string server_addr; if (!option.GetAsString(&server_addr)) return Status(kUnknownError, "must be 'host:port'"); std::vector<std::string> values; base::SplitString(server_addr, ':', &values); if (values.size() != 2) return Status(kUnknownError, "must be 'host:port'"); int port = 0; base::StringToInt(values[1], &port); if (port <= 0) return Status(kUnknownError, "port must be > 0"); capabilities->debugger_address = NetAddress(values[0], port); return Status(kOk); } Status ParseLoggingPrefs(const base::Value& option, Capabilities* capabilities) { const base::DictionaryValue* logging_prefs = NULL; if (!option.GetAsDictionary(&logging_prefs)) return Status(kUnknownError, "must be a dictionary"); for (base::DictionaryValue::Iterator pref(*logging_prefs); !pref.IsAtEnd(); pref.Advance()) { std::string type = pref.key(); Log::Level level; std::string level_name; if (!pref.value().GetAsString(&level_name) || !WebDriverLog::NameToLevel(level_name, &level)) { return Status(kUnknownError, "invalid log level for '" + type + "' log"); } capabilities->logging_prefs.insert(std::make_pair(type, level)); } return Status(kOk); } Status ParseChromeOptions( const base::Value& capability, Capabilities* capabilities) { const base::DictionaryValue* chrome_options = NULL; if (!capability.GetAsDictionary(&chrome_options)) return Status(kUnknownError, "must be a dictionary"); bool is_android = chrome_options->HasKey("androidPackage"); bool is_remote = chrome_options->HasKey("debuggerAddress"); std::map<std::string, Parser> parser_map; // Ignore 'args', 'binary' and 'extensions' capabilities by default, since the // Java client always passes them. parser_map["args"] = base::Bind(&IgnoreCapability); parser_map["binary"] = base::Bind(&IgnoreCapability); parser_map["extensions"] = base::Bind(&IgnoreCapability); if (is_android) { parser_map["androidActivity"] = base::Bind(&ParseString, &capabilities->android_activity); parser_map["androidDeviceSerial"] = base::Bind(&ParseString, &capabilities->android_device_serial); parser_map["androidPackage"] = base::Bind(&ParseString, &capabilities->android_package); parser_map["androidProcess"] = base::Bind(&ParseString, &capabilities->android_process); parser_map["androidUseRunningApp"] = base::Bind(&ParseBoolean, &capabilities->android_use_running_app); parser_map["args"] = base::Bind(&ParseSwitches); parser_map["loadAsync"] = base::Bind(&IgnoreDeprecatedOption, "loadAsync"); } else if (is_remote) { parser_map["debuggerAddress"] = base::Bind(&ParseUseRemoteBrowser); } else { parser_map["args"] = base::Bind(&ParseSwitches); parser_map["binary"] = base::Bind(&ParseFilePath, &capabilities->binary); parser_map["detach"] = base::Bind(&ParseBoolean, &capabilities->detach); parser_map["mobileEmulation"] = base::Bind(&ParseMobileEmulation); parser_map["excludeSwitches"] = base::Bind(&ParseExcludeSwitches); parser_map["extensions"] = base::Bind(&ParseExtensions); parser_map["forceDevToolsScreenshot"] = base::Bind( &ParseBoolean, &capabilities->force_devtools_screenshot); parser_map["loadAsync"] = base::Bind(&IgnoreDeprecatedOption, "loadAsync"); parser_map["localState"] = base::Bind(&ParseDict, &capabilities->local_state); parser_map["logPath"] = base::Bind(&ParseLogPath); parser_map["minidumpPath"] = base::Bind(&ParseString, &capabilities->minidump_path); parser_map["prefs"] = base::Bind(&ParseDict, &capabilities->prefs); } for (base::DictionaryValue::Iterator it(*chrome_options); !it.IsAtEnd(); it.Advance()) { if (parser_map.find(it.key()) == parser_map.end()) { return Status(kUnknownError, "unrecognized chrome option: " + it.key()); } Status status = parser_map[it.key()].Run(it.value(), capabilities); if (status.IsError()) return Status(kUnknownError, "cannot parse " + it.key(), status); } return Status(kOk); } } // namespace Switches::Switches() {} Switches::~Switches() {} void Switches::SetSwitch(const std::string& name) { SetSwitch(name, NativeString()); } void Switches::SetSwitch(const std::string& name, const std::string& value) { #if defined(OS_WIN) SetSwitch(name, base::UTF8ToUTF16(value)); #else switch_map_[name] = value; #endif } void Switches::SetSwitch(const std::string& name, const base::string16& value) { #if defined(OS_WIN) switch_map_[name] = value; #else SetSwitch(name, base::UTF16ToUTF8(value)); #endif } void Switches::SetSwitch(const std::string& name, const base::FilePath& value) { SetSwitch(name, value.value()); } void Switches::SetFromSwitches(const Switches& switches) { for (SwitchMap::const_iterator iter = switches.switch_map_.begin(); iter != switches.switch_map_.end(); ++iter) { switch_map_[iter->first] = iter->second; } } void Switches::SetUnparsedSwitch(const std::string& unparsed_switch) { std::string value; size_t equals_index = unparsed_switch.find('='); if (equals_index != std::string::npos) value = unparsed_switch.substr(equals_index + 1); std::string name; size_t start_index = 0; if (unparsed_switch.substr(0, 2) == "--") start_index = 2; name = unparsed_switch.substr(start_index, equals_index - start_index); SetSwitch(name, value); } void Switches::RemoveSwitch(const std::string& name) { switch_map_.erase(name); } bool Switches::HasSwitch(const std::string& name) const { return switch_map_.count(name) > 0; } std::string Switches::GetSwitchValue(const std::string& name) const { NativeString value = GetSwitchValueNative(name); #if defined(OS_WIN) return base::UTF16ToUTF8(value); #else return value; #endif } Switches::NativeString Switches::GetSwitchValueNative( const std::string& name) const { SwitchMap::const_iterator iter = switch_map_.find(name); if (iter == switch_map_.end()) return NativeString(); return iter->second; } size_t Switches::GetSize() const { return switch_map_.size(); } void Switches::AppendToCommandLine(CommandLine* command) const { for (SwitchMap::const_iterator iter = switch_map_.begin(); iter != switch_map_.end(); ++iter) { command->AppendSwitchNative(iter->first, iter->second); } } std::string Switches::ToString() const { std::string str; SwitchMap::const_iterator iter = switch_map_.begin(); while (iter != switch_map_.end()) { str += "--" + iter->first; std::string value = GetSwitchValue(iter->first); if (value.length()) { if (value.find(' ') != std::string::npos) value = base::GetQuotedJSONString(value); str += "=" + value; } ++iter; if (iter == switch_map_.end()) break; str += " "; } return str; } Capabilities::Capabilities() : android_use_running_app(false), detach(false), force_devtools_screenshot(false) {} Capabilities::~Capabilities() {} bool Capabilities::IsAndroid() const { return !android_package.empty(); } bool Capabilities::IsRemoteBrowser() const { return debugger_address.IsValid(); } Status Capabilities::Parse(const base::DictionaryValue& desired_caps) { std::map<std::string, Parser> parser_map; parser_map["chromeOptions"] = base::Bind(&ParseChromeOptions); parser_map["loggingPrefs"] = base::Bind(&ParseLoggingPrefs); parser_map["proxy"] = base::Bind(&ParseProxy); for (std::map<std::string, Parser>::iterator it = parser_map.begin(); it != parser_map.end(); ++it) { const base::Value* capability = NULL; if (desired_caps.Get(it->first, &capability)) { Status status = it->second.Run(*capability, this); if (status.IsError()) { return Status( kUnknownError, "cannot parse capability: " + it->first, status); } } } return Status(kOk); }