| /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
| /* |
| * Copyright (c) 2014-2022, Regents of the University of California. |
| * |
| * This file is part of NDNS (Named Data Networking Domain Name Service). |
| * See AUTHORS.md for complete list of NDNS authors and contributors. |
| * |
| * NDNS is free software: you can redistribute it and/or modify it under the terms |
| * of the GNU General Public License as published by the Free Software Foundation, |
| * either version 3 of the License, or (at your option) any later version. |
| * |
| * NDNS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; |
| * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR |
| * PURPOSE. See the GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License along with |
| * NDNS, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| #include "db-mgr.hpp" |
| #include "logger.hpp" |
| #include "clients/response.hpp" |
| #include "util/util.hpp" |
| |
| namespace ndn { |
| namespace ndns { |
| |
| NDNS_LOG_INIT(DbMgr); |
| |
| static const std::string NDNS_SCHEMA = R"SQL( |
| CREATE TABLE IF NOT EXISTS zones ( |
| id INTEGER NOT NULL PRIMARY KEY, |
| name BLOB NOT NULL UNIQUE, |
| ttl INTEGER NOT NULL |
| ); |
| |
| CREATE TABLE IF NOT EXISTS zone_info ( |
| zone_id INTEGER NOT NULL, |
| key TEXT NOT NULL, |
| value BLOB NOT NULL, |
| PRIMARY KEY(zone_id, key), |
| FOREIGN KEY(zone_id) REFERENCES zones(id) ON UPDATE CASCADE ON DELETE CASCADE |
| ); |
| |
| CREATE TABLE IF NOT EXISTS rrsets ( |
| id INTEGER NOT NULL PRIMARY KEY, |
| zone_id INTEGER NOT NULL, |
| label BLOB NOT NULL, |
| type BLOB NOT NULL, |
| version BLOB NOT NULL, |
| ttl INTEGER NOT NULL, |
| data BLOB NOT NULL, |
| FOREIGN KEY(zone_id) REFERENCES zones(id) ON UPDATE CASCADE ON DELETE CASCADE |
| ); |
| |
| CREATE UNIQUE INDEX rrsets_zone_id_label_type_version |
| ON rrsets(zone_id, label, type, version); |
| )SQL"; |
| |
| DbMgr::DbMgr(const std::string& dbFile) |
| : m_dbFile(dbFile) |
| , m_conn(nullptr) |
| { |
| if (m_dbFile.empty()) |
| m_dbFile = getDefaultDatabaseFile(); |
| |
| open(); |
| |
| NDNS_LOG_INFO("open database: " << m_dbFile); |
| } |
| |
| DbMgr::~DbMgr() |
| { |
| close(); |
| } |
| |
| void |
| DbMgr::open() |
| { |
| int res = sqlite3_open_v2(m_dbFile.data(), &m_conn, |
| SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, |
| #ifdef DISABLE_SQLITE3_FS_LOCKING |
| "unix-dotfile" |
| #else |
| nullptr |
| #endif |
| ); |
| |
| if (res != SQLITE_OK) { |
| NDNS_LOG_FATAL("Cannot open the db file: " << m_dbFile); |
| NDN_THROW(ConnectError("Cannot open the db file: " + m_dbFile)); |
| } |
| |
| // ignore any errors from DB creation (command will fail for the existing database, which is ok) |
| sqlite3_exec(m_conn, NDNS_SCHEMA.data(), nullptr, nullptr, nullptr); |
| } |
| |
| void |
| DbMgr::close() |
| { |
| if (m_conn == nullptr) |
| return; |
| |
| int ret = sqlite3_close(m_conn); |
| if (ret != SQLITE_OK) { |
| NDNS_LOG_FATAL("Cannot close the db: " << m_dbFile); |
| } |
| else { |
| m_conn = nullptr; |
| NDNS_LOG_INFO("Close database: " << m_dbFile); |
| } |
| } |
| |
| void |
| DbMgr::clearAllData() |
| { |
| const char* sql = "DELETE FROM zones; DELETE FROM rrsets;"; |
| |
| // sqlite3_step cannot execute multiple SQL statements |
| int rc = sqlite3_exec(m_conn, sql, nullptr, nullptr, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(ExecuteError(sql)); |
| } |
| |
| NDNS_LOG_INFO("clear all the data in the database: " << m_dbFile); |
| } |
| |
| void |
| DbMgr::saveName(const Name& name, sqlite3_stmt* stmt, int iCol, bool isStatic) |
| { |
| static const uint8_t dummy = 0; |
| const auto& wire = name.wireEncode(); |
| const auto* ptr = wire.value(); |
| if (ptr == nullptr) { |
| // if value() returns nullptr (i.e., value_size() == 0), pass a non-null dummy |
| // pointer instead; we cannot bind nullptr because the column may be "NOT NULL" |
| ptr = &dummy; |
| } |
| sqlite3_bind_blob(stmt, iCol, ptr, wire.value_size(), isStatic ? SQLITE_STATIC : SQLITE_TRANSIENT); |
| } |
| |
| Name |
| DbMgr::restoreName(sqlite3_stmt* stmt, int iCol) |
| { |
| Name name; |
| |
| const uint8_t* buffer = static_cast<const uint8_t*>(sqlite3_column_blob(stmt, iCol)); |
| size_t nBytesLeft = sqlite3_column_bytes(stmt, iCol); |
| |
| while (nBytesLeft > 0) { |
| bool hasDecodingSucceeded; |
| name::Component component; |
| std::tie(hasDecodingSucceeded, component) = Block::fromBuffer({buffer, nBytesLeft}); |
| if (!hasDecodingSucceeded) { |
| NDN_THROW(Error("Error while decoding name from the database")); |
| } |
| name.append(component); |
| buffer += component.size(); |
| nBytesLeft -= component.size(); |
| } |
| |
| return name; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////////////////////// |
| // Zone |
| /////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| void |
| DbMgr::insert(Zone& zone) |
| { |
| if (zone.getId() > 0) |
| return; |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "INSERT INTO zones (name, ttl) VALUES (?, ?)"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| saveName(zone.getName(), stmt, 1); |
| sqlite3_bind_int(stmt, 2, zone.getTtl().count()); |
| |
| rc = sqlite3_step(stmt); |
| if (rc != SQLITE_DONE) { |
| sqlite3_finalize(stmt); |
| NDN_THROW(ExecuteError(sql)); |
| } |
| |
| zone.setId(sqlite3_last_insert_rowid(m_conn)); |
| sqlite3_finalize(stmt); |
| } |
| |
| void |
| DbMgr::setZoneInfo(Zone& zone, |
| const std::string& key, |
| const Block& value) |
| { |
| if (zone.getId() == 0) { |
| NDN_THROW(Error("zone has not been initialized")); |
| } |
| |
| if (key.length() > 10) { |
| NDN_THROW(Error("key length should not exceed 10")); |
| } |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "INSERT OR REPLACE INTO zone_info (zone_id, key, value) VALUES (?, ?, ?)"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int(stmt, 1, zone.getId()); |
| sqlite3_bind_text(stmt, 2, key.data(), key.length(), SQLITE_STATIC); |
| sqlite3_bind_blob(stmt, 3, value.data(), value.size(), SQLITE_STATIC); |
| |
| rc = sqlite3_step(stmt); |
| if (rc != SQLITE_DONE) { |
| sqlite3_finalize(stmt); |
| NDN_THROW(ExecuteError(sql)); |
| } |
| |
| sqlite3_finalize(stmt); |
| } |
| |
| std::map<std::string, Block> |
| DbMgr::getZoneInfo(Zone& zone) |
| { |
| using std::string; |
| std::map<string, Block> rtn; |
| |
| if (zone.getId() == 0) { |
| find(zone); |
| } |
| |
| if (zone.getId() == 0) { |
| NDN_THROW(Error("zone has not been initialized")); |
| } |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "SELECT key, value FROM zone_info WHERE zone_id=?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int(stmt, 1, zone.getId()); |
| |
| while (sqlite3_step(stmt) == SQLITE_ROW) { |
| const char* key = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)); |
| rtn[string(key)] = Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 1)), |
| sqlite3_column_bytes(stmt, 1))); |
| } |
| |
| sqlite3_finalize(stmt); |
| return rtn; |
| } |
| |
| bool |
| DbMgr::find(Zone& zone) |
| { |
| sqlite3_stmt* stmt; |
| const char* sql = "SELECT id, ttl FROM zones WHERE name=?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| saveName(zone.getName(), stmt, 1); |
| |
| if (sqlite3_step(stmt) == SQLITE_ROW) { |
| zone.setId(sqlite3_column_int64(stmt, 0)); |
| zone.setTtl(time::seconds(sqlite3_column_int(stmt, 1))); |
| } |
| else { |
| zone.setId(0); |
| } |
| |
| sqlite3_finalize(stmt); |
| |
| return zone.getId() != 0; |
| } |
| |
| std::vector<Zone> |
| DbMgr::listZones() |
| { |
| sqlite3_stmt* stmt; |
| const char* sql = "SELECT id, name, ttl FROM zones"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| std::vector<Zone> vec; |
| |
| while (sqlite3_step(stmt) == SQLITE_ROW) { |
| vec.emplace_back(); |
| Zone& zone = vec.back(); |
| zone.setId(sqlite3_column_int64(stmt, 0)); |
| zone.setTtl(time::seconds(sqlite3_column_int(stmt, 2))); |
| zone.setName(restoreName(stmt, 1)); |
| } |
| sqlite3_finalize(stmt); |
| |
| return vec; |
| } |
| |
| void |
| DbMgr::remove(Zone& zone) |
| { |
| if (zone.getId() == 0) |
| return; |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "DELETE FROM zones where id=?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, zone.getId()); |
| |
| rc = sqlite3_step(stmt); |
| if (rc != SQLITE_DONE) { |
| sqlite3_finalize(stmt); |
| NDN_THROW(ExecuteError(sql)); |
| } |
| |
| sqlite3_finalize(stmt); |
| |
| zone = Zone(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////////////////////////// |
| // Rrset |
| /////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| void |
| DbMgr::insert(Rrset& rrset) |
| { |
| if (rrset.getId() != 0) |
| return; |
| |
| if (rrset.getZone() == nullptr) { |
| NDN_THROW(RrsetError("Rrset has not been assigned to a zone")); |
| } |
| |
| if (rrset.getZone()->getId() == 0) { |
| insert(*rrset.getZone()); |
| } |
| |
| const char* sql = |
| "INSERT INTO rrsets (zone_id, label, type, version, ttl, data)" |
| " VALUES (?, ?, ?, ?, ?, ?)"; |
| |
| sqlite3_stmt* stmt; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, rrset.getZone()->getId()); |
| |
| saveName(rrset.getLabel(), stmt, 2); |
| sqlite3_bind_blob(stmt, 3, rrset.getType().data(), rrset.getType().size(), SQLITE_STATIC); |
| sqlite3_bind_blob(stmt, 4, rrset.getVersion().data(), rrset.getVersion().size(), SQLITE_STATIC); |
| sqlite3_bind_int64(stmt, 5, rrset.getTtl().count()); |
| sqlite3_bind_blob(stmt, 6, rrset.getData().data(), rrset.getData().size(), SQLITE_STATIC); |
| |
| rc = sqlite3_step(stmt); |
| if (rc != SQLITE_DONE) { |
| sqlite3_finalize(stmt); |
| NDN_THROW(ExecuteError(sql)); |
| } |
| |
| rrset.setId(sqlite3_last_insert_rowid(m_conn)); |
| sqlite3_finalize(stmt); |
| } |
| |
| bool |
| DbMgr::find(Rrset& rrset) |
| { |
| if (rrset.getZone() == nullptr) { |
| NDN_THROW(RrsetError("Rrset has not been assigned to a zone")); |
| } |
| |
| if (rrset.getZone()->getId() == 0) { |
| bool isFound = find(*rrset.getZone()); |
| if (!isFound) { |
| return false; |
| } |
| } |
| |
| sqlite3_stmt* stmt; |
| const char* sql = |
| "SELECT id, ttl, version, data FROM rrsets" |
| " WHERE zone_id=? and label=? and type=?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, rrset.getZone()->getId()); |
| |
| saveName(rrset.getLabel(), stmt, 2); |
| sqlite3_bind_blob(stmt, 3, rrset.getType().data(), rrset.getType().size(), SQLITE_STATIC); |
| |
| if (sqlite3_step(stmt) == SQLITE_ROW) { |
| rrset.setId(sqlite3_column_int64(stmt, 0)); |
| rrset.setTtl(time::seconds(sqlite3_column_int64(stmt, 1))); |
| rrset.setVersion(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 2)), |
| sqlite3_column_bytes(stmt, 2)))); |
| rrset.setData(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 3)), |
| sqlite3_column_bytes(stmt, 3)))); |
| } |
| else { |
| rrset.setId(0); |
| } |
| sqlite3_finalize(stmt); |
| |
| return rrset.getId() != 0; |
| } |
| |
| bool |
| DbMgr::findLowerBound(Rrset& rrset) |
| { |
| if (rrset.getZone() == nullptr) { |
| NDN_THROW(RrsetError("Rrset has not been assigned to a zone")); |
| } |
| |
| if (rrset.getZone()->getId() == 0) { |
| bool isFound = find(*rrset.getZone()); |
| if (!isFound) { |
| return false; |
| } |
| } |
| |
| sqlite3_stmt* stmt; |
| const char* sql = |
| "SELECT id, ttl, version, data FROM rrsets" |
| " WHERE zone_id=? and label<? and type=? ORDER BY label DESC"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, rrset.getZone()->getId()); |
| |
| saveName(rrset.getLabel(), stmt, 2); |
| sqlite3_bind_blob(stmt, 3, rrset.getType().data(), rrset.getType().size(), SQLITE_STATIC); |
| |
| if (sqlite3_step(stmt) == SQLITE_ROW) { |
| rrset.setId(sqlite3_column_int64(stmt, 0)); |
| rrset.setTtl(time::seconds(sqlite3_column_int64(stmt, 1))); |
| rrset.setVersion(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 2)), |
| sqlite3_column_bytes(stmt, 2)))); |
| rrset.setData(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 3)), |
| sqlite3_column_bytes(stmt, 3)))); |
| } |
| else { |
| rrset.setId(0); |
| } |
| sqlite3_finalize(stmt); |
| |
| return rrset.getId() != 0; |
| } |
| |
| std::vector<Rrset> |
| DbMgr::findRrsets(Zone& zone) |
| { |
| if (zone.getId() == 0) |
| find(zone); |
| |
| if (zone.getId() == 0) |
| NDN_THROW(RrsetError("Attempting to find all the rrsets with a zone does not in the database")); |
| |
| std::vector<Rrset> vec; |
| sqlite3_stmt* stmt; |
| const char* sql = "SELECT id, ttl, version, data, label, type " |
| "FROM rrsets where zone_id=? ORDER BY label"; |
| |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| sqlite3_bind_int64(stmt, 1, zone.getId()); |
| |
| while (sqlite3_step(stmt) == SQLITE_ROW) { |
| vec.emplace_back(&zone); |
| Rrset& rrset = vec.back(); |
| |
| rrset.setId(sqlite3_column_int64(stmt, 0)); |
| rrset.setTtl(time::seconds(sqlite3_column_int64(stmt, 1))); |
| rrset.setVersion(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 2)), |
| sqlite3_column_bytes(stmt, 2)))); |
| rrset.setData(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 3)), |
| sqlite3_column_bytes(stmt, 3)))); |
| rrset.setLabel(restoreName(stmt, 4)); |
| rrset.setType(Block(make_span(static_cast<const uint8_t*>(sqlite3_column_blob(stmt, 5)), |
| sqlite3_column_bytes(stmt, 5)))); |
| } |
| sqlite3_finalize(stmt); |
| |
| return vec; |
| } |
| |
| void |
| DbMgr::removeRrsetsOfZoneByType(Zone& zone, const name::Component& type) |
| { |
| if (zone.getId() == 0) |
| find(zone); |
| |
| if (zone.getId() == 0) |
| NDN_THROW(RrsetError("Attempting to find all the rrsets with a zone does not in the database")); |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "DELETE FROM rrsets WHERE zone_id = ? AND type = ?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, zone.getId()); |
| sqlite3_bind_blob(stmt, 2, type.data(), type.size(), SQLITE_STATIC); |
| |
| rc = sqlite3_step(stmt); |
| if (rc != SQLITE_DONE) { |
| sqlite3_finalize(stmt); |
| NDN_THROW(ExecuteError(sql)); |
| } |
| sqlite3_finalize(stmt); |
| } |
| |
| void |
| DbMgr::remove(Rrset& rrset) |
| { |
| if (rrset.getId() == 0) |
| NDN_THROW(RrsetError("Attempting to remove Rrset that has no assigned id")); |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "DELETE FROM rrsets WHERE id=?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, rrset.getId()); |
| |
| rc = sqlite3_step(stmt); |
| if (rc != SQLITE_DONE) { |
| sqlite3_finalize(stmt); |
| NDN_THROW(ExecuteError(sql)); |
| } |
| |
| sqlite3_finalize(stmt); |
| |
| rrset = Rrset(rrset.getZone()); |
| } |
| |
| void |
| DbMgr::update(Rrset& rrset) |
| { |
| if (rrset.getId() == 0) { |
| NDN_THROW(RrsetError("Attempting to replace Rrset that has no assigned id")); |
| } |
| |
| if (rrset.getZone() == nullptr) { |
| NDN_THROW(RrsetError("Rrset has not been assigned to a zone")); |
| } |
| |
| sqlite3_stmt* stmt; |
| const char* sql = "UPDATE rrsets SET ttl=?, version=?, data=? WHERE id=?"; |
| int rc = sqlite3_prepare_v2(m_conn, sql, -1, &stmt, nullptr); |
| if (rc != SQLITE_OK) { |
| NDN_THROW(PrepareError(sql)); |
| } |
| |
| sqlite3_bind_int64(stmt, 1, rrset.getTtl().count()); |
| sqlite3_bind_blob(stmt, 2, rrset.getVersion().data(), rrset.getVersion().size(), SQLITE_STATIC); |
| sqlite3_bind_blob(stmt, 3, rrset.getData().data(), rrset.getData().size(), SQLITE_STATIC); |
| sqlite3_bind_int64(stmt, 4, rrset.getId()); |
| |
| sqlite3_step(stmt); |
| sqlite3_finalize(stmt); |
| } |
| |
| } // namespace ndns |
| } // namespace ndn |