// Copyright (c) 2014-2026 Savoir-faire Linux Inc. // SPDX-License-Identifier: MIT #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "dht_proxy_server.h" #include "default_types.h" #include "dhtrunner.h" #include "base64.h" #include #include #include #include #include #include #include using namespace std::placeholders; using namespace std::chrono_literals; using namespace std::literals; namespace dht { 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\"}"; #ifdef OPENDHT_PROXY_SERVER_IDENTITY constexpr char RESP_MSG_DESTINATION_NOT_FOUND[] = "{\"err\":\"No destination found\"}"; #endif #ifdef OPENDHT_PUSH_NOTIFICATIONS constexpr char RESP_MSG_NO_TOKEN[] = "{\"err\":\"No token\"}"; #endif constexpr const std::chrono::minutes PRINT_STATS_PERIOD {2}; using ResponseByParts = restinio::chunked_output_t; using ResponseByPartsBuilder = restinio::response_builder_t; namespace http { /** * Session value associated with a connection_id_t key. */ struct ListenerSession { ListenerSession() = default; dht::InfoHash hash; std::future token; std::shared_ptr> response; }; } // namespace http class opendht_logger_t { public: opendht_logger_t(std::shared_ptr logger = {}) : m_logger(std::move(logger)) {} template void trace(Builder&& /* msg_builder */) { /* if (m_logger) m_logger->d("[proxy:server] %s", msg_builder().c_str()); */ } template void info(Builder&& msg_builder) { if (m_logger) m_logger->debug("[proxy:server] {}", msg_builder()); } template void warn(Builder&& msg_builder) { if (m_logger) m_logger->warn("[proxy:server] {}", msg_builder()); } template void error(Builder&& msg_builder) { if (m_logger) m_logger->error("[proxy:server] {}", msg_builder()); } private: std::shared_ptr m_logger; }; std::string DhtProxyServer::ServerStats::toString() const { auto ret = fmt::format("Listens: {}, Puts: {}, PushListeners: {}\n" "Push requests in the last {}: [Android: {}], [iOS: {}], [Unified: {}]\n" "Requests: {} per second.", listenCount, putCount, pushListenersCount, print_duration(lastUpdated - serverStartTime), androidPush.toString(), iosPush.toString(), unifiedPush.toString(), requestRate); if (nodeInfo) { auto& ipv4 = nodeInfo->ipv4; if (ipv4.table_depth > 1) ret += fmt::format("IPv4 Network estimation: {}\n", ipv4.getNetworkSizeEstimation()); auto& ipv6 = nodeInfo->ipv6; if (ipv6.table_depth > 1) ret += fmt::format("IPv6 Network estimation: {}\n", ipv6.getNetworkSizeEstimation()); } return ret; } Json::Value DhtProxyServer::ServerStats::toJson() const { Json::Value result; result["listenCount"] = static_cast(listenCount); result["putCount"] = static_cast(putCount); result["totalPermanentPuts"] = static_cast(totalPermanentPuts); result["pushListenersCount"] = static_cast(pushListenersCount); result["serverStartTime"] = static_cast(to_time_t(serverStartTime)); result["lastUpdated"] = static_cast(to_time_t(lastUpdated)); result["androidPush"] = androidPush.toJson(); result["iosPush"] = iosPush.toJson(); result["unifiedPush"] = unifiedPush.toJson(); result["requestRate"] = requestRate; if (nodeInfo) result["nodeInfo"] = nodeInfo->toJson(); return result; } restinio::request_handling_status_t DhtProxyServer::serverError(restinio::request_t& request) { auto response = initHttpResponse(request.create_response(restinio::status_internal_server_error())); response.set_body(RESP_MSG_INTERNAL_SERVER_ERRROR); return response.done(); } // connection listener class DhtProxyServer::ConnectionListener { public: ConnectionListener() {}; explicit ConnectionListener(std::function onClosed) : onClosed_(std::move(onClosed)) {}; ~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& notice) noexcept; private: std::function onClosed_; }; void DhtProxyServer::ConnectionListener::state_changed(const restinio::connection_state::notice_t& notice) noexcept { if (std::holds_alternative(notice.cause())) { onClosed_(notice.connection_id()); } } void DhtProxyServer::onConnectionClosed(restinio::connection_id_t id) { std::lock_guard lock(lockListener_); auto it = listeners_.find(id); if (it != listeners_.end()) { dht_->cancelListen(it->second.hash, std::move(it->second.token)); listeners_.erase(it); if (logger_) logger_->debug("[proxy:server] [connection:{}] listener cancelled, {} still connected", id, listeners_.size()); } } struct DhtProxyServer::RestRouterTraitsTls : public restinio::default_tls_traits_t { using timer_manager_t = restinio::asio_timer_manager_t; using logger_t = opendht_logger_t; using request_handler_t = RestRouter; using connection_state_listener_t = ConnectionListener; }; struct DhtProxyServer::RestRouterTraits : public restinio::default_traits_t { using timer_manager_t = restinio::asio_timer_manager_t; using logger_t = opendht_logger_t; using request_handler_t = RestRouter; using connection_state_listener_t = ConnectionListener; }; void DhtProxyServer::PermanentPut::msgpack_unpack(const msgpack::object& o) { if (auto cid = findMapValue(o, "cid"sv)) { clientId = cid->as(); } if (auto exp = findMapValue(o, "exp"sv)) { expiration = from_time_t(exp->as()); } if (auto token = findMapValue(o, "token"sv)) { pushToken = token->as(); } if (auto sid = findMapValue(o, "sid"sv)) { if (not sessionCtx) sessionCtx = std::make_shared(sid->as()); else sessionCtx->sessionId = sid->as(); } if (auto t = findMapValue(o, "t"sv)) { type = t->as(); } if (auto val = findMapValue(o, "value"sv)) { value = std::make_shared(*val); } if (auto top = findMapValue(o, "top"sv)) { topic = top->as(); } } #ifdef OPENDHT_PUSH_NOTIFICATIONS void DhtProxyServer::Listener::msgpack_unpack(const msgpack::object& o) { if (auto cid = findMapValue(o, "cid"sv)) { clientId = cid->as(); } if (auto exp = findMapValue(o, "exp"sv)) { expiration = from_time_t(exp->as()); } if (auto sid = findMapValue(o, "sid"sv)) { if (not sessionCtx) sessionCtx = std::make_shared(sid->as()); else sessionCtx->sessionId = sid->as(); } if (auto t = findMapValue(o, "t"sv)) { type = t->as(); } if (auto top = findMapValue(o, "top"sv)) { topic = top->as(); } } #endif DhtProxyServer::DhtProxyServer(const std::shared_ptr& dht, const ProxyServerConfig& config, const std::shared_ptr& logger) : ioContext_(std::make_shared()) , dht_(dht) , persistPath_(config.persistStatePath) , logger_(logger) , printStatsTimer_(std::make_unique(*ioContext_, 3s)) , serverStartTime_(clock::now()) , connListener_(std::make_shared( std::bind(&DhtProxyServer::onConnectionClosed, this, std::placeholders::_1))) , pushServer_(config.pushServer) , bundleId_(config.bundleId) { if (not dht_) throw std::invalid_argument("A DHT instance must be provided"); if (logger_) logger_->debug("[proxy:server] [init] running on {:d}", config.port); if (not pushServer_.empty()) { #ifdef OPENDHT_PUSH_NOTIFICATIONS if (logger_) logger_->debug("[proxy:server] [init] using push server {}", pushServer_); #else if (logger_) logger_->error("[proxy:server] [init] opendht built without push notification support"); #endif } jsonBuilder_["commentStyle"] = "None"; jsonBuilder_["indentation"] = ""; if (!pushServer_.empty()) { // no host delim, assume port only if (pushServer_.find(":") == std::string::npos) pushServer_ = "localhost:" + pushServer_; // define http request destination for push notifications pushHostPort_ = splitPort(pushServer_); if (logger_) logger_->debug("Using push server for notifications: {}:{}", pushHostPort_.first, pushHostPort_.second); } if (config.identity.first and config.identity.second) { asio::error_code ec; // define tls context asio::ssl::context tls_context {asio::ssl::context::tlsv13_server}; tls_context.set_options(asio::ssl::context::default_workarounds | asio::ssl::context::no_sslv2 | asio::ssl::context::single_dh_use, ec); if (ec) throw std::runtime_error("Error setting tls context options: " + ec.message()); // add more security options #ifdef SSL_OP_NO_RENEGOTIATION SSL_CTX_set_options(tls_context.native_handle(), SSL_OP_NO_RENEGOTIATION); // CVE-2009-3555 #endif // node private key auto key = config.identity.first->serialize(); tls_context.use_private_key(asio::const_buffer {key.data(), key.size()}, asio::ssl::context::file_format::pem, ec); if (ec) throw std::runtime_error("Error setting node's private key: " + ec.message()); // certificate chain auto certchain = config.identity.second->toString(true /*chain*/); tls_context.use_certificate_chain(asio::const_buffer {certchain.data(), certchain.size()}, ec); if (ec) throw std::runtime_error("Error setting certificate chain: " + ec.message()); if (logger_) logger_->debug("[proxy:server] using certificate chain for ssl:\n{}", certchain); // build http server auto settings = restinio::run_on_this_thread_settings_t(); addServerSettings(settings); if (not config.address.empty()) settings.address(config.address); settings.port(config.port); settings.tls_context(std::move(tls_context)); httpsServer_ = std::make_unique>( ioContext_, std::forward>(std::move(settings))); // run https server serverThread_ = std::thread([this] { httpsServer_->open_async([] { /*ok*/ }, [](std::exception_ptr ex) { std::rethrow_exception(ex); }); httpsServer_->io_context().run(); }); } else { auto settings = restinio::run_on_this_thread_settings_t(); addServerSettings(settings); if (not config.address.empty()) settings.address(config.address); settings.port(config.port); httpServer_ = std::make_unique>( ioContext_, std::forward>(std::move(settings))); // run http server serverThread_ = std::thread([this]() { httpServer_->open_async([] { /*ok*/ }, [](std::exception_ptr ex) { std::rethrow_exception(ex); }); httpServer_->io_context().run(); }); } dht->forwardAllMessages(true); updateStats(); printStatsTimer_->async_wait(std::bind(&DhtProxyServer::handlePrintStats, this, std::placeholders::_1)); if (not persistPath_.empty()) { try { std::ifstream stateFile(persistPath_, std::ios::binary | std::ios::ate); if (stateFile) { std::streamsize size = stateFile.tellg(); stateFile.seekg(0, std::ios::beg); if (logger_) logger_->debug("[proxy:server] Loading proxy state from {} ({} bytes)", persistPath_, size); loadState(stateFile, size); } } catch (const std::exception& e) { if (logger_) logger_->error("[proxy:server] Error loading state from file: {}", e.what()); } } } template void DhtProxyServer::saveState(Os& stream) { msgpack::packer pk(&stream); pk.pack_map(2); { std::lock_guard lock(lockSearchPuts_); pk.pack("puts"); pk.pack(puts_); } #ifdef OPENDHT_PUSH_NOTIFICATIONS { std::lock_guard lock(lockListener_); pk.pack("pushListeners"); pk.pack(pushListeners_); } #endif } template void DhtProxyServer::loadState(Is& is, size_t size) { msgpack::unpacker pac; pac.reserve_buffer(size); if (is.read(pac.buffer(), size)) { pac.buffer_consumed(size); msgpack::object_handle oh; while (pac.next(oh)) { if (oh.get().type != msgpack::type::MAP) continue; if (auto puts = findMapValue(oh.get(), "puts"sv)) { std::lock_guard lock(lockSearchPuts_); puts_ = puts->as(); if (logger_) logger_->debug("[proxy:server] Loading {} persistent puts", puts_.size()); for (auto& put : puts_) { for (auto& pput : put.second.puts) { pput.second.expireTimer = std::make_unique(io_context(), pput.second.expiration); pput.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut, this, std::placeholders::_1, put.first, pput.first)); #ifdef OPENDHT_PUSH_NOTIFICATIONS if (not pput.second.pushToken.empty()) { auto jsonProvider = [infoHash = put.first.toString(), clientId = pput.second.clientId, vid = pput.first, sessionCtx = pput.second.sessionCtx]() { Json::Value json; json["timeout"] = infoHash; json["to"] = clientId; json["vid"] = std::to_string(vid); if (sessionCtx) { std::lock_guard l(sessionCtx->lock); json["s"] = sessionCtx->sessionId; } return json; }; pput.second.expireNotifyTimer = std::make_unique(io_context(), pput.second.expiration - proxy::OP_MARGIN); pput.second.expireNotifyTimer->async_wait( std::bind(&DhtProxyServer::handleNotifyPushListenExpire, this, std::placeholders::_1, pput.second.pushToken, std::move(jsonProvider), pput.second.type, pput.second.topic)); } #endif dht_->put(put.first, pput.second.value, DoneCallbackSimple {}, time_point::max(), true); } } } else { if (logger_) logger_->debug("No persistent puts in state"); } #ifdef OPENDHT_PUSH_NOTIFICATIONS if (auto pushListeners = findMapValue(oh.get(), "pushListeners"sv)) { std::lock_guard lock(lockListener_); pushListeners_ = pushListeners->as(); if (logger_) logger_->debug("[proxy:server] Loading {} push listeners", pushListeners_.size()); for (auto& pushListener : pushListeners_) { for (auto& listeners : pushListener.second.listeners) { for (auto& listener : listeners.second) { // start listening listener.internalToken = dht_->listen(listeners.first, [this, infoHash = listeners.first, pushToken = pushListener.first, type = listener.type, clientId = listener.clientId, sessionCtx = listener.sessionCtx, topic = listener.topic](const std::vector>& values, bool expired) { return this->handlePushListen(infoHash, pushToken, type, clientId, sessionCtx, topic, values, expired); }); // expire notify listener.expireNotifyTimer = std::make_unique(io_context(), listener.expiration - proxy::OP_MARGIN); auto jsonProvider = [infoHash = listeners.first.toString(), clientId = listener.clientId, sessionCtx = listener.sessionCtx]() { Json::Value json; json["timeout"] = infoHash; json["to"] = clientId; std::lock_guard l(sessionCtx->lock); json["s"] = sessionCtx->sessionId; return json; }; listener.expireNotifyTimer->async_wait( std::bind(&DhtProxyServer::handleNotifyPushListenExpire, this, std::placeholders::_1, pushListener.first, std::move(jsonProvider), listener.type, listener.topic)); // cancel push listen listener.expireTimer = std::make_unique(io_context(), listener.expiration); listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen, this, std::placeholders::_1, pushListener.first, listeners.first, listener.clientId)); } } } } else { if (logger_) logger_->debug("[proxy:server] No push listeners in state"); } #endif } if (logger_) logger_->debug("[proxy:server] loading ended"); } } asio::io_context& DhtProxyServer::io_context() const { return *ioContext_; } DhtProxyServer::~DhtProxyServer() { if (not persistPath_.empty()) { if (logger_) logger_->debug("[proxy:server] Saving proxy state to {}", persistPath_); std::ofstream stateFile(persistPath_, std::ios::binary); saveState(stateFile); } if (dht_) { std::lock_guard lock(lockListener_); for (auto& l : listeners_) { dht_->cancelListen(l.second.hash, std::move(l.second.token)); if (l.second.response) l.second.response->done(); } #ifdef OPENDHT_PUSH_NOTIFICATIONS for (auto& lm : pushListeners_) { for (auto& ls : lm.second.listeners) for (auto& l : ls.second) { if (l.expireNotifyTimer) l.expireNotifyTimer->cancel(); if (l.expireTimer) l.expireTimer->cancel(); dht_->cancelListen(ls.first, std::move(l.internalToken)); } } pushListeners_.clear(); #endif } if (logger_) logger_->debug("[proxy:server] closing http server"); ioContext_->stop(); if (serverThread_.joinable()) serverThread_.join(); if (logger_) logger_->debug("[proxy:server] http server closed"); } template void DhtProxyServer::addServerSettings(ServerSettings& settings, const unsigned int max_pipelined_requests) { using namespace std::chrono; /** * 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(createRestRouter()); // time limits // ~ 0.8 month std::chrono::milliseconds timeout_request(std::numeric_limits::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}); options.set_option(asio::socket_base::keep_alive {true}); }); settings.connection_state_listener(connListener_); } std::shared_ptr DhtProxyServer::updateStats(std::shared_ptr info) const { auto now = clock::now(); auto last = lastStatsReset_.exchange(now); auto count = requestNum_.exchange(0); auto dt = std::chrono::duration(now - last); auto sstats = std::make_shared(); auto& stats = *sstats; stats.requestRate = count / dt.count(); #ifdef OPENDHT_PUSH_NOTIFICATIONS stats.pushListenersCount = pushListeners_.size(); { std::lock_guard lk(pushStatsMutex_); stats.androidPush = androidPush_; stats.iosPush = iosPush_; stats.unifiedPush = unifiedPush_; } #endif stats.serverStartTime = serverStartTime_; stats.lastUpdated = now; stats.totalPermanentPuts = 0; std::for_each(puts_.begin(), puts_.end(), [&stats](const auto& put) { stats.totalPermanentPuts += put.second.puts.size(); }); stats.putCount = puts_.size(); stats.listenCount = listeners_.size(); stats.nodeInfo = std::move(info); return sstats; } void DhtProxyServer::updateStats() { dht_->getNodeInfo([this](std::shared_ptr newInfo) { stats_ = updateStats(newInfo); nodeInfo_ = newInfo; if (logger_) { auto str = Json::writeString(jsonBuilder_, newInfo->toJson()); logger_->debug("[proxy:server] [stats] {}", str); } }); } void DhtProxyServer::handlePrintStats(const asio::error_code& ec) { if (ec == asio::error::operation_aborted) return; updateStats(); printStatsTimer_->expires_at(printStatsTimer_->expiry() + PRINT_STATS_PERIOD); printStatsTimer_->async_wait(std::bind(&DhtProxyServer::handlePrintStats, this, std::placeholders::_1)); } template HttpResponse DhtProxyServer::initHttpResponse(HttpResponse response) { 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, "*"); return response; } std::unique_ptr DhtProxyServer::createRestRouter() { using namespace std::placeholders; auto router = std::make_unique(); // **************************** LEGACY ROUTES **************************** // node.info router->http_get("/", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2)); // key.options router->add_handler(restinio::http_method_options(), "/:hash", std::bind(&DhtProxyServer::options, this, _1, _2)); // key.get router->http_get("/:hash", std::bind(&DhtProxyServer::get, this, _1, _2)); // key.post router->http_post("/:hash", std::bind(&DhtProxyServer::put, this, _1, _2)); #ifdef OPENDHT_PUSH_NOTIFICATIONS // key.subscribe router->add_handler(restinio::http_method_subscribe(), "/:hash", std::bind(&DhtProxyServer::subscribe, this, _1, _2)); // key.unsubscribe router->add_handler(restinio::http_method_unsubscribe(), "/:hash", std::bind(&DhtProxyServer::unsubscribe, this, _1, _2)); #endif // OPENDHT_PUSH_NOTIFICATIONS #ifdef OPENDHT_PROXY_SERVER_IDENTITY #endif // OPENDHT_PROXY_SERVER_IDENTITY // **************************** NEW ROUTES **************************** // node.info router->http_get("/node/info", std::bind(&DhtProxyServer::getNodeInfo, this, _1, _2)); // node.stats router->http_get("/node/stats", std::bind(&DhtProxyServer::getStats, this, _1, _2)); // key.options router->http_get("/key/:hash/options", std::bind(&DhtProxyServer::options, this, _1, _2)); // key.get router->http_get("/key/:hash", std::bind(&DhtProxyServer::get, this, _1, _2)); // key.post router->http_post("/key/:hash", std::bind(&DhtProxyServer::put, this, _1, _2)); router->add_handler(restinio::http_method_options(), "/key/:hash/listen", std::bind(&DhtProxyServer::options, this, _1, _2)); // key.listen router->http_get("/key/:hash/listen", std::bind(&DhtProxyServer::listen, this, _1, _2)); #ifdef OPENDHT_PUSH_NOTIFICATIONS // node.pingPush router->http_post("/node/pingPush", std::bind(&DhtProxyServer::pingPush, this, _1, _2)); // key.subscribe router->add_handler(restinio::http_method_subscribe(), "/key/:hash", std::bind(&DhtProxyServer::subscribe, this, _1, _2)); // key.unsubscribe router->add_handler(restinio::http_method_unsubscribe(), "/key/:hash", std::bind(&DhtProxyServer::unsubscribe, this, _1, _2)); #endif // OPENDHT_PUSH_NOTIFICATIONS #ifdef OPENDHT_PROXY_SERVER_IDENTITY // key.sign router->http_post("/key/:hash/sign", std::bind(&DhtProxyServer::putSigned, this, _1, _2)); // key.encrypt router->http_post("/key/:hash/encrypt", std::bind(&DhtProxyServer::putEncrypted, this, _1, _2)); #endif // OPENDHT_PROXY_SERVER_IDENTITY return router; } RequestStatus DhtProxyServer::getNodeInfo(restinio::request_handle_t request, restinio::router::route_params_t /*params*/) const { try { if (auto nodeInfo = nodeInfo_) { auto result = nodeInfo->toJson(); // [ipv6:ipv4]:port or ipv4:port result["public_ip"] = request->remote_endpoint().address().to_string(); auto response = initHttpResponse(request->create_response()); response.append_body(Json::writeString(jsonBuilder_, result) + "\n"); return response.done(); } auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable())); response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); return response.done(); } catch (...) { return serverError(*request); } } RequestStatus DhtProxyServer::getStats(restinio::request_handle_t request, restinio::router::route_params_t /*params*/) { requestNum_++; try { if (auto stats = stats_) { auto response = initHttpResponse(request->create_response()); response.append_body(Json::writeString(jsonBuilder_, stats->toJson()) + "\n"); return response.done(); } else { auto response = initHttpResponse(request->create_response(restinio::status_service_unavailable())); response.set_body(RESP_MSG_SERVICE_UNAVAILABLE); return response.done(); } } catch (...) { return serverError(*request); } } RequestStatus DhtProxyServer::get(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; try { InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); auto response = std::make_shared( initHttpResponse(request->create_response())); response->flush(); dht_->get( infoHash, [this, response](const std::vector>& values) { std::ostringstream output; for (const auto& value : values) { output << Json::writeString(jsonBuilder_, value->toJson()) << "\n"; } response->append_chunk(output.str()); response->flush(); return true; }, [response](bool /*ok*/) { response->done(); }); return restinio::request_handling_status_t::accepted; } catch (const std::exception& e) { return serverError(*request); } } RequestStatus DhtProxyServer::listen(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; try { InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); auto response = std::make_shared( initHttpResponse(request->create_response())); response->flush(); std::lock_guard 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>& values, bool expired) { for (const auto& value : values) { auto jsonVal = value->toJson(); if (expired) jsonVal["expired"] = true; response->append_chunk(Json::writeString(jsonBuilder_, jsonVal) + "\n"); } response->flush(); return true; }); return restinio::request_handling_status_t::accepted; } catch (const std::exception& e) { return serverError(*request); } } #ifdef OPENDHT_PUSH_NOTIFICATIONS PushType DhtProxyServer::getTypeFromString(const std::string& type) { if (type == "android") return PushType::Android; else if (type == "ios") return PushType::iOS; else if (type == "unifiedpush") return PushType::UnifiedPush; return PushType::None; } std::string DhtProxyServer::getDefaultTopic(PushType) { return bundleId_; } RequestStatus DhtProxyServer::pingPush(restinio::request_handle_t request, restinio::router::route_params_t /*params*/) { requestNum_++; try { std::string err; Json::Value r; auto* char_data = reinterpret_cast(request->body().data()); auto reader = std::unique_ptr(jsonReaderBuilder_.newCharReader()); if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) { auto response = initHttpResponse(request->create_response(restinio::status_bad_request())); response.set_body(RESP_MSG_JSON_INCORRECT); return response.done(); } const Json::Value& root(r); // parse using const Json so [] never creates element auto pushToken = root["key"].asString(); if (pushToken.empty()) { auto response = initHttpResponse(request->create_response(restinio::status_bad_request())); response.set_body(RESP_MSG_NO_TOKEN); return response.done(); } auto type = getTypeFromString(root["platform"].asString()); auto topic = root["topic"].asString(); if (topic.empty()) { topic = getDefaultTopic(type); } Json::Value json; json["to"] = root["client_id"]; json["pong"] = true; sendPushNotification(pushToken, std::move(json), type, true, topic); } catch (...) { return serverError(*request); } return restinio::request_handling_status_t::accepted; } RequestStatus DhtProxyServer::subscribe(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; try { InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); std::string err; Json::Value r; auto* char_data = reinterpret_cast(request->body().data()); auto reader = std::unique_ptr(jsonReaderBuilder_.newCharReader()); if (!reader->parse(char_data, char_data + request->body().size(), &r, &err)) { auto response = initHttpResponse(request->create_response(restinio::status_bad_request())); response.set_body(RESP_MSG_JSON_INCORRECT); return response.done(); } const Json::Value& root(r); // parse using const Json so [] never creates element auto pushToken = root["key"].asString(); if (pushToken.empty()) { auto response = initHttpResponse(request->create_response(restinio::status_bad_request())); response.set_body(RESP_MSG_NO_TOKEN); return response.done(); } auto type = getTypeFromString(root["platform"].asString()); auto topic = root["topic"].asString(); if (topic.empty()) { topic = getDefaultTopic(type); } auto clientId = root["client_id"].asString(); auto sessionId = root["session_id"].asString(); if (logger_) logger_->debug("[proxy:server] [subscribe {}] [client {}] [session {}]", infoHash, clientId, sessionId); // Insert new or return existing push listeners of a token std::lock_guard lock(lockListener_); auto& pushListener = pushListeners_[pushToken]; auto& pushListeners = pushListener.listeners[infoHash]; auto listIt = std::find_if(pushListeners.begin(), pushListeners.end(), [&](const Listener& l) { return l.clientId == clientId; }); bool newListener = listIt == pushListeners.end(); if (newListener) { pushListeners.emplace_back(Listener {}); listIt = std::prev(pushListeners.end()); listIt->clientId = clientId; listIt->sessionCtx = std::make_shared(sessionId); } else { std::lock_guard l(listIt->sessionCtx->lock); listIt->sessionCtx->sessionId = sessionId; } auto& listener = *listIt; // Expiration auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT; listener.expiration = timeout; listener.type = type; listener.topic = topic; if (listener.expireNotifyTimer) listener.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN); else listener.expireNotifyTimer = std::make_unique(io_context(), timeout - proxy::OP_MARGIN); auto jsonProvider = [h = infoHash.toString(), clientId, sessionCtx = listener.sessionCtx]() { Json::Value json; json["timeout"] = h; json["to"] = clientId; std::lock_guard l(sessionCtx->lock); json["s"] = sessionCtx->sessionId; return json; }; listener.expireNotifyTimer->async_wait(std::bind(&DhtProxyServer::handleNotifyPushListenExpire, this, std::placeholders::_1, pushToken, std::move(jsonProvider), listener.type, listener.topic)); if (!listener.expireTimer) listener.expireTimer = std::make_unique(io_context(), timeout); else listener.expireTimer->expires_at(timeout); listener.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPushListen, this, std::placeholders::_1, pushToken, infoHash, clientId)); // Send response if (not newListener) { if (logger_) logger_->debug("[proxy:server] [subscribe {}] found [client {}]", infoHash, listener.clientId); // Send response header auto response = std::make_shared( initHttpResponse(request->create_response())); response->flush(); if (!root["refresh"].asBool()) { // No Refresh dht_->get( infoHash, [this, response](const Sp& value) { auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; response->append_chunk(output); response->flush(); return true; }, [response](bool) { response->done(); }); } else { // Refresh response->append_chunk("{}\n"); return response->done(); } } else { // =========== No existing listener for an infoHash ============ // Add listen on dht if (logger_) logger_->debug("[proxy:server] [subscribe {}] new", infoHash); listener.internalToken = dht_->listen(infoHash, [this, infoHash, pushToken, type, clientId, sessionCtx = listener.sessionCtx, topic](const std::vector>& values, bool expired) { return this->handlePushListen(infoHash, pushToken, type, clientId, sessionCtx, topic, values, expired); }); // Send response header auto response = initHttpResponse(request->create_response()); response.set_body("{}\n"); return response.done(); } } catch (...) { return serverError(*request); } return restinio::request_handling_status_t::accepted; } RequestStatus DhtProxyServer::unsubscribe(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); if (logger_) logger_->debug("[proxy:server] [unsubscribe {}]", infoHash); try { std::string err; Json::Value root; auto* char_data = reinterpret_cast(request->body().data()); auto reader = std::unique_ptr(jsonReaderBuilder_.newCharReader()); if (!reader->parse(char_data, char_data + request->body().size(), &root, &err)) { auto response = 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(); handleCancelPushListen(asio::error_code() /*success*/, pushToken, infoHash, clientId); auto response = initHttpResponse(request->create_response()); return response.done(); } catch (...) { return serverError(*request); } } void DhtProxyServer::handleNotifyPushListenExpire(const asio::error_code& ec, const std::string pushToken, std::function jsonProvider, PushType type, const std::string& topic) { if (ec == asio::error::operation_aborted) return; else if (ec) { if (logger_) logger_->error("[proxy:server] [subscribe] error sending put refresh: {}", ec.message()); } if (logger_) logger_->debug("[proxy:server] [subscribe] sending refresh to {} token", pushToken); sendPushNotification(pushToken, jsonProvider(), type, false, topic); } void DhtProxyServer::handleCancelPushListen(const asio::error_code& ec, const std::string pushToken, const InfoHash key, const std::string clientId) { if (ec == asio::error::operation_aborted) return; else if (ec) { if (logger_) logger_->error("[proxy:server] [listen:push {}] error cancel: {}", key, ec.message()); } if (logger_) logger_->debug("[proxy:server] [listen:push {}] cancelled for {}", key, clientId); std::lock_guard 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) { if (dht_) dht_->cancelListen(key, std::move(listener->internalToken)); listener = listeners->second.erase(listener); } else { ++listener; } } if (listeners->second.empty()) pushListener->second.listeners.erase(listeners); if (pushListener->second.listeners.empty()) pushListeners_.erase(pushListener); } bool DhtProxyServer::handlePushListen(const InfoHash& infoHash, const std::string& pushToken, PushType type, const std::string& clientId, const std::shared_ptr& sessionCtx, const std::string& topic, const std::vector>& values, bool expired) { Json::Value json; json["key"] = infoHash.toString(); json["to"] = clientId; using namespace std::chrono; json["t"] = Json::Value::Int64(duration_cast(system_clock::now().time_since_epoch()).count()); { std::lock_guard l(sessionCtx->lock); json["s"] = sessionCtx->sessionId; } // Build a comma-separated list of ids from the values. This is intended to be used when // streaming from the iOS notification extension in order to filter out unwanted values. std::string ids; std::string pushTypes; for (const auto& value : values) { if (!ids.empty()) { ids += ","; pushTypes += ","; } pushTypes += value->pushType; ids += std::to_string(value->id); } json["ids"] = ids; json["pt"] = pushTypes; // If message is expired copy the value ID to the `exp` field. // This is presumably used by the Android notification system. if (expired && values.size() < 2) { json["exp"] = json["ids"]; } auto minPriority = 1000u; for (const auto& v : values) minPriority = std::min(minPriority, v->priority); if (logger_) logger_->debug("[proxy:server] [listen {}] [client {}] [session {}] [expired {}] [priority {}] [values {}]", infoHash, clientId, sessionCtx->sessionId, expired, minPriority, values.size()); sendPushNotification(pushToken, std::move(json), type, !expired and minPriority == 0, topic); return true; } void DhtProxyServer::sendPushNotification( const std::string& token, Json::Value&& json, PushType type, bool highPriority, const std::string& topic) { if (pushServer_.empty() && type != PushType::UnifiedPush) return; unsigned reqid = 0; try { std::shared_ptr request; if (type == PushType::UnifiedPush) { request = std::make_shared(io_context(), token, logger_); } else { request = std::make_shared(io_context(), pushHostPort_.first, pushHostPort_.second, pushHostPort_.first.find("https") == 0, logger_); } reqid = request->id(); request->set_method(restinio::http_method_post()); request->set_header_field(restinio::http_field_t::user_agent, "RESTinio client"); request->set_header_field(restinio::http_field_t::accept, "*/*"); if (type == PushType::UnifiedPush) { std::string_view topicView = topic; Blob pubKey; ///< P-256 Public key Blob auth; ///< Auth secret auto delim = topicView.find('|'); if (delim != std::string_view::npos) { pubKey = base64_default_or_url_decode(topicView.substr(0, delim)); auth = base64_default_or_url_decode(topicView.substr(delim + 1)); } request->set_header_field(restinio::http_field_t::ttl, "86400"); request->set_header_field(restinio::http_field_t::urgency, highPriority ? "high" : "normal"); request->set_header_field(restinio::http_field_t::content_type, "application/json"); std::string payloadStr = Json::writeString(jsonBuilder_, std::move(json)); if (!pubKey.empty() && !auth.empty()) { request->set_header_field(restinio::http_field_t::content_encoding, "aes128gcm"); try { Blob encrypted = crypto::webPushEncrypt(pubKey, auth, reinterpret_cast(payloadStr.data()), payloadStr.size()); request->set_body(std::string(encrypted.begin(), encrypted.end())); } catch (const std::exception& e) { if (logger_) logger_->error("[proxy:server] [notification] encryption failed: {}", e.what()); return; } } else { request->set_body(payloadStr); } } else { request->set_target("/api/push"); request->set_header_field(restinio::http_field_t::host, pushServer_); request->set_header_field(restinio::http_field_t::content_type, "application/json"); // NOTE: see https://github.com/appleboy/gorush auto isResubscribe = json.isMember("timeout"); Json::Value notification(Json::objectValue); Json::Value tokens(Json::arrayValue); tokens[0] = token; notification["tokens"] = std::move(tokens); notification["platform"] = type == PushType::Android ? 2 : 1; notification["data"] = std::move(json); auto priority = highPriority ? "high" : "normal"; if (type == PushType::Android) { Json::Value androidConfig(Json::objectValue); androidConfig["priority"] = priority; androidConfig["ttl"] = "86400s"; // time to live = 24 hours notification["android"] = std::move(androidConfig); } else { notification["priority"] = priority; const auto expiration = std::chrono::system_clock::now() + std::chrono::hours(24); uint32_t exp = std::chrono::duration_cast(expiration.time_since_epoch()).count(); notification["expiration"] = exp; if (!topic.empty()) notification["topic"] = topic; if (highPriority || isResubscribe) { Json::Value alert(Json::objectValue); alert["title"] = "hello"; notification["push_type"] = "alert"; notification["alert"] = alert; notification["mutable_content"] = true; notification["priority"] = "high"; } else { notification["push_type"] = "background"; notification["content_available"] = true; } } Json::Value notifications(Json::arrayValue); notifications[0] = std::move(notification); Json::Value content; content["notifications"] = std::move(notifications); request->set_body(Json::writeString(jsonBuilder_, content)); } request->add_on_state_change_callback([this, reqid](http::Request::State state, const http::Response& response) { if (state == http::Request::State::DONE) { if (logger_ and response.status_code != 200) logger_->error("[proxy:server] [notification] push failed: {}", response.status_code); std::lock_guard l(requestLock_); requests_.erase(reqid); } }); { std::lock_guard l(requestLock_); requests_[reqid] = request; } request->send(); // For monitoring purposes std::lock_guard lk(pushStatsMutex_); switch (type) { case PushType::Android: androidPush_.increment(highPriority); break; case PushType::iOS: iosPush_.increment(highPriority); break; case PushType::UnifiedPush: unifiedPush_.increment(highPriority); break; default: break; } } catch (const std::exception& e) { if (logger_) logger_->error("[proxy:server] [notification] error send push: {}", e.what()); if (reqid) { std::lock_guard l(requestLock_); requests_.erase(reqid); } } } #endif // OPENDHT_PUSH_NOTIFICATIONS void DhtProxyServer::handleCancelPermamentPut(const asio::error_code& ec, const InfoHash& key, Value::Id vid) { if (ec == asio::error::operation_aborted) return; else if (ec) { if (logger_) logger_->error("[proxy:server] [put:permament] error sending put refresh: {}", ec.message()); } if (logger_) logger_->debug("[proxy:server] [put {}] cancel permament put {}", key, vid); std::lock_guard lock(lockSearchPuts_); auto sPuts = puts_.find(key); if (sPuts == puts_.end()) return; auto& sPutsMap = sPuts->second.puts; auto put = sPutsMap.find(vid); if (put == sPutsMap.end()) return; if (dht_) dht_->cancelPut(key, vid); if (put->second.expireTimer) put->second.expireTimer->cancel(); if (put->second.expireNotifyTimer) put->second.expireNotifyTimer->cancel(); sPutsMap.erase(put); if (sPutsMap.empty()) puts_.erase(sPuts); } RequestStatus DhtProxyServer::put(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); if (request->body().empty()) { auto response = 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; auto* char_data = reinterpret_cast(request->body().data()); auto reader = std::unique_ptr(jsonReaderBuilder_.newCharReader()); if (reader->parse(char_data, char_data + request->body().size(), &root, &err)) { auto value = std::make_shared(root); bool permanent = root.isMember("permanent"); if (logger_) logger_->debug("[proxy:server] [put {}] {} {}", infoHash, value->toString(), (permanent ? "permanent" : "")); if (permanent) { std::string pushToken, clientId, sessionId, platform, topic; auto& pVal = root["permanent"]; if (pVal.isObject()) { pushToken = pVal["key"].asString(); clientId = pVal["client_id"].asString(); platform = pVal["platform"].asString(); sessionId = pVal["session_id"].asString(); topic = pVal["topic"].asString(); } std::lock_guard lock(lockSearchPuts_); auto timeout = std::chrono::steady_clock::now() + proxy::OP_TIMEOUT; auto& sPuts = puts_[infoHash]; if (value->id == Value::INVALID_ID) { for (auto& pp : sPuts.puts) { if (pp.second.pushToken == pushToken and pp.second.clientId == clientId and pp.second.value->contentEquals(*value)) { pp.second.expireTimer->expires_at(timeout); pp.second.expireTimer->async_wait(std::bind(&DhtProxyServer::handleCancelPermamentPut, this, std::placeholders::_1, infoHash, pp.second.value->id)); if (not sessionId.empty()) { if (not pp.second.sessionCtx) pp.second.sessionCtx = std::make_shared(sessionId); else { std::lock_guard l(pp.second.sessionCtx->lock); pp.second.sessionCtx->sessionId = sessionId; } } auto response = initHttpResponse(request->create_response()); response.append_body(Json::writeString(jsonBuilder_, pp.second.value->toJson()) + "\n"); return response.done(); } } value->id = std::uniform_int_distribution {1}(rd); } auto vid = value->id; auto& pput = sPuts.puts[vid]; pput.value = value; pput.expiration = timeout; if (not pput.expireTimer) { // cancel permanent put pput.expireTimer = std::make_unique(io_context(), timeout); #ifdef OPENDHT_PUSH_NOTIFICATIONS if (not pushToken.empty()) { pput.pushToken = pushToken; pput.clientId = clientId; pput.type = getTypeFromString(platform); if (topic.empty()) topic = getDefaultTopic(pput.type); pput.topic = topic; pput.sessionCtx = std::make_shared(sessionId); } #endif } else { if (not sessionId.empty()) { if (not pput.sessionCtx) pput.sessionCtx = std::make_shared(sessionId); else { std::lock_guard l(pput.sessionCtx->lock); pput.sessionCtx->sessionId = sessionId; } } pput.expireTimer->expires_at(timeout); } pput.expireTimer->async_wait( std::bind(&DhtProxyServer::handleCancelPermamentPut, this, std::placeholders::_1, infoHash, vid)); #ifdef OPENDHT_PUSH_NOTIFICATIONS // notify put permanent expiration if (pput.sessionCtx) { auto jsonProvider = [infoHash, clientId, vid, sessionCtx = pput.sessionCtx]() { Json::Value json; json["timeout"] = infoHash.toString(); json["to"] = clientId; json["vid"] = std::to_string(vid); std::lock_guard l(sessionCtx->lock); json["s"] = sessionCtx->sessionId; return json; }; if (!pput.expireNotifyTimer) pput.expireNotifyTimer = std::make_unique(io_context(), timeout - proxy::OP_MARGIN); else pput.expireNotifyTimer->expires_at(timeout - proxy::OP_MARGIN); pput.expireNotifyTimer->async_wait(std::bind(&DhtProxyServer::handleNotifyPushListenExpire, this, std::placeholders::_1, pushToken, std::move(jsonProvider), pput.type, pput.topic)); } #endif } dht_->put( infoHash, value, [this, request, value](bool ok) { if (ok) { auto response = initHttpResponse(request->create_response()); response.append_body(Json::writeString(jsonBuilder_, value->toJson()) + "\n"); response.done(); } else { auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway())); response.set_body(RESP_MSG_PUT_FAILED); response.done(); } }, time_point::max(), permanent); return restinio::request_handling_status_t::accepted; } else { auto response = 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_->error("[proxy:server] error in put: {}", e.what()); return serverError(*request); } } #ifdef OPENDHT_PROXY_SERVER_IDENTITY RequestStatus DhtProxyServer::putSigned(restinio::request_handle_t request, restinio::router::route_params_t params) const { requestNum_++; InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); if (request->body().empty()) { auto response = 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; auto* char_data = reinterpret_cast(request->body().data()); auto reader = std::unique_ptr(jsonReaderBuilder_.newCharReader()); if (reader->parse(char_data, char_data + request->body().size(), &root, &err)) { auto value = std::make_shared(root); dht_->putSigned(infoHash, value, [this, request, value](bool ok) { if (ok) { auto output = Json::writeString(jsonBuilder_, value->toJson()) + "\n"; auto response = initHttpResponse(request->create_response()); response.append_body(output); response.done(); } else { auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway())); response.set_body(RESP_MSG_PUT_FAILED); response.done(); } }); return restinio::request_handling_status_t::accepted; } else { auto response = 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_->error("[proxy:server] error in putSigned: {}", e.what()); return serverError(*request); } } RequestStatus DhtProxyServer::putEncrypted(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); if (request->body().empty()) { auto response = 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; auto* char_data = reinterpret_cast(request->body().data()); auto reader = std::unique_ptr(jsonReaderBuilder_.newCharReader()); if (reader->parse(char_data, char_data + request->body().size(), &root, &err)) { InfoHash to(root["to"].asString()); if (!to) { auto response = initHttpResponse(request->create_response(restinio::status_bad_request())); response.set_body(RESP_MSG_DESTINATION_NOT_FOUND); return response.done(); } auto value = std::make_shared(root); dht_->putEncrypted(infoHash, to, value, [this, request, value](bool ok) { if (ok) { auto response = initHttpResponse(request->create_response()); response.append_body(Json::writeString(jsonBuilder_, value->toJson()) + "\n"); response.done(); } else { auto response = initHttpResponse(request->create_response(restinio::status_bad_gateway())); response.set_body(RESP_MSG_PUT_FAILED); response.done(); } }); return restinio::request_handling_status_t::accepted; } else { auto response = 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_->error("[proxy:server] error in put: {}", e.what()); return serverError(*request); } } #endif // OPENDHT_PROXY_SERVER_IDENTITY RequestStatus DhtProxyServer::options(restinio::request_handle_t request, restinio::router::route_params_t /*params*/) { requestNum_++; #ifdef OPENDHT_PROXY_SERVER_IDENTITY const auto methods = "OPTIONS, GET, POST, LISTEN, SIGN, ENCRYPT"; #else 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(); } RequestStatus DhtProxyServer::getFiltered(restinio::request_handle_t request, restinio::router::route_params_t params) { requestNum_++; auto query = params["value"]; InfoHash infoHash(params["hash"]); if (!infoHash) infoHash = InfoHash::get(params["hash"]); try { auto response = std::make_shared( initHttpResponse(request->create_response())); response->flush(); dht_->get( infoHash, [this, response](const Sp& value) { response->append_chunk(Json::writeString(jsonBuilder_, value->toJson()) + "\n"); response->flush(); return true; }, [response](bool /*ok*/) { response->done(); }, {}, query); return restinio::request_handling_status_t::accepted; } catch (const std::exception& e) { return serverError(*request); } } } // namespace dht