diff --git a/.gitignore b/.gitignore
index 9d50e4d5..c79e0d15 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,11 @@ Makefile.in
# Python
*.pyc
+python/opendht.cpp
+python/setup.py
+
+# Doxygen
+doc/Doxyfile
# git backup files
*.orig
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2ea5b834..4efc5af5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,6 +16,9 @@ option (OPENDHT_SYSTEMD "Install systemd module" OFF)
option (OPENDHT_ARGON2 "Use included argon2 sources" OFF)
option (OPENDHT_LTO "Build with LTO" OFF)
option (OPENDHT_SANITIZE "Build with address sanitizer and stack protector" OFF)
+option (OPENDHT_PROXY_SERVER "Enable DHT proxy server, use Restbed and jsoncpp" OFF)
+option (OPENDHT_PROXY_SERVER_IDENTITY "Allow clients to use the node identity" OFF)
+option (OPENDHT_PROXY_CLIENT "Enable DHT proxy client, use Restbed and jsoncpp" OFF)
find_package(Doxygen)
option (OPENDHT_DOCUMENTATION "Create and install the HTML based API documentation (requires Doxygen)" ${DOXYGEN_FOUND})
@@ -41,6 +44,10 @@ if (NOT OPENDHT_ARGON2)
set(OPENDHT_ARGON2 ON)
endif()
endif ()
+if (OPENDHT_PROXY_SERVER OR OPENDHT_PROXY_CLIENT)
+ find_package(Restbed REQUIRED)
+ find_package(Jsoncpp REQUIRED)
+endif()
# Build flags
set (CMAKE_CXX_STANDARD 11)
@@ -77,6 +84,9 @@ endif ()
if (Nettle_INCLUDE_DIRS)
include_directories (SYSTEM "${Nettle_INCLUDE_DIRS}")
endif ()
+if (Jsoncpp_INCLUDE_DIRS)
+ include_directories (SYSTEM "${Jsoncpp_INCLUDE_DIRS}")
+endif ()
link_directories (${Nettle_LIBRARY_DIRS})
include_directories (
./
@@ -144,6 +154,25 @@ list (APPEND opendht_HEADERS
include/opendht/indexation/pht.h
)
+if (OPENDHT_PROXY_SERVER)
+ add_definitions(-DOPENDHT_PROXY_SERVER=true)
+ if (OPENDHT_PROXY_SERVER_IDENTITY)
+ add_definitions(-DOPENDHT_PROXY_SERVER_IDENTITY=true)
+ else ()
+ add_definitions(-DOPENDHT_PROXY_SERVER_IDENTITY=false)
+ endif()
+ list (APPEND opendht_HEADERS
+ include/opendht/dht_proxy_server.h
+ )
+ list (APPEND opendht_SOURCES
+ src/dht_proxy_server.cpp
+ src/base64.h
+ src/base64.cpp
+ )
+else ()
+ add_definitions(-DENABLE_PROXY_SERVER=false)
+endif ()
+
if(OPENDHT_ARGON2)
# make sure argon2 submodule is up to date and initialized
message("Initializing Argon2 submodule")
@@ -177,7 +206,7 @@ if (OPENDHT_STATIC)
target_link_libraries(opendht-static ${argon2_LIBRARIES})
target_include_directories(opendht-static SYSTEM PRIVATE ${argon2_INCLUDE_DIRS})
endif ()
- target_link_libraries(opendht-static ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES})
+ target_link_libraries(opendht-static ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES} ${Restbed_LIBRARIES} ${Jsoncpp_LIBRARIES})
install (TARGETS opendht-static DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht)
endif ()
@@ -198,7 +227,7 @@ if (OPENDHT_SHARED)
target_link_libraries(opendht PRIVATE ${argon2_LIBRARIES})
target_include_directories(opendht SYSTEM PRIVATE ${argon2_INCLUDE_DIRS})
endif ()
- target_link_libraries(opendht PRIVATE ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES})
+ target_link_libraries(opendht PRIVATE ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES} ${Restbed_LIBRARIES} ${Jsoncpp_LIBRARIES})
install (TARGETS opendht DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht)
endif ()
diff --git a/README.md b/README.md
index 0b6d58bb..1ce85436 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
- OpenDHT
+ OpenDHT
A lightweight C++11 Distributed Hash Table implementation originally based on https://github.com/jech/dht by Juliusz Chroboczek.
@@ -18,7 +18,7 @@ See the wiki:
#### How-to build and install
-Build instructions :
+Build instructions:
#### How-to build a simple client app
```bash
@@ -91,6 +91,8 @@ for r in results:
- msgpack-c 1.2+, used for data serialization.
- GnuTLS 3.3+, used for cryptographic operations.
- Nettle 2.4+, a GnuTLS dependency for crypto.
+- (optional) restbed 4.0+, used for the REST API.
+- (optional) jsoncpp 1.7.4-3+, used for the REST API.
- Build tested with GCC 4.8+ (GNU/Linux, Android, Windows with MinGW), Clang/LLVM (Linux, macOS).
- Build tested with Microsoft Visual Studio 2015
diff --git a/cmake/FindJsoncpp.cmake b/cmake/FindJsoncpp.cmake
new file mode 100644
index 00000000..fccc2da3
--- /dev/null
+++ b/cmake/FindJsoncpp.cmake
@@ -0,0 +1,18 @@
+find_path (JSONCPP_INCLUDE jsoncpp
+ HINTS
+ "/usr/include"
+ "/usr/local/include"
+ "/opt/local/include"
+)
+
+if (JSONCPP_INCLUDE)
+ message(STATUS "${green}Found Jsoncpp: ${JSONCPP_INCLUDE}")
+else()
+ message(FATAL_ERROR "${red}Failed to locate Jsoncpp.}")
+endif()
+
+if (JSONCPP_INCLUDE)
+ set(JSONCPP_FOUND TRUE)
+ set(Jsoncpp_LIBRARIES jsoncpp)
+ set(Jsoncpp_INCLUDE_DIRS ${JSONCPP_INCLUDE}/jsoncpp)
+endif()
diff --git a/cmake/FindRestbed.cmake b/cmake/FindRestbed.cmake
new file mode 100644
index 00000000..05f1e814
--- /dev/null
+++ b/cmake/FindRestbed.cmake
@@ -0,0 +1,17 @@
+find_path (RESTBED_INCLUDE restbed
+ HINTS
+ "/usr/include"
+ "/usr/local/include"
+ "/opt/local/include"
+)
+
+if (RESTBED_INCLUDE)
+ message(STATUS "${green}Found Restbed: ${RESTBED_INCLUDE}")
+else()
+ message(FATAL_ERROR "${red}Failed to locate Restbed.}")
+endif()
+
+if (RESTBED_INCLUDE)
+ set(RESTBED_FOUND TRUE)
+ set(Restbed_LIBRARIES restbed)
+endif()
diff --git a/configure.ac b/configure.ac
index 504de170..5863244b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -128,6 +128,26 @@ AM_COND_IF([ENABLE_TOOLS], [
])
])
+AC_ARG_ENABLE([proxy_server], AS_HELP_STRING([--enable-proxy-server], [Enable proxy server ability]), proxy_server=yes, proxy_server=no)
+AM_CONDITIONAL(ENABLE_PROXY_SERVER, test x$proxy_server == xyes)
+AM_COND_IF([ENABLE_PROXY_SERVER], [
+ AC_CHECK_LIB(restbed, exit,, AC_MSG_ERROR([Missing restbed files]))
+ PKG_CHECK_MODULES([Jsoncpp], [jsoncpp >= 1.7.4])
+ CPPFLAGS+=" -DOPENDHT_PROXY_SERVER=true -ljsoncpp -lrestbed"
+
+ AC_ARG_ENABLE([proxy_server_identity], AS_HELP_STRING([--enable-proxy-server-identity],
+ [Enable proxy server ability]), proxy_server_identity=yes, proxy_server_identity=no)
+ AM_CONDITIONAL(ENABLE_PROXY_SERVER_IDENTITY, test x$proxy_server_identity == xyes)
+ AM_COND_IF([ENABLE_PROXY_SERVER_IDENTITY], [
+ CPPFLAGS+=" -DOPENDHT_PROXY_SERVER_IDENTITY=true"
+ ], [
+ CPPFLAGS+=" -DOPENDHT_PROXY_SERVER_IDENTITY=false"
+ ])
+], [
+ CPPFLAGS+=" -DOPENDHT_PROXY_SERVER=false"
+])
+
+
AC_CONFIG_FILES([doc/Doxyfile doc/Makefile])
dnl Configure setup.py if we build the python module
diff --git a/include/opendht.h b/include/opendht.h
index 98d3ece9..e003a216 100644
--- a/include/opendht.h
+++ b/include/opendht.h
@@ -19,6 +19,9 @@
#pragma once
#include "opendht/dhtrunner.h"
+#if OPENDHT_PROXY_SERVER
+#include "opendht/dht_proxy_server.h"
+#endif
#include "opendht/log.h"
#include "opendht/default_types.h"
#include "opendht/indexation/pht.h"
diff --git a/include/opendht/callbacks.h b/include/opendht/callbacks.h
index 21f2dd0c..e354ba21 100644
--- a/include/opendht/callbacks.h
+++ b/include/opendht/callbacks.h
@@ -26,6 +26,10 @@
#include
#include
+#if OPENDHT_PROXY_SERVER
+#include
+#endif //OPENDHT_PROXY_SERVER
+
namespace dht {
struct Node;
@@ -47,6 +51,12 @@ struct OPENDHT_PUBLIC NodeStats {
unsigned table_depth;
unsigned getKnownNodes() const { return good_nodes + dubious_nodes; }
std::string toString() const;
+#if OPENDHT_PROXY_SERVER
+ /**
+ * Build a json object from a NodeStats
+ */
+ Json::Value toJson() const;
+#endif //OPENDHT_PROXY_SERVER
};
/**
diff --git a/include/opendht/dht_proxy_server.h b/include/opendht/dht_proxy_server.h
new file mode 100644
index 00000000..d2eb6a38
--- /dev/null
+++ b/include/opendht/dht_proxy_server.h
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2017 Savoir-faire Linux Inc.
+ * Author : Sébastien Blin
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#if OPENDHT_PROXY_SERVER
+
+#pragma once
+
+#include "def.h"
+#include "sockaddr.h"
+#include "infohash.h"
+
+#include
+#include
+#include
+
+namespace dht {
+
+class DhtRunner;
+
+/**
+ * Describes the REST API
+ */
+class OPENDHT_PUBLIC DhtProxyServer
+{
+public:
+ /**
+ * Start the Http server for OpenDHT
+ * @param dht the DhtRunner linked to this proxy server
+ * @param port to listen
+ * @note if the server fails to start (if port is already used or reserved),
+ * it will fails silently
+ */
+ DhtProxyServer(std::shared_ptr dht, in_port_t port = 8000);
+ virtual ~DhtProxyServer();
+
+ DhtProxyServer(const DhtProxyServer& other) = delete;
+ DhtProxyServer(DhtProxyServer&& other) = delete;
+ DhtProxyServer& operator=(const DhtProxyServer& other) = delete;
+ DhtProxyServer& operator=(DhtProxyServer&& other) = delete;
+
+ /**
+ * Stop the DhtProxyServer
+ */
+ void stop();
+
+private:
+ /**
+ * Return the PublicKey id, the node id and node stats
+ * Method: GET "/"
+ * Result: HTTP 200, body: Node infos in JSON format
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ void getNodeInfo(const std::shared_ptr& session) const;
+
+ /**
+ * Return Values of a InfoHash
+ * Method: GET "/{InfoHash: .*}"
+ * Return: Multiple JSON object in parts. Example:
+ * Value in JSON format\n
+ * Value in JSON format
+ *
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ void get(const std::shared_ptr& session) const;
+
+ /**
+ * Listen incoming Values of a InfoHash.
+ * Method: LISTEN "/{InfoHash: .*}"
+ * Return: Multiple JSON object in parts. Example:
+ * Value in JSON format\n
+ * Value in JSON format
+ *
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ void listen(const std::shared_ptr& session) const;
+
+ /**
+ * Put a value on the DHT
+ * Method: POST "/{InfoHash: .*}"
+ * body = Value to put in JSON
+ * Return: HTTP 200 if success and the value put in JSON
+ * On error: HTTP 503, body: {"err":"xxxx"} if no dht
+ * HTTP 400, body: {"err":"xxxx"} if bad json or HTTP 502 if put fails
+ * @param session
+ */
+ void put(const std::shared_ptr& session) const;
+
+#if OPENDHT_PROXY_SERVER_IDENTITY
+ /**
+ * Put a value to sign by the proxy on the DHT
+ * Method: SIGN "/{InfoHash: .*}"
+ * body = Value to put in JSON
+ * Return: HTTP 200 if success and the value put in JSON
+ * On error: HTTP 503, body: {"err":"xxxx"} if no dht
+ * HTTP 400, body: {"err":"xxxx"} if bad json
+ * @param session
+ */
+ void putSigned(const std::shared_ptr& session) const;
+
+ /**
+ * Put a value to encrypt by the proxy on the DHT
+ * Method: ENCRYPT "/{hash: .*}"
+ * body = Value to put in JSON + "to":"infoHash"
+ * Return: HTTP 200 if success and the value put in JSON
+ * On error: HTTP 503, body: {"err":"xxxx"} if no dht
+ * HTTP 400, body: {"err":"xxxx"} if bad json
+ * @param session
+ */
+ void putEncrypted(const std::shared_ptr& session) const;
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+
+ /**
+ * Return Values of a InfoHash filtered by a value id
+ * Method: GET "/{InfoHash: .*}/{ValueId: .*}"
+ * Return: Multiple JSON object in parts. Example:
+ * Value in JSON format\n
+ * Value in JSON format
+ *
+ * On error: HTTP 503, body: {"err":"xxxx"}
+ * @param session
+ */
+ void getFiltered(const std::shared_ptr& session) const;
+
+ /**
+ * Respond allowed Methods
+ * Method: OPTIONS "/{hash: .*}"
+ * Return: HTTP 200 + Allow: allowed methods
+ * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
+ */
+ void handleOptionsMethod(const std::shared_ptr& session) const;
+
+ std::thread server_thread {};
+ std::unique_ptr service_;
+ std::shared_ptr dht_;
+
+ // Handle client quit for listen.
+ // NOTE: can be simplified when we will supports restbed 5.0
+ std::thread listenThread_;
+ struct SessionToHashToken {
+ std::shared_ptr session;
+ InfoHash hash;
+ std::future token;
+ };
+ mutable std::vector currentListeners_;
+ std::atomic_bool stopListeners {false};
+};
+
+}
+
+#endif //OPENDHT_PROXY_SERVER
diff --git a/include/opendht/value.h b/include/opendht/value.h
index 6e5a33db..60dd1d99 100644
--- a/include/opendht/value.h
+++ b/include/opendht/value.h
@@ -37,6 +37,10 @@
#include
#include
+#if OPENDHT_PROXY_SERVER
+#include
+#endif //OPENDHT_PROXY_SERVER
+
namespace dht {
struct Value;
@@ -346,6 +350,14 @@ struct OPENDHT_PUBLIC Value
Value(ValueType::Id t, const uint8_t* dat_ptr, size_t dat_len, Id id = INVALID_ID)
: id(id), type(t), data(dat_ptr, dat_ptr+dat_len) {}
+#if OPENDHT_PROXY_SERVER
+ /**
+ * Build a value from a json object
+ * @param json
+ */
+ Value(Json::Value& json);
+#endif //OPENDHT_PROXY_SERVER
+
template
Value(ValueType::Id t, const Type& d, Id id = INVALID_ID)
: id(id), type(t), data(packMsg(d)) {}
@@ -417,6 +429,18 @@ struct OPENDHT_PUBLIC Value
return ss.str();
}
+#if OPENDHT_PROXY_SERVER
+ /**
+ * Build a json object from a value
+ * Example:
+ * {
+ * "data":"base64ofdata",
+ * id":"0", "seq":0,"type":3
+ * }
+ */
+ Json::Value toJson() const;
+#endif //OPENDHT_PROXY_SERVER
+
/** Return the size in bytes used by this value in memory (minimum). */
size_t size() const;
diff --git a/src/Makefile.am b/src/Makefile.am
index f4614795..4ff14741 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -54,6 +54,11 @@ nobase_include_HEADERS = \
../include/opendht/rng.h \
../include/opendht/indexation/pht.h
+if ENABLE_PROXY_SERVER
+libopendht_la_SOURCES += base64.h base64.cpp dht_proxy_server.cpp
+nobase_include_HEADERS += ../include/opendht/dht_proxy_server.h
+endif
+
clean-local:
rm -rf libargon2.la
diff --git a/src/base64.cpp b/src/base64.cpp
new file mode 100644
index 00000000..7e255582
--- /dev/null
+++ b/src/base64.cpp
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2004-2017 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "base64.h"
+
+#include
+#include
+
+/* Mainly based on the following stackoverflow question:
+ * http://stackoverflow.com/questions/342409/how-do-i-base64-encode-decode-in-c
+ */
+static const char encoding_table[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
+ 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
+ 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
+ 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2',
+ '3', '4', '5', '6', '7', '8', '9', '+', '/'
+};
+
+static const size_t mod_table[] = { 0, 2, 1 };
+
+char *base64_encode(const uint8_t *input, size_t input_length,
+ char *output, size_t *output_length)
+{
+ size_t i, j;
+ size_t out_sz = *output_length;
+ *output_length = 4 * ((input_length + 2) / 3);
+ if (out_sz < *output_length || output == nullptr)
+ return nullptr;
+
+ for (i = 0, j = 0; i < input_length; ) {
+ uint8_t octet_a = i < input_length ? input[i++] : 0;
+ uint8_t octet_b = i < input_length ? input[i++] : 0;
+ uint8_t octet_c = i < input_length ? input[i++] : 0;
+
+ uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c;
+
+ output[j++] = encoding_table[(triple >> 3 * 6) & 0x3F];
+ output[j++] = encoding_table[(triple >> 2 * 6) & 0x3F];
+ output[j++] = encoding_table[(triple >> 1 * 6) & 0x3F];
+ output[j++] = encoding_table[(triple >> 0 * 6) & 0x3F];
+ }
+
+ for (i = 0; i < mod_table[input_length % 3]; i++)
+ output[*output_length - 1 - i] = '=';
+
+ return output;
+}
+
+uint8_t *base64_decode(const char *input, size_t input_length,
+ uint8_t *output, size_t *output_length)
+{
+ size_t i, j;
+ uint8_t decoding_table[256];
+
+ uint8_t c;
+ for (c = 0; c < 64; c++)
+ decoding_table[static_cast(encoding_table[c])] = c;
+
+ if (input_length % 4 != 0 || input_length < 2)
+ return nullptr;
+
+ size_t out_sz = *output_length;
+ *output_length = input_length / 4 * 3;
+ if (input[input_length - 1] == '=')
+ (*output_length)--;
+ if (input[input_length - 2] == '=')
+ (*output_length)--;
+
+ if (out_sz < *output_length || output == nullptr)
+ return nullptr;
+
+ for (i = 0, j = 0; i < input_length;) {
+ uint8_t sextet_a = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast(input[i++])];
+ uint8_t sextet_b = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast(input[i++])];
+ uint8_t sextet_c = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast(input[i++])];
+ uint8_t sextet_d = input[i] == '=' ? 0 & i++
+ : decoding_table[static_cast(input[i++])];
+
+ uint32_t triple = (sextet_a << 3 * 6) +
+ (sextet_b << 2 * 6) +
+ (sextet_c << 1 * 6) +
+ (sextet_d << 0 * 6);
+
+ if (j < *output_length)
+ output[j++] = (triple >> 2 * 8) & 0xFF;
+ if (j < *output_length)
+ output[j++] = (triple >> 1 * 8) & 0xFF;
+ if (j < *output_length)
+ output[j++] = (triple >> 0 * 8) & 0xFF;
+ }
+
+ return output;
+}
+
+std::string
+base64_encode(const std::vector::const_iterator begin,
+ const std::vector::const_iterator end)
+{
+ size_t output_length = 4 * ((std::distance(begin, end) + 2) / 3);
+ std::string out;
+ out.resize(output_length);
+ base64_encode(&(*begin), std::distance(begin, end),
+ &(*out.begin()), &output_length);
+ out.resize(output_length);
+ return out;
+}
+
+
+std::string
+base64_encode(const std::vector& str)
+{
+ return base64_encode(str.cbegin(), str.cend());
+}
+
+std::string
+base64_decode(const std::string& str)
+{
+ size_t output_length = str.length() / 4 * 3 + 2;
+ std::vector output;
+ output.resize(output_length);
+ base64_decode(str.data(), str.size(), output.data(), &output_length);
+ output.resize(output_length);
+ return std::string(output.begin(), output.end());
+}
diff --git a/src/base64.h b/src/base64.h
new file mode 100644
index 00000000..3f5f39e1
--- /dev/null
+++ b/src/base64.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2004-2017 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+/**
+ * Encode a buffer in base64.
+ *
+ * @param str the input buffer
+ * @return a base64-encoded buffer
+ */
+std::string base64_encode(const std::vector& str);
+/**
+ * Decode a buffer in base64.
+ *
+ * @param str the input buffer
+ * @return a base64-decoded buffer
+ */
+std::string base64_decode(const std::string& str);
diff --git a/src/callbacks.cpp b/src/callbacks.cpp
index 8908a2df..89719ef1 100644
--- a/src/callbacks.cpp
+++ b/src/callbacks.cpp
@@ -68,4 +68,24 @@ NodeStats::toString() const
return ss.str();
}
+#if OPENDHT_PROXY_SERVER
+/**
+ * Build a json object from a NodeStats
+ */
+Json::Value
+NodeStats::toJson() const
+{
+ Json::Value val;
+ val["good"] = static_cast(good_nodes);
+ val["dubious"] = static_cast(dubious_nodes);
+ val["incoming"] = static_cast(incoming_nodes);
+ if (table_depth > 1) {
+ val["table_depth"] = static_cast(table_depth);
+ unsigned long tot_nodes = 8 * std::exp2(table_depth);
+ val["network_size_estimation"] = static_cast(tot_nodes);
+ }
+ return val;
+}
+#endif //OPENDHT_PROXY_SERVER
+
}
diff --git a/src/dht_proxy_server.cpp b/src/dht_proxy_server.cpp
new file mode 100644
index 00000000..586a8b77
--- /dev/null
+++ b/src/dht_proxy_server.cpp
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2017 Savoir-faire Linux Inc.
+ * Author : Sébastien Blin
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#if OPENDHT_PROXY_SERVER
+#include "dht_proxy_server.h"
+
+#include "default_types.h"
+#include "dhtrunner.h"
+#include "msgpack.hpp"
+
+#include
+#include
+#include
+#include
+
+using namespace std::placeholders;
+
+namespace dht {
+
+DhtProxyServer::DhtProxyServer(std::shared_ptr dht, in_port_t port)
+: dht_(dht)
+{
+ // NOTE in c++14, use make_unique
+ service_ = std::unique_ptr(new restbed::Service());
+
+ server_thread = std::thread([this, port]() {
+ // Create endpoints
+ auto resource = std::make_shared();
+ resource->set_path("/");
+ resource->set_method_handler("GET", std::bind(&DhtProxyServer::getNodeInfo, this, _1));
+ service_->publish(resource);
+ resource = std::make_shared();
+ resource->set_path("/{hash: .*}");
+ resource->set_method_handler("GET", std::bind(&DhtProxyServer::get, this, _1));
+ resource->set_method_handler("LISTEN", std::bind(&DhtProxyServer::listen, this, _1));
+ resource->set_method_handler("POST", std::bind(&DhtProxyServer::put, this, _1));
+#if OPENDHT_PROXY_SERVER_IDENTITY
+ resource->set_method_handler("SIGN", std::bind(&DhtProxyServer::putSigned, this, _1));
+ resource->set_method_handler("ENCRYPT", std::bind(&DhtProxyServer::putEncrypted, this, _1));
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+ resource->set_method_handler("OPTIONS", std::bind(&DhtProxyServer::handleOptionsMethod, this, _1));
+ service_->publish(resource);
+ resource = std::make_shared();
+ resource->set_path("/{hash: .*}/{value: .*}");
+ resource->set_method_handler("GET", std::bind(&DhtProxyServer::getFiltered, this, _1));
+ service_->publish(resource);
+
+ // Start server
+ auto settings = std::make_shared();
+ settings->set_default_header("Content-Type", "application/json");
+ settings->set_default_header("Connection", "keep-alive");
+ std::chrono::milliseconds timeout(std::numeric_limits::max());
+ settings->set_connection_timeout(timeout); // there is a timeout, but really huge
+ settings->set_port(port);
+ try {
+ service_->start(settings);
+ } catch(std::system_error& e) {
+ std::cerr << "Error running server on port " << port << ": " << e.what() << std::endl;
+ }
+ });
+
+ listenThread_ = std::thread([this]() {
+ auto stop = false;
+ while (!stop) {
+ auto listener = currentListeners_.begin();
+ while (listener != currentListeners_.end()) {
+ if (listener->session->is_closed() && dht_) {
+ dht_->cancelListen(listener->hash, std::move(listener->token));
+ // Remove listener if unused
+ listener = currentListeners_.erase(listener);
+ } else {
+ ++listener;
+ }
+ }
+ //NOTE: When supports restbed 5.0: service_->is_up() and remove stopListeners
+ stop = stopListeners;
+ if (!stop)
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ }
+ });
+}
+
+DhtProxyServer::~DhtProxyServer()
+{
+ stop();
+}
+
+void
+DhtProxyServer::stop()
+{
+ service_->stop();
+ stopListeners = true;
+ // listenThreads_ will stop because there is no more sessions
+ if (listenThread_.joinable())
+ listenThread_.join();
+ if (server_thread.joinable())
+ server_thread.join();
+}
+
+void
+DhtProxyServer::getNodeInfo(const std::shared_ptr& session) const
+{
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& /*b*/)
+ {
+ if (dht_) {
+ Json::Value result;
+ auto id = dht_->getId();
+ if (id)
+ result["id"] = id.toString();
+ result["node_id"] = dht_->getNodeId().toString();
+ result["ipv4"] = dht_->getNodesStats(AF_INET).toJson();
+ result["ipv6"] = dht_->getNodesStats(AF_INET6).toJson();
+ Json::FastWriter writer;
+ s->close(restbed::OK, writer.write(result));
+ }
+ else
+ s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ );
+}
+
+void
+DhtProxyServer::get(const std::shared_ptr& session) const
+{
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ auto hash = request->get_path_parameter("hash");
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& /*b* */)
+ {
+ if (dht_) {
+ InfoHash infoHash(hash);
+ if (!infoHash) {
+ infoHash = InfoHash::get(hash);
+ }
+ s->yield(restbed::OK, "", [=]( const std::shared_ptr< restbed::Session > s) {
+ dht_->get(infoHash, [s](std::shared_ptr value) {
+ // Send values as soon as we get them
+ Json::FastWriter writer;
+ s->yield(writer.write(value->toJson()), [](const std::shared_ptr /*session*/){ });
+ return true;
+ }, [s](bool /*ok* */) {
+ // Communication is finished
+ s->close();
+ });
+ });
+ } else {
+ s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ }
+ );
+
+}
+
+void
+DhtProxyServer::listen(const std::shared_ptr& session) const
+{
+
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ auto hash = request->get_path_parameter("hash");
+ InfoHash infoHash(hash);
+ if (!infoHash)
+ infoHash = InfoHash::get(hash);
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& /*b* */)
+ {
+ if (dht_) {
+ InfoHash infoHash(hash);
+ if (!infoHash) {
+ infoHash = InfoHash::get(hash);
+ }
+ s->yield(restbed::OK);
+ // Handle client deconnection
+ // NOTE: for now, there is no handler, so we test the session in a thread
+ // will be the case in restbed 5.0
+ SessionToHashToken listener;
+ listener.session = session;
+ listener.hash = infoHash;
+ listener.token = dht_->listen(infoHash, [s](std::shared_ptr value) {
+ // Send values as soon as we get them
+ if (!s->is_closed()) {
+ Json::FastWriter writer;
+ s->yield(writer.write(value->toJson()), [](const std::shared_ptr /*session*/){ });
+ }
+ return !s->is_closed();
+ });
+ currentListeners_.emplace_back(std::move(listener));
+ } else {
+ session->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ }
+ );
+}
+
+void
+DhtProxyServer::put(const std::shared_ptr& session) const
+{
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ auto hash = request->get_path_parameter("hash");
+ InfoHash infoHash(hash);
+ if (!infoHash)
+ infoHash = InfoHash::get(hash);
+
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& b)
+ {
+ if (dht_) {
+ if(b.empty()) {
+ std::string response("{\"err\":\"Missing parameters\"}");
+ s->close(restbed::BAD_REQUEST, response);
+ } else {
+ restbed::Bytes buf(b);
+ Json::Value root;
+ Json::Reader reader;
+ std::string strJson(buf.begin(), buf.end());
+ bool parsingSuccessful = reader.parse(strJson.c_str(), root);
+ if (parsingSuccessful) {
+ // Build the Value from json
+ auto value = std::make_shared(root);
+
+ dht_->put(infoHash, value, [s, value](bool ok) {
+ if (ok) {
+ Json::FastWriter writer;
+ s->close(restbed::OK, writer.write(value->toJson()));
+ } else {
+ s->close(restbed::BAD_GATEWAY, "{\"err\":\"put failed\"}");
+ }
+ });
+ } else {
+ s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}");
+ }
+ }
+ } else {
+ s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ }
+ );
+}
+
+#if OPENDHT_PROXY_SERVER_IDENTITY
+void
+DhtProxyServer::putSigned(const std::shared_ptr& session) const
+{
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ auto hash = request->get_path_parameter("hash");
+ InfoHash infoHash(hash);
+ if (!infoHash)
+ infoHash = InfoHash::get(hash);
+
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& b)
+ {
+ if (dht_) {
+ if(b.empty()) {
+ std::string response("{\"err\":\"Missing parameters\"}");
+ s->close(restbed::BAD_REQUEST, response);
+ } else {
+ restbed::Bytes buf(b);
+ Json::Value root;
+ Json::Reader reader;
+ std::string strJson(buf.begin(), buf.end());
+ bool parsingSuccessful = reader.parse(strJson.c_str(), root);
+ if (parsingSuccessful) {
+ auto value = std::make_shared(root);
+
+ Json::FastWriter writer;
+ dht_->putSigned(infoHash, value);
+ s->close(restbed::OK, writer.write(value->toJson()));
+ } else {
+ s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}");
+ }
+ }
+ } else {
+ s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ }
+ );
+}
+
+void
+DhtProxyServer::putEncrypted(const std::shared_ptr& session) const
+{
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ auto hash = request->get_path_parameter("hash");
+ InfoHash key(hash);
+ if (!key)
+ key = InfoHash::get(hash);
+
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& b)
+ {
+ if (dht_) {
+ if(b.empty()) {
+ std::string response("{\"err\":\"Missing parameters\"}");
+ s->close(restbed::BAD_REQUEST, response);
+ } else {
+ restbed::Bytes buf(b);
+ Json::Value root;
+ Json::Reader reader;
+ std::string strJson(buf.begin(), buf.end());
+ bool parsingSuccessful = reader.parse(strJson.c_str(), root);
+ InfoHash to(root["to"].asString());
+ if (parsingSuccessful && toInfoHash) {
+ auto value = std::make_shared(root);
+ Json::FastWriter writer;
+ dht_->putEncrypted(key, to, value);
+ s->close(restbed::OK, writer.write(value->toJson()));
+ } else {
+ if(!parsingSuccessful)
+ s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}");
+ else
+ s->close(restbed::BAD_REQUEST, "{\"err\":\"No destination found\"}");
+ }
+ }
+ } else {
+ s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ }
+ );
+}
+#endif // OPENDHT_PROXY_SERVER_IDENTITY
+
+void
+DhtProxyServer::handleOptionsMethod(const std::shared_ptr& session) const
+{
+#if OPENDHT_PROXY_SERVER_IDENTITY
+ const auto allowed = "OPTIONS, GET, POST, LISTEN, SIGN, ENCRYPT";
+#else
+ const auto allowed = "OPTIONS, GET, POST, LISTEN";
+#endif //OPENDHT_PROXY_SERVER_IDENTITY
+ session->close(restbed::OK, {{"Access-Control-Allow-Methods", allowed},
+ {"Access-Control-Allow-Headers", "content-type"},
+ {"Access-Control-Max-Age", "86400"}});
+}
+
+void
+DhtProxyServer::getFiltered(const std::shared_ptr& session) const
+{
+ const auto request = session->get_request();
+ int content_length = std::stoi(request->get_header("Content-Length", "0"));
+ auto hash = request->get_path_parameter("hash");
+ auto value = request->get_path_parameter("value");
+ session->fetch(content_length,
+ [=](const std::shared_ptr s, const restbed::Bytes& /*b* */)
+ {
+ if (dht_) {
+ InfoHash infoHash(hash);
+ if (!infoHash) {
+ infoHash = InfoHash::get(hash);
+ }
+ s->yield(restbed::OK, "", [=]( const std::shared_ptr< restbed::Session > s) {
+ dht_->get(infoHash, [s](std::shared_ptr v) {
+ // Send values as soon as we get them
+ Json::FastWriter writer;
+ s->yield(writer.write(v->toJson()), [](const std::shared_ptr /*session*/){ });
+ return true;
+ }, [s](bool /*ok* */) {
+ // Communication is finished
+ s->close();
+ }, {}, value);
+ });
+ } else {
+ s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}");
+ }
+ }
+ );
+}
+
+}
+#endif //OPENDHT_PROXY_SERVER
diff --git a/src/value.cpp b/src/value.cpp
index e3d3aece..759c17fb 100644
--- a/src/value.cpp
+++ b/src/value.cpp
@@ -22,6 +22,11 @@
#include "default_types.h"
#include "securedht.h" // print certificate ID
+#if OPENDHT_PROXY_SERVER
+#include "base64.h"
+#endif //OPENDHT_PROXY_SERVER
+
+
namespace dht {
const std::string Query::QUERY_PARSE_ERROR {"Error parsing query."};
@@ -167,6 +172,71 @@ Value::msgpack_unpack_body(const msgpack::object& o)
}
}
+#if OPENDHT_PROXY_SERVER
+Value::Value(Json::Value& json)
+{
+ try {
+ if (json.isMember("id"))
+ id = ValueType::Id(json["id"].asInt());
+ } catch (...) { }
+ if (json.isMember("cypher")) {
+ auto cypherStr = json["cypher"].asString();
+ cypherStr = base64_decode(cypherStr);
+ cypher = std::vector(cypherStr.begin(), cypherStr.end());
+ }
+ if (json.isMember("sig")) {
+ auto sigStr = json["sig"].asString();
+ sigStr = base64_decode(sigStr);
+ signature = std::vector(sigStr.begin(), sigStr.end());
+ }
+ if (json.isMember("seq"))
+ seq = json["seq"].asInt();
+ if (json.isMember("owner")) {
+ auto ownerStr = json["owner"].asString();
+ auto ownerBlob = std::vector(ownerStr.begin(), ownerStr.end());
+ owner = std::make_shared(ownerBlob);
+ }
+ if (json.isMember("to")) {
+ auto toStr = json["to"].asString();
+ recipient = InfoHash(toStr);
+ }
+ if (json.isMember("type"))
+ type = json["type"].asInt();
+ if (json.isMember("data")){
+ auto dataStr = json["data"].asString();
+ dataStr = base64_decode(dataStr);
+ data = std::vector(dataStr.begin(), dataStr.end());
+ }
+ if (json.isMember("utype"))
+ user_type = json["utype"].asString();
+}
+
+Json::Value
+Value::toJson() const
+{
+ Json::Value val;
+ val["id"] = std::to_string(id);
+ if (isEncrypted()) {
+ val["cypher"] = base64_encode(cypher);
+ } else {
+ if (isSigned())
+ val["sig"] = base64_encode(signature);
+ bool has_owner = owner && *owner;
+ if (has_owner) { // isSigned
+ val["seq"] = seq;
+ val["owner"] = owner->toString();
+ if (recipient != InfoHash())
+ val["to"] = recipient.toString();
+ }
+ val["type"] = type;
+ val["data"] = base64_encode(data);
+ if (not user_type.empty())
+ val["utype"] = user_type;
+ }
+ return val;
+}
+#endif //OPENDHT_PROXY_SERVER
+
bool
FieldValue::operator==(const FieldValue& vfd) const
{
diff --git a/tools/dhtnode.cpp b/tools/dhtnode.cpp
index a55d3d20..efad9767 100644
--- a/tools/dhtnode.cpp
+++ b/tools/dhtnode.cpp
@@ -29,7 +29,7 @@ extern "C" {
using namespace dht;
void print_usage() {
- std::cout << "Usage: dhtnode [-v [-l logfile]] [-i] [-d] [-n network_id] [-p local_port] [-b bootstrap_host[:port]]" << std::endl << std::endl;
+ std::cout << "Usage: dhtnode [-v [-l logfile]] [-i] [-d] [-n network_id] [-p local_port] [-b bootstrap_host[:port]] [--proxyserver local_port]" << std::endl << std::endl;
std::cout << "dhtnode, a simple OpenDHT command line node runner." << std::endl;
std::cout << "Report bugs to: http://opendht.net" << std::endl;
}
@@ -55,16 +55,23 @@ void print_help() {
<< " ll Print basic information and stats about the current node." << std::endl
<< " ls [key] Print basic information about current search(es)." << std::endl
<< " ld [key] Print basic information about currenty stored values on this node (or key)." << std::endl
- << " lr Print the full current routing table of this node" << std::endl;
+ << " lr Print the full current routing table of this node." << std::endl;
+
+#if OPENDHT_PROXY_SERVER
+ std::cout << std::endl << "Operations with the proxy:" << std::endl
+ << " pst [port] Start the proxy interface on port." << std::endl
+ << " psp [port] Stop the proxy interface on port." << std::endl;
+#endif //OPENDHT_PROXY_SERVER
std::cout << std::endl << "Operations on the DHT:" << std::endl
- << " b Ping potential node at given IP address/port." << std::endl
+ << " b Ping potential node at given IP address/port." << std::endl
<< " g Get values at ." << std::endl
<< " l Listen for value changes at ." << std::endl
<< " p Put string value at ." << std::endl
<< " pp Put string value at (persistent version)." << std::endl
<< " s Put string value at , signed with our generated private key." << std::endl
<< " e Put string value at , encrypted for with its public key (if found)." << std::endl;
+
std::cout << std::endl << "Indexation operations on the DHT:" << std::endl
<< " il [exact match] Lookup the index named with the key ." << std::endl
<< " Set [exact match] to 'false' for inexact match lookup." << std::endl
@@ -83,6 +90,12 @@ void cmd_loop(std::shared_ptr& dht, dht_params& params)
#endif
std::map indexes;
+#if OPENDHT_PROXY_SERVER
+ std::map> proxies;
+ if (params.proxyserver != 0) {
+ proxies.emplace(params.proxyserver, new DhtProxyServer(dht, params.proxyserver));
+ }
+#endif //OPENDHT_PROXY_SERVER
while (true)
{
@@ -160,11 +173,29 @@ void cmd_loop(std::shared_ptr& dht, dht_params& params)
dht->setLogFilter(filter);
continue;
}
+#if OPENDHT_PROXY_SERVER
+ else if (op == "pst") {
+ iss >> idstr;
+ try {
+ unsigned int port = std::stoi(idstr);
+ proxies.emplace(port, new DhtProxyServer(dht, port));
+ } catch (...) { }
+ continue;
+ } else if (op == "psp") {
+ iss >> idstr;
+ try {
+ auto it = proxies.find(std::stoi(idstr));
+ if (it != proxies.end())
+ proxies.erase(it);
+ } catch (...) { }
+ continue;
+ }
+#endif //OPENDHT_PROXY_SERVER
if (op.empty())
continue;
- static const std::set VALID_OPS {"g", "l", "cl", "il", "ii", "p", "pp", "cpp", "s", "e", "a"};
+ static const std::set VALID_OPS {"g", "l", "cl", "il", "ii", "p", "pp", "cpp", "s", "e", "a", "q"};
if (VALID_OPS.find(op) == VALID_OPS.cend()) {
std::cout << "Unknown command: " << op << std::endl;
std::cout << " (type 'h' or 'help' for a list of possible commands)" << std::endl;
diff --git a/tools/tools_common.h b/tools/tools_common.h
index 2e4d72b0..39974fad 100644
--- a/tools/tools_common.h
+++ b/tools/tools_common.h
@@ -119,6 +119,7 @@ struct dht_params {
bool daemonize {false};
bool service {false};
std::pair bootstrap {};
+ in_port_t proxyserver {0};
};
static const constexpr struct option long_options[] = {
@@ -132,6 +133,7 @@ static const constexpr struct option long_options[] = {
{"service", no_argument , nullptr, 's'},
{"logfile", required_argument, nullptr, 'l'},
{"syslog", no_argument , nullptr, 'L'},
+ {"proxyserver",required_argument, nullptr, 'S'},
{nullptr, 0 , nullptr, 0}
};
@@ -149,6 +151,14 @@ parseArgs(int argc, char **argv) {
std::cout << "Invalid port: " << port_arg << std::endl;
}
break;
+ case 'S': {
+ int port_arg = atoi(optarg);
+ if (port_arg >= 0 && port_arg < 0x10000)
+ params.proxyserver = port_arg;
+ else
+ std::cout << "Invalid port: " << port_arg << std::endl;
+ }
+ break;
case 'n':
params.network = strtoul(optarg, nullptr, 0);
break;