// Copyright (c) 2009 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/socket/ssl_test_util.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")));
}

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 (!base::LaunchApp(command_line, false, true, &process_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(NULL));
  net::HostResolver::RequestInfo info(host_name, port);
  int rv = resolver->Resolve(info, &addr, NULL, NULL, NULL);
  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;
}

}  // namespace net