diff --git a/.gitignore b/.gitignore index 9d50e4d515febddf4cd80638f47681e418c55567..c79e0d153e35d6b063097a957b056815a9c8f10e 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 2ea5b8347b1f2bfc8ef80b9c4b3b8a8ebb7e2dbf..4efc5af519e480a7622a1e80631786c7d141e16c 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 0b6d58bb3abd74f0679d6982d3d8fc32211bca77..1ce85436b4ef5d173dc7bb47d74e338f7c806600 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ <img src="https://raw.githubusercontent.com/savoirfairelinux/opendht/master/resources/opendht_logo_512.png" width="100" align="right"> <br> <h1 style="margin-top:10px"> - <a id="user-content-opendht-" class="anchor" href="/savoirfairelinux/opendht/blob/master/README.md#opendht-" aria-hidden="true"></a>OpenDHT + <a id="user-content-opendht-" class="anchor" href="/savoirfairelinux/opendht/blob/master/README.md#opendht-" aria-hidden="true"></a>OpenDHT </h1> 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: <https://github.com/savoirfairelinux/opendht/wiki> #### How-to build and install -Build instructions : <https://github.com/savoirfairelinux/opendht/wiki/Build-the-library> +Build instructions: <https://github.com/savoirfairelinux/opendht/wiki/Build-the-library> #### 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 0000000000000000000000000000000000000000..fccc2da33b82c31d32fd478aa6c06f96f6bb0e7b --- /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 0000000000000000000000000000000000000000..05f1e814d5e14b5b9e977c6d610d648c03c8a3e8 --- /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 504de170e45aa8b7d2f2b94e3a329110cbe80d88..5863244b04083e5af9886df5f3edab36dd7e7d77 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 98d3ece943b652bf5624687a3951b9bff96b9373..e003a21631f8c4612412db8ed5d493037579f93e 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 21f2dd0c0562d00e8306ffaccea394eca7c1acec..e354ba2184c9ca38f8fd2c887e8da7a6f9964e49 100644 --- a/include/opendht/callbacks.h +++ b/include/opendht/callbacks.h @@ -26,6 +26,10 @@ #include <memory> #include <functional> +#if OPENDHT_PROXY_SERVER +#include <json/json.h> +#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 0000000000000000000000000000000000000000..d2eb6a3896072c03096c846bfd12380eb65c83f7 --- /dev/null +++ b/include/opendht/dht_proxy_server.h @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * Author : Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#if OPENDHT_PROXY_SERVER + +#pragma once + +#include "def.h" +#include "sockaddr.h" +#include "infohash.h" + +#include <thread> +#include <memory> +#include <restbed> + +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<DhtRunner> 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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session>& session) const; + + std::thread server_thread {}; + std::unique_ptr<restbed::Service> service_; + std::shared_ptr<DhtRunner> 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<restbed::Session> session; + InfoHash hash; + std::future<size_t> token; + }; + mutable std::vector<SessionToHashToken> currentListeners_; + std::atomic_bool stopListeners {false}; +}; + +} + +#endif //OPENDHT_PROXY_SERVER diff --git a/include/opendht/value.h b/include/opendht/value.h index 6e5a33dbb62c307b29cb51f76262d7a7a4d79c9b..60dd1d99d8bca134e90291dde8548fe6f79625c9 100644 --- a/include/opendht/value.h +++ b/include/opendht/value.h @@ -37,6 +37,10 @@ #include <chrono> #include <set> +#if OPENDHT_PROXY_SERVER +#include <json/json.h> +#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 <typename Type> 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 f4614795c8fc331ef881b806cc7ee69720a6c025..4ff14741cfdbfd909b1966869eb8cb623d09a111 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 0000000000000000000000000000000000000000..7e255582c8bbc60bbeb974bf5951324262be6757 --- /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 <https://www.gnu.org/licenses/>. + */ + +#include "base64.h" + +#include <stdint.h> +#include <stdlib.h> + +/* 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<int>(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<int>(input[i++])]; + uint8_t sextet_b = input[i] == '=' ? 0 & i++ + : decoding_table[static_cast<int>(input[i++])]; + uint8_t sextet_c = input[i] == '=' ? 0 & i++ + : decoding_table[static_cast<int>(input[i++])]; + uint8_t sextet_d = input[i] == '=' ? 0 & i++ + : decoding_table[static_cast<int>(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<uint8_t>::const_iterator begin, + const std::vector<uint8_t>::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<unsigned char>& 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<uint8_t> 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 0000000000000000000000000000000000000000..3f5f39e1ab55efdd9a07f006179882312a4f3f14 --- /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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <string> +#include <vector> + +/** + * Encode a buffer in base64. + * + * @param str the input buffer + * @return a base64-encoded buffer + */ +std::string base64_encode(const std::vector<unsigned char>& 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 8908a2dff2405223e1c2715e6c2b2b1262cbbfaf..89719ef1113a98a593ae745b37646f52f401f75f 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<Json::LargestUInt>(good_nodes); + val["dubious"] = static_cast<Json::LargestUInt>(dubious_nodes); + val["incoming"] = static_cast<Json::LargestUInt>(incoming_nodes); + if (table_depth > 1) { + val["table_depth"] = static_cast<Json::LargestUInt>(table_depth); + unsigned long tot_nodes = 8 * std::exp2(table_depth); + val["network_size_estimation"] = static_cast<Json::LargestUInt>(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 0000000000000000000000000000000000000000..586a8b775cccfe169b21aab15d8867a164b2f89d --- /dev/null +++ b/src/dht_proxy_server.cpp @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2017 Savoir-faire Linux Inc. + * Author : Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#if OPENDHT_PROXY_SERVER +#include "dht_proxy_server.h" + +#include "default_types.h" +#include "dhtrunner.h" +#include "msgpack.hpp" + +#include <chrono> +#include <functional> +#include <json/json.h> +#include <limits> + +using namespace std::placeholders; + +namespace dht { + +DhtProxyServer::DhtProxyServer(std::shared_ptr<DhtRunner> dht, in_port_t port) +: dht_(dht) +{ + // NOTE in c++14, use make_unique + service_ = std::unique_ptr<restbed::Service>(new restbed::Service()); + + server_thread = std::thread([this, port]() { + // Create endpoints + auto resource = std::make_shared<restbed::Resource>(); + resource->set_path("/"); + resource->set_method_handler("GET", std::bind(&DhtProxyServer::getNodeInfo, this, _1)); + service_->publish(resource); + resource = std::make_shared<restbed::Resource>(); + 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<restbed::Resource>(); + 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<restbed::Settings>(); + settings->set_default_header("Content-Type", "application/json"); + settings->set_default_header("Connection", "keep-alive"); + std::chrono::milliseconds timeout(std::numeric_limits<int>::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<restbed::Session>& 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<restbed::Session> 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<restbed::Session>& 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<restbed::Session> 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> value) { + // Send values as soon as we get them + Json::FastWriter writer; + s->yield(writer.write(value->toJson()), [](const std::shared_ptr<restbed::Session> /*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<restbed::Session>& 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<restbed::Session> 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> 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<restbed::Session> /*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<restbed::Session>& 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<restbed::Session> 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<Value>(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<restbed::Session>& 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<restbed::Session> 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<Value>(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<restbed::Session>& 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<restbed::Session> 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<Value>(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<restbed::Session>& 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<restbed::Session>& 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<restbed::Session> 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> v) { + // Send values as soon as we get them + Json::FastWriter writer; + s->yield(writer.write(v->toJson()), [](const std::shared_ptr<restbed::Session> /*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 e3d3aece8679e061a56f831897298c40d348cc61..759c17fb6f3016af8e85ee5752939d802f410f60 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<unsigned char>(cypherStr.begin(), cypherStr.end()); + } + if (json.isMember("sig")) { + auto sigStr = json["sig"].asString(); + sigStr = base64_decode(sigStr); + signature = std::vector<unsigned char>(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<unsigned char>(ownerStr.begin(), ownerStr.end()); + owner = std::make_shared<const crypto::PublicKey>(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<unsigned char>(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 a55d3d207e122b6ee3114ec5271762f65ab953c0..efad9767dc544d1b2cf24a611ecd1ef519347b70 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 <ip:port> Ping potential node at given IP address/port." << std::endl + << " b <ip:port> Ping potential node at given IP address/port." << std::endl << " g <key> Get values at <key>." << std::endl << " l <key> Listen for value changes at <key>." << std::endl << " p <key> <str> Put string value at <key>." << std::endl << " pp <key> <str> Put string value at <key> (persistent version)." << std::endl << " s <key> <str> Put string value at <key>, signed with our generated private key." << std::endl << " e <key> <dest> <str> Put string value at <key>, encrypted for <dest> with its public key (if found)." << std::endl; + std::cout << std::endl << "Indexation operations on the DHT:" << std::endl << " il <name> <key> [exact match] Lookup the index named <name> with the key <key>." << std::endl << " Set [exact match] to 'false' for inexact match lookup." << std::endl @@ -83,6 +90,12 @@ void cmd_loop(std::shared_ptr<DhtRunner>& dht, dht_params& params) #endif std::map<std::string, indexation::Pht> indexes; +#if OPENDHT_PROXY_SERVER + std::map<in_port_t, std::unique_ptr<DhtProxyServer>> 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<DhtRunner>& 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<std::string> VALID_OPS {"g", "l", "cl", "il", "ii", "p", "pp", "cpp", "s", "e", "a"}; + static const std::set<std::string> 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 2e4d72b082ff796923d879cbc363c7fd80f07722..39974fad6b57be732028302a2c1b3d316caa742c 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<std::string, std::string> 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;