// Copyright (c) 2006-2008 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 <string>
#include <algorithm>

#include "net/socket/ssl_test_util.h"

#include "build/build_config.h"

#if defined(OS_WIN)
#include <windows.h>
#include <wincrypt.h>
#elif defined(OS_LINUX)
#include <nspr.h>
#include <nss.h>
#include <secerr.h>
// Work around https://bugzilla.mozilla.org/show_bug.cgi?id=455424
// until NSS 3.12.2 comes out and we update to it.
#define Lock FOO_NSS_Lock
#include <ssl.h>
#include <sslerr.h>
#include <pk11pub.h>
#undef Lock
#include "base/nss_init.h"
#endif

#include "base/file_util.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/string_util.h"
#include "net/base/host_resolver.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 {

#if defined(OS_LINUX)
static CERTCertificate* LoadTemporaryCert(const FilePath& filename) {
  base::EnsureNSSInit();

  std::string rawcert;
  if (!file_util::ReadFileToString(filename.ToWStringHack(), &rawcert)) {
    LOG(ERROR) << "Can't load certificate " << filename.ToWStringHack();
    return NULL;
  }

  CERTCertificate *cert;
  cert = CERT_DecodeCertFromPackage(const_cast<char *>(rawcert.c_str()),
                                    rawcert.length());
  if (!cert) {
    LOG(ERROR) << "Can't convert certificate " << filename.ToWStringHack();
    return NULL;
  }

  // TODO(port): remove this const_cast after NSS 3.12.3 is released
  CERTCertTrust trust;
  int rv = CERT_DecodeTrustString(&trust, const_cast<char *>("TCu,Cu,Tu"));
  if (rv != SECSuccess) {
    LOG(ERROR) << "Can't decode trust string";
    CERT_DestroyCertificate(cert);
    return NULL;
  }

  rv = CERT_ChangeCertTrust(CERT_GetDefaultCertDB(), cert, &trust);
  if (rv != SECSuccess) {
    LOG(ERROR) << "Can't change trust for certificate "
               << filename.ToWStringHack();
    CERT_DestroyCertificate(cert);
    return NULL;
  }

  return cert;
}
#endif

}  // namespace

namespace net {

// 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_(NULL)
#if defined(OS_LINUX)
, cert_(NULL)
#endif
{
  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";
  // FIXME(dkegel): handle longer PYTHONPATH variables
  wchar_t oldpath[4096];
  if (GetEnvironmentVariable(kPythonPath, oldpath, sizeof(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;
  ASSERT_TRUE(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"\"");
  }

  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(WideToUTF8(testserver_path.ToWStringHack()));
  command_line.push_back("--port=" + port_str);
  command_line.push_back("--data-dir=" +
                         WideToUTF8(document_root_dir_.ToWStringHack()));
  if (protocol == ProtoFTP)
    command_line.push_back("-f");
  if (!cert_path.value().empty())
    command_line.push_back("--https=" + WideToUTF8(cert_path.ToWStringHack()));

  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(new net::HostResolver);
  net::HostResolver::RequestInfo info(host_name, port);
  int rv = resolver->Resolve(info, &addr, NULL, NULL);
  if (rv != net::OK)
    return false;

  net::TCPPinger pinger(addr);
  rv = pinger.Ping();
  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_ = NULL;
    LOG(INFO) << "Finished.";
  } else {
    LOG(INFO) << "Timed out.";
  }
  return ret;
}

bool TestServerLauncher::Stop() {
  if (!process_handle_)
    return true;

  bool ret = base::KillProcess(process_handle_, 1, true);
  if (ret) {
    base::CloseProcessHandle(process_handle_);
    process_handle_ = NULL;
    LOG(INFO) << "Stopped.";
  } else {
    LOG(INFO) << "Kill failed?";
  }

  return ret;
}

TestServerLauncher::~TestServerLauncher() {
#if defined(OS_LINUX)
  if (cert_)
    CERT_DestroyCertificate(reinterpret_cast<CERTCertificate*>(cert_));
#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(OS_LINUX)
  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/suppressions.txt
  cert_ = reinterpret_cast<PrivateCERTCertificate*>(
          LoadTemporaryCert(GetRootCertPath()));
  DCHECK(cert_);
  return (cert_ != NULL);
#else
  return true;
#endif
}

bool TestServerLauncher::CheckCATrusted() {
// TODO(port): Port either this or LoadTemporaryCert to MacOSX.
#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