blob: 4abed54e4686929680d7fabc10793bcf3121eba5 [file] [log] [blame]
// Copyright (c) 2010 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 "net/test/test_server.h"
#include <algorithm>
#include <string>
#include <vector>
#include "build/build_config.h"
#if defined(OS_WIN)
#include <windows.h>
#include <wincrypt.h>
#elif defined(OS_MACOSX)
#include "net/base/x509_certificate.h"
#endif
#include "base/file_util.h"
#include "base/leak_annotations.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/utf_string_conversions.h"
#include "net/base/cert_test_util.h"
#include "net/base/host_resolver.h"
#include "net/base/net_test_constants.h"
#include "net/base/test_completion_callback.h"
#include "net/socket/tcp_client_socket.h"
#include "net/socket/tcp_pinger.h"
#include "testing/platform_test.h"
#if defined(OS_WIN)
#pragma comment(lib, "crypt32.lib")
#endif
namespace net {
#if defined(OS_MACOSX)
void SetMacTestCertificate(X509Certificate* cert);
#endif
// static
const char TestServerLauncher::kHostName[] = "127.0.0.1";
const char TestServerLauncher::kMismatchedHostName[] = "localhost";
const int TestServerLauncher::kOKHTTPSPort = 9443;
const int TestServerLauncher::kBadHTTPSPort = 9666;
// The issuer name of the cert that should be trusted for the test to work.
const wchar_t TestServerLauncher::kCertIssuerName[] = L"Test CA";
TestServerLauncher::TestServerLauncher() : process_handle_(
base::kNullProcessHandle),
forking_(false),
connection_attempts_(kDefaultTestConnectionAttempts),
connection_timeout_(kDefaultTestConnectionTimeout)
{
InitCertPath();
}
TestServerLauncher::TestServerLauncher(int connection_attempts,
int connection_timeout)
: process_handle_(base::kNullProcessHandle),
forking_(false),
connection_attempts_(connection_attempts),
connection_timeout_(connection_timeout)
{
InitCertPath();
}
void TestServerLauncher::InitCertPath() {
PathService::Get(base::DIR_SOURCE_ROOT, &cert_dir_);
cert_dir_ = cert_dir_.Append(FILE_PATH_LITERAL("net"))
.Append(FILE_PATH_LITERAL("data"))
.Append(FILE_PATH_LITERAL("ssl"))
.Append(FILE_PATH_LITERAL("certificates"));
}
namespace {
void AppendToPythonPath(const FilePath& dir) {
// Do nothing if dir already on path.
#if defined(OS_WIN)
const wchar_t kPythonPath[] = L"PYTHONPATH";
// TODO(dkegel): handle longer PYTHONPATH variables
wchar_t oldpath[4096];
if (GetEnvironmentVariable(kPythonPath, oldpath, arraysize(oldpath)) == 0) {
SetEnvironmentVariableW(kPythonPath, dir.value().c_str());
} else if (!wcsstr(oldpath, dir.value().c_str())) {
std::wstring newpath(oldpath);
newpath.append(L";");
newpath.append(dir.value());
SetEnvironmentVariableW(kPythonPath, newpath.c_str());
}
#elif defined(OS_POSIX)
const char kPythonPath[] = "PYTHONPATH";
const char* oldpath = getenv(kPythonPath);
// setenv() leaks memory intentionally on Mac
if (!oldpath) {
setenv(kPythonPath, dir.value().c_str(), 1);
} else if (!strstr(oldpath, dir.value().c_str())) {
std::string newpath(oldpath);
newpath.append(":");
newpath.append(dir.value());
setenv(kPythonPath, newpath.c_str(), 1);
}
#endif
}
} // end namespace
void TestServerLauncher::SetPythonPath() {
FilePath third_party_dir;
CHECK(PathService::Get(base::DIR_SOURCE_ROOT, &third_party_dir));
third_party_dir = third_party_dir.Append(FILE_PATH_LITERAL("third_party"));
AppendToPythonPath(third_party_dir.Append(FILE_PATH_LITERAL("tlslite")));
AppendToPythonPath(third_party_dir.Append(FILE_PATH_LITERAL("pyftpdlib")));
// Locate the Python code generated by the protocol buffers compiler.
FilePath generated_code_dir;
CHECK(PathService::Get(base::DIR_EXE, &generated_code_dir));
generated_code_dir = generated_code_dir.Append(FILE_PATH_LITERAL("pyproto"));
AppendToPythonPath(generated_code_dir);
AppendToPythonPath(generated_code_dir.Append(FILE_PATH_LITERAL("sync_pb")));
}
bool TestServerLauncher::Start(Protocol protocol,
const std::string& host_name, int port,
const FilePath& document_root,
const FilePath& cert_path,
const std::wstring& file_root_url) {
if (!cert_path.value().empty()) {
if (!LoadTestRootCert())
return false;
if (!CheckCATrusted())
return false;
}
std::string port_str = IntToString(port);
// Get path to python server script
FilePath testserver_path;
if (!PathService::Get(base::DIR_SOURCE_ROOT, &testserver_path))
return false;
testserver_path = testserver_path
.Append(FILE_PATH_LITERAL("net"))
.Append(FILE_PATH_LITERAL("tools"))
.Append(FILE_PATH_LITERAL("testserver"))
.Append(FILE_PATH_LITERAL("testserver.py"));
PathService::Get(base::DIR_SOURCE_ROOT, &document_root_dir_);
document_root_dir_ = document_root_dir_.Append(document_root);
SetPythonPath();
#if defined(OS_WIN)
// Get path to python interpreter
if (!PathService::Get(base::DIR_SOURCE_ROOT, &python_runtime_))
return false;
python_runtime_ = python_runtime_
.Append(FILE_PATH_LITERAL("third_party"))
.Append(FILE_PATH_LITERAL("python_24"))
.Append(FILE_PATH_LITERAL("python.exe"));
std::wstring command_line =
L"\"" + python_runtime_.ToWStringHack() + L"\" " +
L"\"" + testserver_path.ToWStringHack() +
L"\" --port=" + UTF8ToWide(port_str) +
L" --data-dir=\"" + document_root_dir_.ToWStringHack() + L"\"";
if (protocol == ProtoFTP)
command_line.append(L" -f");
if (!cert_path.value().empty()) {
command_line.append(L" --https=\"");
command_line.append(cert_path.ToWStringHack());
command_line.append(L"\"");
}
if (!file_root_url.empty()) {
command_line.append(L" --file-root-url=\"");
command_line.append(file_root_url);
command_line.append(L"\"");
}
// Deliberately do not pass the --forking flag. It breaks the tests
// on Windows.
if (!LaunchTestServerAsJob(command_line,
true,
&process_handle_,
&job_handle_)) {
LOG(ERROR) << "Failed to launch " << command_line;
return false;
}
#elif defined(OS_POSIX)
std::vector<std::string> command_line;
command_line.push_back("python");
command_line.push_back(testserver_path.value());
command_line.push_back("--port=" + port_str);
command_line.push_back("--data-dir=" + document_root_dir_.value());
if (protocol == ProtoFTP)
command_line.push_back("-f");
if (!cert_path.value().empty())
command_line.push_back("--https=" + cert_path.value());
if (forking_)
command_line.push_back("--forking");
base::file_handle_mapping_vector no_mappings;
LOG(INFO) << "Trying to launch " << command_line[0] << " ...";
if (!base::LaunchApp(command_line, no_mappings, false, &process_handle_)) {
LOG(ERROR) << "Failed to launch " << command_line[0] << " ...";
return false;
}
#endif
// Let the server start, then verify that it's up.
// Our server is Python, and takes about 500ms to start
// up the first time, and about 200ms after that.
if (!WaitToStart(host_name, port)) {
LOG(ERROR) << "Failed to connect to server";
Stop();
return false;
}
LOG(INFO) << "Started on port " << port_str;
return true;
}
bool TestServerLauncher::WaitToStart(const std::string& host_name, int port) {
// Verify that the webserver is actually started.
// Otherwise tests can fail if they run faster than Python can start.
net::AddressList addr;
scoped_refptr<net::HostResolver> resolver(
net::CreateSystemHostResolver(net::HostResolver::kDefaultParallelism));
net::HostResolver::RequestInfo info(host_name, port);
int rv = resolver->Resolve(info, &addr, NULL, NULL, BoundNetLog());
if (rv != net::OK)
return false;
net::TCPPinger pinger(addr);
rv = pinger.Ping(base::TimeDelta::FromMilliseconds(connection_timeout_),
connection_attempts_);
return rv == net::OK;
}
bool TestServerLauncher::WaitToFinish(int timeout_ms) {
if (!process_handle_)
return true;
bool ret = base::WaitForSingleProcess(process_handle_, timeout_ms);
if (ret) {
base::CloseProcessHandle(process_handle_);
process_handle_ = base::kNullProcessHandle;
LOG(INFO) << "Finished.";
} else {
LOG(INFO) << "Timed out.";
}
return ret;
}
bool TestServerLauncher::Stop() {
if (!process_handle_)
return true;
// First check if the process has already terminated.
bool ret = base::WaitForSingleProcess(process_handle_, 0);
if (!ret)
ret = base::KillProcess(process_handle_, 1, true);
if (ret) {
base::CloseProcessHandle(process_handle_);
process_handle_ = base::kNullProcessHandle;
LOG(INFO) << "Stopped.";
} else {
LOG(INFO) << "Kill failed?";
}
return ret;
}
TestServerLauncher::~TestServerLauncher() {
#if defined(OS_MACOSX)
SetMacTestCertificate(NULL);
#endif
Stop();
}
FilePath TestServerLauncher::GetRootCertPath() {
FilePath path(cert_dir_);
path = path.AppendASCII("root_ca_cert.crt");
return path;
}
FilePath TestServerLauncher::GetOKCertPath() {
FilePath path(cert_dir_);
path = path.AppendASCII("ok_cert.pem");
return path;
}
FilePath TestServerLauncher::GetExpiredCertPath() {
FilePath path(cert_dir_);
path = path.AppendASCII("expired_cert.pem");
return path;
}
bool TestServerLauncher::LoadTestRootCert() {
#if defined(USE_NSS)
if (cert_)
return true;
// TODO(dkegel): figure out how to get this to only happen once?
// This currently leaks a little memory.
// TODO(dkegel): fix the leak and remove the entry in
// tools/valgrind/memcheck/suppressions.txt
ANNOTATE_SCOPED_MEMORY_LEAK; // Tell heap checker about the leak.
cert_ = LoadTemporaryRootCert(GetRootCertPath());
DCHECK(cert_);
return (cert_ != NULL);
#elif defined(OS_MACOSX)
X509Certificate* cert = LoadTemporaryRootCert(GetRootCertPath());
if (!cert)
return false;
SetMacTestCertificate(cert);
return true;
#else
return true;
#endif
}
bool TestServerLauncher::CheckCATrusted() {
#if defined(OS_WIN)
HCERTSTORE cert_store = CertOpenSystemStore(NULL, L"ROOT");
if (!cert_store) {
LOG(ERROR) << " could not open trusted root CA store";
return false;
}
PCCERT_CONTEXT cert =
CertFindCertificateInStore(cert_store,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_ISSUER_STR,
kCertIssuerName,
NULL);
if (cert)
CertFreeCertificateContext(cert);
CertCloseStore(cert_store, 0);
if (!cert) {
LOG(ERROR) << " TEST CONFIGURATION ERROR: you need to import the test ca "
"certificate to your trusted roots for this test to work. "
"For more info visit:\n"
"http://dev.chromium.org/developers/testing\n";
return false;
}
#endif
return true;
}
#if defined(OS_WIN)
bool LaunchTestServerAsJob(const std::wstring& cmdline,
bool start_hidden,
base::ProcessHandle* process_handle,
ScopedHandle* job_handle) {
// Launch test server process.
STARTUPINFO startup_info = {0};
startup_info.cb = sizeof(startup_info);
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = start_hidden ? SW_HIDE : SW_SHOW;
PROCESS_INFORMATION process_info;
// If this code is run under a debugger, the test server process is
// automatically associated with a job object created by the debugger.
// The CREATE_BREAKAWAY_FROM_JOB flag is used to prevent this.
if (!CreateProcess(NULL,
const_cast<wchar_t*>(cmdline.c_str()), NULL, NULL,
FALSE, CREATE_BREAKAWAY_FROM_JOB, NULL, NULL,
&startup_info, &process_info)) {
LOG(ERROR) << "Could not create process.";
return false;
}
CloseHandle(process_info.hThread);
// If the caller wants the process handle, we won't close it.
if (process_handle) {
*process_handle = process_info.hProcess;
} else {
CloseHandle(process_info.hProcess);
}
// Create a JobObject and associate the test server process with it.
job_handle->Set(CreateJobObject(NULL, NULL));
if (!job_handle->IsValid()) {
LOG(ERROR) << "Could not create JobObject.";
return false;
} else {
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limit_info = {0};
limit_info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (0 == SetInformationJobObject(job_handle->Get(),
JobObjectExtendedLimitInformation, &limit_info, sizeof(limit_info))) {
LOG(ERROR) << "Could not SetInformationJobObject.";
return false;
}
if (0 == AssignProcessToJobObject(job_handle->Get(),
process_info.hProcess)) {
LOG(ERROR) << "Could not AssignProcessToObject.";
return false;
}
}
return true;
}
#endif
} // namespace net