diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c5892e..4fa8d39 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,7 +15,7 @@ list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") ############################ # Find required packages # ############################ -find_package(opendht 1.2.0 REQUIRED) +find_package(opendht 1.8.2 REQUIRED) find_package(CURLpp REQUIRED) find_package(glibmm REQUIRED) find_package(B64 REQUIRED) diff --git a/README.md b/README.md index 16ad1f0..ac05441 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Milis Linux: mps kur dpaste (https://github.com/milisarge/malfs-milis/blob/maste ## Dependencies -- [OpenDHT](https://github.com/savoirfairelinux/opendht/) (minimal version: 1.2.0) +- [OpenDHT](https://github.com/savoirfairelinux/opendht/) (minimal version: 1.8.2) - [msgpack-c](https://github.com/msgpack/msgpack-c) - [gpgmepp](https://github.com/KDE/gpgmepp) - [json.hpp](https://github.com/nlohmann/json) (required version for CMake: 2.1.1) @@ -89,6 +89,16 @@ Milis Linux: mps kur dpaste (https://github.com/milisarge/malfs-milis/blob/maste - [catch](https://github.com/catchorg/Catch2) for unit tests - getopt (util-linux) +## Caching + +To avoid the multi-second DHT cold start on every invocation, `dpaste` caches +the OpenDHT node state (including its routing table) on disk. The cache lives +in `${XDG_CACHE_HOME}/dpaste` (usually `~/.cache/dpaste`) and can be relocated +with the `DPASTE_CACHE_DIR` environment variable. The state is refreshed at +the end of every run, allowing known peers to be reused and improving +bootstrap resilience. The first run remains slow, and random-key lookups on +the public DHT still cannot be eliminated. + ## Pastebin over DHT A DHT is efficient and requires no infrastructure. In practice, you can always @@ -115,4 +125,3 @@ not likely to be "down". - Simon Désaulniers - Adrien Béraud - diff --git a/configure.ac b/configure.ac index 3ff8a0d..304ccb4 100644 --- a/configure.ac +++ b/configure.ac @@ -18,7 +18,7 @@ AS_IF([test "x$enable_debug" = "xyes"], AC_PROG_CXX AC_PROG_RANLIB -PKG_CHECK_MODULES([OpenDHT], [opendht >= 1.2]) +PKG_CHECK_MODULES([OpenDHT], [opendht >= 1.8.2]) PKG_CHECK_MODULES([CURLPP], [curlpp]) PKG_CHECK_MODULES([GLIBMM], [glibmm-2.4]) @@ -45,4 +45,3 @@ AM_COND_IF([DPASTE_TEST], AC_OUTPUT # vim: set ts=2 sw=2 tw=120 et : - diff --git a/src/bin.cpp b/src/bin.cpp index 42b04b6..81718f8 100644 --- a/src/bin.cpp +++ b/src/bin.cpp @@ -36,7 +36,7 @@ namespace dpaste { const constexpr uint8_t Bin::PROTO_VERSION; -Bin::Bin() { +Bin::Bin() : node_ready_(node.run()) { /* load dpaste config */ auto config_file = conf::ConfigurationFile(); config_file.load(); @@ -48,7 +48,6 @@ Bin::Bin() { conv >> port; } - node.run(); http_client_ = std::make_unique(conf_.at("host"), port); } @@ -69,13 +68,16 @@ std::pair Bin::get(std::string&& code, bool no_decrypt) { std::vector data {data_str.begin(), data_str.end()}; /* if fail, then perform request from local node */ - if (data.empty()) { + if (data.empty() and node_ready_) { /* get a pasted blob */ auto values = node.get(lcode); if (not values.empty()) data = values.front(); } + if (data.empty() and not node_ready_) + return {false, ""}; + if (not data.empty()) { Packet p; try { @@ -182,7 +184,7 @@ std::string Bin::paste(std::vector&& data, std::unique_ptrput(code, {bin_packet.begin(), bin_packet.end()}); - if (not success) + if (not success and node_ready_) success = node.paste(code, std::move(bin_packet)); return success ? DPASTE_URI_PREFIX+code+pwd : ""; @@ -225,4 +227,3 @@ void Bin::Packet::deserialize(const std::vector& pbuffer) { } /* dpaste */ /* vim:set et sw=4 ts=4 tw=120: */ - diff --git a/src/bin.h b/src/bin.h index 50acf0d..b0abea5 100644 --- a/src/bin.h +++ b/src/bin.h @@ -124,9 +124,9 @@ class Bin { /* transport */ std::unique_ptr http_client_ {}; Node node {}; + bool node_ready_ {false}; }; } /* dpaste */ /* vim:set et sw=4 ts=4 tw=120: */ - diff --git a/src/node.cpp b/src/node.cpp index dcbeca2..aa9c526 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -21,15 +21,61 @@ #include #include #include +#include +#include +#include +#include #include +#include #include "node.h" +#include "log.h" namespace dpaste { const constexpr char* Node::DPASTE_USER_TYPE; +namespace { + +/** + * Directory holding the on-disk OpenDHT node state. + * Can be overridden with the DPASTE_CACHE_DIR environment variable + * (e.g. for tests). Defaults to ${XDG_CACHE_HOME}/dpaste. + */ +std::optional create_cache_dir() { + const char* env = std::getenv("DPASTE_CACHE_DIR"); + const std::filesystem::path cache_dir = env and *env ? env : Glib::get_user_cache_dir() + "/dpaste"; + std::error_code ec; + std::filesystem::create_directories(cache_dir, ec); + if (ec) { + DPASTE_MSG("Failed to create cache directory '%s': %s", cache_dir.string().c_str(), ec.message().c_str()); + return std::nullopt; + } + return cache_dir; +} + +} /* anonymous namespace */ + +bool Node::run(uint16_t port, std::string bootstrap_hostname, std::string bootstrap_port) { + if (running_) + return true; + + const auto cache_dir = create_cache_dir(); + if (not cache_dir) + return false; + /* Ask OpenDHT to load its state (routing table) on start and save it on + * shutdown; this reuses known peers and improves bootstrap resilience. */ + dht::DhtRunner::Config config; + config.dht_config.node_config.persist_path = (*cache_dir / "nodes").string(); + config.threaded = true; + node_.run(port, config); + + node_.bootstrap(bootstrap_hostname, bootstrap_port); + running_ = true; + return true; +} + bool Node::paste(const std::string& code, dht::Blob&& blob, dht::DoneCallbackSimple&& cb) { auto v = std::make_shared(std::forward(blob)); v->user_type = DPASTE_USER_TYPE; diff --git a/src/node.h b/src/node.h index 48f1f75..64204ca 100644 --- a/src/node.h +++ b/src/node.h @@ -54,15 +54,22 @@ class Node { static const constexpr char* DPASTE_USER_TYPE = "dpaste"; Node() {} - virtual ~Node () {} - - void run(uint16_t port = 0, std::string bootstrap_hostname = DEFAULT_BOOTSTRAP_NODE, std::string bootstrap_port = DEFAULT_BOOTSTRAP_PORT) { + virtual ~Node() { + /* Persist the node state (routing table) on disk so the next run can + * reuse known peers instead of cold-bootstrapping the DHT. */ if (running_) - return; - node_.run(port, dht::crypto::generateIdentity(), true); - node_.bootstrap(bootstrap_hostname, bootstrap_port); - running_ = true; - }; + stop(); + } + + /** + * Start the DHT node. Its routing state is cached on disk (see + * DPASTE_CACHE_DIR below) so subsequent runs can connect more quickly. + * + * @param port Local port to bind (0 for random). + * @param bootstrap_hostname Hostname of the bootstrap node. + * @param bootstrap_port Port of the bootstrap node. + */ + bool run(uint16_t port = 0, std::string bootstrap_hostname = DEFAULT_BOOTSTRAP_NODE, std::string bootstrap_port = DEFAULT_BOOTSTRAP_PORT); void stop() { std::condition_variable cv; @@ -124,4 +131,3 @@ class Node { }; } /* dpaste */ - diff --git a/tests/node.cpp b/tests/node.cpp index 4624061..106f383 100644 --- a/tests/node.cpp +++ b/tests/node.cpp @@ -20,6 +20,10 @@ #include +#include +#include +#include + #include "tests.h" #include "node.h" @@ -35,13 +39,58 @@ class PirateNodeTester { bool is_running(const dpaste::Node& n) const { return n.running_; } }; +class CacheFileGuard { + int fd_ {-1}; + std::string path_; + bool had_previous_value_ {false}; + std::string previous_value_; + +public: + CacheFileGuard() { + const char* previous = std::getenv("DPASTE_CACHE_DIR"); + had_previous_value_ = previous != nullptr; + if (had_previous_value_) + previous_value_ = previous; + + char path[] = "/tmp/dpaste-cache-XXXXXX"; + fd_ = mkstemp(path); + if (fd_ == -1) + throw std::runtime_error("could not create temporary cache file"); + path_ = path; + + if (setenv("DPASTE_CACHE_DIR", path_.c_str(), 1) != 0) { + close(fd_); + unlink(path_.c_str()); + throw std::runtime_error("could not set DPASTE_CACHE_DIR"); + } + } + + ~CacheFileGuard() { + if (had_previous_value_) + setenv("DPASTE_CACHE_DIR", previous_value_.c_str(), 1); + else + unsetenv("DPASTE_CACHE_DIR"); + close(fd_); + unlink(path_.c_str()); + } +}; + +TEST_CASE("Node refuses a cache path that is a regular file", "[Node][cache]") { + PirateNodeTester pt; + CacheFileGuard cache; + + dpaste::Node node {}; + REQUIRE_FALSE(node.run()); + REQUIRE_FALSE(pt.is_running(node)); +} + TEST_CASE("Node get/paste on DHT", "[Node][get][paste]") { PirateNodeTester pt; const std::string PIN = random_pin(); std::vector data = {0, 1, 2, 3, 4}; dpaste::Node node {}; - node.run(); + REQUIRE(node.run()); SECTION ( "pasting data {0,1,2,3,4}" ) { REQUIRE ( node.paste(PIN, std::vector {data}) ); @@ -60,4 +109,3 @@ TEST_CASE("Node get/paste on DHT", "[Node][get][paste]") { } /* dpaste */ /* vim: set ts=4 sw=4 tw=120 et :*/ -