blob: 57a0b04f0587e359f7aacd187a1fcf1cde401e24 [file] [log] [blame]
// 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