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
7 changes: 5 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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")

Expand Down Expand Up @@ -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 :
7 changes: 5 additions & 2 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 = \
Expand All @@ -18,12 +21,12 @@ DOC_FILES = \
COPYING

EXTRA_DIST = \
$(DOC_FILES)
$(DOC_FILES) \
systemd/dpaste-dhtnode.service

test: check
if DPASTE_TEST
./tests/dptest $(DPTEST_ARGS)
endif

# vim: set ts=4 sw=4 tw=120 noet :

34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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/)
Expand Down Expand Up @@ -115,4 +146,3 @@ not likely to be "down".

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

6 changes: 3 additions & 3 deletions config/dpaste.conf
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions configure.ac
Original file line number Diff line number Diff line change
@@ -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])
Expand Down Expand Up @@ -45,4 +45,3 @@ AM_COND_IF([DPASTE_TEST],
AC_OUTPUT

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

25 changes: 23 additions & 2 deletions doc/dpaste.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/bin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ Bin::Bin() {
conv >> port;
}

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

Expand All @@ -71,6 +70,7 @@ std::pair<bool, std::string> 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();
Expand Down Expand Up @@ -182,8 +182,10 @@ 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) {
node.run();
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: */

1 change: 0 additions & 1 deletion src/bin.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,3 @@ class Bin {
} /* dpaste */

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

99 changes: 74 additions & 25 deletions src/http_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
* along with dpaste. If not, see <http://www.gnu.org/licenses/>.
*/

#include <fstream>
#include <algorithm>
#include <cstdint>
#include <list>
#include <sstream>

#include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp>
Expand All @@ -28,69 +30,116 @@
#include <curlpp/Infos.hpp>
#include <nlohmann/json.hpp>
#include <b64/decode.h>
#include <b64/encode.h>

#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<curlpp::options::Port>(port);
std::stringstream response, oss;
req.setOpt<curlpp::options::Url>(HTTP_PROTO+
host+"/"+dht::InfoHash::get(code).toString()
+"?user_type="+dpaste::Node::DPASTE_USER_TYPE

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You removed the user_type spec from the URL. That makes it so the get operation on the DHT node service's side won't have this spec and it will then pull more values than it should. This usertype spec is used in the DHT protocol to filter out values so requests are as light as possible and also per-application specific.

I know that you are filtering values out afterwards, but at this point it's too late, network traffic was wasted for no reason.

Let's keep this network level optimization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is the migration to OpenDHT’s DhtProxyServer: its new GET /key/<hash> handler ignores query parameters entirely. Adding ?user_type=dpaste back would look correct but provide no DHT-level filtering.

);
req.setOpt<curlpp::options::Url>(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)) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to be sure that parsing line by line the output of the service will lead to valid JSON content? We can't know. The server's response should rather be parsed in its entirety.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are reading NDJSON files. By definition, each line is a valid JSON document.

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::string>());
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 {
try {
curlpp::Cleanup mycleanup;
curlpp::Easy req;
req.setOpt<curlpp::options::Port>(port);
req.setOpt<curlpp::options::Url>(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<curlpp::options::Url>(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));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use curlpp::Options::PostFields instead of keeping curlpp::options::HttpPost? You also create a temporary body variable instead of simply create the form parts in-place as we were doing before with curlpp::FormParts::Content which was taking care of creating the dictionary of values by itself. It's not a dealbreaker, but why not use the same approach that we were using before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proxy POST requires raw JSON PostFields, not multipart HttpPost.

req.setOpt(new curlpp::options::HttpHeader(
std::list<std::string> {"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: */

3 changes: 1 addition & 2 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down Expand Up @@ -124,4 +124,3 @@ class Node {
};

} /* dpaste */

13 changes: 13 additions & 0 deletions systemd/dpaste-dhtnode.service
Original file line number Diff line number Diff line change
@@ -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
Loading