// Copyright 2013 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 "content/browser/media/webrtc_identity_store_backend.h"

#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/memory/scoped_vector.h"
#include "base/strings/string_util.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/net_errors.h"
#include "sql/error_delegate_util.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "url/gurl.h"
#include "webkit/browser/quota/special_storage_policy.h"

namespace content {

static const char* kWebRTCIdentityStoreDBName = "webrtc_identity_store";

static const base::FilePath::CharType kWebRTCIdentityStoreDirectory[] =
    FILE_PATH_LITERAL("WebRTCIdentityStore");

// Initializes the identity table, returning true on success.
static bool InitDB(sql::Connection* db) {
  if (db->DoesTableExist(kWebRTCIdentityStoreDBName)) {
    if (db->DoesColumnExist(kWebRTCIdentityStoreDBName, "origin") &&
        db->DoesColumnExist(kWebRTCIdentityStoreDBName, "identity_name") &&
        db->DoesColumnExist(kWebRTCIdentityStoreDBName, "common_name") &&
        db->DoesColumnExist(kWebRTCIdentityStoreDBName, "certificate") &&
        db->DoesColumnExist(kWebRTCIdentityStoreDBName, "private_key") &&
        db->DoesColumnExist(kWebRTCIdentityStoreDBName, "creation_time"))
      return true;

    if (!db->Execute("DROP TABLE webrtc_identity_store"))
      return false;
  }

  return db->Execute(
      "CREATE TABLE webrtc_identity_store"
      " ("
      "origin TEXT NOT NULL,"
      "identity_name TEXT NOT NULL,"
      "common_name TEXT NOT NULL,"
      "certificate BLOB NOT NULL,"
      "private_key BLOB NOT NULL,"
      "creation_time INTEGER)");
}

struct WebRTCIdentityStoreBackend::IdentityKey {
  IdentityKey(const GURL& origin, const std::string& identity_name)
      : origin(origin), identity_name(identity_name) {}

  bool operator<(const IdentityKey& other) const {
    return origin < other.origin ||
           (origin == other.origin && identity_name < other.identity_name);
  }

  GURL origin;
  std::string identity_name;
};

struct WebRTCIdentityStoreBackend::Identity {
  Identity(const std::string& common_name,
           const std::string& certificate,
           const std::string& private_key)
      : common_name(common_name),
        certificate(certificate),
        private_key(private_key),
        creation_time(base::Time::Now().ToInternalValue()) {}

  Identity(const std::string& common_name,
           const std::string& certificate,
           const std::string& private_key,
           int64 creation_time)
      : common_name(common_name),
        certificate(certificate),
        private_key(private_key),
        creation_time(creation_time) {}

  std::string common_name;
  std::string certificate;
  std::string private_key;
  int64 creation_time;
};

struct WebRTCIdentityStoreBackend::PendingFindRequest {
  PendingFindRequest(const GURL& origin,
                     const std::string& identity_name,
                     const std::string& common_name,
                     const FindIdentityCallback& callback)
      : origin(origin),
        identity_name(identity_name),
        common_name(common_name),
        callback(callback) {}

  ~PendingFindRequest() {}

  GURL origin;
  std::string identity_name;
  std::string common_name;
  FindIdentityCallback callback;
};

// The class encapsulates the database operations. All members except ctor and
// dtor should be accessed on the DB thread.
// It can be created/destroyed on any thread.
class WebRTCIdentityStoreBackend::SqlLiteStorage
    : public base::RefCountedThreadSafe<SqlLiteStorage> {
 public:
  SqlLiteStorage(base::TimeDelta validity_period,
                 const base::FilePath& path,
                 quota::SpecialStoragePolicy* policy)
      : validity_period_(validity_period), special_storage_policy_(policy) {
    if (!path.empty())
      path_ = path.Append(kWebRTCIdentityStoreDirectory);
  }

  void Load(IdentityMap* out_map);
  void Close();
  void AddIdentity(const GURL& origin,
                   const std::string& identity_name,
                   const Identity& identity);
  void DeleteIdentity(const GURL& origin,
                      const std::string& identity_name,
                      const Identity& identity);
  void DeleteBetween(base::Time delete_begin, base::Time delete_end);

  void SetValidityPeriodForTesting(base::TimeDelta validity_period) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
    DCHECK(!db_.get());
    validity_period_ = validity_period;
  }

 private:
  friend class base::RefCountedThreadSafe<SqlLiteStorage>;

  enum OperationType {
    ADD_IDENTITY,
    DELETE_IDENTITY
  };
  struct PendingOperation {
    PendingOperation(OperationType type,
                     const GURL& origin,
                     const std::string& identity_name,
                     const Identity& identity)
        : type(type),
          origin(origin),
          identity_name(identity_name),
          identity(identity) {}

    OperationType type;
    GURL origin;
    std::string identity_name;
    Identity identity;
  };
  typedef ScopedVector<PendingOperation> PendingOperationList;

  virtual ~SqlLiteStorage() {}
  void OnDatabaseError(int error, sql::Statement* stmt);
  void BatchOperation(OperationType type,
                      const GURL& origin,
                      const std::string& identity_name,
                      const Identity& identity);
  void Commit();

  base::TimeDelta validity_period_;
  // The file path of the DB. Empty if temporary.
  base::FilePath path_;
  scoped_refptr<quota::SpecialStoragePolicy> special_storage_policy_;
  scoped_ptr<sql::Connection> db_;
  // Batched DB operations pending to commit.
  PendingOperationList pending_operations_;

  DISALLOW_COPY_AND_ASSIGN(SqlLiteStorage);
};

WebRTCIdentityStoreBackend::WebRTCIdentityStoreBackend(
    const base::FilePath& path,
    quota::SpecialStoragePolicy* policy,
    base::TimeDelta validity_period)
    : validity_period_(validity_period),
      state_(NOT_STARTED),
      sql_lite_storage_(new SqlLiteStorage(validity_period, path, policy)) {}

bool WebRTCIdentityStoreBackend::FindIdentity(
    const GURL& origin,
    const std::string& identity_name,
    const std::string& common_name,
    const FindIdentityCallback& callback) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  if (state_ == CLOSED)
    return false;

  if (state_ != LOADED) {
    // Queues the request to wait for the DB to load.
    pending_find_requests_.push_back(
        new PendingFindRequest(origin, identity_name, common_name, callback));
    if (state_ == LOADING)
      return true;

    DCHECK_EQ(state_, NOT_STARTED);

    // Kick off loading the DB.
    scoped_ptr<IdentityMap> out_map(new IdentityMap());
    base::Closure task(
        base::Bind(&SqlLiteStorage::Load, sql_lite_storage_, out_map.get()));
    // |out_map| will be NULL after this call.
    if (BrowserThread::PostTaskAndReply(
            BrowserThread::DB,
            FROM_HERE,
            task,
            base::Bind(&WebRTCIdentityStoreBackend::OnLoaded,
                       this,
                       base::Passed(&out_map)))) {
      state_ = LOADING;
      return true;
    }
    // If it fails to post task, falls back to ERR_FILE_NOT_FOUND.
  }

  IdentityKey key(origin, identity_name);
  IdentityMap::iterator iter = identities_.find(key);
  if (iter != identities_.end() && iter->second.common_name == common_name) {
    base::TimeDelta age = base::Time::Now() - base::Time::FromInternalValue(
                                                  iter->second.creation_time);
    if (age < validity_period_) {
      // Identity found.
      return BrowserThread::PostTask(BrowserThread::IO,
                                     FROM_HERE,
                                     base::Bind(callback,
                                                net::OK,
                                                iter->second.certificate,
                                                iter->second.private_key));
    }
    // Removes the expired identity from the in-memory cache. The copy in the
    // database will be removed on the next load.
    identities_.erase(iter);
  }

  return BrowserThread::PostTask(
      BrowserThread::IO,
      FROM_HERE,
      base::Bind(callback, net::ERR_FILE_NOT_FOUND, "", ""));
}

void WebRTCIdentityStoreBackend::AddIdentity(const GURL& origin,
                                             const std::string& identity_name,
                                             const std::string& common_name,
                                             const std::string& certificate,
                                             const std::string& private_key) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  if (state_ == CLOSED)
    return;

  // If there is an existing identity for the same origin and identity_name,
  // delete it.
  IdentityKey key(origin, identity_name);
  Identity identity(common_name, certificate, private_key);

  if (identities_.find(key) != identities_.end()) {
    if (!BrowserThread::PostTask(BrowserThread::DB,
                                 FROM_HERE,
                                 base::Bind(&SqlLiteStorage::DeleteIdentity,
                                            sql_lite_storage_,
                                            origin,
                                            identity_name,
                                            identities_.find(key)->second)))
      return;
  }
  identities_.insert(std::pair<IdentityKey, Identity>(key, identity));

  BrowserThread::PostTask(BrowserThread::DB,
                          FROM_HERE,
                          base::Bind(&SqlLiteStorage::AddIdentity,
                                     sql_lite_storage_,
                                     origin,
                                     identity_name,
                                     identity));
}

void WebRTCIdentityStoreBackend::Close() {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        base::Bind(&WebRTCIdentityStoreBackend::Close, this));
    return;
  }

  if (state_ == CLOSED)
    return;

  state_ = CLOSED;
  BrowserThread::PostTask(
      BrowserThread::DB,
      FROM_HERE,
      base::Bind(&SqlLiteStorage::Close, sql_lite_storage_));
}

void WebRTCIdentityStoreBackend::DeleteBetween(base::Time delete_begin,
                                               base::Time delete_end,
                                               const base::Closure& callback) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  if (state_ == CLOSED)
    return;

  // Delete the in-memory cache.
  IdentityMap::iterator it = identities_.begin();
  while (it != identities_.end()) {
    if (it->second.creation_time >= delete_begin.ToInternalValue() &&
        it->second.creation_time <= delete_end.ToInternalValue()) {
      identities_.erase(it++);
    } else {
      ++it;
    }
  }
  BrowserThread::PostTaskAndReply(BrowserThread::DB,
                                  FROM_HERE,
                                  base::Bind(&SqlLiteStorage::DeleteBetween,
                                             sql_lite_storage_,
                                             delete_begin,
                                             delete_end),
                                  callback);
}

void WebRTCIdentityStoreBackend::SetValidityPeriodForTesting(
    base::TimeDelta validity_period) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  validity_period_ = validity_period;
  BrowserThread::PostTask(
      BrowserThread::DB,
      FROM_HERE,
      base::Bind(&SqlLiteStorage::SetValidityPeriodForTesting,
                 sql_lite_storage_,
                 validity_period));
}

WebRTCIdentityStoreBackend::~WebRTCIdentityStoreBackend() {}

void WebRTCIdentityStoreBackend::OnLoaded(scoped_ptr<IdentityMap> out_map) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  if (state_ != LOADING)
    return;

  DVLOG(3) << "WebRTC identity store has loaded.";

  state_ = LOADED;
  identities_.swap(*out_map);

  for (size_t i = 0; i < pending_find_requests_.size(); ++i) {
    FindIdentity(pending_find_requests_[i]->origin,
                 pending_find_requests_[i]->identity_name,
                 pending_find_requests_[i]->common_name,
                 pending_find_requests_[i]->callback);
    delete pending_find_requests_[i];
  }
  pending_find_requests_.clear();
}

//
// Implementation of SqlLiteStorage.
//

void WebRTCIdentityStoreBackend::SqlLiteStorage::Load(IdentityMap* out_map) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  DCHECK(!db_.get());

  // Ensure the parent directory for storing certs is created before reading
  // from it.
  const base::FilePath dir = path_.DirName();
  if (!base::PathExists(dir) && !base::CreateDirectory(dir)) {
    DVLOG(2) << "Unable to open DB file path.";
    return;
  }

  db_.reset(new sql::Connection());

  db_->set_error_callback(base::Bind(&SqlLiteStorage::OnDatabaseError, this));

  if (!db_->Open(path_)) {
    DVLOG(2) << "Unable to open DB.";
    db_.reset();
    return;
  }

  if (!InitDB(db_.get())) {
    DVLOG(2) << "Unable to init DB.";
    db_.reset();
    return;
  }

  db_->Preload();

  // Delete expired identities.
  DeleteBetween(base::Time(), base::Time::Now() - validity_period_);

  // Slurp all the identities into the out_map.
  sql::Statement stmt(db_->GetUniqueStatement(
      "SELECT origin, identity_name, common_name, "
      "certificate, private_key, creation_time "
      "FROM webrtc_identity_store"));
  CHECK(stmt.is_valid());

  while (stmt.Step()) {
    IdentityKey key(GURL(stmt.ColumnString(0)), stmt.ColumnString(1));
    std::string common_name(stmt.ColumnString(2));
    std::string cert, private_key;
    stmt.ColumnBlobAsString(3, &cert);
    stmt.ColumnBlobAsString(4, &private_key);
    int64 creation_time = stmt.ColumnInt64(5);
    std::pair<IdentityMap::iterator, bool> result =
        out_map->insert(std::pair<IdentityKey, Identity>(
            key, Identity(common_name, cert, private_key, creation_time)));
    DCHECK(result.second);
  }
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::Close() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  Commit();
  db_.reset();
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::AddIdentity(
    const GURL& origin,
    const std::string& identity_name,
    const Identity& identity) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  if (!db_.get())
    return;

  // Do not add for session only origins.
  if (special_storage_policy_.get() &&
      !special_storage_policy_->IsStorageProtected(origin) &&
      special_storage_policy_->IsStorageSessionOnly(origin)) {
    return;
  }
  BatchOperation(ADD_IDENTITY, origin, identity_name, identity);
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::DeleteIdentity(
    const GURL& origin,
    const std::string& identity_name,
    const Identity& identity) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  if (!db_.get())
    return;
  BatchOperation(DELETE_IDENTITY, origin, identity_name, identity);
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::DeleteBetween(
    base::Time delete_begin,
    base::Time delete_end) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  if (!db_.get())
    return;

  // Commit pending operations first.
  Commit();

  sql::Statement del_stmt(db_->GetCachedStatement(
      SQL_FROM_HERE,
      "DELETE FROM webrtc_identity_store"
      " WHERE creation_time >= ? AND creation_time <= ?"));
  CHECK(del_stmt.is_valid());

  del_stmt.BindInt64(0, delete_begin.ToInternalValue());
  del_stmt.BindInt64(1, delete_end.ToInternalValue());

  sql::Transaction transaction(db_.get());
  if (!transaction.Begin()) {
    DVLOG(2) << "Failed to begin the transaction.";
    return;
  }

  if (!del_stmt.Run()) {
    DVLOG(2) << "Failed to run the delete statement.";
    return;
  }

  if (!transaction.Commit())
    DVLOG(2) << "Failed to commit the transaction.";
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::OnDatabaseError(
    int error,
    sql::Statement* stmt) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));

  db_->RazeAndClose();
  // It's not safe to reset |db_| here.
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::BatchOperation(
    OperationType type,
    const GURL& origin,
    const std::string& identity_name,
    const Identity& identity) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  // Commit every 30 seconds.
  static const base::TimeDelta kCommitInterval(
      base::TimeDelta::FromSeconds(30));
  // Commit right away if we have more than 512 outstanding operations.
  static const size_t kCommitAfterBatchSize = 512;

  // We do a full copy of the cert here, and hopefully just here.
  scoped_ptr<PendingOperation> operation(
      new PendingOperation(type, origin, identity_name, identity));

  pending_operations_.push_back(operation.release());

  if (pending_operations_.size() == 1) {
    // We've gotten our first entry for this batch, fire off the timer.
    BrowserThread::PostDelayedTask(BrowserThread::DB,
                                   FROM_HERE,
                                   base::Bind(&SqlLiteStorage::Commit, this),
                                   kCommitInterval);
  } else if (pending_operations_.size() >= kCommitAfterBatchSize) {
    // We've reached a big enough batch, fire off a commit now.
    BrowserThread::PostTask(BrowserThread::DB,
                            FROM_HERE,
                            base::Bind(&SqlLiteStorage::Commit, this));
  }
}

void WebRTCIdentityStoreBackend::SqlLiteStorage::Commit() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB));
  // Maybe an old timer fired or we are already Close()'ed.
  if (!db_.get() || pending_operations_.empty())
    return;

  sql::Statement add_stmt(db_->GetCachedStatement(
      SQL_FROM_HERE,
      "INSERT INTO webrtc_identity_store "
      "(origin, identity_name, common_name, certificate,"
      " private_key, creation_time) VALUES"
      " (?,?,?,?,?,?)"));

  CHECK(add_stmt.is_valid());

  sql::Statement del_stmt(db_->GetCachedStatement(
      SQL_FROM_HERE,
      "DELETE FROM webrtc_identity_store WHERE origin=? AND identity_name=?"));

  CHECK(del_stmt.is_valid());

  sql::Transaction transaction(db_.get());
  if (!transaction.Begin()) {
    DVLOG(2) << "Failed to begin the transaction.";
    return;
  }

  // Swaps |pending_operations_| into a temporary list to make sure
  // |pending_operations_| is always cleared in case of DB errors.
  PendingOperationList pending_operations_copy;
  pending_operations_.swap(pending_operations_copy);

  for (PendingOperationList::const_iterator it =
           pending_operations_copy.begin();
       it != pending_operations_copy.end();
       ++it) {
    switch ((*it)->type) {
      case ADD_IDENTITY: {
        add_stmt.Reset(true);
        add_stmt.BindString(0, (*it)->origin.spec());
        add_stmt.BindString(1, (*it)->identity_name);
        add_stmt.BindString(2, (*it)->identity.common_name);
        const std::string& cert = (*it)->identity.certificate;
        add_stmt.BindBlob(3, cert.data(), cert.size());
        const std::string& private_key = (*it)->identity.private_key;
        add_stmt.BindBlob(4, private_key.data(), private_key.size());
        add_stmt.BindInt64(5, (*it)->identity.creation_time);
        if (!add_stmt.Run()) {
          DVLOG(2) << "Failed to add the identity to DB.";
          return;
        }
        break;
      }
      case DELETE_IDENTITY:
        del_stmt.Reset(true);
        del_stmt.BindString(0, (*it)->origin.spec());
        del_stmt.BindString(1, (*it)->identity_name);
        if (!del_stmt.Run()) {
          DVLOG(2) << "Failed to delete the identity from DB.";
          return;
        }
        break;

      default:
        NOTREACHED();
        break;
    }
  }

  if (!transaction.Commit())
    DVLOG(2) << "Failed to commit the transaction.";
}

}  // namespace content
