diff --git a/CMakeLists.txt b/CMakeLists.txt index c5bdf21a9f15bdcfd30e3eb964ebc07041986145..b83c9c115eed47bdaaaa33b0bb3072e8a929cbcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,10 @@ 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 "Enable DHT proxy server, use Restinio and jsoncpp" OFF) option (OPENDHT_PUSH_NOTIFICATIONS "Enable push notifications support" 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) +option (OPENDHT_PROXY_CLIENT "Enable DHT proxy client, use Restinio and jsoncpp" OFF) option (OPENDHT_INDEX "Build DHT indexation feature" OFF) option (OPENDHT_TESTS "Add unit tests executable" OFF) @@ -58,14 +58,20 @@ if (Jsoncpp_FOUND) endif() if (OPENDHT_PROXY_SERVER OR OPENDHT_PROXY_CLIENT) - find_package(Restbed REQUIRED) + find_package(Restinio REQUIRED) + if (Restinio_FOUND) + find_library(FMT_LIBRARY fmt) + add_library(fmt SHARED IMPORTED) + find_library(HTTP_PARSER_LIBRARY http_parser) + add_library(http_parser SHARED IMPORTED) + endif() if (NOT Jsoncpp_FOUND) message(SEND_ERROR "Jsoncpp is required for DHT proxy support") endif() endif() # Build flags -set (CMAKE_CXX_STANDARD 11) +set (CMAKE_CXX_STANDARD 14) set (CMAKE_CXX_STANDARD_REQUIRED on) set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-return-type -Wall -Wextra -Wnon-virtual-dtor -pedantic-errors -fvisibility=hidden") if (OPENDHT_SANITIZE) @@ -100,8 +106,8 @@ endif () if (Nettle_INCLUDE_DIRS) include_directories (SYSTEM "${Nettle_INCLUDE_DIRS}") endif () -if (Restbed_INCLUDE_DIR) - include_directories (SYSTEM "${Restbed_INCLUDE_DIR}") +if (Restinio_INCLUDE_DIR) + include_directories (SYSTEM "${Restinio_INCLUDE_DIR}") endif () if (Jsoncpp_INCLUDE_DIRS) include_directories (SYSTEM "${Jsoncpp_INCLUDE_DIRS}") @@ -153,6 +159,7 @@ list (APPEND opendht_SOURCES src/peer_discovery.cpp src/network_utils.cpp src/thread_pool.cpp + src/http.cpp ) list (APPEND opendht_HEADERS @@ -179,6 +186,7 @@ list (APPEND opendht_HEADERS include/opendht/peer_discovery.h include/opendht/thread_pool.h include/opendht/network_utils.h + include/opendht/http.h include/opendht.h ) @@ -255,8 +263,9 @@ if (OPENDHT_STATIC) target_include_directories(opendht-static SYSTEM PRIVATE ${argon2_INCLUDE_DIRS}) endif () target_link_libraries(opendht-static - PRIVATE ${Restbed_LIBRARY} ${argon2_LIBRARIES} - PUBLIC ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES} ${Jsoncpp_LIBRARIES}) + PRIVATE ${argon2_LIBRARIES} + PUBLIC ${CMAKE_THREAD_LIBS_INIT} ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES} + ${Jsoncpp_LIBRARIES} ${FMT_LIBRARY} ${HTTP_PARSER_LIBRARY}) install (TARGETS opendht-static DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht) endif () @@ -274,7 +283,12 @@ 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} ${Restbed_LIBRARY} ${Jsoncpp_LIBRARIES}) + target_link_libraries(opendht + PUBLIC ${CMAKE_THREAD_LIBS_INIT} + PRIVATE ${GNUTLS_LIBRARIES} ${Nettle_LIBRARIES} + ${Jsoncpp_LIBRARIES} + ${FMT_LIBRARY} ${HTTP_PARSER_LIBRARY}) + install (TARGETS opendht DESTINATION ${CMAKE_INSTALL_LIBDIR} EXPORT opendht) endif () diff --git a/README.md b/README.md index bacb0950311fe53f213e37cb53e1ec918d77ba68..59f0ed1138d1690f5260c2763b4637b004d65ff7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ <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. +A lightweight C++14 Distributed Hash Table implementation. OpenDHT provides an easy to use distributed in-memory data store. Every node in the network can read and write values to the store. @@ -14,7 +14,7 @@ Values are distributed over the network, with redundancy. * High resilience to network disruption * Public key cryptography layer providing optional data signature and encryption (using GnuTLS) * IPv4 and IPv6 support - * Clean and powerful C++11 map API + * Clean and powerful C++14 map API * Python 3 bindings * REST API @@ -27,7 +27,7 @@ Build instructions: <https://github.com/savoirfairelinux/opendht/wiki/Build-the- #### How-to build a simple client app ```bash -g++ main.cpp -std=c++11 -lopendht -lgnutls +g++ main.cpp -std=c++14 -lopendht -lgnutls ``` ## Examples @@ -96,7 +96,7 @@ 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 used for the REST API. commit fb84213e170bc171fecd825a8e47ed9f881a12cd (https://github.com/AmarOk1412/restbed/tree/async_read_until) +- (optional) restinio used for the REST API. - (optional) jsoncpp 1.7.4-3+, used for the REST API. - Build tested with GCC 5.2+ (GNU/Linux, Windows with MinGW), Clang/LLVM (GNU/Linux, Android, macOS, iOS). - Build tested with Microsoft Visual Studio 2015 diff --git a/cmake/FindRestbed.cmake b/cmake/FindRestbed.cmake deleted file mode 100644 index bdf58b818e7aad85edf88384bc161072503cd9d5..0000000000000000000000000000000000000000 --- a/cmake/FindRestbed.cmake +++ /dev/null @@ -1,16 +0,0 @@ -if(NOT Restbed_FOUND) - find_path (Restbed_INCLUDE_DIR restbed - HINTS - "/usr/include" - "/usr/local/include" - "/opt/local/include") - find_library(Restbed_LIBRARY restbed - HINTS ${Restbed_ROOT_DIR} PATH_SUFFIXES lib) - include(FindPackageHandleStandardArgs) - find_package_handle_standard_args(Restbed DEFAULT_MSG Restbed_LIBRARY Restbed_INCLUDE_DIR) - if (Restbed_INCLUDE_DIR) - set(Restbed_FOUND TRUE) - set(Restbed_LIBRARIES ${Restbed_LIBRARY}) - set(Restbed_INCLUDE_DIRS ${Restbed_INCLUDE_DIR}) - endif() -endif() diff --git a/cmake/FindRestinio.cmake b/cmake/FindRestinio.cmake new file mode 100644 index 0000000000000000000000000000000000000000..a0887b3f33f4dd930e2d5d13aa027904f59232ce --- /dev/null +++ b/cmake/FindRestinio.cmake @@ -0,0 +1,14 @@ +# header-only does not produce a library +if(NOT Restinio_FOUND) + find_path (Restinio_INCLUDE_DIR restinio + HINTS + "/usr/include" + "/usr/local/include" + "/opt/local/include") + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Restinio DEFAULT_MSG Restinio_INCLUDE_DIR) + if (Restinio_INCLUDE_DIR) + set(Restinio_FOUND TRUE) + set(Restinio_INCLUDE_DIRS ${Restinio_INCLUDE_DIR}) + endif() +endif() diff --git a/configure.ac b/configure.ac index 592365a6a9e1aca3f9fef005efd1201bc903a1dd..ffd7522051d4a0242d50bca1e15c1be67f0a3219 100644 --- a/configure.ac +++ b/configure.ac @@ -142,8 +142,8 @@ AS_IF([test "x$have_jsoncpp" = "xyes"], [ ]) AM_COND_IF([PROXY_CLIENT_OR_SERVER], [ - AC_CHECK_LIB(restbed, exit,, AC_MSG_ERROR([Missing restbed files])) - LDFLAGS="${LDFLAGS} -lrestbed" + #AC_CHECK_LIB(<libname>, exit,, AC_MSG_ERROR([Missing <libname> files])) + #LDFLAGS="${LDFLAGS} -l<libname>" ]) CXXFLAGS="${CXXFLAGS} -DMSGPACK_DISABLE_LEGACY_NIL -DMSGPACK_DISABLE_LEGACY_CONVERT" diff --git a/docker/DockerfileDeps b/docker/DockerfileDeps index 0af9f19684dee39c52a5de22c9e61f289f8fed0a..f6724bba226f23d102d6cdd6a9007a1194dd1643 100644 --- a/docker/DockerfileDeps +++ b/docker/DockerfileDeps @@ -1,18 +1,45 @@ FROM ubuntu:16.04 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com> -RUN apt-get update && apt-get install -y build-essential cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev cython3 python3-dev libcppunit-dev libjsoncpp-dev libasio-dev libssl-dev python3-setuptools && apt-get clean +RUN apt-get update && apt-get install -y \ + build-essential cmake git wget libncurses5-dev libreadline-dev nettle-dev \ + libgnutls28-dev libuv1-dev cython3 python3-dev libcppunit-dev libjsoncpp-dev \ + libasio-dev libssl-dev python3-setuptools \ + && apt-get clean -# build restbed from sources -RUN git clone --recursive https://github.com/corvusoft/restbed.git \ - && cd restbed && mkdir build && cd build \ - && cmake -DBUILD_TESTS=NO -DBUILD_EXAMPLES=NO -DBUILD_SSL=NO -DBUILD_SHARED=YES -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib .. \ - && make -j8 install \ - && cd .. && rm -rf restbed +#patch for https://github.com/Stiffstream/restinio-conan-example/issues/2 +RUN apt-get update && apt-get install -y \ + python3-pip libasio-dev +RUN pip3 install --upgrade cmake +#install conan & add restinio remotes +RUN pip3 install conan && \ + conan remote add stiffstream https://api.bintray.com/conan/stiffstream/public && \ + conan remote add public-conan https://api.bintray.com/conan/bincrafters/public-conan +#setup restinio docker project +RUN mkdir restinio-conan +COPY conan/restinio/conanfile.txt restinio-conan/conanfile.txt +COPY conan/restinio/conanfile.py restinio-conan/conanfile.py +#build restinio from source +RUN echo "*** Installing RESTinio & dependencies ***" \ + && cd restinio-conan \ + && conan source . \ + && conan install -o restinio:boost_libs=none --build=missing . \ + && conan package . -pf /usr/local \ + && cd ../ && rm -rf restinio* +#installing dependencies +RUN echo "*** Installing asio & fmt dependencies ***" \ + && cp -r ~/.conan/data/asio/*/bincrafters/stable/package/*/include/* /usr/local/include/ \ + && cp -r ~/.conan/data/fmt/*/bincrafters/stable/package/*/lib/* /usr/local/lib/ \ + && cp -r ~/.conan/data/fmt/*/bincrafters/stable/package/*/include/* /usr/local/include/ +#build http_parser fork +RUN echo "*** Building http_parser dependency for custom HTTP methods ***" \ + && git clone https://github.com/eao197/http-parser.git \ + && cd http-parser && make -j8 && make install PREFIX=/usr \ + && cd ../ && rm -rf restinio-conan/ #build msgpack from source RUN wget https://github.com/msgpack/msgpack-c/releases/download/cpp-2.1.5/msgpack-2.1.5.tar.gz \ - && tar -xzf msgpack-2.1.5.tar.gz \ - && cd msgpack-2.1.5 && mkdir build && cd build \ - && cmake -DMSGPACK_CXX11=ON -DMSGPACK_BUILD_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX=/usr .. \ - && make -j8 && make install \ - && cd ../.. && rm -rf msgpack-2.1.5 msgpack-2.1.5.tar.gz + && tar -xzf msgpack-2.1.5.tar.gz \ + && cd msgpack-2.1.5 && mkdir build && cd build \ + && cmake -DMSGPACK_CXX11=ON -DMSGPACK_BUILD_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX=/usr .. \ + && make -j8 && make install \ + && cd ../.. && rm -rf msgpack-2.1.5 msgpack-2.1.5.tar.gz diff --git a/docker/DockerfileDepsLlvm b/docker/DockerfileDepsLlvm index e407f5528a4076001cc54bd5b592cee0b2a80f1a..0064b774f22d7a8e0796f35e11edf3d34c3f20cc 100644 --- a/docker/DockerfileDepsLlvm +++ b/docker/DockerfileDepsLlvm @@ -1,23 +1,46 @@ FROM ubuntu:16.04 MAINTAINER Adrien Béraud <adrien.beraud@savoirfairelinux.com> RUN apt-get update \ - && apt-get install -y llvm llvm-dev clang make cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libjsoncpp-dev libasio-dev cython3 python3-dev python3-setuptools libcppunit-dev \ - && apt-get remove -y gcc g++ && apt-get autoremove -y && apt-get clean + && apt-get install -y llvm llvm-dev clang make cmake git wget libncurses5-dev libreadline-dev nettle-dev libgnutls28-dev libuv1-dev libmsgpack-dev libjsoncpp-dev libasio-dev cython3 python3-dev python3-setuptools libcppunit-dev python3-pip \ + && apt-get remove -y gcc g++ && apt-get autoremove -y && apt-get clean ENV CC cc ENV CXX c++ -# build restbed from sources -RUN git clone --recursive https://github.com/corvusoft/restbed.git \ - && cd restbed && mkdir build && cd build \ - && cmake -DBUILD_TESTS=NO -DBUILD_EXAMPLES=NO -DBUILD_SSL=NO -DBUILD_SHARED=YES -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib .. \ - && make -j8 install \ - && cd .. && rm -rf restbed +#patch for https://github.com/Stiffstream/restinio-conan-example/issues/2 +RUN apt-get update && apt-get install -y \ + python3-pip libasio-dev +RUN pip3 install --upgrade cmake +#install conan & add restinio remotes +RUN pip3 install conan && \ + conan remote add stiffstream https://api.bintray.com/conan/stiffstream/public && \ + conan remote add public-conan https://api.bintray.com/conan/bincrafters/public-conan +#setup restinio docker project +RUN mkdir restinio-conan +COPY conan/restinio/conanfile.txt restinio-conan/conanfile.txt +COPY conan/restinio/conanfile.py restinio-conan/conanfile.py +#build restinio from source +RUN echo "*** Installing RESTinio & dependencies ***" \ + && cd restinio-conan \ + && conan source . \ + && conan install -o restinio:boost_libs=none --build=missing . \ + && conan package . -pf /usr/local \ + && cd ../ && rm -rf restinio* +#installing dependencies +RUN echo "*** Installing asio & fmt dependencies ***" \ + && cp -r ~/.conan/data/asio/*/bincrafters/stable/package/*/include/* /usr/local/include/ \ + && cp -r ~/.conan/data/fmt/*/bincrafters/stable/package/*/lib/* /usr/local/lib/ \ + && cp -r ~/.conan/data/fmt/*/bincrafters/stable/package/*/include/* /usr/local/include/ +#build http_parser fork +RUN echo "*** Building http_parser dependency for custom HTTP methods ***" \ + && git clone https://github.com/eao197/http-parser.git \ + && cd http-parser && make -j8 && make install PREFIX=/usr \ + && cd ../ && rm -rf restinio-conan/ #build msgpack from source RUN wget https://github.com/msgpack/msgpack-c/releases/download/cpp-2.1.5/msgpack-2.1.5.tar.gz \ - && tar -xzf msgpack-2.1.5.tar.gz \ - && cd msgpack-2.1.5 && mkdir build && cd build \ - && cmake -DMSGPACK_CXX11=ON -DMSGPACK_BUILD_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX=/usr .. \ - && make -j8 && make install \ - && cd ../.. && rm -rf msgpack-2.1.5 msgpack-2.1.5.tar.gz + && tar -xzf msgpack-2.1.5.tar.gz \ + && cd msgpack-2.1.5 && mkdir build && cd build \ + && cmake -DMSGPACK_CXX11=ON -DMSGPACK_BUILD_EXAMPLES=OFF -DCMAKE_INSTALL_PREFIX=/usr .. \ + && make -j8 && make install \ + && cd ../.. && rm -rf msgpack-2.1.5 msgpack-2.1.5.tar.gz diff --git a/docker/conan/restinio/conanfile.py b/docker/conan/restinio/conanfile.py new file mode 100644 index 0000000000000000000000000000000000000000..a434c5ac7d265bf24f9df2c8ded33358924427aa --- /dev/null +++ b/docker/conan/restinio/conanfile.py @@ -0,0 +1,59 @@ +from conans import ConanFile, CMake, tools +import os + + +class SobjectizerConan(ConanFile): + name = "restinio" + version = "0.5.1" + + license = "BSD-3-Clause" + url = "https://github.com/Stiffstream/restinio-conan" + + description = ( + "RESTinio is a header-only C++14 library that gives you " + "an embedded HTTP/Websocket server." + ) + + settings = "os", "compiler", "build_type", "arch" + options = {'boost_libs': ['none', 'static', 'shared']} + default_options = {'boost_libs': 'none'} + generators = "cmake" + source_subfolder = "restinio" + build_policy = "missing" + + def requirements(self): + self.requires.add("http-parser/2.8.1@bincrafters/stable") + self.requires.add("fmt/5.3.0@bincrafters/stable") + + if self.options.boost_libs == "none": + self.requires.add("asio/1.12.2@bincrafters/stable") + else: + self.requires.add("boost/1.69.0@conan/stable") + if self.options.boost_libs == "shared": + self.options["boost"].shared = True + else: + self.options["boost"].shared = False + + def source(self): + source_url = "https://bitbucket.org/sobjectizerteam/restinio/downloads" + tools.get("{0}/restinio-{1}.zip".format(source_url, self.version)) + extracted_dir = "restinio-" + self.version + os.rename(extracted_dir, self.source_subfolder) + + def _configure_cmake(self): + cmake = CMake(self) + cmake.definitions['RESTINIO_INSTALL'] = True + cmake.definitions['RESTINIO_FIND_DEPS'] = False + cmake.definitions['RESTINIO_USE_BOOST_ASIO'] = self.options.boost_libs + cmake.configure(source_folder = self.source_subfolder + "/dev/restinio") + return cmake + + def package(self): + cmake = self._configure_cmake() + self.output.info(cmake.definitions) + cmake.install() + + def package_info(self): + self.info.header_only() + if self.options.boost_libs != "none": + self.cpp_info.defines.append("RESTINIO_USE_BOOST_ASIO") diff --git a/docker/conan/restinio/conanfile.txt b/docker/conan/restinio/conanfile.txt new file mode 100644 index 0000000000000000000000000000000000000000..472fd614635649ab6541e2c9aa28ba1474921424 --- /dev/null +++ b/docker/conan/restinio/conanfile.txt @@ -0,0 +1,11 @@ +[requires] +restinio/0.5.1@stiffstream/stable + +[generators] +cmake + +[options] + +[imports] +bin, *.dll -> ./bin # Copies all dll files from packages bin folder to my "bin" folder +lib, *.dylib* -> ./bin # Copies all dylib files from packages lib folder to my "bin" folder diff --git a/include/opendht/dht_proxy_client.h b/include/opendht/dht_proxy_client.h index 5dbd8eb9c9638cb3f624cad18780f2951ce47060..dbaedafcb3de1397ed995a4b7b7e200629189974 100644 --- a/include/opendht/dht_proxy_client.h +++ b/include/opendht/dht_proxy_client.h @@ -2,6 +2,7 @@ * Copyright (C) 2016-2019 Savoir-faire Linux Inc. * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> * Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Vsevolod Ivanov <vsevolod.ivanov@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 @@ -20,23 +21,30 @@ #pragma once #include <functional> -#include <thread> #include <mutex> #include "callbacks.h" #include "def.h" #include "dht_interface.h" -#include "scheduler.h" #include "proxy.h" -namespace restbed { - class Request; -} +#include <restinio/all.hpp> +#include <http_parser.h> +#include <json/json.h> +#include "http.h" + +#include <chrono> +#include <vector> +#include <functional> namespace Json { class Value; } +namespace http { + class Client; +} + namespace dht { class OPENDHT_PUBLIC DhtProxyClient final : public DhtInterface { @@ -44,7 +52,11 @@ public: DhtProxyClient(); - explicit DhtProxyClient(std::function<void()> loopSignal, const std::string& serverHost, const std::string& pushClientId = "", const Logger& = {}); + explicit DhtProxyClient(std::function<void()> loopSignal, const std::string& serverHost, + const std::string& pushClientId = "", + std::shared_ptr<dht::Logger> logger = {}); + + restinio::http_header_fields_t initHeaderFields(); virtual void setPushNotificationToken(const std::string& token) { #ifdef OPENDHT_PUSH_NOTIFICATIONS @@ -172,6 +184,10 @@ public: virtual size_t listen(const InfoHash& key, GetCallbackSimple cb, Value::Filter f={}, Where w={}) { return listen(key, bindGetCb(cb), std::forward<Value::Filter>(f), std::forward<Where>(w)); } + /* + * This function relies on the cache implementation. + * It means that there are no true cancel here, it keeps the caching in higher priority. + */ virtual bool cancelListen(const InfoHash& key, size_t token); /** @@ -185,7 +201,6 @@ public: return periodic(buf, buflen, SockAddr(from, fromlen)); } - /** * Similar to Dht::get, but sends a Query to filter data remotely. * @param key the key for which to query data for. @@ -269,6 +284,8 @@ private: */ struct InfoState; void getProxyInfos(); + void handleProxyStatus(const asio::error_code &ec, + std::shared_ptr<InfoState> infoState); void onProxyInfos(const Json::Value& val, sa_family_t family); SockAddr parsePublicAddress(const Json::Value& val); @@ -282,10 +299,13 @@ private: SUBSCRIBE, RESUBSCRIBE, }; - void sendListen(const std::shared_ptr<restbed::Request> &request, - const ValueCallback &, const Value::Filter &filter, - const Sp<ListenState> &state, - ListenMethod method = ListenMethod::LISTEN); + /** + * Send Listen with httpClient_ + * Return a Connection Id + */ + uint16_t sendListen(const restinio::http_request_header_t header, + const ValueCallback &cb, const Value::Filter &filter, + const Sp<ListenState> &state, ListenMethod method = ListenMethod::LISTEN); void doPut(const InfoHash&, Sp<Value>, DoneCallback, time_point created, bool permanent); @@ -303,8 +323,24 @@ private: void cancelAllOperations(); std::string serverHost_; + std::string serverHostIp_; + uint16_t serverHostPort_; std::string pushClientId_; + /* + * ASIO I/O Context for sockets in httpClient_ + * Note: Each context is used in one thread only + */ + asio::io_context httpContext_; + /* + * http::Client instance used on http io_context + */ + std::unique_ptr<http::Client> httpClient_; + /* + * Thread for executing the http io_context.run() blocking call + */ + std::thread httpClientThread_; + mutable std::mutex lockCurrentProxyInfos_; NodeStatus statusIpv4_ {NodeStatus::Disconnected}; NodeStatus statusIpv6_ {NodeStatus::Disconnected}; @@ -328,34 +364,22 @@ private: std::map<InfoHash, ProxySearch> searches_; mutable std::mutex searchLock_; - /** - * Store current put and get requests. - */ - struct Operation - { - std::shared_ptr<restbed::Request> req; - std::thread thread; - std::shared_ptr<std::atomic_bool> finished; - }; - std::vector<Operation> operations_; - std::mutex lockOperations_; /** * Callbacks should be executed in the main thread. */ std::vector<std::function<void()>> callbacks_; - std::mutex lockCallbacks; + std::mutex lockCallbacks_; Sp<InfoState> infoState_; - std::thread statusThread_; + Sp<asio::steady_timer> statusTimer_; mutable std::mutex statusLock_; - Scheduler scheduler; /** * Retrieve if we can connect to the proxy (update statusIpvX_) */ void confirmProxy(); - Sp<Scheduler::Job> nextProxyConfirmation {}; - Sp<Scheduler::Job> listenerRestart {}; + Sp<asio::steady_timer> nextProxyConfirmationTimer_; + Sp<asio::steady_timer> listenerRestartTimer_; /** * Relaunch LISTEN requests if the client disconnect/reconnect. @@ -377,11 +401,13 @@ private: const std::function<void()> loopSignal_; #ifdef OPENDHT_PUSH_NOTIFICATIONS - void fillBody(std::shared_ptr<restbed::Request> request, bool resubscribe); + std::string fillBody(bool resubscribe); void getPushRequest(Json::Value&) const; #endif // OPENDHT_PUSH_NOTIFICATIONS std::atomic_bool isDestroying_ {false}; + + std::shared_ptr<dht::Logger> logger_; }; } diff --git a/include/opendht/dht_proxy_server.h b/include/opendht/dht_proxy_server.h index c48d9778948ad56734c9f739e9275adcaeb01724..dc6019e67d53f7a1a04d148bd127b70fe6706923 100644 --- a/include/opendht/dht_proxy_server.h +++ b/include/opendht/dht_proxy_server.h @@ -2,6 +2,7 @@ * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> * Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Vsevolod Ivanov <vsevolod.ivanov@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 @@ -26,16 +27,43 @@ #include "scheduler.h" #include "sockaddr.h" #include "value.h" +#include "dht_proxy_client.h" -#include <thread> #include <memory> #include <mutex> -#include <restbed> +#include <restinio/all.hpp> +#include "http.h" #ifdef OPENDHT_JSONCPP #include <json/json.h> #endif +namespace http { + class Client; + class opendht_logger_t; + struct ListenerSession; + class ConnectionListener; +} + +namespace restinio { + class opendht_logger_t; + struct custom_http_methods_t; +} + +using RestRouter = restinio::router::express_router_t<>; +struct RestRouterTraits : public restinio::default_traits_t +{ + using timer_manager_t = restinio::asio_timer_manager_t; + using http_methods_mapper_t = restinio::custom_http_methods_t; + using logger_t = restinio::opendht_logger_t; + using request_handler_t = RestRouter; + using connection_state_listener_t = http::ConnectionListener; +}; +using ServerSettings = restinio::run_on_this_thread_settings_t<RestRouterTraits>; +using RequestStatus = restinio::request_handling_status_t; +using ResponseByParts = restinio::chunked_output_t; +using ResponseByPartsBuilder = restinio::response_builder_t<ResponseByParts>; + namespace Json { class Value; } @@ -43,7 +71,6 @@ namespace Json { namespace dht { class DhtRunner; -class ThreadPool; /** * Describes the REST API @@ -59,7 +86,9 @@ public: * @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, const std::string& pushServer = ""); + DhtProxyServer(std::shared_ptr<DhtRunner> dht, in_port_t port = 8000, + const std::string& pushServer = "", + std::shared_ptr<dht::Logger> logger = {}); virtual ~DhtProxyServer(); DhtProxyServer(const DhtProxyServer& other) = delete; @@ -123,6 +152,14 @@ public: void stop(); private: + template <typename HttpResponse> + HttpResponse initHttpResponse(HttpResponse response) const; + + ServerSettings makeHttpServerSettings( + const unsigned int max_pipelined_requests = 16); + + std::unique_ptr<RestRouter> createRestRouter(); + /** * Return the PublicKey id, the node id and node stats * Method: GET "/" @@ -130,7 +167,8 @@ private: * On error: HTTP 503, body: {"err":"xxxx"} * @param session */ - void getNodeInfo(const std::shared_ptr<restbed::Session>& session) const; + RequestStatus getNodeInfo(restinio::request_handle_t request, + restinio::router::route_params_t params) const; /** * Return ServerStats in JSON format @@ -138,7 +176,8 @@ private: * Result: HTTP 200, body: Node infos in JSON format * @param session */ - void getStats(const std::shared_ptr<restbed::Session>& session) const; + RequestStatus getStats(restinio::request_handle_t request, + restinio::router::route_params_t params); /** * Return Values of an infoHash @@ -150,7 +189,8 @@ private: * On error: HTTP 503, body: {"err":"xxxx"} * @param session */ - void get(const std::shared_ptr<restbed::Session>& session) const; + RequestStatus get(restinio::request_handle_t request, + restinio::router::route_params_t params); /** * Listen incoming Values of an infoHash. @@ -162,7 +202,8 @@ private: * On error: HTTP 503, body: {"err":"xxxx"} * @param session */ - void listen(const std::shared_ptr<restbed::Session>& session); + RequestStatus listen(restinio::request_handle_t request, + restinio::router::route_params_t params); /** * Put a value on the DHT @@ -173,7 +214,8 @@ private: * 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); + RequestStatus put(restinio::request_handle_t request, + restinio::router::route_params_t params); void cancelPut(const InfoHash& key, Value::Id vid); @@ -187,7 +229,8 @@ private: * HTTP 400, body: {"err":"xxxx"} if bad json * @param session */ - void putSigned(const std::shared_ptr<restbed::Session>& session) const; + RequestStatus putSigned(restinio::request_handle_t request, + restinio::router::route_params_t params) const; /** * Put a value to encrypt by the proxy on the DHT @@ -198,7 +241,9 @@ private: * HTTP 400, body: {"err":"xxxx"} if bad json * @param session */ - void putEncrypted(const std::shared_ptr<restbed::Session>& session) const; + RequestStatus putEncrypted(restinio::request_handle_t request, + restinio::router::route_params_t params); + #endif // OPENDHT_PROXY_SERVER_IDENTITY /** @@ -211,7 +256,8 @@ private: * On error: HTTP 503, body: {"err":"xxxx"} * @param session */ - void getFiltered(const std::shared_ptr<restbed::Session>& session) const; + RequestStatus getFiltered(restinio::request_handle_t request, + restinio::router::route_params_t params); /** * Respond allowed Methods @@ -220,13 +266,8 @@ private: * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS * @param session */ - void handleOptionsMethod(const std::shared_ptr<restbed::Session>& session) const; - - /** - * Remove finished listeners - * @param testSession if we remove the listener only if the session is closed - */ - void removeClosedListeners(bool testSession = true); + RequestStatus options(restinio::request_handle_t request, + restinio::router::route_params_t params); #ifdef OPENDHT_PUSH_NOTIFICATIONS /** @@ -238,7 +279,9 @@ private: * so you need to refresh the operation each six hours. * @param session */ - void subscribe(const std::shared_ptr<restbed::Session>& session); + RequestStatus subscribe(restinio::request_handle_t request, + restinio::router::route_params_t params); + /** * Unsubscribe to push notifications for an iOS or Android device. * Method: UNSUBSCRIBE "/{InfoHash: .*}" @@ -246,7 +289,9 @@ private: * Return: nothing * @param session */ - void unsubscribe(const std::shared_ptr<restbed::Session>& session); + RequestStatus unsubscribe(restinio::request_handle_t request, + restinio::router::route_params_t params); + /** * Send a push notification via a gorush push gateway * @param key of the device @@ -262,41 +307,45 @@ private: */ void cancelPushListen(const std::string& pushToken, const InfoHash& key, const std::string& clientId); - #endif //OPENDHT_PUSH_NOTIFICATIONS + void asyncPrintStats(); + using clock = std::chrono::steady_clock; using time_point = clock::time_point; - std::thread server_thread {}; - std::unique_ptr<restbed::Service> service_; std::shared_ptr<DhtRunner> dht_; + std::shared_ptr<dht::Logger> logger_; Json::StreamWriterBuilder jsonBuilder_; - std::mutex schedulerLock_; - std::condition_variable schedulerCv_; - Scheduler scheduler_; - std::thread schedulerThread_; - std::unique_ptr<ThreadPool> threadPool_; + std::thread httpServerThread_; + std::unique_ptr<restinio::http_server_t<RestRouterTraits>> httpServer_; + std::unique_ptr<http::Client> httpClient_; - Sp<Scheduler::Job> printStatsJob_; mutable std::mutex statsMutex_; + mutable ServerStats stats_; mutable NodeInfo nodeInfo_ {}; - - // 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; + std::unique_ptr<asio::steady_timer> printStatsTimer_; + + // Thread-safe access to listeners map. + std::shared_ptr<std::mutex> lockListener_; + // Shared with connection listener. + std::shared_ptr<std::map<restinio::connection_id_t, + http::ListenerSession>> listeners_; + // Connection Listener observing conn state changes. + std::shared_ptr<http::ConnectionListener> connListener_; + + struct PermanentPut { + time_point expiration; + std::string pushToken; + std::string clientId; + std::unique_ptr<asio::steady_timer> expireTimer; + std::unique_ptr<asio::steady_timer> expireNotifyTimer; }; - std::vector<SessionToHashToken> currentListeners_; - std::mutex lockListener_; - std::atomic_bool stopListeners {false}; - - struct PermanentPut; - struct SearchPuts; + struct SearchPuts { + std::map<dht::Value::Id, PermanentPut> puts; + }; + std::mutex lockSearchPuts_; std::map<InfoHash, SearchPuts> puts_; mutable std::atomic<size_t> requestNum_ {0}; @@ -304,11 +353,16 @@ private: const std::string pushServer_; - mutable ServerStats stats_; - #ifdef OPENDHT_PUSH_NOTIFICATIONS - struct Listener; - struct PushListener; + struct Listener { + std::string clientId; + std::future<size_t> internalToken; + std::unique_ptr<asio::steady_timer> expireTimer; + std::unique_ptr<asio::steady_timer> expireNotifyTimer; + }; + struct PushListener { + std::map<InfoHash, std::vector<Listener>> listeners; + }; std::mutex lockPushListeners_; std::map<std::string, PushListener> pushListeners_; proxy::ListenToken tokenPushNotif_ {0}; diff --git a/include/opendht/dhtrunner.h b/include/opendht/dhtrunner.h index 8fed0277c54d3fc680836eb28b7e5e5776b1824a..3a180ec40598290a9ea94ca9b3f0d999fb943689 100644 --- a/include/opendht/dhtrunner.h +++ b/include/opendht/dhtrunner.h @@ -65,7 +65,7 @@ public: }; struct Context { - std::unique_ptr<Logger> logger {}; + std::shared_ptr<Logger> logger {}; std::unique_ptr<net::DatagramSocket> sock; std::shared_ptr<PeerDiscovery> peerDiscovery {}; StatusCallback statusChangedCallback {}; @@ -518,6 +518,12 @@ private: /** PeerDiscovery Parameters */ std::shared_ptr<PeerDiscovery> peerDiscovery_; + + /** + * The Logger instance is used in enableProxy and other methods that + * would create instances of classes using a common logger. + */ + std::shared_ptr<dht::Logger> logger_; }; } diff --git a/include/opendht/http.h b/include/opendht/http.h new file mode 100644 index 0000000000000000000000000000000000000000..7c677f6ae1844606e3b618dc047f5f2c780c2cbb --- /dev/null +++ b/include/opendht/http.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016-2019 Savoir-faire Linux Inc. + * Author: Vsevolod Ivanov <vsevolod.ivanov@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/>. + */ + +#pragma once + +#include <asio.hpp> +#include <json/json.h> +#include <http_parser.h> +#include <restinio/all.hpp> +#include <opendht.h> +#include <opendht/log.h> + +namespace http { + +class Connection +{ +public: + Connection(const uint16_t id, asio::ip::tcp::socket socket); + ~Connection(); + + uint16_t id(); + void start(asio::ip::tcp::resolver::iterator &r_iter); + bool is_open(); + void close(); + +private: + friend class Client; + + uint16_t id_; + asio::ip::tcp::socket socket_; + asio::streambuf request_; + asio::streambuf response_; +}; + +/** + * Session value associated with a connection_id_t key. + */ +struct ListenerSession +{ + ListenerSession() = default; + dht::InfoHash hash; + std::future<size_t> token; + std::shared_ptr<restinio::response_builder_t<restinio::chunked_output_t>> response; +}; + +/** + * Request is the context of an active connection allowing it to parse responses + */ +struct Request +{ + std::string content; + std::shared_ptr<http_parser> parser; + std::shared_ptr<http_parser_settings> parser_settings; + std::shared_ptr<Connection> connection; +}; + +class ConnectionListener +{ +public: + ConnectionListener(); + ConnectionListener(std::shared_ptr<dht::DhtRunner> dht, + std::shared_ptr<std::map<restinio::connection_id_t, http::ListenerSession>> listeners, + std::shared_ptr<std::mutex> lock, std::shared_ptr<dht::Logger> logger); + ~ConnectionListener(); + + /** + * Connection state change used to handle Listeners disconnects. + * RESTinio >= 0.5.1 https://github.com/Stiffstream/restinio/issues/28 + */ + void state_changed(const restinio::connection_state::notice_t ¬ice) noexcept; + +private: + std::string to_str( restinio::connection_state::cause_t cause ) noexcept; + + std::shared_ptr<dht::DhtRunner> dht_; + std::shared_ptr<std::mutex> lock_; + std::shared_ptr<std::map<restinio::connection_id_t, + http::ListenerSession>> listeners_; + std::shared_ptr<dht::Logger> logger_; +}; + +class Client +{ +public: + Client(asio::io_context &ctx, std::string host, uint16_t port, + std::shared_ptr<dht::Logger> logger = {}); + + asio::io_context& io_context(); + + void set_logger(std::shared_ptr<dht::Logger> logger); + void set_query_address(const std::string host, const uint16_t port); + + bool active_connection(uint16_t conn_id); + void close_connection(uint16_t conn_id); + + std::string create_request(const restinio::http_request_header_t header, + const restinio::http_header_fields_t header_fields, + const restinio::http_connection_header_t connection, + const std::string body); + + uint16_t post_request(std::string request, + std::shared_ptr<http_parser> parser, + std::shared_ptr<http_parser_settings> parser_s); + +private: + std::shared_ptr<Connection> create_connection(); + + void handle_connect(const asio::error_code &ec, + asio::ip::tcp::resolver::iterator endpoint_it, + std::shared_ptr<Connection> conn = {}); + + void handle_resolve(const asio::error_code &ec, + asio::ip::tcp::resolver::iterator endpoint_it, + std::shared_ptr<Connection> conn = {}); + + void handle_request(const asio::error_code &ec, + std::shared_ptr<Connection> conn = {}); + + void handle_response(const asio::error_code &ec, + std::shared_ptr<Connection> conn = {}); + + uint16_t port_; + std::string host_; + + // contains the io_context + asio::ip::tcp::resolver resolver_; + + uint16_t connId_ {1}; + /* + * An association between an active connection and its context, a Request. + */ + std::map<uint16_t, Request> requests_; + + std::shared_ptr<dht::Logger> logger_; +}; + +} // namespace http + +namespace restinio +{ + +class opendht_logger_t +{ +public: + opendht_logger_t(std::shared_ptr<dht::Logger> logger = {}){ + if (logger) + m_logger = logger; + } + + template <typename Builder> + void trace(Builder && msg_builder){ + if (m_logger) + m_logger->d("[proxy:server] %s", msg_builder().c_str()); + } + + template <typename Builder> + void info(Builder && msg_builder){ + if (m_logger) + m_logger->d("[proxy:server] %s", msg_builder().c_str()); + } + + template <typename Builder> + void warn(Builder && msg_builder){ + if (m_logger) + m_logger->w("[proxy:server] %s", msg_builder().c_str()); + } + + template <typename Builder> + void error(Builder && msg_builder){ + if (m_logger) + m_logger->e("[proxy:server] %s", msg_builder().c_str()); + } + +private: + std::shared_ptr<dht::Logger> m_logger; +}; + +/* Custom HTTP-methods for RESTinio > 0.5.0. + * https://github.com/Stiffstream/restinio/issues/26 + */ +constexpr const restinio::http_method_id_t method_listen{HTTP_LISTEN, "LISTEN"}; +constexpr const restinio::http_method_id_t method_stats{HTTP_STATS, "STATS"}; +constexpr const restinio::http_method_id_t method_sign{HTTP_SIGN, "SIGN"}; +constexpr const restinio::http_method_id_t method_encrypt{HTTP_ENCRYPT, "ENCRYPT"}; + +struct custom_http_methods_t +{ + static constexpr restinio::http_method_id_t from_nodejs(int m) noexcept { + if(m == method_listen.raw_id()) + return method_listen; + else if(m == method_stats.raw_id()) + return method_stats; + else if(m == method_sign.raw_id()) + return method_sign; + else if(m == method_encrypt.raw_id()) + return method_encrypt; + else + return restinio::default_http_methods_t::from_nodejs(m); + } +}; + +} // namespace restinio diff --git a/python/opendht.pyx b/python/opendht.pyx index 1fa3c239db40db5501b092b9353e1ba2f03256ad..538276b85703bc5aee72faaa4b6d24747aec406f 100644 --- a/python/opendht.pyx +++ b/python/opendht.pyx @@ -1,5 +1,5 @@ # distutils: language = c++ -# distutils: extra_compile_args = -std=c++11 +# distutils: extra_compile_args = -std=c++14 # distutils: include_dirs = ../../include # distutils: library_dirs = ../../src # distutils: libraries = opendht gnutls diff --git a/python/setup.py.in b/python/setup.py.in index 2f67164a9e9024a5d2134b4d216c4c93a0fde4a4..09ff43d7a50d3c29534ea31066a6aebf2f237a64 100644 --- a/python/setup.py.in +++ b/python/setup.py.in @@ -50,8 +50,8 @@ setup(name="opendht", ["@CURRENT_SOURCE_DIR@/opendht.pyx"], include_dirs = ['@PROJECT_SOURCE_DIR@/include'], language="c++", - extra_compile_args=["-std=c++11"], - extra_link_args=["-std=c++11"], + extra_compile_args=["-std=c++14"], + extra_link_args=["-std=c++14"], libraries=["opendht"], library_dirs = ['@CURRENT_BINARY_DIR@', '@PROJECT_BINARY_DIR@'] )) diff --git a/src/dht_proxy_client.cpp b/src/dht_proxy_client.cpp index 153a29614ec17eab8620d8ae4ff5271a53b81e08..ac0c05ec83ae37082f652932f575890079704553 100644 --- a/src/dht_proxy_client.cpp +++ b/src/dht_proxy_client.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2016-2019 Savoir-faire Linux Inc. * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> * Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Vsevolod Ivanov <vsevolod.ivanov@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 @@ -18,17 +19,10 @@ */ #include "dht_proxy_client.h" - #include "dhtrunner.h" #include "op_cache.h" #include "utils.h" -#include <restbed> -#include <json/json.h> - -#include <chrono> -#include <vector> - namespace dht { struct DhtProxyClient::InfoState { @@ -43,40 +37,71 @@ struct DhtProxyClient::ListenState { struct DhtProxyClient::Listener { + Listener(OpValueCache&& c, Value::Filter&& f): + cache(std::move(c)), filter(std::move(f)) + {} + + uint16_t connId {0}; + unsigned callbackId; OpValueCache cache; ValueCallback cb; Value::Filter filter; - Sp<restbed::Request> req; - std::thread thread; - unsigned callbackId; Sp<ListenState> state; - Sp<Scheduler::Job> refreshJob; - Listener(OpValueCache&& c, const Sp<restbed::Request>& r, Value::Filter&& f) - : cache(std::move(c)), filter(std::move(f)),req(r) {} + Sp<asio::steady_timer> refreshTimer; + }; struct PermanentPut { + PermanentPut(const Sp<Value>& v, Sp<asio::steady_timer>&& j, + const Sp<std::atomic_bool>& o): + value(v), refreshTimer(std::move(j)), ok(o) + {} + Sp<Value> value; - Sp<Scheduler::Job> refreshJob; + Sp<asio::steady_timer> refreshTimer; Sp<std::atomic_bool> ok; - PermanentPut(const Sp<Value>& v, Sp<Scheduler::Job>&& j, const Sp<std::atomic_bool>& o) - : value(v), refreshJob(std::move(j)), ok(o) {} }; struct DhtProxyClient::ProxySearch { SearchCache ops {}; - Sp<Scheduler::Job> opExpirationJob {}; + Sp<asio::steady_timer> opExpirationTimer; std::map<size_t, Listener> listeners {}; std::map<Value::Id, PermanentPut> puts {}; }; DhtProxyClient::DhtProxyClient() {} -DhtProxyClient::DhtProxyClient(std::function<void()> signal, const std::string& serverHost, const std::string& pushClientId, const Logger& l) -: DhtInterface(l), serverHost_(serverHost), pushClientId_(pushClientId), loopSignal_(signal) +DhtProxyClient::DhtProxyClient(std::function<void()> signal, const std::string &serverHost, + const std::string &pushClientId, std::shared_ptr<dht::Logger> logger): + serverHost_(serverHost), pushClientId_(pushClientId), loopSignal_(signal), + logger_(logger) { + // build http client + auto hostAndPort = splitPort(serverHost_); + serverHostIp_ = hostAndPort.first; + serverHostPort_ = std::atoi(hostAndPort.second.c_str()); + httpClient_ = std::make_unique<http::Client>( + httpContext_, serverHostIp_, serverHostPort_, logger); + // run http client + httpClientThread_ = std::thread([this](){ + try { + if (logger_) + logger_->d("[proxy:client] starting io context"); + // Ensures the httpContext_ won't run out of work + auto work = asio::make_work_guard(httpContext_); + httpContext_.run(); + if (logger_) + logger_->d("[proxy:client] http client io context stopped"); + } + catch(const std::exception &ex){ + if (logger_) + logger_->e("[proxy:client] error starting io context"); + } + }); + if (serverHost_.find("://") == std::string::npos) serverHost_ = proxy::HTTP_PROTO + serverHost_; + if (!serverHost_.empty()) startProxy(); } @@ -84,17 +109,27 @@ DhtProxyClient::DhtProxyClient(std::function<void()> signal, const std::string& void DhtProxyClient::confirmProxy() { - if (serverHost_.empty()) return; + if (serverHost_.empty()) + return; getConnectivityStatus(); } void DhtProxyClient::startProxy() { - if (serverHost_.empty()) return; - DHT_LOG.w("Staring proxy client to %s", serverHost_.c_str()); - nextProxyConfirmation = scheduler.add(scheduler.time(), std::bind(&DhtProxyClient::confirmProxy, this)); - listenerRestart = std::make_shared<Scheduler::Job>(std::bind(&DhtProxyClient::restartListeners, this)); + if (serverHost_.empty()) + return; + + if (logger_) + logger_->d("[proxy:client] staring proxy with %s", serverHost_.c_str()); + + nextProxyConfirmationTimer_ = std::make_shared<asio::steady_timer>( + httpContext_, std::chrono::steady_clock::now()); + nextProxyConfirmationTimer_->async_wait(std::bind(&DhtProxyClient::confirmProxy, this)); + + listenerRestartTimer_ = std::make_shared<asio::steady_timer>(httpContext_); + listenerRestartTimer_->async_wait(std::bind(&DhtProxyClient::restartListeners, this)); + loopSignal_(); } @@ -105,8 +140,10 @@ DhtProxyClient::~DhtProxyClient() cancelAllListeners(); if (infoState_) infoState_->cancel = true; - if (statusThread_.joinable()) - statusThread_.join(); + if (statusTimer_) + statusTimer_->cancel(); + if (httpClientThread_.joinable()) + httpClientThread_.join(); } std::vector<Sp<Value>> @@ -130,49 +167,30 @@ DhtProxyClient::getLocalById(const InfoHash& k, Value::Id id) const { void DhtProxyClient::cancelAllOperations() { - std::lock_guard<std::mutex> lock(lockOperations_); - auto operation = operations_.begin(); - while (operation != operations_.end()) { - if (operation->thread.joinable()) { - // Close connection to stop operation? - if (operation->req) { - try { - restbed::Http::close(operation->req); - } catch (const std::exception& e) { - DHT_LOG.w("Error closing socket: %s", e.what()); - } - operation->req.reset(); - } - operation->thread.join(); - operation = operations_.erase(operation); - } else { - ++operation; - } - } + if (!httpContext_.stopped()) + httpContext_.stop(); } void DhtProxyClient::cancelAllListeners() { std::lock_guard<std::mutex> lock(searchLock_); - DHT_LOG.w("Cancelling all listeners for %zu searches", searches_.size()); + if (logger_) + logger_->d("[proxy:client] [listeners:cancel:all] [%zu searches]", searches_.size()); for (auto& s: searches_) { s.second.ops.cancelAll([&](size_t token){ auto l = s.second.listeners.find(token); if (l == s.second.listeners.end()) return; - if (l->second.thread.joinable()) { - // Close connection to stop listener? + if (httpClient_->active_connection(l->second.connId)){ l->second.state->cancel = true; - if (l->second.req) { - try { - restbed::Http::close(l->second.req); - } catch (const std::exception& e) { - DHT_LOG.w("Error closing socket: %s", e.what()); - } - l->second.req.reset(); + try { + httpClient_->close_connection(l->second.connId); + } catch (const std::exception& e) { + if (logger_) + logger_->e("[proxy:client] [listeners:cancel:all] error closing socket: %s", e.what()); } - l->second.thread.join(); + l->second.connId = 0; } s.second.listeners.erase(token); }); @@ -222,134 +240,145 @@ time_point DhtProxyClient::periodic(const uint8_t*, size_t, SockAddr) { // Exec all currently stored callbacks - scheduler.syncTime(); decltype(callbacks_) callbacks; { - std::lock_guard<std::mutex> lock(lockCallbacks); + std::lock_guard<std::mutex> lock(lockCallbacks_); callbacks = std::move(callbacks_); } for (auto& callback : callbacks) callback(); callbacks.clear(); + return time_point::max(); +} - // Remove finished operations - { - std::lock_guard<std::mutex> lock(lockOperations_); - auto operation = operations_.begin(); - while (operation != operations_.end()) { - if (*(operation->finished)) { - if (operation->thread.joinable()) { - // Close connection to stop operation? - if (operation->req) { - try { - restbed::Http::close(operation->req); - } catch (const std::exception& e) { - DHT_LOG.w("Error closing socket: %s", e.what()); - } - operation->req.reset(); - } - operation->thread.join(); - } - operation = operations_.erase(operation); - } else { - ++operation; - } - } - } - return scheduler.run(); +restinio::http_header_fields_t +DhtProxyClient::initHeaderFields(){ + restinio::http_header_fields_t header_fields; + header_fields.append_field(restinio::http_field_t::host, + (serverHostIp_ + ":" + std::to_string(serverHostPort_)).c_str()); + header_fields.append_field(restinio::http_field_t::user_agent, "RESTinio client"); + header_fields.append_field(restinio::http_field_t::accept, "*/*"); + header_fields.append_field(restinio::http_field_t::content_type, "application/json"); + return header_fields; } void -DhtProxyClient::get(const InfoHash& key, GetCallback cb, DoneCallback donecb, Value::Filter&& f, Where&& w) +DhtProxyClient::get(const InfoHash& key, GetCallback cb, DoneCallback donecb, + Value::Filter&& f, Where&& w) { - DHT_LOG.d(key, "[search %s]: get", key.to_c_str()); - restbed::Uri uri(serverHost_ + "/" + key.toString()); - auto req = std::make_shared<restbed::Request>(uri); - Value::Filter filter = w.empty() ? f : f.chain(w.getFilter()); - - auto finished = std::make_shared<std::atomic_bool>(false); - Operation o; - o.req = req; - o.finished = finished; - o.thread = std::thread([=](){ - // Try to contact the proxy and set the status to connected when done. - // will change the connectivity status - struct GetState{ std::atomic_bool ok {true}; std::atomic_bool stop {false}; }; - auto state = std::make_shared<GetState>(); - try { - restbed::Http::async(req, - [=](const std::shared_ptr<restbed::Request>& req, - const std::shared_ptr<restbed::Response>& reply) { - auto code = reply->get_status_code(); - - if (code == 200) { - try { - while (restbed::Http::is_open(req) and not *finished and not state->stop) { - restbed::Http::fetch("\n", reply); - if (*finished or state->stop) - break; - std::string body; - reply->get_body(body); - reply->set_body(""); // Reset the body for the next fetch - - std::string err; - Json::Value json; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(&body[0]); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (reader->parse(char_data, char_data + body.size(), &json, &err)) { - auto value = std::make_shared<Value>(json); - if ((not filter or filter(*value)) and cb) { - { - std::lock_guard<std::mutex> lock(lockCallbacks); - callbacks_.emplace_back([cb, value, state]() { - if (not state->stop and not cb({value})) - state->stop = true; - }); - } - loopSignal_(); - } - } else { - state->ok = false; - } - } - } catch (std::runtime_error& e) { } - } else { - state->ok = false; + if (logger_) + logger_->d("[proxy:client] [get] [search %s]", key.to_c_str()); + restinio::http_request_header_t header; + header.request_target("/" + key.toString()); + header.method(restinio::http_method_get()); + auto header_fields = this->initHeaderFields(); + auto request = httpClient_->create_request(header, header_fields, + restinio::http_connection_header_t::keep_alive, ""/*body*/); + if (logger_) + logger_->d(request.c_str()); + + struct GetContext { + GetCallback cb; + DoneCallbackSimple donecb; // wrapper + Value::Filter filter; + std::atomic_bool ok {true}; + std::atomic_bool stop {false}; + std::shared_ptr<dht::Logger> logger; + }; + auto context = std::make_shared<GetContext>(); + context->filter = w.empty() ? f : f.chain(w.getFilter()); + context->cb = [this, context, cb] + (const std::vector<dht::Sp<dht::Value>>& values) -> bool { + { + std::lock_guard<std::mutex> lock(lockCallbacks_); + callbacks_.emplace_back([context, cb, values](){ + if (not context->stop and not cb(values)){ + context->stop = true; } - }).wait(); - } catch(const std::exception& e) { - state->ok = false; + }); } - if (donecb) { - { - std::lock_guard<std::mutex> lock(lockCallbacks); - callbacks_.emplace_back([=](){ - donecb(state->ok, {}); - state->stop = true; - }); + loopSignal_(); + return context->ok; + }; + // keeping context data alive + context->donecb = [this, context, donecb](bool ok){ + { + std::lock_guard<std::mutex> lock(lockCallbacks_); + callbacks_.emplace_back([=](){ + donecb(context->ok, {}); + context->stop = true; + }); + } + loopSignal_(); + }; + if (logger_) + context->logger = logger_; + + auto parser = std::make_shared<http_parser>(); + http_parser_init(parser.get(), HTTP_RESPONSE); + parser->data = static_cast<void*>(context.get()); + + auto parser_s = std::make_shared<http_parser_settings>(); + http_parser_settings_init(parser_s.get()); + parser_s->on_status = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<GetContext*>(parser->data); + if (parser->status_code != 200){ + if (context->logger) + context->logger->e("[proxy:client] [get] status error: %i", parser->status_code); + context->ok = true; + } + return 0; + }; + parser_s->on_body = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<GetContext*>(parser->data); + try{ + Json::Value json; + std::string err; + Json::CharReaderBuilder rbuilder; + auto body = std::string(at, length); + auto* char_data = static_cast<const char*>(&body[0]); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (!reader->parse(char_data, char_data + body.size(), &json, &err)){ + context->ok = false; + return 1; + } + auto value = std::make_shared<Value>(json); + if ((not context->filter or context->filter(*value)) and context->cb){ + context->cb({value}); } - loopSignal_(); + } catch(const std::exception& e) { + if (context->logger) + context->logger->e("[proxy:client] [get] body parsing error: %s", e.what()); + context->ok = false; + return 1; } - if (!state->ok) { - // Connection failed, update connectivity - opFailed(); + return 0; + }; + parser_s->on_message_complete = [](http_parser *parser) -> int { + auto context = static_cast<GetContext*>(parser->data); + try { + if (context->donecb) + context->donecb(context->ok); + } catch(const std::exception& e) { + if (context->logger) + context->logger->e("[proxy:client] [get] message complete parsing error: %i", + parser->status_code); + return 1; } - *finished = true; - }); - { - std::lock_guard<std::mutex> lock(lockOperations_); - operations_.emplace_back(std::move(o)); - } + return 0; + }; + httpClient_->post_request(request, parser, parser_s); } void -DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_point created, bool permanent) +DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, + time_point created, bool permanent) { - DHT_LOG.d(key, "[search %s]: put", key.to_c_str()); - scheduler.syncTime(); - if (not val) { - if (cb) cb(false, {}); + if (logger_) + logger_->d("[proxy:client] [put] [search %s]", key.to_c_str()); + if (not val){ + if (cb) + cb(false, {}); return; } if (val->id == Value::INVALID_ID) { @@ -360,26 +389,35 @@ DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_po if (permanent) { std::lock_guard<std::mutex> lock(searchLock_); auto id = val->id; - auto& search = searches_[key]; - auto nextRefresh = scheduler.time() + proxy::OP_TIMEOUT - proxy::OP_MARGIN; + auto &search = searches_[key]; + auto refreshTimer = std::make_shared<asio::steady_timer>(httpContext_, + std::chrono::steady_clock::now() + proxy::OP_TIMEOUT - proxy::OP_MARGIN); auto ok = std::make_shared<std::atomic_bool>(false); + // define refresh timer handler + refreshTimer->async_wait([this, key, id, ok](const asio::error_code &ec){ + if (ec){ + if (logger_) + logger_->e("[proxy:client] [listener:refresh] error key=%s", key.toString().c_str()); + return; + } + std::lock_guard<std::mutex> lock(searchLock_); + auto s = searches_.find(key); + if (s != searches_.end()) { + auto p = s->second.puts.find(id); + if (p != s->second.puts.end()) { + doPut(key, p->second.value, [ok] + (bool result, const std::vector<std::shared_ptr<dht::Node> >&){ + *ok = result; + }, time_point::max(), true); + p->second.refreshTimer->expires_at(std::chrono::steady_clock::now() + + proxy::OP_TIMEOUT - proxy::OP_MARGIN); + } + } + }); search.puts.erase(id); search.puts.emplace(std::piecewise_construct, std::forward_as_tuple(id), - std::forward_as_tuple(val, scheduler.add(nextRefresh, [this, key, id, ok]{ - std::lock_guard<std::mutex> lock(searchLock_); - auto s = searches_.find(key); - if (s != searches_.end()) { - auto p = s->second.puts.find(id); - if (p != s->second.puts.end()) { - doPut(key, p->second.value, - [ok](bool result, const std::vector<std::shared_ptr<dht::Node> >&){ - *ok = result; - }, time_point::max(), true); - scheduler.edit(p->second.refreshJob, scheduler.time() + proxy::OP_TIMEOUT - proxy::OP_MARGIN); - } - } - }), ok)); + std::forward_as_tuple(val, std::move(refreshTimer), ok)); } doPut(key, val, std::move(cb), created, permanent); } @@ -387,10 +425,12 @@ DhtProxyClient::put(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_po void DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_point /*created*/, bool permanent) { - DHT_LOG.d(key, "[search %s] performing put of %s", key.to_c_str(), val->toString().c_str()); - restbed::Uri uri(serverHost_ + "/" + key.toString()); - auto req = std::make_shared<restbed::Request>(uri); - req->set_method("POST"); + if (logger_) + logger_->d("[proxy:client] [put] [search %s] executing for %s", key.to_c_str(), val->toString().c_str()); + restinio::http_request_header_t header; + header.request_target("/" + key.toString()); + header.method(restinio::http_method_post()); + auto header_fields = this->initHeaderFields(); auto json = val->toJson(); if (permanent) { @@ -409,65 +449,60 @@ DhtProxyClient::doPut(const InfoHash& key, Sp<Value> val, DoneCallback cb, time_ Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; - auto body = Json::writeString(wbuilder, json) + "\n"; - req->set_body(body); - req->set_header("Content-Length", std::to_string(body.size())); - - auto finished = std::make_shared<std::atomic_bool>(false); - Operation o; - o.req = req; - o.finished = finished; - o.thread = std::thread([=](){ - auto ok = std::make_shared<std::atomic_bool>(true); - try { - restbed::Http::async(req, - [ok](const std::shared_ptr<restbed::Request>& /*req*/, - const std::shared_ptr<restbed::Response>& reply) { - auto code = reply->get_status_code(); - - if (code == 200) { - restbed::Http::fetch("\n", reply); - std::string body; - reply->get_body(body); - reply->set_body(""); // Reset the body for the next fetch - - try { - std::string err; - Json::Value json; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(&body[0]); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (not reader->parse(char_data, char_data + body.size(), &json, &err)) - *ok = false; - } catch (...) { - *ok = false; - } - } else { - *ok = false; - } - }).wait(); - } catch(const std::exception& e) { - *ok = false; + auto body = Json::writeString(wbuilder, json); + auto request = httpClient_->create_request(header, header_fields, + restinio::http_connection_header_t::close, body); + if (logger_) + logger_->d("%s", request.c_str()); + + struct GetContext { + DoneCallbackSimple donecb; // wrapper + std::atomic_bool ok {false}; + std::shared_ptr<dht::Logger> logger; + }; + auto context = std::make_shared<GetContext>(); + // keeping context data alive + context->donecb = [this, context, cb](bool ok){ + { + std::lock_guard<std::mutex> lock(lockCallbacks_); + callbacks_.emplace_back([=](){ + cb(context->ok, {}); + }); } - if (cb) { - { - std::lock_guard<std::mutex> lock(lockCallbacks); - callbacks_.emplace_back([=](){ - cb(*ok, {}); - }); - } - loopSignal_(); + loopSignal_(); + }; + if (logger_) + context->logger = logger_; + + auto parser = std::make_shared<http_parser>(); + http_parser_init(parser.get(), HTTP_RESPONSE); + parser->data = static_cast<void*>(context.get()); + + auto parser_s = std::make_shared<http_parser_settings>(); + http_parser_settings_init(parser_s.get()); + parser_s->on_status = [](http_parser *parser, const char *at, size_t length) -> int { + GetContext* context = static_cast<GetContext*>(parser->data); + if (parser->status_code == 200){ + context->ok = true; + } else { + if (context->logger) + context->logger->e("[proxy:client] [put] status error: %i", parser->status_code); } - if (!ok) { - // Connection failed, update connectivity - opFailed(); + return 0; + }; + parser_s->on_message_complete = [](http_parser * parser) -> int { + auto context = static_cast<GetContext*>(parser->data); + try { + if (context->donecb) + context->donecb(context->ok); + } catch(const std::exception& e) { + if (context->logger) + context->logger->e("[proxy:client] [put] message complete error: %s", e.what()); + return 1; } - *finished = true; - }); - { - std::lock_guard<std::mutex> lock(lockOperations_); - operations_.emplace_back(std::move(o)); - } + return 0; + }; + httpClient_->post_request(request, parser, parser_s); } /** @@ -509,7 +544,8 @@ DhtProxyClient::cancelPut(const InfoHash& key, const Value::Id& id) auto search = searches_.find(key); if (search == searches_.end()) return false; - DHT_LOG.d(key, "[search %s] cancel put", key.to_c_str()); + if (logger_) + logger_->d("[proxy:client] [put:cancel] [search %s]", key.to_c_str()); return search->second.puts.erase(id) > 0; } @@ -522,14 +558,14 @@ DhtProxyClient::getNodesStats(sa_family_t af) const void DhtProxyClient::getProxyInfos() { - DHT_LOG.d("Requesting proxy server node information"); + if (logger_) + logger_->d("[proxy:client] [info] requesting proxy server node information"); std::lock_guard<std::mutex> l(statusLock_); auto infoState = std::make_shared<InfoState>(); if (infoState_) infoState_->cancel = true; infoState_ = infoState; - { std::lock_guard<std::mutex> l(lockCurrentProxyInfos_); if (statusIpv4_ == NodeStatus::Disconnected) @@ -537,85 +573,125 @@ DhtProxyClient::getProxyInfos() if (statusIpv6_ == NodeStatus::Disconnected) statusIpv6_ = NodeStatus::Connecting; } - - // A node can have a Ipv4 and a Ipv6. So, we need to retrieve all public ips - auto serverHost = serverHost_; - // Try to contact the proxy and set the status to connected when done. // will change the connectivity status - if (statusThread_.joinable()) { - try { - statusThread_.detach(); - statusThread_ = {}; - } catch (const std::exception& e) { - DHT_LOG.e("Error detaching thread: %s", e.what()); + if (!statusTimer_) + statusTimer_ = std::make_shared<asio::steady_timer>(httpContext_); + + statusTimer_->expires_at(std::chrono::steady_clock::now()); + statusTimer_->async_wait(std::bind(&DhtProxyClient::handleProxyStatus, this, + std::placeholders::_1, infoState)); +} + +void +DhtProxyClient::handleProxyStatus(const asio::error_code &ec, + std::shared_ptr<InfoState> infoState) +{ + if (ec){ + if (logger_){ + logger_->e("[proxy:client] [status] handling error: %s", ec.message().c_str()); + return; } } - statusThread_ = std::thread([this, serverHost, infoState]{ - try { - auto endpointStr = serverHost; - auto protocol = std::string(proxy::HTTP_PROTO); - auto protocolIdx = serverHost.find("://"); - if (protocolIdx != std::string::npos) { - protocol = endpointStr.substr(0, protocolIdx + 3); - endpointStr = endpointStr.substr(protocolIdx + 3); - } - auto hostAndService = splitPort(endpointStr); - auto resolved_proxies = SockAddr::resolve(hostAndService.first, hostAndService.second); - std::vector<std::future<Sp<restbed::Response>>> reqs; - reqs.reserve(resolved_proxies.size()); - for (const auto& resolved_proxy: resolved_proxies) { - auto server = resolved_proxy.toString(); - if (resolved_proxy.getFamily() == AF_INET6) { - // HACK restbed seems to not correctly handle directly http://[ipv6] - // See https://github.com/Corvusoft/restbed/issues/290. - server = endpointStr; + auto serverHost = serverHost_; + try { + // A node can have a Ipv4 and a Ipv6. So, we need to retrieve all public ips + auto endpointStr = serverHost; + auto protocol = std::string(proxy::HTTP_PROTO); + auto protocolIdx = serverHost.find("://"); + if (protocolIdx != std::string::npos) { + protocol = endpointStr.substr(0, protocolIdx + 3); + endpointStr = endpointStr.substr(protocolIdx + 3); + } + auto hostAndService = splitPort(endpointStr); + auto resolvedProxies = SockAddr::resolve(hostAndService.first, hostAndService.second); + + for (const auto& resolvedProxy: resolvedProxies){ + auto server = resolvedProxy.toString(); + // make an http header + restinio::http_request_header_t header; + header.request_target("/"); + header.method(restinio::http_method_get()); + auto header_fields = this->initHeaderFields(); + auto request = httpClient_->create_request(header, header_fields, + restinio::http_connection_header_t::keep_alive, ""/*body*/); + if (logger_) + logger_->d("[proxy:client] [status] sending request:\n%s", request.c_str()); + // initalise the parser callback data + struct GetContext { + unsigned int family; + std::function<void(Json::Value infos)> cb; // wrapper + std::atomic_bool ok {true}; + std::shared_ptr<InfoState> infoState; + std::function<void(const Json::Value&, sa_family_t)> proxyInfo; + std::shared_ptr<dht::Logger> logger; + }; + auto context = std::make_shared<GetContext>(); + context->infoState = infoState; + context->family = resolvedProxy.getFamily(); + context->proxyInfo = std::bind(&DhtProxyClient::onProxyInfos, this, + std::placeholders::_1, std::placeholders::_2); + // keeping context data alive + context->cb = [this, context](Json::Value infos){ + if (context->family == AF_INET) + context->infoState->ipv4++; + else if (context->family == AF_INET6) + context->infoState->ipv6++; + if (not context->infoState->cancel) + context->proxyInfo(infos, context->family); + }; + if (logger_) + context->logger = logger_; + + // initialize the parser + auto parser = std::make_shared<http_parser>(); + http_parser_init(parser.get(), HTTP_RESPONSE); + parser->data = static_cast<void*>(context.get()); + + // init the parser callbacks + auto parser_s = std::make_shared<http_parser_settings>(); + http_parser_settings_init(parser_s.get()); + parser_s->on_status = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<GetContext*>(parser->data); + if (parser->status_code != 200){ + if (context->logger) + context->logger->e("[proxy:client] [status] error: %i", parser->status_code); + context->ok = true; } - restbed::Uri uri(protocol + server + "/"); - auto req = std::make_shared<restbed::Request>(uri); - if (infoState->cancel) - return; - reqs.emplace_back(restbed::Http::async(req, - [this, resolved_proxy, infoState]( - const std::shared_ptr<restbed::Request>&, - const std::shared_ptr<restbed::Response>& reply) - { - auto code = reply->get_status_code(); + return 0; + }; + parser_s->on_body = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<GetContext*>(parser->data); + try{ + std::string err; Json::Value proxyInfos; - if (code == 200) { - restbed::Http::fetch("\n", reply); - auto& state = *infoState; - if (state.cancel) return; - std::string body; - reply->get_body(body); - - std::string err; - Json::CharReaderBuilder rbuilder; - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - try { - reader->parse(body.data(), body.data() + body.size(), &proxyInfos, &err); - } catch (...) { - return; - } - auto family = resolved_proxy.getFamily(); - if (family == AF_INET) state.ipv4++; - else if (family == AF_INET6) state.ipv6++; - if (not state.cancel) - onProxyInfos(proxyInfos, family); + Json::CharReaderBuilder rbuilder; + auto body = std::string(at, length); + auto* char_data = static_cast<const char*>(&body[0]); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (!reader->parse(char_data, char_data + body.size(), &proxyInfos, &err)){ + context->ok = false; + return 1; } - })); - } - for (auto& r : reqs) - r.get(); - reqs.clear(); - } catch (const std::exception& e) { - DHT_LOG.e("Error sending proxy info request: %s", e.what()); + context->cb(proxyInfos); + } + catch (const std::exception& e) { + if (context->logger) + context->logger->e("[proxy:client] [status] body error: %s", e.what()); + context->ok = false; + return 1; + } + return 0; + }; + if (context->infoState->cancel) + return; + httpClient_->post_request(request, parser, parser_s); } - const auto& state = *infoState; - if (state.cancel) return; - if (state.ipv4 == 0) onProxyInfos(Json::Value{}, AF_INET); - if (state.ipv6 == 0) onProxyInfos(Json::Value{}, AF_INET6); - }); + } + catch (const std::exception& e) { + if (logger_) + logger_->e("[proxy:client] [info] error sending request: %s", e.what()); + } } void @@ -627,10 +703,14 @@ DhtProxyClient::onProxyInfos(const Json::Value& proxyInfos, sa_family_t family) auto oldStatus = std::max(statusIpv4_, statusIpv6_); auto& status = family == AF_INET ? statusIpv4_ : statusIpv6_; if (not proxyInfos.isMember("node_id")) { - DHT_LOG.e("Proxy info request failed for %s", family == AF_INET ? "IPv4" : "IPv6"); + if (logger_) + logger_->e("[proxy:client] [info] request failed for %s", + family == AF_INET ? "ipv4" : "ipv6"); status = NodeStatus::Disconnected; } else { - DHT_LOG.d("Got proxy reply for %s", family == AF_INET ? "IPv4" : "IPv6"); + if (logger_) + logger_->d("[proxy:client] [info] got proxy reply for %s", + family == AF_INET ? "ipv4" : "ipv6"); try { myid = InfoHash(proxyInfos["node_id"].asString()); stats4_ = NodeStats(proxyInfos["ipv4"]); @@ -649,19 +729,21 @@ DhtProxyClient::onProxyInfos(const Json::Value& proxyInfos, sa_family_t family) else if (publicFamily == AF_INET6) publicAddressV6_ = publicIp; } catch (const std::exception& e) { - DHT_LOG.w("Error processing proxy infos: %s", e.what()); + if (logger_) + logger_->e("[proxy:client] [info] error processing: %s", e.what()); } } - auto newStatus = std::max(statusIpv4_, statusIpv6_); if (newStatus == NodeStatus::Connected) { if (oldStatus == NodeStatus::Disconnected || oldStatus == NodeStatus::Connecting) { - scheduler.edit(listenerRestart, scheduler.time()); + listenerRestartTimer_->expires_at(std::chrono::steady_clock::now()); } - scheduler.edit(nextProxyConfirmation, scheduler.time() + std::chrono::minutes(15)); + nextProxyConfirmationTimer_->expires_at(std::chrono::steady_clock::now() + + std::chrono::minutes(15)); } else if (newStatus == NodeStatus::Disconnected) { - scheduler.edit(nextProxyConfirmation, scheduler.time() + std::chrono::minutes(1)); + nextProxyConfirmationTimer_->expires_at(std::chrono::steady_clock::now() + + std::chrono::minutes(1)); } loopSignal_(); } @@ -688,28 +770,31 @@ DhtProxyClient::getPublicAddress(sa_family_t family) size_t DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filter, Where where) { - DHT_LOG.d(key, "[search %s]: listen", key.to_c_str()); + if (logger_) + logger_->d("[proxy:client] [listen] [search %s]", key.to_c_str()); + auto& search = searches_[key]; auto query = std::make_shared<Query>(Select{}, where); - auto token = search.ops.listen(cb, query, filter, [this, key, filter](Sp<Query> /*q*/, ValueCallback cb, SyncCallback /*scb*/) -> size_t { - scheduler.syncTime(); - restbed::Uri uri(serverHost_ + "/" + key.toString()); + auto token = search.ops.listen(cb, query, filter, [this, key, filter]( + Sp<Query>, ValueCallback cb, SyncCallback) -> size_t { std::lock_guard<std::mutex> lock(searchLock_); auto search = searches_.find(key); if (search == searches_.end()) { - DHT_LOG.e(key, "[search %s] listen: search not found", key.to_c_str()); + if (logger_) + logger_->e("[proxy:client] [listen] [search %s] search not found", key.to_c_str()); return 0; } - DHT_LOG.d(key, "[search %s] sending %s", key.to_c_str(), deviceKey_.empty() ? "listen" : "subscribe"); + if (logger_) + logger_->d("[proxy:client] [listen] [search %s] sending %s", key.to_c_str(), + deviceKey_.empty() ? "listen" : "subscribe"); - auto req = std::make_shared<restbed::Request>(uri); auto token = ++listenerToken_; auto l = search->second.listeners.find(token); if (l == search->second.listeners.end()) { auto f = filter; l = search->second.listeners.emplace(std::piecewise_construct, std::forward_as_tuple(token), - std::forward_as_tuple(std::move(cb), req, std::move(f))).first; + std::forward_as_tuple(std::move(cb), std::move(f))).first; } else { if (l->second.state) l->second.state->cancel = true; @@ -731,18 +816,30 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt } return l->second.cache.onValue(values, expired); }; - auto vcb = l->second.cb; - l->second.req = req; if (not deviceKey_.empty()) { - // Relaunch push listeners even if a timeout is not received (if the proxy crash for any reason) - l->second.refreshJob = scheduler.add(scheduler.time() + proxy::OP_TIMEOUT - proxy::OP_MARGIN, [this, key, token, state] { + /* + * Relaunch push listeners even if a timeout is not received + * (if the proxy crash for any reason) + */ + if (!l->second.refreshTimer) + l->second.refreshTimer = std::make_shared<asio::steady_timer>(httpContext_); + l->second.refreshTimer->expires_at(std::chrono::steady_clock::now() + + proxy::OP_TIMEOUT - proxy::OP_MARGIN); + l->second.refreshTimer->async_wait( + [this, key, token, state](const asio::error_code &ec) + { + if (ec){ + if (logger_) + logger_->d("[proxy:client] [listen] refresh error key=%s", key.toString().c_str()); + return; + } if (state->cancel) return; std::lock_guard<std::mutex> lock(searchLock_); auto s = searches_.find(key); - if (s != searches_.end()) { + if (s != searches_.end()){ auto l = s->second.listeners.find(token); if (l != s->second.listeners.end()) { resubscribe(key, l->second); @@ -750,10 +847,19 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt } }); } - l->second.thread = std::thread([this, req, vcb, filter, state]() { - sendListen(req, vcb, filter, state, - deviceKey_.empty() ? ListenMethod::LISTEN : ListenMethod::SUBSCRIBE); - }); + ListenMethod method; + restinio::http_request_header_t header; + if (deviceKey_.empty()){ // listen + method = ListenMethod::LISTEN; + header.method(restinio::http_method_get()); + header.request_target("/" + key.toString() + "/listen"); + } + else { + method = ListenMethod::SUBSCRIBE; + header.method(restinio::http_method_subscribe()); + header.request_target("/" + key.toString()); + } + l->second.connId = sendListen(header, vcb, filter, state, method); return token; }); return token; @@ -761,103 +867,126 @@ DhtProxyClient::listen(const InfoHash& key, ValueCallback cb, Value::Filter filt bool DhtProxyClient::cancelListen(const InfoHash& key, size_t gtoken) { - scheduler.syncTime(); - DHT_LOG.d(key, "[search %s]: cancelListen %zu", key.to_c_str(), gtoken); + if (logger_) + logger_->d(key, "[proxy:client] [search %s] cancel listen %zu", key.to_c_str(), gtoken); auto it = searches_.find(key); if (it == searches_.end()) return false; auto& ops = it->second.ops; - bool canceled = ops.cancelListen(gtoken, scheduler.time()); - if (not it->second.opExpirationJob) { - it->second.opExpirationJob = scheduler.add(time_point::max(), [this,key](){ + bool canceled = ops.cancelListen(gtoken, std::chrono::steady_clock::now()); + // on new listener set the expiration to the max, + // in case a user redo a listen right after cancel, we won't impact the network. + if (!it->second.opExpirationTimer) { + it->second.opExpirationTimer = std::make_shared<asio::steady_timer>(httpContext_); + it->second.opExpirationTimer->expires_at(time_point::max()); + it->second.opExpirationTimer->async_wait([this, key](const asio::error_code ec){ + if (ec){ + if (logger_) + logger_->d("[proxy:client] [listen %s] error in cancel", key.toString().c_str()); + return false; + } auto it = searches_.find(key); if (it != searches_.end()) { - auto next = it->second.ops.expire(scheduler.time(), [this,key](size_t ltoken){ + auto next = it->second.ops.expire(std::chrono::steady_clock::now(), + [this, key](size_t ltoken){ doCancelListen(key, ltoken); }); if (next != time_point::max()) { - scheduler.edit(it->second.opExpirationJob, next); + if (!it->second.opExpirationTimer){ + it->second.opExpirationTimer = std::make_shared< + asio::steady_timer>(httpContext_); + } + it->second.opExpirationTimer->expires_at(next); } } }); } - scheduler.edit(it->second.opExpirationJob, ops.getExpiration()); + // Let it expire when it is due. + it->second.opExpirationTimer->expires_at(ops.getExpiration()); loopSignal_(); return canceled; } -void DhtProxyClient::sendListen(const std::shared_ptr<restbed::Request> &req, - const ValueCallback &cb, - const Value::Filter &filter, - const Sp<ListenState> &state, - ListenMethod method) { - auto settings = std::make_shared<restbed::Settings>(); - if (method != ListenMethod::LISTEN) { - req->set_method("SUBSCRIBE"); - } else { - std::chrono::milliseconds timeout(std::numeric_limits<int>::max()); - settings->set_connection_timeout(timeout); // Avoid the client to close the socket after 5 seconds. - req->set_method("LISTEN"); - } - try { +uint16_t +DhtProxyClient::sendListen(const restinio::http_request_header_t header, + const ValueCallback &cb, const Value::Filter &filter, + const Sp<ListenState> &state, ListenMethod method) +{ + auto headers = this->initHeaderFields(); + auto conn = restinio::http_connection_header_t::close; + if (method == ListenMethod::LISTEN) + conn = restinio::http_connection_header_t::keep_alive; + std::string body; #ifdef OPENDHT_PUSH_NOTIFICATIONS - if (method != ListenMethod::LISTEN) - fillBody(req, method == ListenMethod::RESUBSCRIBE); - #endif - restbed::Http::async(req, - [this, filter, cb, state](const std::shared_ptr<restbed::Request>& req, - const std::shared_ptr<restbed::Response>& reply) - { - auto code = reply->get_status_code(); - if (code == 200) { - try { - while (restbed::Http::is_open(req) and not state->cancel) { - restbed::Http::fetch("\n", reply); - if (state->cancel) - break; - std::string body; - reply->get_body(body); - reply->set_body(""); // Reset the body for the next fetch - - Json::Value json; - std::string err; - Json::CharReaderBuilder rbuilder; - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (reader->parse(body.data(), body.data() + body.size(), &json, &err)) { - if (json.size() == 0) { - // Empty value, it's the end - break; - } - auto expired = json.get("expired", Json::Value(false)).asBool(); - auto value = std::make_shared<Value>(json); - if ((not filter or filter(*value)) and cb) { - { - std::lock_guard<std::mutex> lock(lockCallbacks); - callbacks_.emplace_back([cb, value, state, expired]() { - if (not state->cancel and not cb({value}, expired)) - state->cancel = true; - }); - } - loopSignal_(); - } - } - } - } catch (const std::exception& e) { - if (not state->cancel) { - DHT_LOG.w("Listen closed by the proxy server: %s", e.what()); - state->ok = false; - } - } - } else { - state->ok = false; + if (method != ListenMethod::LISTEN) + body = fillBody(method == ListenMethod::RESUBSCRIBE); +#endif + auto request = httpClient_->create_request(header, headers, conn, body); + if (logger_) + logger_->d(request.c_str()); + + struct ListenContext { + std::shared_ptr<Logger> logger; + ValueCallback cb; // wrapper + Value::Filter filter; + std::shared_ptr<ListenState> state; + }; + auto context = std::make_shared<ListenContext>(); + if (logger_) + context->logger = logger_; + // keeping context data alive + context->cb = [context, cb](const std::vector<std::shared_ptr<Value>>& values, bool expired){ + return cb(values, expired); + }; + context->state = state; + context->filter = filter; + + auto parser = std::make_shared<http_parser>(); + http_parser_init(parser.get(), HTTP_RESPONSE); + parser->data = static_cast<void*>(context.get()); + + auto parser_s = std::make_shared<http_parser_settings>(); + http_parser_settings_init(parser_s.get()); + parser_s->on_status = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<ListenContext*>(parser->data); + if (parser->status_code != 200){ + if (context->logger) + context->logger->e("[proxy:client] [listen] status error: %i", parser->status_code); + context->state->ok = false; + } + return 0; + }; + parser_s->on_body = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<ListenContext*>(parser->data); + try { + Json::Value json; + std::string err; + Json::CharReaderBuilder rbuilder; + auto body = std::string(at, length); + auto* char_data = static_cast<const char*>(&body[0]); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (!reader->parse(char_data, char_data + body.size(), &json, &err)){ + context->state->ok = false; + return 1; } - }, settings).get(); - } catch (const std::exception& e) { - state->ok = false; - } - auto& s = *state; - if (not s.ok and not s.cancel) - opFailed(); + if (json.size() == 0){ // it's the end + context->state->cancel = true; + } + auto value = std::make_shared<Value>(json); + auto expired = json.get("expired", Json::Value(false)).asBool(); + if ((not context->filter or context->filter(*value)) and context->cb){ + context->cb({value}, expired); + } + } catch(const std::exception& e) { + if (context->logger) + context->logger->e("[proxy:client] [listen] error in parsing: %s", e.what()); + context->state->ok = false; + return 1; + } + return 0; + }; + auto connId = httpClient_->post_request(request, parser, parser_s); + return connId; } bool @@ -873,19 +1002,17 @@ DhtProxyClient::doCancelListen(const InfoHash& key, size_t ltoken) if (it == search->second.listeners.end()) return false; - DHT_LOG.d(key, "[search %s] cancel listen", key.to_c_str()); + if (logger_) + logger_->d("[proxy:client] [listen:cancel] [search %s]", key.to_c_str()); auto& listener = it->second; listener.state->cancel = true; if (not deviceKey_.empty()) { - // First, be sure to have a token - if (listener.thread.joinable()) { - listener.thread.join(); - } // UNSUBSCRIBE - restbed::Uri uri(serverHost_ + "/" + key.toString()); - auto req = std::make_shared<restbed::Request>(uri); - req->set_method("UNSUBSCRIBE"); + restinio::http_request_header_t header; + header.request_target("/" + key.toString()); + header.method(restinio::http_method_unsubscribe()); + auto header_fields = this->initHeaderFields(); // fill request body Json::Value body; body["key"] = deviceKey_; @@ -895,41 +1022,64 @@ DhtProxyClient::doCancelListen(const InfoHash& key, size_t ltoken) wbuilder["indentation"] = ""; auto content = Json::writeString(wbuilder, body) + "\n"; std::replace(content.begin(), content.end(), '\n', ' '); - req->set_body(content); - req->set_header("Content-Length", std::to_string(content.size())); - try { - restbed::Http::async(req, [](const std::shared_ptr<restbed::Request>&, const std::shared_ptr<restbed::Response>&){}); - } catch (const std::exception& e) { - DHT_LOG.w(key, "[search %s] cancelListen: Http::async failed: %s", key.to_c_str(), e.what()); - } + // build the request + auto request = httpClient_->create_request(header, header_fields, + restinio::http_connection_header_t::keep_alive, content); + if (logger_) + logger_->d(request.c_str()); + // define context + struct UnsubscribeContext { + InfoHash key; + std::shared_ptr<dht::Logger> logger; + }; + auto context = std::make_shared<UnsubscribeContext>(); + context->key = key; + if (logger_) + context->logger = logger_; + // define parser + auto parser = std::make_shared<http_parser>(); + http_parser_init(parser.get(), HTTP_RESPONSE); + parser->data = static_cast<void*>(context.get()); + // define callbacks + auto parser_s = std::make_shared<http_parser_settings>(); + http_parser_settings_init(parser_s.get()); + parser_s->on_status = [](http_parser *parser, const char *at, size_t length) -> int { + auto context = static_cast<UnsubscribeContext*>(parser->data); + if (parser->status_code != 200){ + if (context->logger) + context->logger->e("[proxy:client] [search %s] cancel listen failed: %i", + context->key.to_c_str(), parser->status_code); + } + return 0; + }; + httpClient_->post_request(request, parser, parser_s); } else { // Just stop the request - if (listener.thread.joinable()) { - // Close connection to stop listener - if (listener.req) { - try { - restbed::Http::close(listener.req); - } catch (const std::exception& e) { - DHT_LOG.w("Error closing socket: %s", e.what()); - } - listener.req.reset(); + if (httpClient_->active_connection(listener.connId)){ + try { + httpClient_->close_connection(listener.connId); + } + catch (const std::exception& e){ + if (logger_) + logger_->e("[proxy:client] [listen:cancel] error closing socket: %s", e.what()); } - listener.thread.join(); } } search->second.listeners.erase(it); - DHT_LOG.d(key, "[search %s] cancelListen: %zu listener remaining", key.to_c_str(), search->second.listeners.size()); - if (search->second.listeners.empty()) { + if (logger_) + logger_->d("[proxy:client] [listen:cancel] [search %s] %zu listener remaining", + key.to_c_str(), search->second.listeners.size()); + if (search->second.listeners.empty()){ searches_.erase(search); } - return true; } void DhtProxyClient::opFailed() { - DHT_LOG.e("Proxy request failed"); + if (logger_) + logger_->e("[proxy:client] proxy request failed"); { std::lock_guard<std::mutex> l(lockCurrentProxyInfos_); statusIpv4_ = NodeStatus::Disconnected; @@ -942,7 +1092,8 @@ DhtProxyClient::opFailed() void DhtProxyClient::getConnectivityStatus() { - if (!isDestroying_) getProxyInfos(); + if (!isDestroying_) + getProxyInfos(); } void @@ -950,7 +1101,8 @@ DhtProxyClient::restartListeners() { if (isDestroying_) return; std::lock_guard<std::mutex> lock(searchLock_); - DHT_LOG.d("Refresh permanent puts"); + if (logger_) + logger_->d("[proxy:client] [listeners:restart] refresh permanent puts"); for (auto& search : searches_) { for (auto& put : search.second.puts) { if (!*put.second.ok) { @@ -959,12 +1111,18 @@ DhtProxyClient::restartListeners() [ok](bool result, const std::vector<std::shared_ptr<dht::Node> >&){ *ok = result; }, time_point::max(), true); - scheduler.edit(put.second.refreshJob, scheduler.time() + proxy::OP_TIMEOUT - proxy::OP_MARGIN); + if (!put.second.refreshTimer){ + put.second.refreshTimer = std::make_shared< + asio::steady_timer>(httpContext_); + } + put.second.refreshTimer->expires_at(std::chrono::steady_clock::now() + + proxy::OP_TIMEOUT - proxy::OP_MARGIN); } } } if (not deviceKey_.empty()) { - DHT_LOG.d("resubscribe due to a connectivity change"); + if (logger_) + logger_->d("[proxy:client] [listeners:restart] resubscribe due to a connectivity change"); // Connectivity changed, refresh all subscribe for (auto& search : searches_) for (auto& listener : search.second.listeners) @@ -972,19 +1130,21 @@ DhtProxyClient::restartListeners() resubscribe(search.first, listener.second); return; } - DHT_LOG.d("Restarting listeners"); + if (logger_) + logger_->d("[proxy:client] [listeners:restart] restarting listeners"); for (auto& search: searches_) { for (auto& l: search.second.listeners) { auto& listener = l.second; if (auto state = listener.state) state->cancel = true; - if (listener.req) { + if (httpClient_->active_connection(listener.connId)){ try { - restbed::Http::close(listener.req); + httpClient_->close_connection(listener.connId); } catch (const std::exception& e) { - DHT_LOG.w("Error closing socket: %s", e.what()); + if (logger_) + logger_->e("[proxy:client] [listeners:restart] error closing socket: %s", e.what()); } - listener.req.reset(); + l.second.connId = 0; } } } @@ -992,21 +1152,17 @@ DhtProxyClient::restartListeners() for (auto& l: search.second.listeners) { auto& listener = l.second; auto state = listener.state; - if (listener.thread.joinable()) { - listener.thread.join(); - } // Redo listen state->cancel = false; state->ok = true; auto filter = listener.filter; auto cb = listener.cb; - restbed::Uri uri(serverHost_ + "/" + search.first.toString()); - auto req = std::make_shared<restbed::Request>(uri); - req->set_method("LISTEN"); - listener.req = req; - listener.thread = std::thread([this, req, cb, filter, state]() { - sendListen(req, cb, filter, state); - }); + // define header + restinio::http_request_header_t header; + header.method(restinio::http_method_get()); + header.request_target("/" + search.first.toString() + "/listen"); + // send listen + listener.connId = sendListen(header, cb, filter, state, ListenMethod::LISTEN); } } } @@ -1015,7 +1171,6 @@ void DhtProxyClient::pushNotificationReceived(const std::map<std::string, std::string>& notification) { #ifdef OPENDHT_PUSH_NOTIFICATIONS - scheduler.syncTime(); { // If a push notification is received, the proxy is up and running std::lock_guard<std::mutex> l(lockCurrentProxyInfos_); @@ -1033,7 +1188,11 @@ DhtProxyClient::pushNotificationReceived(const std::map<std::string, std::string // Refresh put auto vid = std::stoull(vidIt->second); auto& put = search.puts.at(vid); - scheduler.edit(put.refreshJob, scheduler.time()); + if (!put.refreshTimer){ + put.refreshTimer = std::make_shared< + asio::steady_timer>(httpContext_); + } + put.refreshTimer->expires_at(std::chrono::steady_clock::now()); loopSignal_(); } else { // Refresh listen @@ -1046,7 +1205,8 @@ DhtProxyClient::pushNotificationReceived(const std::map<std::string, std::string for (auto& list : search.listeners) { if (list.second.state->cancel) continue; - DHT_LOG.d(key, "[search %s] handling push notification", key.to_c_str()); + if (logger_) + logger_->d("[proxy:client] [push:received] [search %s] handling", key.to_c_str()); auto expired = notification.find("exp"); auto token = list.first; auto state = list.second.state; @@ -1057,7 +1217,8 @@ DhtProxyClient::pushNotificationReceived(const std::map<std::string, std::string get(key, [cb](const std::vector<Sp<Value>>& vals) { return cb(vals, false); }, [cb, oldValues](bool /*ok*/) { - // Decrement old values refcount to expire values not present in the new list + // Decrement old values refcount to expire values not + // present in the new list cb(oldValues, true); }, std::move(filter)); } else { @@ -1069,14 +1230,17 @@ DhtProxyClient::pushNotificationReceived(const std::map<std::string, std::string ids.emplace_back(std::stoull(substr)); } { - std::lock_guard<std::mutex> lock(lockCallbacks); + std::lock_guard<std::mutex> lock(lockCallbacks_); callbacks_.emplace_back([this, key, token, state, ids]() { - if (state->cancel) return; + if (state->cancel) + return; std::lock_guard<std::mutex> lock(searchLock_); auto s = searches_.find(key); - if (s == searches_.end()) return; + if (s == searches_.end()) + return; auto l = s->second.listeners.find(token); - if (l == s->second.listeners.end()) return; + if (l == s->second.listeners.end()) + return; if (not state->cancel and not l->second.cache.onValuesExpired(ids)) state->cancel = true; }); @@ -1086,7 +1250,8 @@ DhtProxyClient::pushNotificationReceived(const std::map<std::string, std::string } } } catch (const std::exception& e) { - DHT_LOG.e("Error handling push notification: %s", e.what()); + if (logger_) + logger_->e("[proxy:client] [push:received] error handling: %s", e.what()); } #else (void) notification; @@ -1097,34 +1262,35 @@ void DhtProxyClient::resubscribe(const InfoHash& key, Listener& listener) { #ifdef OPENDHT_PUSH_NOTIFICATIONS - if (deviceKey_.empty()) return; - scheduler.syncTime(); - DHT_LOG.d(key, "[search %s] resubscribe push listener", key.to_c_str()); + if (deviceKey_.empty()) + return; + if (logger_) + logger_->d("[proxy:client] [resubscribe] [search %s] resubscribe push listener", key.to_c_str()); // Subscribe auto state = listener.state; - if (listener.thread.joinable()) { - state->cancel = true; - if (listener.req) { - try { - restbed::Http::close(listener.req); - } catch (const std::exception& e) { - DHT_LOG.w("Error closing socket: %s", e.what()); - } - listener.req.reset(); + state->cancel = true; + if (listener.connId) { + try { + httpClient_->close_connection(listener.connId); + } catch (const std::exception& e) { + if (logger_) + logger_->e("[proxy:client] [resubscribe] error closing socket: %s", e.what()); } - listener.thread.join(); } state->cancel = false; state->ok = true; - auto req = std::make_shared<restbed::Request>(restbed::Uri {serverHost_ + "/" + key.toString()}); - req->set_method("SUBSCRIBE"); - listener.req = req; - scheduler.edit(listener.refreshJob, scheduler.time() + proxy::OP_TIMEOUT - proxy::OP_MARGIN); + + restinio::http_request_header_t header; + header.method(restinio::http_method_subscribe()); + header.request_target("/" + key.toString()); + if (!listener.refreshTimer){ + listener.refreshTimer = std::make_shared<asio::steady_timer>(httpContext_); + } + listener.refreshTimer->expires_at(std::chrono::steady_clock::now() + + proxy::OP_TIMEOUT - proxy::OP_MARGIN); auto vcb = listener.cb; auto filter = listener.filter; - listener.thread = std::thread([this, req, vcb, filter, state]() { - sendListen(req, vcb, filter, state, ListenMethod::RESUBSCRIBE); - }); + listener.connId = sendListen(header, vcb, filter, state, ListenMethod::RESUBSCRIBE); #else (void) key; (void) listener; @@ -1145,8 +1311,8 @@ DhtProxyClient::getPushRequest(Json::Value& body) const #endif } -void -DhtProxyClient::fillBody(std::shared_ptr<restbed::Request> req, bool resubscribe) +std::string +DhtProxyClient::fillBody(bool resubscribe) { // Fill body with // { @@ -1163,8 +1329,7 @@ DhtProxyClient::fillBody(std::shared_ptr<restbed::Request> req, bool resubscribe wbuilder["indentation"] = ""; auto content = Json::writeString(wbuilder, body) + "\n"; std::replace(content.begin(), content.end(), '\n', ' '); - req->set_body(content); - req->set_header("Content-Length", std::to_string(content.size())); + return content; } #endif // OPENDHT_PUSH_NOTIFICATIONS diff --git a/src/dht_proxy_server.cpp b/src/dht_proxy_server.cpp index 744ba986dc7cc1a3abcc14e4a44a319cf49d2f68..3e029b42300fc461931c2a4788526e08a5eeed8d 100644 --- a/src/dht_proxy_server.cpp +++ b/src/dht_proxy_server.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2017-2019 Savoir-faire Linux Inc. * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> * Adrien Béraud <adrien.beraud@savoirfairelinux.com> + * Vsevolod Ivanov <vsevolod.ivanov@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 @@ -19,7 +20,6 @@ #include "dht_proxy_server.h" -#include "thread_pool.h" #include "default_types.h" #include "dhtrunner.h" @@ -35,123 +35,75 @@ using namespace std::placeholders; namespace dht { -struct DhtProxyServer::PermanentPut { - time_point expiration; - std::string pushToken; - std::string clientId; - Sp<Scheduler::Job> expireJob; - Sp<Scheduler::Job> expireNotifyJob; -}; -struct DhtProxyServer::SearchPuts { - std::map<dht::Value::Id, PermanentPut> puts; -}; +constexpr char RESP_MSG_DESTINATION_NOT_FOUND[] = "{\"err\":\"No destination found\"}"; +constexpr char RESP_MSG_NO_TOKEN[] = "{\"err\":\"No token\"}"; +constexpr char RESP_MSG_JSON_NOT_ENABLED[] = "{\"err\":\"JSON not enabled on this instance\"}"; +constexpr char RESP_MSG_JSON_INCORRECT[] = "{\"err:\":\"Incorrect JSON\"}"; +constexpr char RESP_MSG_SERVICE_UNAVAILABLE[] = "{\"err\":\"Incorrect DhtRunner\"}"; +constexpr char RESP_MSG_INTERNAL_SERVER_ERRROR[] = "{\"err\":\"Internal server error\"}"; +constexpr char RESP_MSG_MISSING_PARAMS[] = "{\"err\":\"Missing parameters\"}"; +constexpr char RESP_MSG_PUT_FAILED[] = "{\"err\":\"Put failed\"}"; constexpr const std::chrono::minutes PRINT_STATS_PERIOD {2}; -constexpr const size_t IO_THREADS_MAX {64}; - -DhtProxyServer::DhtProxyServer(std::shared_ptr<DhtRunner> dht, in_port_t port , const std::string& pushServer) -: dht_(dht), threadPool_(new ThreadPool(IO_THREADS_MAX)), pushServer_(pushServer) +DhtProxyServer::DhtProxyServer(std::shared_ptr<DhtRunner> dht, in_port_t port, + const std::string& pushServer, + std::shared_ptr<dht::Logger> logger +): + dht_(dht), logger_(logger), lockListener_(std::make_shared<std::mutex>()), + listeners_(std::make_shared<std::map<restinio::connection_id_t, http::ListenerSession>>()), + connListener_(std::make_shared<http::ConnectionListener>( + dht, listeners_, lockListener_, logger)), + pushServer_(pushServer) { if (not dht_) throw std::invalid_argument("A DHT instance must be provided"); - // NOTE in c++14, use make_unique - service_ = std::unique_ptr<restbed::Service>(new restbed::Service()); - std::cout << "Running DHT proxy server on port " << port << std::endl; - if (not pushServer.empty()) { + if (logger_) + logger_->d("[proxy:server] [init] running on %i", port); + if (not pushServer.empty()){ #ifdef OPENDHT_PUSH_NOTIFICATIONS - std::cout << "Using push notification server: " << pushServer << std::endl; + if (logger_) + logger_->d("[proxy:server] [init] using push server %s", pushServer.c_str()); #else - std::cerr << "Push server defined but built OpenDHT built without push notification support" << std::endl; + if (logger_) + logger_->e("[proxy:server] [init] opendht built without push notification support"); #endif } jsonBuilder_["commentStyle"] = "None"; jsonBuilder_["indentation"] = ""; - 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)); - resource->set_method_handler("STATS", std::bind(&DhtProxyServer::getStats, 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", [this](const Sp<restbed::Session>& session) mutable { listen(session); } ); -#ifdef OPENDHT_PUSH_NOTIFICATIONS - resource->set_method_handler("SUBSCRIBE", [this](const Sp<restbed::Session>& session) mutable { subscribe(session); } ); - resource->set_method_handler("UNSUBSCRIBE", [this](const Sp<restbed::Session>& session) mutable { unsubscribe(session); } ); -#endif //OPENDHT_PUSH_NOTIFICATIONS - resource->set_method_handler("POST", [this](const Sp<restbed::Session>& session) mutable { put(session); }); -#ifdef 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"); - settings->set_default_header("Access-Control-Allow-Origin", "*"); - 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); - auto maxThreads = std::thread::hardware_concurrency() - 1; - settings->set_worker_limit(maxThreads > 1 ? maxThreads : 1); - lastStatsReset_ = clock::now(); + // build http server + auto settings = makeHttpServerSettings(); + settings.port(port); + httpServer_.reset(new restinio::http_server_t<RestRouterTraits>( + restinio::own_io_context(), + std::forward<ServerSettings>(settings) + )); + // build http client + auto pushHostPort = splitPort(pushServer_); + uint16_t pushPort = std::atoi(pushHostPort.second.c_str()); + httpClient_.reset(new http::Client(httpServer_->io_context(), + pushHostPort.first, pushPort, logger_)); + // run http server + httpServerThread_ = std::thread([this](){ 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]() { - while (not service_->is_up() and not stopListeners) { - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - while (service_->is_up() and not stopListeners) { - removeClosedListeners(); - std::this_thread::sleep_for(std::chrono::seconds(1)); + httpServer_->open_async([]{/*Ok.*/}, [](std::exception_ptr ex){ + std::rethrow_exception(ex); + }); + httpServer_->io_context().run(); } - // Remove last listeners - removeClosedListeners(false); - }); - schedulerThread_ = std::thread([this]() { - while (not service_->is_up() and not stopListeners) { - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - while (service_->is_up() and not stopListeners) { - std::unique_lock<std::mutex> lock(schedulerLock_); - auto next = scheduler_.run(); - if (next == time_point::max()) - schedulerCv_.wait(lock); - else - schedulerCv_.wait_until(lock, next); + catch(const std::exception &ex){ + if (logger_) + logger_->e("[proxy:server] error starting: %s", ex.what()); } }); dht->forwardAllMessages(true); - printStatsJob_ = scheduler_.add(scheduler_.time() + PRINT_STATS_PERIOD, [this] { - if (stopListeners) return; - if (service_->is_up()) - updateStats(); - // Refresh stats cache - auto newInfo = dht_->getNodeInfo(); - { - std::lock_guard<std::mutex> lck(statsMutex_); - nodeInfo_ = std::move(newInfo); - } - scheduler_.edit(printStatsJob_, scheduler_.time() + PRINT_STATS_PERIOD); - }); + + printStatsTimer_ = std::make_unique<asio::steady_timer>( + httpServer_->io_context(), PRINT_STATS_PERIOD); + printStatsTimer_->async_wait(std::bind(&DhtProxyServer::asyncPrintStats, this)); } DhtProxyServer::~DhtProxyServer() @@ -159,30 +111,49 @@ DhtProxyServer::~DhtProxyServer() stop(); } +ServerSettings +DhtProxyServer::makeHttpServerSettings(const unsigned int max_pipelined_requests) +{ + using namespace std::chrono; + auto settings = ServerSettings(); + /** + * If max_pipelined_requests is greater than 1 then RESTinio will continue + * to read from the socket after parsing the first request. + * In that case, RESTinio can detect the disconnection + * and calls state listener as expected. + * https://github.com/Stiffstream/restinio/issues/28 + */ + settings.max_pipelined_requests(max_pipelined_requests); + // one less to detect the listener disconnect + settings.concurrent_accepts_count(max_pipelined_requests - 1); + settings.separate_accept_and_create_connect(true); + settings.logger(logger_); + settings.protocol(restinio::asio_ns::ip::tcp::v6()); + settings.request_handler(this->createRestRouter()); + // time limits // ~ 0.8 month + std::chrono::milliseconds timeout_request(std::numeric_limits<int>::max()); + settings.read_next_http_message_timelimit(timeout_request); + settings.write_http_response_timelimit(60s); + settings.handle_request_timeout(timeout_request); + // socket options + settings.socket_options_setter([](auto & options){ + options.set_option(asio::ip::tcp::no_delay{true}); + }); + settings.connection_state_listener(connListener_); + return settings; +} + void DhtProxyServer::stop() { - if (printStatsJob_) - printStatsJob_->cancel(); - service_->stop(); - { - std::lock_guard<std::mutex> lock(lockListener_); - auto listener = currentListeners_.begin(); - while (listener != currentListeners_.end()) { - listener->session->close(); - ++listener; - } - } - stopListeners = true; - schedulerCv_.notify_all(); - // listenThreads_ will stop because there is no more sessions - if (listenThread_.joinable()) - listenThread_.join(); - if (schedulerThread_.joinable()) - schedulerThread_.join(); - if (server_thread.joinable()) - server_thread.join(); - threadPool_->stop(); + if (logger_) + logger_->d("[proxy:server] closing http server async operations"); + httpServer_->io_context().reset(); + httpServer_->io_context().stop(); + if (httpServerThread_.joinable()) + httpServerThread_.join(); + if (logger_) + logger_->d("[proxy:server] http server closed"); } void @@ -197,362 +168,428 @@ DhtProxyServer::updateStats() const stats_.pushListenersCount = pushListeners_.size(); #endif stats_.putCount = puts_.size(); - stats_.listenCount = currentListeners_.size(); + stats_.listenCount = listeners_->size(); stats_.nodeInfo = nodeInfo_; } void -DhtProxyServer::getNodeInfo(const Sp<restbed::Session>& session) const +DhtProxyServer::asyncPrintStats() { - requestNum_++; - const auto request = session->get_request(); - int content_length = std::stoi(request->get_header("Content-Length", "0")); - session->fetch(content_length, - [this](const Sp<restbed::Session>& s, const restbed::Bytes& /*b*/) mutable - { - try { - if (dht_) { - Json::Value result; - { - std::lock_guard<std::mutex> lck(statsMutex_); - if (nodeInfo_.ipv4.good_nodes == 0 && nodeInfo_.ipv6.good_nodes == 0) { - // NOTE: we want to avoid the disconnected state as much as possible - // So, if the node is disconnected, we should force the update of the cache - // and reconnect as soon as possible - // This should not happen much - nodeInfo_ = dht_->getNodeInfo(); - } - result = nodeInfo_.toJson(); - } - result["public_ip"] = s->get_origin(); // [ipv6:ipv4]:port or ipv4:port - auto output = Json::writeString(jsonBuilder_, result) + "\n"; - s->close(restbed::OK, output); - } - else - s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } - } - ); + if (httpServer_->io_context().stopped()) + return; + + if (dht_){ + updateStats(); + // Refresh stats cache + auto newInfo = dht_->getNodeInfo(); + std::lock_guard<std::mutex> lck(statsMutex_); + nodeInfo_ = std::move(newInfo); + auto json = nodeInfo_.toJson(); + auto str = Json::writeString(jsonBuilder_, json); + if (logger_) + logger_->d("[proxy:server] [stats] %s", str.c_str()); + } + printStatsTimer_->expires_at(printStatsTimer_->expiry() + PRINT_STATS_PERIOD); + printStatsTimer_->async_wait(std::bind(&DhtProxyServer::asyncPrintStats, this)); } -void -DhtProxyServer::getStats(const Sp<restbed::Session>& session) const +template <typename HttpResponse> +HttpResponse DhtProxyServer::initHttpResponse(HttpResponse response) const +{ + response.append_header("Server", "RESTinio"); + response.append_header(restinio::http_field::content_type, "application/json"); + response.append_header(restinio::http_field::access_control_allow_origin, "*"); + response.connection_keep_alive(); + return response; +} + +std::unique_ptr<RestRouter> +DhtProxyServer::createRestRouter() +{ + using namespace std::placeholders; + auto router = std::make_unique<RestRouter>(); + router->http_get("/", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2)); + // LEGACY STATS ROUTE + router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_stats.raw_id()), + "/", std::bind(&DhtProxyServer::getStats, this, _1, _2)); + // } + router->http_get("/stats", std::bind(&DhtProxyServer::getStats, this, _1, _2)); + router->http_get("/:hash", std::bind(&DhtProxyServer::get, this, _1, _2)); + router->http_post("/:hash", std::bind(&DhtProxyServer::put, this, _1, _2)); + // LEGACY LISTEN ROUTE + router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_listen.raw_id()), + "/:hash", std::bind(&DhtProxyServer::listen, this, _1, _2)); + // } + router->http_get("/:hash/listen", std::bind(&DhtProxyServer::listen, this, _1, _2)); +#ifdef OPENDHT_PUSH_NOTIFICATIONS + router->add_handler(restinio::http_method_subscribe(), + "/:hash", std::bind(&DhtProxyServer::subscribe, this, _1, _2)); + router->add_handler(restinio::http_method_unsubscribe(), + "/:hash", std::bind(&DhtProxyServer::unsubscribe, this, _1, _2)); +#endif //OPENDHT_PUSH_NOTIFICATIONS +#ifdef OPENDHT_PROXY_SERVER_IDENTITY + // LEGACY SIGN ROUTE + router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_sign.raw_id()), + "/:hash", std::bind(&DhtProxyServer::putSigned, this, _1, _2)); + // } + router->http_post("/:hash/sign", std::bind(&DhtProxyServer::putSigned, this, _1, _2)); + // LEGACY ENCRYPT ROUTE + router->add_handler(restinio::custom_http_methods_t::from_nodejs(restinio::method_encrypt.raw_id()), + "/:hash", std::bind(&DhtProxyServer::putEncrypted, this, _1, _2)); + // } + router->http_post("/:hash/encrypt", std::bind(&DhtProxyServer::putEncrypted, this, _1, _2)); +#endif // OPENDHT_PROXY_SERVER_IDENTITY + router->add_handler(restinio::http_method_options(), + "/:hash", std::bind(&DhtProxyServer::options, this, _1, _2)); + router->http_get("/:hash/:value", std::bind(&DhtProxyServer::getFiltered, this, _1, _2)); + return router; +} + +RequestStatus +DhtProxyServer::getNodeInfo(restinio::request_handle_t request, + restinio::router::route_params_t params) const +{ + Json::Value result; + std::lock_guard<std::mutex> lck(statsMutex_); + if (nodeInfo_.ipv4.good_nodes == 0 && + nodeInfo_.ipv6.good_nodes == 0){ + nodeInfo_ = this->dht_->getNodeInfo(); + } + result = nodeInfo_.toJson(); + // [ipv6:ipv4]:port or ipv4:port + result["public_ip"] = request->remote_endpoint().address().to_string(); + auto output = Json::writeString(jsonBuilder_, result) + "\n"; + + auto response = this->initHttpResponse(request->create_response()); + response.append_body(output); + return response.done(); +} + +RequestStatus +DhtProxyServer::getStats(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - const auto request = session->get_request(); - int content_length = std::stoi(request->get_header("Content-Length", "0")); - session->fetch(content_length, - [this](const Sp<restbed::Session>& s, const restbed::Bytes& /*b*/) mutable - { - try { - if (dht_) { + try { + if (dht_){ #ifdef OPENDHT_JSONCPP - auto output = Json::writeString(jsonBuilder_, stats_.toJson()) + "\n"; - s->close(restbed::OK, output); + auto output = Json::writeString(jsonBuilder_, stats_.toJson()) + "\n"; + auto response = this->initHttpResponse(request->create_response()); + response.append_body(output); + response.done(); #else - s->close(restbed::NotFound, "{\"err\":\"JSON not enabled on this instance\"}"); + auto response = this->initHttpResponse( + request->create_response(restinio::status_not_found())); + response.set_body(RESP_MSG_JSON_NOT_ENABLED); + return response.done(); #endif - } - else - s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } + } else { + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); } - ); + } catch (...){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } -void -DhtProxyServer::get(const Sp<restbed::Session>& session) const +RequestStatus +DhtProxyServer::get(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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 Sp<restbed::Session>& s, const restbed::Bytes& /*b* */) - { - try { - if (dht_) { - InfoHash infoHash(hash); - if (!infoHash) { - infoHash = InfoHash::get(hash); - } - s->yield(restbed::OK, "", [=](const Sp<restbed::Session>&) {}); - dht_->get(infoHash, [this,s](const Sp<Value>& value) { - if (s->is_closed()) return false; - // Send values as soon as we get them - auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; - s->yield(output, [](const Sp<restbed::Session>& /*session*/){ }); - return true; - }, [s](bool /*ok* */) { - // Communication is finished - if (not s->is_closed()) { - s->close(); - } - }); - } else { - s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); - } - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } - } - ); + dht::InfoHash infoHash(params["hash"].to_string()); + if (!infoHash) + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + + auto response = std::make_shared<ResponseByPartsBuilder>( + this->initHttpResponse(request->create_response<ResponseByParts>())); + response->flush(); + try { + dht_->get(infoHash, [this, response](const dht::Sp<dht::Value>& value){ + auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; + response->append_chunk(output); + response->flush(); + return true; + }, + [response] (bool /*ok*/){ + response->done(); + }); + } catch (const std::exception& e){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } -void -DhtProxyServer::listen(const Sp<restbed::Session>& session) +RequestStatus +DhtProxyServer::listen(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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); + dht::InfoHash infoHash(params["hash"].to_string()); if (!infoHash) - infoHash = InfoHash::get(hash); - session->fetch(content_length, - [=](const Sp<restbed::Session>& s, const restbed::Bytes& /*b* */) - { - try { - 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; - // cache the session to avoid an incrementation of the shared_ptr's counter - // else, the session->close() will not close the socket. - auto cacheSession = std::weak_ptr<restbed::Session>(s); - listener.token = dht_->listen(infoHash, [this,cacheSession](const std::vector<Sp<Value>>& values, bool expired) { - auto s = cacheSession.lock(); - if (!s) return false; - // Send values as soon as we get them - if (!s->is_closed()) { - for (const auto& value : values) { - auto val = value->toJson(); - if (expired) - val["expired"] = true; - auto output = Json::writeString(jsonBuilder_, val) + "\n"; - s->yield(output, [](const Sp<restbed::Session>&){ }); - } - } - return !s->is_closed(); - }); - { - std::lock_guard<std::mutex> lock(lockListener_); - currentListeners_.emplace_back(std::move(listener)); - } - } else { - session->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); - } - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + auto response = std::make_shared<ResponseByPartsBuilder>( + this->initHttpResponse(request->create_response<ResponseByParts>())); + response->flush(); + try { + std::lock_guard<std::mutex> lock(*lockListener_); + // save the listener to handle a disconnect + auto &session = (*listeners_)[request->connection_id()]; + session.hash = infoHash; + session.response = response; + session.token = dht_->listen(infoHash, [this, response] + (const std::vector<dht::Sp<dht::Value>>& values, bool expired){ + for (const auto& value: values){ + auto jsonVal = value->toJson(); + if (expired) + jsonVal["expired"] = true; + auto output = Json::writeString(jsonBuilder_, jsonVal) + "\n"; + response->append_chunk(output); + response->flush(); } - } - ); + return true; + }); + + } catch (const std::exception& e){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } #ifdef OPENDHT_PUSH_NOTIFICATIONS -struct DhtProxyServer::Listener { - std::string clientId; - std::future<size_t> internalToken; - Sp<Scheduler::Job> expireJob; - Sp<Scheduler::Job> expireNotifyJob; -}; -struct DhtProxyServer::PushListener { - std::map<InfoHash, std::vector<Listener>> listeners; - bool isAndroid; -}; - -void -DhtProxyServer::subscribe(const std::shared_ptr<restbed::Session>& session) +RequestStatus +DhtProxyServer::subscribe(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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); + + dht::InfoHash infoHash(params["hash"].to_string()); if (!infoHash) - infoHash = InfoHash::get(hash); - session->fetch(content_length, - [=](const std::shared_ptr<restbed::Session> s, const restbed::Bytes& b) mutable - { - try { - std::string err; - Json::Value root; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(b.data()); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (!reader->parse(char_data, char_data + b.size(), &root, &err)) { - s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}"); - return; - } - auto pushToken = root["key"].asString(); - if (pushToken.empty()) { - s->close(restbed::BAD_REQUEST, "{\"err\":\"No token\"}"); - return; + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + try { + std::string err; + Json::Value root; + Json::CharReaderBuilder rbuilder; + auto* char_data = reinterpret_cast<const char*>(request->body().data()); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (!reader->parse(char_data, char_data + request->body().size(), &root, &err)){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_JSON_INCORRECT); + return response.done(); + } + auto pushToken = root["key"].asString(); + if (pushToken.empty()){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_NO_TOKEN); + return response.done(); + } + auto platform = root["platform"].asString(); + auto isAndroid = platform == "android"; + auto clientId = root.isMember("client_id") ? root["client_id"].asString() : std::string(); + + if (logger_) + logger_->d("[proxy:server] [subscribe %s] [client %s]", infoHash.toString().c_str(), clientId.c_str()); + // ================ Search for existing listener =================== + // start the timer + auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT; + std::lock_guard<std::mutex> lock(lockPushListeners_); + + // Insert new or return existing push listeners of a token + auto pushListener = pushListeners_.emplace(pushToken, PushListener{}).first; + auto pushListeners = pushListener->second.listeners.emplace(infoHash, std::vector<Listener>{}).first; + + for (auto &listener: pushListeners->second){ + if (logger_) + logger_->d("[proxy:server] [subscribe] found [client %s]", listener.clientId.c_str()); + // Found -> Resubscribe + if (listener.clientId == clientId){ + // Reset timers + listener.expireTimer->expires_at(timeout); + listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN); + // Send response header + auto response = std::make_shared<ResponseByPartsBuilder>( + this->initHttpResponse(request->create_response<ResponseByParts>())); + response->flush(); + // No Refresh + if (!root.isMember("refresh") or !root["refresh"].asBool()){ + dht_->get(infoHash, [this, response](const dht::Sp<dht::Value>& value){ + auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; + response->append_chunk(output); + response->flush(); + return true; + }, + [response] (bool){ + response->done(); + }); + // Refresh + } else { + response->append_chunk("{}\n"); + response->done(); } - auto platform = root["platform"].asString(); - auto isAndroid = platform == "android"; - auto clientId = root.isMember("client_id") ? root["client_id"].asString() : std::string(); - - std::cout << "Subscribe " << infoHash << " client:" << clientId << std::endl; - - { - std::lock(schedulerLock_, lockListener_); - std::lock_guard<std::mutex> lk1(lockListener_, std::adopt_lock); - std::lock_guard<std::mutex> lk2(schedulerLock_, std::adopt_lock); - scheduler_.syncTime(); - auto timeout = scheduler_.time() + proxy::OP_TIMEOUT; - // Check if listener is already present and refresh timeout if launched - // One push listener per pushToken.infoHash.clientId - auto pushListener = pushListeners_.emplace(pushToken, PushListener{}).first; - auto listeners = pushListener->second.listeners.emplace(infoHash, std::vector<Listener>{}).first; - for (auto& listener: listeners->second) { - if (listener.clientId == clientId) { - scheduler_.edit(listener.expireJob, timeout); - scheduler_.edit(listener.expireNotifyJob, timeout - proxy::OP_MARGIN); - s->yield(restbed::OK); - - if (!root.isMember("refresh") or !root["refresh"].asBool()) { - dht_->get( - infoHash, - [this, s](const Sp<Value> &value) { - if (s->is_closed()) - return false; - // Send values as soon as we get them - auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; - s->yield(output, [](const Sp<restbed::Session> - & /*session*/) {}); - return true; - }, - [s](bool /*ok* */) { - // Communication is finished - if (not s->is_closed()) { - s->close("{}\n"); - } - }); - } else { - // Communication is finished - if (not s->is_closed()) { - s->close("{}\n"); - } - } - schedulerCv_.notify_one(); - return; - } + return restinio::request_handling_status_t::accepted; + } + } + // =========== No existing listener for an infoHash ============ + // Add new listener to list of listeners + pushListeners->second.emplace_back(Listener{}); + auto &listener = pushListeners->second.back(); + listener.clientId = clientId; + + // Add listen on dht + listener.internalToken = dht_->listen(infoHash, + [this, infoHash, pushToken, isAndroid, clientId] + (const std::vector<std::shared_ptr<Value>>& values, bool expired){ + // Build message content + Json::Value json; + json["key"] = infoHash.toString(); + json["to"] = clientId; + if (expired and values.size() < 2){ + std::stringstream ss; + for(size_t i = 0; i < values.size(); ++i){ + if(i != 0) ss << ","; + ss << values[i]->id; } - listeners->second.emplace_back(Listener{}); - auto& listener = listeners->second.back(); - listener.clientId = clientId; - - // New listener - pushListener->second.isAndroid = isAndroid; - - // The listener is not found, so add it. - listener.internalToken = dht_->listen(infoHash, - [this, infoHash, pushToken, isAndroid, clientId](const std::vector<std::shared_ptr<Value>>& values, bool expired) { - threadPool_->run([this, infoHash, pushToken, isAndroid, clientId, values, expired]() { - // Build message content - Json::Value json; - json["key"] = infoHash.toString(); - json["to"] = clientId; - if (expired and values.size() < 3) { - std::stringstream ss; - for(size_t i = 0; i < values.size(); ++i) { - if(i != 0) ss << ","; - ss << values[i]->id; - } - json["exp"] = ss.str(); - } - sendPushNotification(pushToken, std::move(json), isAndroid); - }); - return true; - } - ); - listener.expireJob = scheduler_.add(timeout, - [this, clientId, infoHash, pushToken] { - cancelPushListen(pushToken, infoHash, clientId); - } - ); - listener.expireNotifyJob = scheduler_.add(timeout - proxy::OP_MARGIN, - [this, infoHash, pushToken, isAndroid, clientId] { - std::cout << "Listener: sending refresh " << infoHash << std::endl; - Json::Value json; - json["timeout"] = infoHash.toString(); - json["to"] = clientId; - sendPushNotification(pushToken, std::move(json), isAndroid); - } - ); + json["exp"] = ss.str(); } - schedulerCv_.notify_one(); - s->close(restbed::OK, "{}\n"); - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); + sendPushNotification(pushToken, std::move(json), isAndroid); + return true; } - } - ); + ); + // Init & set timers + auto &ctx = httpServer_->io_context(); + listener.expireTimer = std::make_unique<asio::steady_timer>(ctx, timeout); + listener.expireNotifyTimer = std::make_unique<asio::steady_timer>(ctx, + timeout - proxy::OP_MARGIN); + // Launch timers + listener.expireTimer->async_wait(std::bind( + &DhtProxyServer::cancelPushListen, this, pushToken, infoHash, clientId)); + + listener.expireNotifyTimer->async_wait( + [this, infoHash, pushToken, isAndroid, clientId](const asio::error_code &ec){ + if (logger_) + logger_->d("[proxy:server] [subscribe] sending refresh %s", infoHash.toString().c_str()); + if (ec){ + if (logger_) + logger_->d("[proxy:server] [subscribe] error sending refresh: %s", ec.message().c_str()); + } + Json::Value json; + json["timeout"] = infoHash.toString(); + json["to"] = clientId; + sendPushNotification(pushToken, std::move(json), isAndroid); + }); + auto response = this->initHttpResponse(request->create_response()); + response.set_body("{}\n"); + return response.done(); + } + catch (...) { + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } -void -DhtProxyServer::unsubscribe(const std::shared_ptr<restbed::Session>& session) +RequestStatus +DhtProxyServer::unsubscribe(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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); + + dht::InfoHash infoHash(params["hash"].to_string()); if (!infoHash) - infoHash = InfoHash::get(hash); - session->fetch(content_length, - [=](const std::shared_ptr<restbed::Session> s, const restbed::Bytes& b) - { - try { - std::string err; - Json::Value root; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(b.data()); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (!reader->parse(char_data, char_data + b.size(), &root, &err)) { - s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}"); - return; - } - auto pushToken = root["key"].asString(); - if (pushToken.empty()) return; - auto clientId = root["client_id"].asString(); - - cancelPushListen(pushToken, infoHash, clientId); - s->close(restbed::OK); - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + + try { + std::string err; + Json::Value root; + Json::CharReaderBuilder rbuilder; + auto* char_data = reinterpret_cast<const char*>(request->body().data()); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + + if (!reader->parse(char_data, char_data + request->body().size(), &root, &err)){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_JSON_INCORRECT); + return response.done(); } - ); + auto pushToken = root["key"].asString(); + if (pushToken.empty()) + return restinio::request_handling_status_t::rejected; + auto clientId = root["client_id"].asString(); + + cancelPushListen(pushToken, infoHash, clientId); + auto response = this->initHttpResponse(request->create_response()); + return response.done(); + } + catch (...) { + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } } void DhtProxyServer::cancelPushListen(const std::string& pushToken, const dht::InfoHash& key, const std::string& clientId) { - std::cout << "cancelPushListen: " << key << " clientId:" << clientId << std::endl; - std::lock_guard<std::mutex> lock(lockListener_); + if (logger_) + logger_->d("[proxy:server] [listen:push %s] cancelled for %s", + key.toString().c_str(), clientId.c_str()); + std::lock_guard<std::mutex> lock(*lockListener_); + auto pushListener = pushListeners_.find(pushToken); if (pushListener == pushListeners_.end()) return; auto listeners = pushListener->second.listeners.find(key); if (listeners == pushListener->second.listeners.end()) return; - for (auto listener = listeners->second.begin(); listener != listeners->second.end();) { - if (listener->clientId == clientId) { + + for (auto listener = listeners->second.begin(); listener != listeners->second.end();){ + if (listener->clientId == clientId){ if (dht_) dht_->cancelListen(key, std::move(listener->internalToken)); listener = listeners->second.erase(listener); @@ -560,12 +597,10 @@ DhtProxyServer::cancelPushListen(const std::string& pushToken, const dht::InfoHa ++listener; } } - if (listeners->second.empty()) { + if (listeners->second.empty()) pushListener->second.listeners.erase(listeners); - } - if (pushListener->second.listeners.empty()) { + if (pushListener->second.listeners.empty()) pushListeners_.erase(pushListener); - } } void @@ -573,9 +608,16 @@ DhtProxyServer::sendPushNotification(const std::string& token, Json::Value&& jso { if (pushServer_.empty()) return; - restbed::Uri uri(proxy::HTTP_PROTO + pushServer_ + "/api/push"); - auto req = std::make_shared<restbed::Request>(uri); - req->set_method("POST"); + + restinio::http_request_header_t header; + header.request_target("/api/push"); + header.method(restinio::http_method_post()); + + restinio::http_header_fields_t header_fields; + header_fields.append_field(restinio::http_field_t::host, pushServer_.c_str()); + header_fields.append_field(restinio::http_field_t::user_agent, "RESTinio client"); + header_fields.append_field(restinio::http_field_t::accept, "*/*"); + header_fields.append_field(restinio::http_field_t::content_type, "application/json"); // NOTE: see https://github.com/appleboy/gorush Json::Value notification(Json::objectValue); @@ -596,16 +638,32 @@ DhtProxyServer::sendPushNotification(const std::string& token, Json::Value&& jso Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; - auto valueStr = Json::writeString(wbuilder, content); - - req->set_header("Content-Type", "application/json"); - req->set_header("Accept", "*/*"); - req->set_header("Host", pushServer_); - req->set_header("Content-Length", std::to_string(valueStr.length())); - req->set_body(valueStr); - - // Send request. - restbed::Http::async(req, {}); + auto body = Json::writeString(wbuilder, content); + + auto parser = std::make_shared<http_parser>(); + http_parser_init(parser.get(), HTTP_RESPONSE); + + struct PushContext { + std::shared_ptr<Logger> logger; + }; + auto context = std::make_shared<PushContext>(); + if (logger_) + context->logger = logger_; + parser->data = static_cast<void*>(context.get()); + + auto parser_s = std::make_shared<http_parser_settings>(); + http_parser_settings_init(parser_s.get()); + parser_s->on_status = []( http_parser * parser, const char * at, size_t length ) -> int { + auto context = static_cast<PushContext*>(parser->data); + if (parser->status_code == 200) + return 0; + if (context->logger) + context->logger->e("[proxy:server] [notification] error send push: %i", parser->status_code); + return 1; + }; + auto request = httpClient_->create_request(header, header_fields, + restinio::http_connection_header_t::close, body); + httpClient_->post_request(request, parser, parser_s); } #endif //OPENDHT_PUSH_NOTIFICATIONS @@ -613,7 +671,8 @@ DhtProxyServer::sendPushNotification(const std::string& token, Json::Value&& jso void DhtProxyServer::cancelPut(const InfoHash& key, Value::Id vid) { - std::cout << "cancelPut " << key << " " << vid << std::endl; + if (logger_) + logger_->d("[proxy:server] [put %s] cancel put %i", key.toString().c_str(), vid); auto sPuts = puts_.find(key); if (sPuts == puts_.end()) return; @@ -623,288 +682,323 @@ DhtProxyServer::cancelPut(const InfoHash& key, Value::Id vid) return; if (dht_) dht_->cancelPut(key, vid); - if (put->second.expireNotifyJob) - put->second.expireNotifyJob->cancel(); + if (put->second.expireNotifyTimer) + put->second.expireNotifyTimer->cancel(); sPutsMap.erase(put); if (sPutsMap.empty()) puts_.erase(sPuts); } -void -DhtProxyServer::put(const std::shared_ptr<restbed::Session>& session) +RequestStatus +DhtProxyServer::put(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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); + dht::InfoHash infoHash(params["hash"].to_string()); if (!infoHash) - infoHash = InfoHash::get(hash); - - session->fetch(content_length, - [=](const std::shared_ptr<restbed::Session> s, const restbed::Bytes& b) - { - try { - if (dht_) { - if(b.empty()) { - std::string response("{\"err\":\"Missing parameters\"}"); - s->close(restbed::BAD_REQUEST, response); - } else { - std::string err; - Json::Value root; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(b.data()); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (reader->parse(char_data, char_data + b.size(), &root, &err)) { - // Build the Value from json - auto value = std::make_shared<Value>(root); - bool permanent = root.isMember("permanent"); - std::cout << "Got put " << infoHash << " " << *value << " " << (permanent ? "permanent" : "") << std::endl; - - if (permanent) { - std::string pushToken, clientId, platform; - auto& pVal = root["permanent"]; - if (pVal.isObject()) { - pushToken = pVal["key"].asString(); - clientId = pVal["client_id"].asString(); - platform = pVal["platform"].asString(); - } - std::unique_lock<std::mutex> lock(schedulerLock_); - scheduler_.syncTime(); - auto timeout = scheduler_.time() + proxy::OP_TIMEOUT; - auto vid = value->id; - auto sPuts = puts_.emplace(infoHash, SearchPuts{}).first; - auto r = sPuts->second.puts.emplace(vid, PermanentPut{}); - auto& pput = r.first->second; - if (r.second) { - pput.expireJob = scheduler_.add(timeout, [this, infoHash, vid]{ - std::cout << "Permanent put expired: " << infoHash << " " << vid << std::endl; - cancelPut(infoHash, vid); - }); -#ifdef OPENDHT_PUSH_NOTIFICATIONS - if (not pushToken.empty()) { - bool isAndroid = platform == "android"; - pput.expireNotifyJob = scheduler_.add(timeout - proxy::OP_MARGIN, - [this, infoHash, vid, pushToken, clientId, isAndroid] - { - std::cout << "Permanent put refresh: " << infoHash << " " << vid << std::endl; - Json::Value json; - json["timeout"] = infoHash.toString(); - json["to"] = clientId; - json["vid"] = std::to_string(vid); - sendPushNotification(pushToken, std::move(json), isAndroid); - }); - } -#endif - } else { - scheduler_.edit(pput.expireJob, timeout); - if (pput.expireNotifyJob) - scheduler_.edit(pput.expireNotifyJob, timeout - proxy::OP_MARGIN); - } - lock.unlock(); - schedulerCv_.notify_one(); - } + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + else if (request->body().empty()){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_MISSING_PARAMS); + return response.done(); + } - dht_->put(infoHash, value, [s, value](bool ok) { - if (ok) { - Json::StreamWriterBuilder wbuilder; - wbuilder["commentStyle"] = "None"; - wbuilder["indentation"] = ""; - if (s->is_open()) - s->close(restbed::OK, Json::writeString(wbuilder, value->toJson()) + "\n"); - } else { - if (s->is_open()) - s->close(restbed::BAD_GATEWAY, "{\"err\":\"put failed\"}"); - } - }, time_point::max(), permanent); - } else { - s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}"); + try { + std::string err; + Json::Value root; + Json::CharReaderBuilder rbuilder; + auto* char_data = reinterpret_cast<const char*>(request->body().data()); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + + if (reader->parse(char_data, char_data + request->body().size(), &root, &err)){ + auto value = std::make_shared<dht::Value>(root); + bool permanent = root.isMember("permanent"); + if (logger_) + logger_->d("[proxy:server] [put %s] %s %s", infoHash.toString().c_str(), + value->toString().c_str(), (permanent ? "permanent" : "")); + if (permanent){ + std::string pushToken, clientId, platform; + auto& pVal = root["permanent"]; + if (pVal.isObject()){ + pushToken = pVal["key"].asString(); + clientId = pVal["client_id"].asString(); + platform = pVal["platform"].asString(); + } + std::unique_lock<std::mutex> lock(lockSearchPuts_); + auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT; + auto vid = value->id; + auto sPuts = puts_.emplace(infoHash, SearchPuts{}).first; + auto r = sPuts->second.puts.emplace(vid, PermanentPut{}); + auto& pput = r.first->second; + if (r.second){ + auto &ctx = httpServer_->io_context(); + pput.expireTimer = std::make_unique<asio::steady_timer>(ctx, timeout); + pput.expireTimer->async_wait([this, infoHash, vid](const asio::error_code &ec){ + if (logger_) + logger_->d("[proxy:server] [put %s] permanent expired: %i", infoHash.toString().c_str(), vid); + if (ec){ + if (logger_) + logger_->e("[proxy:server] error in permanent put: %s", ec.message().c_str()); } + cancelPut(infoHash, vid); + }); +#ifdef OPENDHT_PUSH_NOTIFICATIONS + if (not pushToken.empty()){ + bool isAndroid = platform == "android"; + pput.expireNotifyTimer = std::make_unique<asio::steady_timer>(ctx, + timeout - proxy::OP_MARGIN); + pput.expireNotifyTimer->async_wait( + [this, infoHash, vid, pushToken, clientId, isAndroid](const asio::error_code &ec) + { + if (logger_) + logger_->d("[proxy:server] [put %s] refresh: %i", infoHash.toString().c_str(), vid); + if (ec){ + if (logger_) + logger_->e("[proxy:server] error in refresh put: %s", ec.message().c_str()); + } + Json::Value json; + json["timeout"] = infoHash.toString(); + json["to"] = clientId; + json["vid"] = std::to_string(vid); + sendPushNotification(pushToken, std::move(json), isAndroid); + }); } +#endif } else { - s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); + pput.expireTimer->expires_at(timeout); + if (pput.expireNotifyTimer) + pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN); } - } catch (const std::exception& e) { - std::cout << "Error performing put: " << e.what() << std::endl; - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); + lock.unlock(); } + dht_->put(infoHash, value, [this, request, value](bool ok){ + if (ok){ + auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; + auto response = this->initHttpResponse(request->create_response()); + response.append_body(output); + response.done(); + } else { + auto response = this->initHttpResponse(request->create_response( + restinio::status_bad_gateway())); + response.set_body(RESP_MSG_PUT_FAILED); + response.done(); + } + }, dht::time_point::max(), permanent); + } else { + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_JSON_INCORRECT); + return response.done(); } - ); + } catch (const std::exception& e){ + if (logger_) + logger_->d("[proxy:server] error in put: %s", e.what()); + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } #ifdef OPENDHT_PROXY_SERVER_IDENTITY -void -DhtProxyServer::putSigned(const std::shared_ptr<restbed::Session>& session) const + +RequestStatus DhtProxyServer::putSigned(restinio::request_handle_t request, + restinio::router::route_params_t params) const { requestNum_++; - 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); + dht::InfoHash infoHash(params["hash"].to_string()); if (!infoHash) - infoHash = InfoHash::get(hash); - - session->fetch(content_length, - [=](const std::shared_ptr<restbed::Session> s, const restbed::Bytes& b) - { - try { - if (dht_) { - if(b.empty()) { - std::string response("{\"err\":\"Missing parameters\"}"); - s->close(restbed::BAD_REQUEST, response); - } else { - std::string err; - Json::Value root; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(b.data()); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (reader->parse(char_data, char_data + b.size(), &root, &err)) { - auto value = std::make_shared<Value>(root); - - Json::StreamWriterBuilder wbuilder; - wbuilder["commentStyle"] = "None"; - wbuilder["indentation"] = ""; - auto output = Json::writeString(wbuilder, value->toJson()) + "\n"; - dht_->putSigned(infoHash, value); - s->close(restbed::OK, output); - } else { - s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}"); - } - } + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + else if (request->body().empty()){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_MISSING_PARAMS); + return response.done(); + } + + try { + std::string err; + Json::Value root; + Json::CharReaderBuilder rbuilder; + auto* char_data = reinterpret_cast<const char*>(request->body().data()); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + + if (reader->parse(char_data, char_data + request->body().size(), &root, &err)){ + + auto value = std::make_shared<Value>(root); + + dht_->putSigned(infoHash, value, [this, request, value](bool ok){ + if (ok){ + auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; + auto response = this->initHttpResponse(request->create_response()); + response.append_body(output); + response.done(); } else { - s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); + auto response = this->initHttpResponse(request->create_response( + restinio::status_bad_gateway())); + response.set_body(RESP_MSG_PUT_FAILED); + response.done(); } - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } + }); + } else { + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_JSON_INCORRECT); + return response.done(); } - ); + } catch (const std::exception& e){ + if (logger_) + logger_->d("[proxy:server] error in put: %s", e.what()); + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } -void -DhtProxyServer::putEncrypted(const std::shared_ptr<restbed::Session>& session) const +RequestStatus +DhtProxyServer::putEncrypted(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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) - { - try { - if (dht_) { - if(b.empty()) { - std::string response("{\"err\":\"Missing parameters\"}"); - s->close(restbed::BAD_REQUEST, response); - } else { - std::string err; - Json::Value root; - Json::CharReaderBuilder rbuilder; - auto* char_data = reinterpret_cast<const char*>(b.data()); - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - bool parsingSuccessful = reader->parse(char_data, char_data + b.size(), &root, &err); - InfoHash to(root["to"].asString()); - if (parsingSuccessful && to) { - auto value = std::make_shared<Value>(root); - Json::StreamWriterBuilder wbuilder; - wbuilder["commentStyle"] = "None"; - wbuilder["indentation"] = ""; - auto output = Json::writeString(wbuilder, value->toJson()) + "\n"; - dht_->putEncrypted(key, to, value); - s->close(restbed::OK, output); - } else { - if(!parsingSuccessful) - s->close(restbed::BAD_REQUEST, "{\"err\":\"Incorrect JSON\"}"); - else - s->close(restbed::BAD_REQUEST, "{\"err\":\"No destination found\"}"); - } - } + dht::InfoHash infoHash(params["hash"].to_string()); + if (!infoHash) + infoHash = dht::InfoHash::get(params["hash"].to_string()); + + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + else if (request->body().empty()){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_MISSING_PARAMS); + return response.done(); + } + + try { + std::string err; + Json::Value root; + Json::CharReaderBuilder rbuilder; + auto* char_data = reinterpret_cast<const char*>(request->body().data()); + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + + if (reader->parse(char_data, char_data + request->body().size(), &root, &err)){ + InfoHash to(root["to"].asString()); + if (!to){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_DESTINATION_NOT_FOUND); + return response.done(); + } + auto value = std::make_shared<Value>(root); + dht_->putEncrypted(infoHash, to, value, [this, request, value](bool ok){ + if (ok){ + auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; + auto response = this->initHttpResponse(request->create_response()); + response.append_body(output); + response.done(); } else { - s->close(restbed::SERVICE_UNAVAILABLE, "{\"err\":\"Incorrect DhtRunner\"}"); + auto response = this->initHttpResponse(request->create_response( + restinio::status_bad_gateway())); + response.set_body(RESP_MSG_PUT_FAILED); + response.done(); } - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } + }); + } else { + auto response = this->initHttpResponse( + request->create_response(restinio::status_bad_request())); + response.set_body(RESP_MSG_JSON_INCORRECT); + return response.done(); } - ); + } catch (const std::exception& e){ + if (logger_) + logger_->d("[proxy:server] error in put: %s", e.what()); + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); + } + return restinio::request_handling_status_t::accepted; } + #endif // OPENDHT_PROXY_SERVER_IDENTITY -void -DhtProxyServer::handleOptionsMethod(const std::shared_ptr<restbed::Session>& session) const +RequestStatus +DhtProxyServer::options(restinio::request_handle_t request, + restinio::router::route_params_t params) { - requestNum_++; + this->requestNum_++; #ifdef OPENDHT_PROXY_SERVER_IDENTITY - const auto allowed = "OPTIONS, GET, POST, LISTEN, SIGN, ENCRYPT"; + const auto methods = "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"}}); + const auto methods = "OPTIONS, GET, POST, LISTEN"; +#endif + auto response = initHttpResponse(request->create_response()); + response.append_header(restinio::http_field::access_control_allow_methods, methods); + response.append_header(restinio::http_field::access_control_allow_headers, "content-type"); + response.append_header(restinio::http_field::access_control_max_age, "86400"); + return response.done(); } -void -DhtProxyServer::getFiltered(const std::shared_ptr<restbed::Session>& session) const +RequestStatus +DhtProxyServer::getFiltered(restinio::request_handle_t request, + restinio::router::route_params_t params) { requestNum_++; - 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* */) - { - try { - 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::StreamWriterBuilder wbuilder; - wbuilder["commentStyle"] = "None"; - wbuilder["indentation"] = ""; - auto output = Json::writeString(wbuilder, v->toJson()) + "\n"; - s->yield(output, [](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\"}"); - } - } catch (...) { - s->close(restbed::INTERNAL_SERVER_ERROR, "{\"err\":\"Internal server error\"}"); - } - } - ); -} + auto value = params["value"].to_string(); + dht::InfoHash infoHash(params["hash"].to_string()); + if (!infoHash) + infoHash = dht::InfoHash::get(params["hash"].to_string()); -void -DhtProxyServer::removeClosedListeners(bool testSession) -{ - // clean useless listeners - std::lock_guard<std::mutex> lock(lockListener_); - auto listener = currentListeners_.begin(); - while (listener != currentListeners_.end()) { - auto cancel = dht_ and (not testSession or listener->session->is_closed()); - if (cancel) { - dht_->cancelListen(listener->hash, std::move(listener->token)); - // Remove listener if unused - listener = currentListeners_.erase(listener); - } else { - ++listener; - } + if (!dht_){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_service_unavailable())); + response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); + return response.done(); + } + + auto response = std::make_shared<ResponseByPartsBuilder>( + this->initHttpResponse(request->create_response<ResponseByParts>())); + response->flush(); + try { + dht_->get(infoHash, [this, response](const dht::Sp<dht::Value>& value){ + auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; + response->append_chunk(output); + response->flush(); + return true; + }, + [response] (bool /*ok*/){ + response->done(); + }, + {}, value + ); + } catch (const std::exception& e){ + auto response = this->initHttpResponse( + request->create_response(restinio::status_internal_server_error())); + response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); + return response.done(); } + return restinio::request_handling_status_t::accepted; } } diff --git a/src/dhtrunner.cpp b/src/dhtrunner.cpp index af782b3c536c4618c2e2289ef0b2d9e8599c8862..cc53237b84a26f6e7bc7c78b35f0e166e02ab4d2 100644 --- a/src/dhtrunner.cpp +++ b/src/dhtrunner.cpp @@ -114,6 +114,9 @@ DhtRunner::run(const Config& config, Context&& context) if (running) return; + if (context.logger) + logger_ = context.logger; + context.sock->setOnReceive([&] (std::unique_ptr<net::ReceivedPacket>&& pkt) { { std::lock_guard<std::mutex> lck(sock_mtx); @@ -969,7 +972,7 @@ DhtRunner::enableProxy(bool proxify) } cv.notify_all(); } - }, config_.proxy_server, config_.push_node_id) + }, config_.proxy_server, config_.push_node_id, logger_) ); dht_via_proxy_ = std::unique_ptr<SecureDht>(new SecureDht(std::move(dht_via_proxy), config_.dht_config)); #ifdef OPENDHT_PUSH_NOTIFICATIONS diff --git a/src/http.cpp b/src/http.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4b22f1e2452bec69bdd22cc6dd17cf67a7a851fd --- /dev/null +++ b/src/http.cpp @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2016-2019 Savoir-faire Linux Inc. + * Author: Vsevolod Ivanov <vsevolod.ivanov@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/>. + */ + +#include "http.h" + +namespace http { + +// connection + +Connection::Connection(const uint16_t id, asio::ip::tcp::socket socket): + id_(id), socket_(std::move(socket)) +{} + +Connection::~Connection(){ + close(); +} + +uint16_t +Connection::id(){ + return id_; +} + +bool +Connection::is_open(){ + return socket_.is_open(); +} + +void +Connection::close(){ + socket_.close(); +} + +// connection listener + +ConnectionListener::ConnectionListener() +{} + +ConnectionListener::ConnectionListener(std::shared_ptr<dht::DhtRunner> dht, + std::shared_ptr<std::map<restinio::connection_id_t, http::ListenerSession>> listeners, + std::shared_ptr<std::mutex> lock, std::shared_ptr<dht::Logger> logger): + dht_(dht), listeners_(listeners), lock_(lock), logger_(logger) +{} + +ConnectionListener::~ConnectionListener() +{} + +void +ConnectionListener::state_changed(const restinio::connection_state::notice_t ¬ice) noexcept +{ + std::lock_guard<std::mutex> lock(*lock_); + auto id = notice.connection_id(); + auto cause = to_str(notice.cause()); + + if (listeners_->find(id) != listeners_->end()){ + if (notice.cause() == restinio::connection_state::cause_t::closed){ + if (logger_) + logger_->d("[proxy:server] [connection:%li] cancelling listener", id); + dht_->cancelListen(listeners_->at(id).hash, + std::move(listeners_->at(id).token)); + listeners_->erase(id); + if (logger_) + logger_->d("[proxy:server] %li listeners are connected", listeners_->size()); + } + } +} + +std::string +ConnectionListener::to_str(restinio::connection_state::cause_t cause) noexcept +{ + std::string result; + switch(cause) + { + case restinio::connection_state::cause_t::accepted: + result = "accepted"; + break; + case restinio::connection_state::cause_t::closed: + result = "closed"; + break; + case restinio::connection_state::cause_t::upgraded_to_websocket: + result = "upgraded"; + break; + default: + result = "unknown"; + } + return result; +} + +// client + +Client::Client(asio::io_context &ctx, const std::string host, const uint16_t port, + std::shared_ptr<dht::Logger> logger): + resolver_(ctx), logger_(logger) +{ + set_query_address(host, port); +} + +asio::io_context& +Client::io_context() +{ + return resolver_.get_io_context(); +} + +void +Client::set_logger(std::shared_ptr<dht::Logger> logger) +{ + logger_ = logger; +} +void +Client::set_query_address(const std::string host, const uint16_t port) +{ + host_ = host; + port_ = port; +} + +std::shared_ptr<Connection> +Client::create_connection() +{ + auto conn = std::make_shared<Connection>(connId_, + std::move(asio::ip::tcp::socket{resolver_.get_io_context()})); + if (logger_) + logger_->d("[proxy:client] [connection:%i] created", conn->id()); + connId_++; + return conn; +} + +bool +Client::active_connection(const uint16_t conn_id) +{ + auto req = requests_.find(conn_id); + if (req == requests_.end()) + return false; + return req->second.connection->is_open(); +} + +void +Client::close_connection(const uint16_t conn_id) +{ + auto &req = requests_[conn_id]; + // close the socket + req.connection->close(); + // remove from active requests + requests_.erase(conn_id); + if (logger_) + logger_->d("[proxy:client] [connection:%i] closed", conn_id); +} + +std::string +Client::create_request(const restinio::http_request_header_t header, + const restinio::http_header_fields_t header_fields, + const restinio::http_connection_header_t connection, + const std::string body) +{ + std::stringstream request; + + // first header + request << header.method().c_str() << " " << header.request_target() << " " << + "HTTP/" << header.http_major() << "." << header.http_minor() << "\r\n"; + + // other headers + for (auto header_field: header_fields) + request << header_field.name() << ": " << header_field.value() << "\r\n"; + + // last connection header + std::string conn_str; + switch (connection){ + case restinio::http_connection_header_t::keep_alive: + conn_str = "keep-alive"; + break; + case restinio::http_connection_header_t::close: + conn_str = "close"; + break; + case restinio::http_connection_header_t::upgrade: + throw std::invalid_argument("upgrade"); + break; + } + request << "Connection: " << conn_str << "\r\n"; + + // body & content-length + if (!body.empty()){ + request << "Content-Length: " << body.size() << "\r\n\r\n"; + request << body; + } + + // last delim + request << "\r\n"; + return request.str(); +} + +uint16_t +Client::post_request(std::string request, + std::shared_ptr<http_parser> parser, + std::shared_ptr<http_parser_settings> parser_s) +{ + auto conn = create_connection(); + + // save the request context + Request req = {}; + req.connection = conn; + req.content = request; + req.parser = parser; + req.parser_settings = parser_s; + requests_[conn->id()] = req; + + // write the request to buffer + std::ostream request_stream(&conn->request_); + request_stream << request; + + // resolve the query to the server + asio::ip::tcp::resolver::query query(host_, std::to_string(port_)); + resolver_.async_resolve(query, + std::bind(&Client::handle_resolve, this, + std::placeholders::_1, std::placeholders::_2, conn)); + + return conn->id(); +} + +void +Client::handle_resolve(const asio::error_code &ec, + asio::ip::tcp::resolver::iterator endpoint_it, + std::shared_ptr<Connection> conn) +{ + if (ec){ + if (logger_) + logger_->e("[proxy:client] [connection:%i] error resolving", conn->id()); + conn->close(); + return; + } + if (logger_){ + logger_->d("[proxy:client] [connection:%i] resolved host=%s service=%s", conn->id(), + endpoint_it->host_name().c_str(), endpoint_it->service_name().c_str()); + } + asio::ip::tcp::endpoint endpoint = *endpoint_it; + conn->socket_.async_connect(endpoint, + std::bind(&Client::handle_connect, this, + std::placeholders::_1, ++endpoint_it, conn)); +} + +void +Client::handle_connect(const asio::error_code &ec, + asio::ip::tcp::resolver::iterator endpoint_it, + std::shared_ptr<Connection> conn) +{ + if (ec){ + if (logger_) + logger_->e("[proxy:client] [connection:%i] error opening: %s", + conn->id(), ec.message().c_str()); + close(conn->id()); + return; + } + else if (endpoint_it != asio::ip::tcp::resolver::iterator()){ + if (logger_) + logger_->e("[proxy:client] [connection:%i] error connecting, trying next endpoint", conn->id()); + conn->socket_.close(); + // connect to next one + asio::ip::tcp::endpoint endpoint = *endpoint_it; + conn->socket_.async_connect(endpoint, + std::bind(&Client::handle_connect, this, + std::placeholders::_1, ++endpoint_it, conn)); + return; + } + // send the request + asio::async_write(conn->socket_, conn->request_, + std::bind(&Client::handle_request, this, std::placeholders::_1, conn)); +} + +void +Client::handle_request(const asio::error_code &ec, std::shared_ptr<Connection> conn) +{ + if (!conn->is_open()) + return; + + if (ec and ec != asio::error::eof){ + if (logger_) + logger_->e("[proxy:client] [connection:%i] error handling request: %s", + conn->id(), ec.message().c_str()); + close_connection(conn->id()); + return; + } + if (logger_) + logger_->d("[proxy:client] [connection:%i] request write", conn->id()); + + // read response + asio::async_read_until(conn->socket_, conn->response_, "\r\n\r\n", + std::bind(&Client::handle_response, this, std::placeholders::_1, conn)); +} + +void +Client::handle_response(const asio::error_code &ec, std::shared_ptr<Connection> conn) +{ + if (!conn->is_open()) + return; + + if (ec && ec != asio::error::eof){ + if (logger_) + logger_->e("[proxy:client] [connection:%i] error handling response: %s", + conn->id(), ec.message().c_str()); + return; + } + else if ((ec == asio::error::eof) || (ec == asio::error::connection_reset)){ + close_connection(conn->id()); + return; + } + if (logger_) + logger_->d("[proxy:client] [connection:%i] response read", conn->id()); + + // read the response buffer + std::ostringstream str_s; + str_s << &conn->response_; + + // parse the request + auto &req = requests_[conn->id()]; + http_parser_execute(req.parser.get(), req.parser_settings.get(), + str_s.str().c_str(), str_s.str().size()); + + // detect parsing errors + if (HPE_OK != req.parser->http_errno && HPE_PAUSED != req.parser->http_errno){ + if (logger_){ + auto err = HTTP_PARSER_ERRNO(req.parser.get()); + logger_->e("[proxy:client] [connection:%i] error parsing: %s", + conn->id(), http_errno_name(err)); + } + } + asio::async_read(conn->socket_, conn->response_, asio::transfer_at_least(1), + std::bind(&Client::handle_response, this, std::placeholders::_1, conn)); +} + +} // namespace http diff --git a/tests/dhtproxytester.cpp b/tests/dhtproxytester.cpp index 77a462a2cd37ee7c9aaa13b20ca1b018790e08f6..e1f2dd7eeac6a3223064d07689e304f750a94417 100644 --- a/tests/dhtproxytester.cpp +++ b/tests/dhtproxytester.cpp @@ -2,6 +2,7 @@ * Copyright (C) 2019 Savoir-faire Linux Inc. * * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * Vsevolod Ivanov <vsevolod.ivanov@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 @@ -32,26 +33,32 @@ CPPUNIT_TEST_SUITE_REGISTRATION(DhtProxyTester); void DhtProxyTester::setUp() { - nodePeer.run(0); - nodeProxy = std::make_shared<dht::DhtRunner>(); - nodeClient = std::make_shared<dht::DhtRunner>(); + logger = dht::log::getStdLogger(); + + nodePeer.run(0, /*identity*/{}, /*threaded*/true); - nodeProxy->run(0); + nodeProxy = std::make_shared<dht::DhtRunner>(); + nodeProxy->run(0, /*identity*/{}, /*threaded*/true); nodeProxy->bootstrap(nodePeer.getBound()); - server = std::unique_ptr<dht::DhtProxyServer>(new dht::DhtProxyServer(nodeProxy, 8080)); + serverProxy = std::unique_ptr<dht::DhtProxyServer>(new dht::DhtProxyServer( + nodeProxy, 8080, /*pushServer*/"127.0.0.1:8090", logger)); - nodeClient->run(0); + nodeClient = std::make_shared<dht::DhtRunner>(); + nodeClient->run(0, /*identity*/{}, /*threaded*/true); nodeClient->bootstrap(nodePeer.getBound()); nodeClient->setProxyServer("127.0.0.1:8080"); - nodeClient->enableProxy(true); + nodeClient->enableProxy(true); // creates DhtProxyClient } void DhtProxyTester::tearDown() { + logger->d("[tester:proxy] stopping peer node"); nodePeer.join(); nodeClient->join(); - server->stop(); - server = nullptr; + logger->d("[tester:proxy] stopping proxy server"); + serverProxy->stop(); + serverProxy = nullptr; + logger->d("[tester:proxy] stopping proxy node"); nodeProxy->join(); } @@ -161,7 +168,7 @@ DhtProxyTester::testResubscribeGetValues() { // Reboot node (to avoid cache) nodeClient->join(); - nodeClient->run(42242, {}, true); + nodeClient->run(0, {}, true); nodeClient->bootstrap(nodePeer.getBound()); nodeClient->setProxyServer("127.0.0.1:8080"); nodeClient->enableProxy(true); diff --git a/tests/dhtproxytester.h b/tests/dhtproxytester.h index 067f70687d6e82514057fe9bcfea8230ae618bef..464ca0e9d7dd732278f6c8ccc4881164aae76928 100644 --- a/tests/dhtproxytester.h +++ b/tests/dhtproxytester.h @@ -25,6 +25,7 @@ #include <opendht/dhtrunner.h> #include <opendht/dht_proxy_server.h> +#include <opendht/log.h> namespace test { @@ -61,11 +62,14 @@ class DhtProxyTester : public CppUnit::TestFixture { void testResubscribeGetValues(); private: - dht::DhtRunner nodePeer {}; + dht::DhtRunner nodePeer; std::shared_ptr<dht::DhtRunner> nodeClient; std::shared_ptr<dht::DhtRunner> nodeProxy; - std::unique_ptr<dht::DhtProxyServer> server; + std::unique_ptr<dht::DhtProxyServer> serverProxy; + + dht::DhtRunner::Config config {}; + std::shared_ptr<dht::Logger> logger {}; }; } // namespace test diff --git a/tools/dhtnode.cpp b/tools/dhtnode.cpp index fc9e220dd89afd4dad1b2e17370a130151bcc9c0..2c9973aa1f4033c56b9796c1dc0bde42e50ec3f2 100644 --- a/tools/dhtnode.cpp +++ b/tools/dhtnode.cpp @@ -540,11 +540,12 @@ main(int argc, char **argv) else context.logger = log::getStdLogger(); } - node->run(params.port, config, std::move(context)); + if (context.logger) + log::enableLogging(*node); if (not params.bootstrap.first.empty()) { - //std::cout << "Bootstrap: " << params.bootstrap.first << ":" << params.bootstrap.second << std::endl; + std::cout << "Bootstrap: " << params.bootstrap.first << ":" << params.bootstrap.second << std::endl; node->bootstrap(params.bootstrap.first.c_str(), params.bootstrap.second.c_str()); } @@ -553,7 +554,7 @@ main(int argc, char **argv) #endif if (params.proxyserver != 0) { #ifdef OPENDHT_PROXY_SERVER - proxies.emplace(params.proxyserver, std::unique_ptr<DhtProxyServer>(new DhtProxyServer(node, params.proxyserver, params.pushserver))); + proxies.emplace(params.proxyserver, std::unique_ptr<DhtProxyServer>(new DhtProxyServer(node, params.proxyserver, params.pushserver, context.logger))); #else std::cerr << "DHT proxy server requested but OpenDHT built without proxy server support." << std::endl; exit(EXIT_FAILURE); diff --git a/tools/proxy_loadtester.py b/tools/proxy_loadtester.py new file mode 100644 index 0000000000000000000000000000000000000000..10da6bd584e1cfeb752173166b2ce95be4f0b327 --- /dev/null +++ b/tools/proxy_loadtester.py @@ -0,0 +1,82 @@ +# Copyright (C) 2017-2019 Savoir-faire Linux Inc. +# Author: Vsevolod Ivanov <vsevolod.ivanov@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/>. +# +# Manually run with Web UI: +# locust -f tester.py --host http://127.0.0.1:8080 +# +# Run in Terminal: +# locust -f tester.py --host http://127.0.0.1:8080 \ +# --clients 100 --hatch-rate 1 --run-time 10s --no-web --only-summary + +from locust import HttpLocust, TaskSet +from random import randint +import urllib.request +import base64 +import json + +words_url = "http://svnweb.freebsd.org/csrg/share/dict/words?view=co&content-type=text/plain" +words_resp = urllib.request.urlopen(words_url) +words = words_resp.read().decode().splitlines() + +headers = {'content-type': 'application/json'} + +def rand_list_value(mylist): + return mylist[randint(0, len(mylist) - 1)] + +def put_key(l): + key = rand_list_value(words) + val = rand_list_value(words) + print("Put/get: key={} value={}".format(key, val)) + data = base64.b64encode(val.encode()).decode() + print("Base64 encoding: value={} encoded={}".format(val, data)) + l.client.post("/" + key, data=json.dumps({"data": data}), + headers=headers, catch_response=True) + +def get_key(l): + key = rand_list_value(words) + print("Get: key={}".format(key)) + l.client.get("/" + key) + +def get_stats(l): + l.client.get("/stats") + +def subscribe(l): + key = rand_list_value(words) + print("Subscribe: key={}".format(key)) + l.client.get("/" + key + "/subscribe") + +def listen(l): + key = rand_list_value(words) + print("Listen: key={}".format(key)) + l.client.get("/" + key + "/listen") + +class UserBehavior(TaskSet): + tasks = {get_key: 5, put_key: 5, get_stats: 1, subscribe: 1, listen: 1} + + def on_start(self): + put_key(self) + get_key(self) + subscribe(self) + listen(self) + + def on_stop(self): + get_stats(self) + +class WebsiteUser(HttpLocust): + task_set = UserBehavior + min_wait = 5000 + max_wait = 9000 + print("Initiate the benchmark at http://127.0.0.1:8089/")