// Copyright 2015 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/ssl/ssl_key_logger_impl.h"

#include <stdio.h>

#include <algorithm>
#include <utility>

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/sequence_checker.h"
#include "base/sequenced_task_runner.h"
#include "base/synchronization/lock.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/thread_annotations.h"

namespace net {

namespace {
// Bound the number of outstanding writes to bound memory usage. Some
// antiviruses point this at a pipe and then read too slowly. See
// https://crbug.com/566951 and https://crbug.com/914880.
static constexpr size_t kMaxOutstandingLines = 512;
}  // namespace

// An object which performs the blocking file operations on a background
// SequencedTaskRunner.
class SSLKeyLoggerImpl::Core
    : public base::RefCountedThreadSafe<SSLKeyLoggerImpl::Core> {
 public:
  Core() {
    DETACH_FROM_SEQUENCE(sequence_checker_);
    // That the user explicitly asked for debugging information would suggest
    // waiting to flush these to disk, but some buggy antiviruses point this at
    // a pipe and hang, so we avoid blocking shutdown. If writing to a real
    // file, writes should complete quickly enough that this does not matter.
    task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
        {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});
  }

  void SetFile(base::File file) {
    file_.reset(base::FileToFILE(std::move(file), "a"));
    if (!file_)
      DVLOG(1) << "Could not adopt file";
  }

  void OpenFile(const base::FilePath& path) {
    task_runner_->PostTask(FROM_HERE,
                           base::BindOnce(&Core::OpenFileImpl, this, path));
  }

  void WriteLine(const std::string& line) {
    bool was_empty;
    {
      base::AutoLock lock(lock_);
      was_empty = buffer_.empty();
      if (buffer_.size() < kMaxOutstandingLines) {
        buffer_.push_back(line);
      } else {
        lines_dropped_ = true;
      }
    }
    if (was_empty) {
      task_runner_->PostTask(FROM_HERE, base::BindOnce(&Core::Flush, this));
    }
  }

 private:
  friend class base::RefCountedThreadSafe<Core>;
  ~Core() = default;

  void OpenFileImpl(const base::FilePath& path) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    DCHECK(!file_);
    file_.reset(base::OpenFile(path, "a"));
    if (!file_)
      DVLOG(1) << "Could not open " << path.value();
  }

  void Flush() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    bool lines_dropped = false;
    std::vector<std::string> buffer;
    {
      base::AutoLock lock(lock_);
      std::swap(lines_dropped, lines_dropped_);
      std::swap(buffer, buffer_);
    }

    if (file_) {
      for (const auto& line : buffer) {
        fprintf(file_.get(), "%s\n", line.c_str());
      }
      if (lines_dropped) {
        fprintf(file_.get(), "# Some lines were dropped due to slow writes.\n");
      }
      fflush(file_.get());
    }
  }

  scoped_refptr<base::SequencedTaskRunner> task_runner_;
  base::ScopedFILE file_;
  SEQUENCE_CHECKER(sequence_checker_);

  base::Lock lock_;
  bool lines_dropped_ GUARDED_BY(lock_) = false;
  std::vector<std::string> buffer_ GUARDED_BY(lock_);

  DISALLOW_COPY_AND_ASSIGN(Core);
};

SSLKeyLoggerImpl::SSLKeyLoggerImpl(const base::FilePath& path)
    : core_(base::MakeRefCounted<Core>()) {
  core_->OpenFile(path);
}

SSLKeyLoggerImpl::SSLKeyLoggerImpl(base::File file)
    : core_(base::MakeRefCounted<Core>()) {
  core_->SetFile(std::move(file));
}

SSLKeyLoggerImpl::~SSLKeyLoggerImpl() = default;

void SSLKeyLoggerImpl::WriteLine(const std::string& line) {
  core_->WriteLine(line);
}

}  // namespace net
