From ada02a1bb10698dd0727454e82b1b8e462d80f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=C3=ABl=20Champagne=20Gareau?= Date: Thu, 3 Sep 2026 10:13:10 -0400 Subject: [PATCH] perf: add a persistant systemd DHT node --- CMakeLists.txt | 5 ++- Makefile.am | 7 ++- README.md | 30 ++++++++++++- config/dpaste.conf | 2 +- doc/dpaste.1 | 17 +++++++- src/bin.cpp | 14 ++++-- src/bin.h | 4 +- src/http_client.cpp | 78 +++++++++++++++++++++++----------- src/node.h | 1 - systemd/dpaste-dhtnode.service | 13 ++++++ 10 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 systemd/dpaste-dhtnode.service diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c5892e..eeb9432 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.20) project(dpaste) +include(GNUInstallDirs) set(dpaste_VERSION 0.4.1) add_definitions(-DVERSION="${dpaste_VERSION}") add_definitions(-DPACKAGE_NAME="dpaste") @@ -58,6 +59,8 @@ target_link_libraries(dpaste LINK_PUBLIC -lopendht -lgnutls -lnettle -largon2 -l ##################### # install targets # ##################### -install(TARGETS dpaste DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) +install(TARGETS dpaste RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(FILES systemd/dpaste-dhtnode.service + DESTINATION ${CMAKE_INSTALL_LIBDIR}/systemd/user) # vim: set ts=4 sw=4 tw=120 noet : diff --git a/Makefile.am b/Makefile.am index ea8a03b..32280ba 100644 --- a/Makefile.am +++ b/Makefile.am @@ -10,6 +10,9 @@ endif dist_man1_MANS = doc/dpaste.1 +systemd_userdir = $(prefix)/lib/systemd/user +systemd_user_DATA = systemd/dpaste-dhtnode.service + ACLOCAL_AMFLAGS = -I m4 DOC_FILES = \ @@ -18,7 +21,8 @@ DOC_FILES = \ COPYING EXTRA_DIST = \ - $(DOC_FILES) + $(DOC_FILES) \ + systemd/dpaste-dhtnode.service test: check if DPASTE_TEST @@ -26,4 +30,3 @@ if DPASTE_TEST endif # vim: set ts=4 sw=4 tw=120 noet : - diff --git a/README.md b/README.md index 16ad1f0..229b313 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,35 @@ $ make You'll then find the binary `dpaste` under `build` directory. +### Running the local OpenDHT proxy + +dpaste sends HTTP requests to the OpenDHT `DhtProxyServer` at + `127.0.0.1:6509` first. If the proxy is unavailable, it falls back to a +transient local DHT node. The proxy avoids creating a DHT node for every +invocation and retains routing state between commands. OpenDHT must still +locate the nodes responsible for each random paste key, so this does not +guarantee an instant paste. + +The optional systemd user unit can be installed with either build system (as +part of the normal install): + +```sh +$ make install # Autotools +# or: cmake --install build # CMake +$ systemctl --user daemon-reload +$ systemctl --user enable --now dpaste-dhtnode.service +$ systemctl --user status dpaste-dhtnode.service +``` + +Verify that the loopback proxy is reachable with: + +```sh +$ curl --fail --max-time 5 http://127.0.0.1:6509/node/info +``` + +The command returns information about the local node. The unit runs `dhtnode` +in the foreground and binds its proxy to loopback only. + ## Package Archlinux AUR: https://aur.archlinux.org/packages/dpaste/ @@ -115,4 +144,3 @@ not likely to be "down". - Simon Désaulniers - Adrien Béraud - diff --git a/config/dpaste.conf b/config/dpaste.conf index 7acdc43..9448bb3 100644 --- a/config/dpaste.conf +++ b/config/dpaste.conf @@ -1,7 +1,7 @@ # This is the default dpaste configuration file. ########################### -# OpenDHT's HTTP server # +# OpenDHT DhtProxyServer endpoint # ########################### host = 127.0.0.1 port = 6509 diff --git a/doc/dpaste.1 b/doc/dpaste.1 index 70a9b5f..a140d2c 100644 --- a/doc/dpaste.1 +++ b/doc/dpaste.1 @@ -71,8 +71,21 @@ The program returns 0 on success. Otherwise 1 is returned. .TP \fB$XDG_CONFIG_DIR/dpaste.conf\fP -Main configuration file where. \fBdpaste\fP will look for this file to recover -complementary information. +Main configuration file where \fBdpaste\fP will look for this file to recover +complementary information. The \fBhost\fP and \fBport\fP settings select the +OpenDHT \fBDhtProxyServer\fP endpoint; the defaults are \fB127.0.0.1\fP and +\fB6509\fP. + +.SH DHT PROXY + +An optional persistent local OpenDHT proxy can be run with the +\fBdpaste-dhtnode.service\fP systemd user unit. Install the unit, then run +\fBsystemctl --user daemon-reload\fP and +\fBsystemctl --user enable --now dpaste-dhtnode.service\fP. It listens on the +loopback endpoint above and persists its routing state. It avoids starting a +new local node per invocation, but each random paste key still requires a DHT +lookup. If the proxy is unavailable, dpaste falls back to a transient local +DHT node. .SH AUTHORS \(bu diff --git a/src/bin.cpp b/src/bin.cpp index 42b04b6..54294d9 100644 --- a/src/bin.cpp +++ b/src/bin.cpp @@ -48,10 +48,16 @@ Bin::Bin() { conv >> port; } - node.run(); http_client_ = std::make_unique(conf_.at("host"), port); } +void Bin::ensureNodeRunning() { + if (not node_running_) { + node.run(); + node_running_ = true; + } +} + std::string Bin::code_from_dpaste_uri(const std::string& uri) { static const std::string DUP {DPASTE_URI_PREFIX}; const auto p = uri.find(DUP); @@ -71,6 +77,7 @@ std::pair Bin::get(std::string&& code, bool no_decrypt) { /* if fail, then perform request from local node */ if (data.empty()) { /* get a pasted blob */ + ensureNodeRunning(); auto values = node.get(lcode); if (not values.empty()) data = values.front(); @@ -182,8 +189,10 @@ std::string Bin::paste(std::vector&& data, std::unique_ptrput(code, {bin_packet.begin(), bin_packet.end()}); - if (not success) + if (not success) { + ensureNodeRunning(); success = node.paste(code, std::move(bin_packet)); + } return success ? DPASTE_URI_PREFIX+code+pwd : ""; } @@ -225,4 +234,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..115fdbf 100644 --- a/src/bin.h +++ b/src/bin.h @@ -76,6 +76,8 @@ class Bin { } private: + void ensureNodeRunning(); + /* constants */ static const constexpr char* DPASTE_URI_PREFIX = "dpaste:"; static const constexpr uint8_t PROTO_VERSION = 0; @@ -124,9 +126,9 @@ class Bin { /* transport */ std::unique_ptr http_client_ {}; Node node {}; + bool node_running_ {false}; }; } /* dpaste */ /* vim:set et sw=4 ts=4 tw=120: */ - diff --git a/src/http_client.cpp b/src/http_client.cpp index d342398..ea358ad 100644 --- a/src/http_client.cpp +++ b/src/http_client.cpp @@ -18,8 +18,11 @@ * along with dpaste. If not, see . */ -#include +#include #include +#include +#include +#include #include #include @@ -28,6 +31,7 @@ #include #include #include +#include #include "http_client.h" #include "node.h" @@ -36,34 +40,42 @@ namespace dpaste { using json = nlohmann::json; -static std::ofstream null("/dev/null"); - std::string HttpClient::get(const std::string& code) const { try { curlpp::Cleanup mycleanup; curlpp::Easy req; - req.setOpt(port); - std::stringstream response, oss; - req.setOpt(HTTP_PROTO+ - host+"/"+dht::InfoHash::get(code).toString() - +"?user_type="+dpaste::Node::DPASTE_USER_TYPE - ); + std::stringstream response; + req.setOpt(HTTP_PROTO + host + ":" + + std::to_string(port) + "/key/" + + dht::InfoHash::get(code).toString()); req.setOpt(curlpp::Options::WriteStream(&response)); try { req.perform(); /* server gives code 200 when everything is fine. */ if (curlpp::Infos::ResponseCode::get(req) == 200) { - auto pr = json::parse(response.str()); - if (not pr.empty()) { - std::istringstream iss((*pr.begin())["base64"].dump()); - base64::decoder d; - d.decode(iss, oss); + std::istringstream lines(response.str()); + std::string line; + while (std::getline(lines, line)) { + try { + const auto value = json::parse(line); + if (value.is_object() && + value.value("utype", std::string {}) == Node::DPASTE_USER_TYPE && + value.contains("data") && value["data"].is_string()) { + std::istringstream encoded(value["data"].get()); + std::ostringstream decoded; + base64::decoder decoder; + decoder.decode(encoded, decoded); + return decoded.str(); + } + } catch (const std::exception&) { + /* Ignore malformed or incompatible Value objects. */ + } } } } catch (curlpp::RuntimeError & e) { } - return oss.str(); + return {}; } catch (curlpp::LogicError & e) { return {}; } } @@ -71,15 +83,32 @@ bool HttpClient::put(const std::string& code, const std::string& data) const { try { curlpp::Cleanup mycleanup; curlpp::Easy req; - req.setOpt(port); - req.setOpt(HTTP_PROTO+host+"/"+dht::InfoHash::get(code).toString()); - req.setOpt(curlpp::Options::WriteStream(&null)); - { - curlpp::Forms form_parts; - form_parts.push_back(new curlpp::FormParts::Content("user_type", dpaste::Node::DPASTE_USER_TYPE)); - form_parts.push_back(new curlpp::FormParts::Content("data", data)); - req.setOpt(new curlpp::options::HttpPost(form_parts)); - } + req.setOpt(HTTP_PROTO + host + ":" + + std::to_string(port) + "/key/" + + dht::InfoHash::get(code).toString()); + + std::istringstream input(data); + std::ostringstream encoded; + base64::encoder encoder; + encoder.encode(input, encoded); + + /* libb64 wraps long output lines; the proxy expects one compact + * standard-base64 string in the JSON Value. */ + auto encoded_data = encoded.str(); + encoded_data.erase(std::remove_if(encoded_data.begin(), encoded_data.end(), + [](char c) { return c == '\r' || c == '\n'; }), encoded_data.end()); + + const auto body = json { + {"id", "0"}, + {"type", 0}, + {"data", encoded_data}, + {"utype", Node::DPASTE_USER_TYPE} + }.dump(); + req.setOpt(curlpp::Options::PostFields(body)); + req.setOpt(new curlpp::options::HttpHeader( + std::list {"Content-Type: application/json"})); + std::stringstream response; + req.setOpt(curlpp::Options::WriteStream(&response)); try { req.perform(); @@ -93,4 +122,3 @@ bool HttpClient::put(const std::string& code, const std::string& data) const { } /* dpaste */ /* vim:set et sw=4 ts=4 tw=120: */ - diff --git a/src/node.h b/src/node.h index 48f1f75..d6afee2 100644 --- a/src/node.h +++ b/src/node.h @@ -124,4 +124,3 @@ class Node { }; } /* dpaste */ - diff --git a/systemd/dpaste-dhtnode.service b/systemd/dpaste-dhtnode.service new file mode 100644 index 0000000..e390a30 --- /dev/null +++ b/systemd/dpaste-dhtnode.service @@ -0,0 +1,13 @@ +[Unit] +Description=OpenDHT HTTP proxy for dpaste +Wants=network-online.target +After=network-online.target + +[Service] +Type=simple +ExecStartPre=/usr/bin/mkdir -p %h/.local/state/dpaste +ExecStart=/usr/bin/dhtnode --service --port 0 --bootstrap bootstrap.jami.net:4222 --proxyserver 6509 --proxy-addr 127.0.0.1 --persist %h/.local/state/dpaste/dhtnode +Restart=on-failure + +[Install] +WantedBy=default.target