diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c5892e..37b48a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ cmake_minimum_required(VERSION 3.20) project(dpaste) -set(dpaste_VERSION 0.4.1) +include(GNUInstallDirs) +set(dpaste_VERSION 0.5.0) 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 lib/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..48b1bb3 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,37 @@ $ 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. It stores dhtnode's +persistent routing state in the XDG cache directory via systemd +`CacheDirectory=dpaste`. + ## Package Archlinux AUR: https://aur.archlinux.org/packages/dpaste/ @@ -82,7 +113,7 @@ Milis Linux: mps kur dpaste (https://github.com/milisarge/malfs-milis/blob/maste - [OpenDHT](https://github.com/savoirfairelinux/opendht/) (minimal version: 1.2.0) - [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) +- [json.hpp](https://github.com/nlohmann/json) (required version for CMake: 3) - [cURLpp](https://github.com/jpbarrette/curlpp) (0.8.1 is known to fail to build. Use master branch of curlpp repo until a new release is made) - [glibmm](https://github.com/GNOME/glibmm) - [libb64](http://libb64.sourceforge.net/) @@ -115,4 +146,3 @@ not likely to be "down". - Simon Désaulniers - Adrien Béraud - diff --git a/config/dpaste.conf b/config/dpaste.conf index 7acdc43..2218625 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/configure.ac b/configure.ac index 3ff8a0d..85b428a 100644 --- a/configure.ac +++ b/configure.ac @@ -1,4 +1,4 @@ -AC_INIT(dpaste, 0.4.1) +AC_INIT(dpaste, 0.5.0) AC_CONFIG_AUX_DIR(ac) AM_INIT_AUTOMAKE([foreign subdir-objects]) AC_CONFIG_HEADERS([config.h]) @@ -45,4 +45,3 @@ AM_COND_IF([DPASTE_TEST], AC_OUTPUT # vim: set ts=2 sw=2 tw=120 et : - diff --git a/doc/dpaste.1 b/doc/dpaste.1 index 70a9b5f..a01843b 100644 --- a/doc/dpaste.1 +++ b/doc/dpaste.1 @@ -71,8 +71,29 @@ 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 looks for configuration. + +.SH CONFIGURATION + +.TP +\fBhost\fP +\fBDhtProxyServer\fP host, accepting a DNS name, IPv4 address, or IPv6 +literal (for example, \fB[::1]\fP). Default: \fB127.0.0.1\fP. + +.TP +\fBport\fP +\fBDhtProxyServer\fP port. Default: \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..21ed3ae 100644 --- a/src/bin.cpp +++ b/src/bin.cpp @@ -48,7 +48,6 @@ Bin::Bin() { conv >> port; } - node.run(); http_client_ = std::make_unique(conf_.at("host"), port); } @@ -71,6 +70,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 */ + node.run(); auto values = node.get(lcode); if (not values.empty()) data = values.front(); @@ -182,8 +182,10 @@ std::string Bin::paste(std::vector&& data, std::unique_ptrput(code, {bin_packet.begin(), bin_packet.end()}); - if (not success) + if (not success) { + node.run(); 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..356d224 100644 --- a/src/bin.h +++ b/src/bin.h @@ -129,4 +129,3 @@ class Bin { } /* dpaste */ /* vim:set et sw=4 ts=4 tw=120: */ - diff --git a/src/http_client.cpp b/src/http_client.cpp index d342398..01e59be 100644 --- a/src/http_client.cpp +++ b/src/http_client.cpp @@ -18,8 +18,10 @@ * along with dpaste. If not, see . */ -#include +#include #include +#include +#include #include #include @@ -28,43 +30,72 @@ #include #include #include +#include #include "http_client.h" +#include "log.h" #include "node.h" namespace dpaste { using json = nlohmann::json; -static std::ofstream null("/dev/null"); +namespace { + +std::string urlHost(const std::string& host) { + if (host.size() >= 2 && host.front() == '[' && host.back() == ']') + return host; + if (host.find(':') != std::string::npos) + return "[" + host + "]"; + return host; +} + +} /* anonymous namespace */ std::string HttpClient::get(const std::string& code) const { try { curlpp::Cleanup mycleanup; curlpp::Easy req; + std::stringstream response; req.setOpt(port); - std::stringstream response, oss; - req.setOpt(HTTP_PROTO+ - host+"/"+dht::InfoHash::get(code).toString() - +"?user_type="+dpaste::Node::DPASTE_USER_TYPE - ); + req.setOpt(HTTP_PROTO + urlHost(host) + "/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); + /* DhtProxyServer returns one JSON Value per line; do not parse + * the whole response as a single JSON document. */ + 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) { } + } catch (curlpp::RuntimeError & e) { + DPASTE_MSG("%s", e.what()); + } - return oss.str(); - } catch (curlpp::LogicError & e) { return {}; } + return {}; + } catch (curlpp::LogicError & e) { + DPASTE_MSG("%s", e.what()); + return {}; + } } bool HttpClient::put(const std::string& code, const std::string& data) const { @@ -72,25 +103,43 @@ bool HttpClient::put(const std::string& code, const std::string& data) const { 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 + urlHost(host) + "/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 { + {"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(); return curlpp::Infos::ResponseCode::get(req) == 200; } catch (curlpp::RuntimeError & e) { + DPASTE_MSG("%s", e.what()); return false; } - } catch (curlpp::LogicError & e) { return false; } + } catch (curlpp::LogicError & e) { + DPASTE_MSG("%s", e.what()); + return false; + } } } /* dpaste */ /* vim:set et sw=4 ts=4 tw=120: */ - diff --git a/src/node.h b/src/node.h index 48f1f75..9a948a8 100644 --- a/src/node.h +++ b/src/node.h @@ -43,7 +43,7 @@ class Node { #ifdef DPASTE_TEST friend class tests::PirateNodeTester; #endif - static const constexpr char* DEFAULT_BOOTSTRAP_NODE = "bootstrap.ring.cx"; + static const constexpr char* DEFAULT_BOOTSTRAP_NODE = "bootstrap.jami.net"; static const constexpr char* DEFAULT_BOOTSTRAP_PORT = "4222"; static const constexpr char* CONNECTION_FAILURE_MSG = "err.. Failed to connect to the DHT."; static const constexpr char* OPERATION_FAILURE_MSG = "err.. DHT operation failed."; @@ -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..6a9ae13 --- /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 +CacheDirectory=dpaste +ExecStart=/usr/bin/dhtnode --service --port 0 --bootstrap bootstrap.jami.net:4222 --proxyserver 6509 --proxy-addr 127.0.0.1 --persist ${CACHE_DIRECTORY}/dhtnode +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/tests/bin.cpp b/tests/bin.cpp index ceb54fb..a0257c4 100644 --- a/tests/bin.cpp +++ b/tests/bin.cpp @@ -19,6 +19,13 @@ */ #include +#include +#include +#include + +#include +#include +#include #include @@ -46,8 +53,87 @@ class PirateBinTester { } }; +class DirectDhtConfig final { +public: + DirectDhtConfig() { + const auto previous = std::getenv("XDG_CONFIG_HOME"); + if (previous) { + previous_config_home_ = previous; + had_previous_config_home_ = true; + } + + char directory[] = "/tmp/dpaste-bin-test-XXXXXX"; + if (not mkdtemp(directory)) + throw std::runtime_error("could not create temporary configuration directory"); + config_home_ = directory; + + try { + std::ofstream config(config_home_ + "/dpaste.conf"); + if (not config) + throw std::runtime_error("could not create temporary configuration file"); + config << "host = 127.0.0.1\nport = " << unavailable_port() << '\n'; + config.close(); + if (not config) + throw std::runtime_error("could not write temporary configuration file"); + if (setenv("XDG_CONFIG_HOME", config_home_.c_str(), 1) != 0) + throw std::runtime_error("could not set XDG_CONFIG_HOME"); + } catch (...) { + cleanup(); + throw; + } + } + + ~DirectDhtConfig() { cleanup(); } + + DirectDhtConfig(const DirectDhtConfig&) = delete; + DirectDhtConfig& operator=(const DirectDhtConfig&) = delete; + +private: + static uint16_t unavailable_port() { + const auto socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (socket_fd < 0) + throw std::runtime_error("could not create loopback socket"); + + sockaddr_in address {}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = htons(0); + if (bind(socket_fd, reinterpret_cast(&address), sizeof(address)) != 0) { + close(socket_fd); + throw std::runtime_error("could not bind loopback socket"); + } + + socklen_t address_length = sizeof(address); + if (getsockname(socket_fd, reinterpret_cast(&address), &address_length) != 0) { + close(socket_fd); + throw std::runtime_error("could not inspect loopback socket"); + } + const auto port = ntohs(address.sin_port); + close(socket_fd); + return port; + } + + void cleanup() noexcept { + if (config_home_.empty()) + return; + if (had_previous_config_home_) + setenv("XDG_CONFIG_HOME", previous_config_home_.c_str(), 1); + else + unsetenv("XDG_CONFIG_HOME"); + unlink((config_home_ + "/dpaste.conf").c_str()); + rmdir(config_home_.c_str()); + config_home_.clear(); + } + + std::string config_home_; + std::string previous_config_home_; + bool had_previous_config_home_ = false; +}; + TEST_CASE("Bin get/paste on DHT", "[Bin][get][paste]") { using pbt = PirateBinTester; + // Keep a user's configured proxy from changing this direct-DHT test. + DirectDhtConfig direct_dht_config; std::vector data = {0, 1, 2, 3, 4}; Bin bin {}; crypto::Cipher::init(); @@ -124,4 +210,3 @@ TEST_CASE("Bin conversion of stringstream to vector", "[Bin][data_from_stream]") } /* dpaste */ /* vim: set ts=4 sw=4 tw=120 et :*/ -