blob: c26ae7044eea219a663287f34dfd94b9928b8755 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <array>
#include <locale>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/message_loop/message_pump_type.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/single_thread_task_executor.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/threading/thread.h"
#include "build/build_config.h"
#include "chrome/test/chromedriver/constants/version.h"
#include "chrome/test/chromedriver/keycode_text_conversion.h"
#include "chrome/test/chromedriver/logging.h"
#include "chrome/test/chromedriver/server/http_handler.h"
#include "chrome/test/chromedriver/server/http_server.h"
#include "mojo/core/embedder/embedder.h"
#include "net/base/ip_address.h"
#include "net/base/ip_endpoint.h"
#include "net/base/net_errors.h"
#include "net/log/net_log_source.h"
namespace {
#if BUILDFLAG(IS_LINUX)
// Ensure that there is a writable shared memory directory. We use
// network::SimpleURLLoader to connect to Chrome, and it calls
// base::subtle::PlatformSharedMemoryRegion::Create to get a shared memory
// region. network::SimpleURLLoader would fail if the shared memory directory is
// not accessible. We work around this issue by adding --disable-dev-shm-usage
// to command line, to use an alternative directory for shared memory.
// See https://crbug.com/chromedriver/2782.
void EnsureSharedMemory(base::CommandLine* cmd_line) {
if (!cmd_line->HasSwitch("disable-dev-shm-usage")) {
base::FilePath directory;
if (GetShmemTempDir(false, &directory) &&
access(directory.value().c_str(), W_OK | X_OK) < 0) {
VLOG(0) << directory
<< " not writable, adding --disable-dev-shm-usage switch";
cmd_line->AppendSwitch("disable-dev-shm-usage");
}
}
}
#endif
void SendResponseOnCmdThread(
const scoped_refptr<base::SingleThreadTaskRunner>& io_task_runner,
const HttpResponseSenderFunc& send_response_on_io_func,
std::unique_ptr<net::HttpServerResponseInfo> response) {
io_task_runner->PostTask(
FROM_HERE, base::BindOnce(send_response_on_io_func, std::move(response)));
}
void HandleRequestOnCmdThread(
HttpHandler* handler,
const std::vector<net::IPAddress>& allowed_ips,
const net::HttpServerRequestInfo& request,
const HttpResponseSenderFunc& send_response_func) {
if (!allowed_ips.empty()) {
const net::IPAddress& peer_address = request.peer.address();
if (!base::Contains(allowed_ips, peer_address)) {
LOG(WARNING) << "unauthorized access from " << request.peer.ToString();
std::unique_ptr<net::HttpServerResponseInfo> response(
new net::HttpServerResponseInfo(net::HTTP_UNAUTHORIZED));
response->SetBody("Unauthorized access", "text/plain");
send_response_func.Run(std::move(response));
return;
}
}
handler->Handle(request, send_response_func);
}
void HandleRequestOnIOThread(
const scoped_refptr<base::SingleThreadTaskRunner>& cmd_task_runner,
const HttpRequestHandlerFunc& handle_request_on_cmd_func,
const net::HttpServerRequestInfo& request,
const HttpResponseSenderFunc& send_response_func) {
cmd_task_runner->PostTask(
FROM_HERE,
base::BindOnce(
handle_request_on_cmd_func, request,
base::BindRepeating(&SendResponseOnCmdThread,
base::SingleThreadTaskRunner::GetCurrentDefault(),
send_response_func)));
}
constinit thread_local HttpServer* server_ipv4 = nullptr;
constinit thread_local HttpServer* server_ipv6 = nullptr;
void StopServerOnIOThread() {
delete server_ipv4;
server_ipv4 = nullptr;
delete server_ipv6;
server_ipv4 = nullptr;
}
void StartServerOnIOThread(
uint16_t port,
bool allow_remote,
const std::string& url_base,
const std::vector<net::IPAddress>& allowed_ips,
const std::vector<std::string>& allowed_origins,
const HttpRequestHandlerFunc& handle_request_func,
base::WeakPtr<HttpHandler> handler,
const scoped_refptr<base::SingleThreadTaskRunner>& cmd_task_runner) {
std::unique_ptr<HttpServer> temp_server;
// On Linux and Windows, we listen to IPv6 first, and then optionally listen
// to IPv4 (depending on |need_ipv4| below). The reason is listening to an
// IPv6 port may automatically listen to the same IPv4 port as well, and would
// return an error if the IPv4 port is already in use.
//
// On Mac, however, we listen to IPv4 first before listening to IPv6. If we
// were to listen to IPv6 first, it would succeed whether the corresponding
// IPv4 port is in use or not, and we wouldn't know if we ended up listening
// to both IPv4 and IPv6 ports, or only IPv6 port. Listening to IPv4 first
// ensures that we successfully listen to both IPv4 and IPv6.
#if BUILDFLAG(IS_MAC)
temp_server = std::make_unique<HttpServer>(
url_base, allowed_ips, allowed_origins, handle_request_func, handler,
cmd_task_runner);
int ipv4_status = temp_server->Start(port, allow_remote, true);
if (ipv4_status == net::OK) {
port = temp_server->LocalAddress().port();
server_ipv4 = temp_server.release();
} else if (ipv4_status == net::ERR_ADDRESS_IN_USE) {
// ERR_ADDRESS_IN_USE causes an immediate exit, since it indicates the port
// is being used by another process. Other errors are assumed to indicate
// that IPv4 isn't available for some reason, e.g., on an IPv6-only host.
// Thus the error doesn't cause an exit immediately. The HttpServer::Start
// method has already printed a message indicating what has happened. Later,
// near the end of this function, we exit if both IPv4 and IPv6 failed.
printf("IPv4 port not available. Exiting...\n");
exit(1);
}
#endif
temp_server = std::make_unique<HttpServer>(
url_base, allowed_ips, allowed_origins, handle_request_func, handler,
cmd_task_runner);
int ipv6_status = temp_server->Start(port, allow_remote, false);
if (ipv6_status == net::OK) {
port = temp_server->LocalAddress().port();
server_ipv6 = temp_server.release();
} else if (ipv6_status == net::ERR_ADDRESS_IN_USE) {
printf("IPv6 port not available. Exiting...\n");
exit(1);
}
#if !BUILDFLAG(IS_MAC)
// In some cases, binding to an IPv6 port also binds to the same IPv4 port.
// The following code determines if it is necessary to bind to IPv4 port.
enum class NeedIPv4 { NOT_NEEDED, UNKNOWN, NEEDED } need_ipv4;
// Dual-protocol bind deosn't work while binding to localhost (!allow_remote).
if (!allow_remote || ipv6_status != net::OK) {
need_ipv4 = NeedIPv4::NEEDED;
} else {
// Currently, the network layer provides no way for us to control dual-protocol
// bind option, or to query the current setting of that option, so we do our
// best to determine the current setting. See https://crbug.com/858892.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
// On Linux, dual-protocol bind is controlled by a system file.
// ChromeOS builds also have OS_LINUX defined, so the code below applies.
std::string bindv6only;
base::FilePath bindv6only_filename("/proc/sys/net/ipv6/bindv6only");
if (!base::ReadFileToString(bindv6only_filename, &bindv6only)) {
LOG(WARNING) << "Unable to read " << bindv6only_filename << ".";
need_ipv4 = NeedIPv4::UNKNOWN;
} else if (bindv6only == "1\n") {
need_ipv4 = NeedIPv4::NEEDED;
} else if (bindv6only == "0\n") {
need_ipv4 = NeedIPv4::NOT_NEEDED;
} else {
LOG(WARNING) << "Unexpected " << bindv6only_filename << " contents.";
need_ipv4 = NeedIPv4::UNKNOWN;
}
#elif BUILDFLAG(IS_WIN)
// On Windows, the net component always enables dual-protocol bind. See
// https://chromium.googlesource.com/chromium/src/+/69.0.3464.0/net/socket/socket_descriptor.cc#28.
need_ipv4 = NeedIPv4::NOT_NEEDED;
#else
LOG(WARNING) << "Running on a platform not officially supported by "
<< kChromeDriverProductFullName << ".";
need_ipv4 = NeedIPv4::UNKNOWN;
#endif
}
int ipv4_status;
if (need_ipv4 == NeedIPv4::NOT_NEEDED) {
ipv4_status = ipv6_status;
} else {
temp_server = std::make_unique<HttpServer>(
url_base, allowed_ips, allowed_origins, handle_request_func, handler,
cmd_task_runner);
ipv4_status = temp_server->Start(port, allow_remote, true);
if (ipv4_status == net::OK) {
server_ipv4 = temp_server.release();
} else if (ipv4_status == net::ERR_ADDRESS_IN_USE) {
if (need_ipv4 == NeedIPv4::NEEDED) {
printf("IPv4 port not available. Exiting...\n");
exit(1);
} else {
printf("Unable to determine if bind to IPv4 port was successful.\n");
}
}
}
#endif // !BUILDFLAG(IS_MAC)
if (ipv4_status != net::OK && ipv6_status != net::OK) {
printf("Unable to start server with either IPv4 or IPv6. Exiting...\n");
exit(1);
}
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
if (!cmd_line->HasSwitch("silent") &&
cmd_line->GetSwitchValueASCII("log-level") != "OFF") {
UNSAFE_TODO(printf("%s was started successfully on port %u.\n",
kChromeDriverProductShortName, port));
}
if (cmd_line->HasSwitch("log-path")) {
VLOG(0) << kChromeDriverProductShortName
<< " was started successfully on port " << port;
}
fflush(stdout);
}
void RunServer(uint16_t port,
bool allow_remote,
const std::vector<net::IPAddress>& allowed_ips,
const std::vector<std::string>& allowed_origins,
const std::string& url_base,
int adb_port) {
base::Thread io_thread(
base::StringPrintf("%s IO", kChromeDriverProductShortName));
CHECK(io_thread.StartWithOptions(
base::Thread::Options(base::MessagePumpType::IO, 0)));
base::SingleThreadTaskExecutor main_task_executor;
base::RunLoop cmd_run_loop;
HttpHandler handler(cmd_run_loop.QuitClosure(), io_thread.task_runner(),
main_task_executor.task_runner(), url_base, adb_port);
HttpRequestHandlerFunc handle_request_func =
base::BindRepeating(&HandleRequestOnCmdThread, &handler, allowed_ips);
io_thread.task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&StartServerOnIOThread, port, allow_remote, url_base,
allowed_ips, allowed_origins,
base::BindRepeating(&HandleRequestOnIOThread,
main_task_executor.task_runner(),
handle_request_func),
handler.WeakPtr(), main_task_executor.task_runner()));
// Run the command loop. This loop is quit after the response for a shutdown
// request is posted to the IO loop. After the command loop quits, a task
// is posted to the IO loop to stop the server. Lastly, the IO thread is
// destroyed, which waits until all pending tasks have been completed.
// This assumes the response is sent synchronously as part of the IO task.
cmd_run_loop.Run();
io_thread.task_runner()->PostTask(FROM_HERE,
base::BindOnce(&StopServerOnIOThread));
}
} // namespace
int main(int argc, char *argv[]) {
base::CommandLine::Init(argc, argv);
base::AtExitManager at_exit;
base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess();
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
// Select the locale from the environment by passing an empty string instead
// of the default "C" locale. This is particularly needed for the keycode
// conversion code to work.
setlocale(LC_ALL, "");
#endif
// Parse command line flags.
uint16_t port = 0;
int adb_port = 5037;
bool allow_remote = false;
std::vector<net::IPAddress> allowed_ips;
std::vector<std::string> allowed_origins;
std::string allowlist_ips;
std::string allowlist_origins;
std::string url_base;
if (cmd_line->HasSwitch("h") || cmd_line->HasSwitch("help")) {
std::string options;
const auto kOptionAndDescriptions = std::to_array<const char*>({
"port=PORT",
"port to listen on",
"adb-port=PORT",
"adb server port",
"log-path=FILE",
"write server log to file instead of stderr, "
"increases log level to INFO",
"log-level=LEVEL",
"set log level: ALL, DEBUG, INFO, WARNING, SEVERE, OFF",
"verbose",
"log verbosely (equivalent to --log-level=ALL)",
"silent",
"log nothing (equivalent to --log-level=OFF)",
"append-log",
"append log file instead of rewriting",
"replayable",
"(experimental) log verbosely and don't truncate long "
"strings so that the log can be replayed.",
"version",
"print the version number and exit",
"url-base",
"base URL path prefix for commands, e.g. wd/url",
"readable-timestamp",
"add readable timestamps to log",
"enable-chrome-logs",
"show logs from the browser (overrides other logging options)",
"bidi-mapper-path=PATH",
"custom bidi mapper path",
#if BUILDFLAG(IS_LINUX)
"disable-dev-shm-usage",
"do not use /dev/shm "
"(add this switch if seeing errors related to shared memory)",
#endif
// TODO(crbug.com/354135326): This is a temporary flag needed to
// smooothly migrate the web platform tests to auto-assigned port.
// This switch will be removed in M132. Don't rely on it!
"ignore-explicit-port",
"(experimental) ignore the port specified explicitly, "
"find a free port instead",
});
for (size_t i = 0; i < std::size(kOptionAndDescriptions) - 1; i += 2) {
options += base::StringPrintf(
" --%-30s%s\n",
kOptionAndDescriptions[i], kOptionAndDescriptions[i + 1]);
}
// Add helper info for `allowed-ips` and `allowed-origins` since the product
// name may be different.
options += base::StringPrintf(
" --%-30scomma-separated allowlist of remote IP addresses which are "
"allowed to connect to %s\n",
"allowed-ips=LIST", kChromeDriverProductShortName);
options += base::StringPrintf(
" --%-30scomma-separated allowlist of request origins which are "
"allowed to connect to %s. Using `*` to allow any host origin is "
"dangerous!\n",
"allowed-origins=LIST", kChromeDriverProductShortName);
UNSAFE_TODO(
printf("Usage: %s [OPTIONS]\n\nOptions\n%s", argv[0], options.c_str()));
return 0;
}
bool early_exit = false;
if (cmd_line->HasSwitch("v") || cmd_line->HasSwitch("version")) {
UNSAFE_TODO(
printf("%s %s\n", kChromeDriverProductFullName, kChromeDriverVersion));
early_exit = true;
}
if (early_exit)
return 0;
if (cmd_line->HasSwitch("port")) {
int cmd_line_port;
if (!base::StringToInt(cmd_line->GetSwitchValueASCII("port"),
&cmd_line_port) ||
cmd_line_port < 0 || cmd_line_port > 65535) {
printf("Invalid port. Exiting...\n");
return 1;
}
port = static_cast<uint16_t>(cmd_line_port);
}
if (cmd_line->HasSwitch("ignore-explicit-port")) {
port = 0;
}
if (cmd_line->HasSwitch("adb-port")) {
if (!base::StringToInt(cmd_line->GetSwitchValueASCII("adb-port"),
&adb_port)) {
printf("Invalid adb-port. Exiting...\n");
return 1;
}
}
if (cmd_line->HasSwitch("url-base"))
url_base = cmd_line->GetSwitchValueASCII("url-base");
if (url_base.empty() || url_base.front() != '/')
url_base = "/" + url_base;
if (url_base.back() != '/')
url_base = url_base + "/";
if (cmd_line->HasSwitch("allowed-ips") ||
cmd_line->HasSwitch("whitelisted-ips")) {
allow_remote = true;
if (cmd_line->HasSwitch("allowed-ips"))
allowlist_ips = cmd_line->GetSwitchValueASCII("allowed-ips");
else
allowlist_ips = cmd_line->GetSwitchValueASCII("whitelisted-ips");
std::vector<std::string> allowlist_ip_strs = base::SplitString(
allowlist_ips, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (!allowlist_ip_strs.empty()) {
// Convert IP address strings into net::IPAddress objects.
for (const auto& ip_str : allowlist_ip_strs) {
std::string_view ip_str_piece(ip_str);
if (ip_str_piece.size() >= 2 && ip_str_piece.front() == '[' &&
ip_str_piece.back() == ']') {
ip_str_piece.remove_prefix(1);
ip_str_piece.remove_suffix(1);
}
net::IPAddress ip;
if (!ip.AssignFromIPLiteral(ip_str_piece)) {
printf("Invalid IP address %s. Exiting...\n", ip_str.c_str());
return 1;
}
allowed_ips.push_back(ip);
if (ip.IsIPv4()) {
allowed_ips.push_back(net::ConvertIPv4ToIPv4MappedIPv6(ip));
} else if (ip.IsIPv4MappedIPv6()) {
allowed_ips.push_back(net::ConvertIPv4MappedIPv6ToIPv4(ip));
}
}
allowed_ips.push_back(net::IPAddress::IPv4Localhost());
allowed_ips.push_back(net::IPAddress::IPv6Localhost());
allowed_ips.push_back(
net::ConvertIPv4ToIPv4MappedIPv6(net::IPAddress::IPv4Localhost()));
}
}
if (cmd_line->HasSwitch("allowed-origins")) {
allowlist_origins = cmd_line->GetSwitchValueASCII("allowed-origins");
allowed_origins = base::SplitString(
allowlist_origins, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
}
if (!cmd_line->HasSwitch("silent") &&
cmd_line->GetSwitchValueASCII("log-level") != "OFF") {
UNSAFE_TODO(printf("Starting %s %s on port %u\n",
kChromeDriverProductShortName, kChromeDriverVersion,
port));
if (!allow_remote) {
printf("Only local connections are allowed.\n");
} else if (!allowed_ips.empty()) {
printf("Remote connections are allowed by an allowlist (%s).\n",
allowlist_ips.c_str());
} else {
printf("All remote connections are allowed. Use an allowlist instead!\n");
}
UNSAFE_TODO(printf("%s\n", GetPortProtectionMessage()));
fflush(stdout);
}
if (!InitLogging()) {
printf("Unable to initialize logging. Exiting...\n");
return 1;
}
if (cmd_line->HasSwitch("log-path")) {
VLOG(0) << "Starting " << kChromeDriverProductFullName << " "
<< kChromeDriverVersion << " on port " << port;
VLOG(0) << GetPortProtectionMessage();
}
#if BUILDFLAG(IS_LINUX)
EnsureSharedMemory(cmd_line);
#endif
mojo::core::Init();
#if BUILDFLAG(IS_OZONE)
InitializeOzoneKeyboardEngineManager();
#endif
base::ThreadPoolInstance::CreateAndStartWithDefaultParams(
kChromeDriverProductShortName);
RunServer(port, allow_remote, allowed_ips, allowed_origins, url_base,
adb_port);
// clean up
base::ThreadPoolInstance::Get()->Shutdown();
return 0;
}