Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -115,4 +125,3 @@ not likely to be "down".

- Simon Désaulniers <sim.desaulniers@gmail.com>
- Adrien Béraud <adrien.beraud@savoirfairelinux.com>

3 changes: 1 addition & 2 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -45,4 +45,3 @@ AM_COND_IF([DPASTE_TEST],
AC_OUTPUT

# vim: set ts=2 sw=2 tw=120 et :

11 changes: 6 additions & 5 deletions src/bin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -48,7 +48,6 @@ Bin::Bin() {
conv >> port;
}

node.run();
http_client_ = std::make_unique<HttpClient>(conf_.at("host"), port);
}

Expand All @@ -69,13 +68,16 @@ std::pair<bool, std::string> Bin::get(std::string&& code, bool no_decrypt) {
std::vector<uint8_t> 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 {
Expand Down Expand Up @@ -182,7 +184,7 @@ std::string Bin::paste(std::vector<uint8_t>&& data, std::unique_ptr<crypto::Para
DPASTE_MSG("Pasting data...");
auto bin_packet = p.serialize();
auto success = http_client_->put(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 : "";
Expand Down Expand Up @@ -225,4 +227,3 @@ void Bin::Packet::deserialize(const std::vector<uint8_t>& pbuffer) {
} /* dpaste */

/* vim:set et sw=4 ts=4 tw=120: */

2 changes: 1 addition & 1 deletion src/bin.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ class Bin {
/* transport */
std::unique_ptr<HttpClient> http_client_ {};
Node node {};
bool node_ready_ {false};
};

} /* dpaste */

/* vim:set et sw=4 ts=4 tw=120: */

46 changes: 46 additions & 0 deletions src/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,61 @@
#include <algorithm>
#include <random>
#include <future>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <optional>

#include <opendht.h>
#include <glibmm.h>

#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<std::filesystem::path> 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<dht::Value>(std::forward<dht::Blob>(blob));
v->user_type = DPASTE_USER_TYPE;
Expand Down
24 changes: 15 additions & 9 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -124,4 +131,3 @@ class Node {
};

} /* dpaste */

52 changes: 50 additions & 2 deletions tests/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

#include <catch2/catch.hpp>

#include <cstdlib>
#include <stdexcept>
#include <unistd.h>

#include "tests.h"
#include "node.h"

Expand All @@ -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<uint8_t> 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<uint8_t> {data}) );
Expand All @@ -60,4 +109,3 @@ TEST_CASE("Node get/paste on DHT", "[Node][get][paste]") {
} /* dpaste */

/* vim: set ts=4 sw=4 tw=120 et :*/