Compare commits

..
Author SHA1 Message Date
Cyrille BéraudandCopilot 9e3d8c0200 proxy server: only send call pushes at high priority
Replace the priority-only heuristic in handlePushListen
(highPriority = !expired && minPriority == 0) with isHighPriorityPush,
so the proxy promotes a notification to high (urgent) push priority
only for real-time call signaling.

Previously every value published at the default priority 0 — including
the frequent connection-request refreshes that carry an empty
pushType — triggered a high-priority push. On mobile this floods the
platform's adaptive high-priority quota; once tripped, the platform
silently drops genuine call pushes, so incoming calls fail to ring
while the device is in the background.

Restricting high priority to audioCall/videoCall keeps calls waking the
device while the surrounding presence/sync noise is delivered at normal
priority. This is a strict subset of the previous high-priority set, so
no notification that was normal before becomes high.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 07:51:31 -04:00
Cyrille BéraudandCopilot e0ecf06f52 proxy server: add testable high-priority push decision
Extract the decision of whether a push-listen notification must be
delivered at high (urgent) FCM/APNs priority into a static, pure
helper (isHighPriorityPush) so it can be unit-tested without a push
gateway.

Only real-time call signaling — values whose pushType is "audioCall"
or "videoCall", published at high priority (priority == 0) — warrants
an urgent push. Presence/connection-request refreshes (empty
pushType), non-call types and expirations are not urgent.

The helper is not wired in yet; this commit only adds it and its
tests, keeping the current handlePushListen behavior unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 07:51:22 -04:00
Adrien Beraud 732da0e478 default types: add ValueId to TrustRequest 2026-06-11 16:52:03 -04:00
Adrien Béraud 9a60329f66 proxy server: use token as request url 2026-06-03 20:00:34 -04:00
Adrien Béraud 0bb393ec36 http: allow to build Request by std::string url 2026-06-03 19:30:53 -04:00
vx32 947610003b http: allow to build Url from std::string, cleanup 2026-06-03 19:14:32 -04:00
Adrien Béraud dcbe158554 proxy server: allow url-encoded base64 for UnifiedPush 2026-06-03 19:12:40 -04:00
vx32 38e9a7fd61 base64: add support for url-encoded base64 2026-06-03 17:01:19 -04:00
10 changed files with 148 additions and 55 deletions

No files matched your search

+6
View File
@@ -159,7 +159,13 @@ public:
{}
static Value::Filter getFilter() { return EncryptedValue::getFilter(); }
virtual void unpackValue(const dht::Value& v) override
{
BaseClass::unpackValue(v);
id = v.id;
}
dht::Value::Id id {0}; /// Value ID of the trust request, used for deserialization only.
std::string service;
std::string conversationId;
Blob payload;
+20
View File
@@ -146,6 +146,26 @@ public:
std::shared_ptr<DhtRunner> getNode() const { return dht_; }
#ifdef OPENDHT_PUSH_NOTIFICATIONS
/**
* Decide whether a batch of values notified to a push listener must be
* delivered as a high (urgent) priority push notification.
*
* Only real-time call signaling — values whose pushType is "audioCall" or
* "videoCall", published at high priority (priority == 0) — warrants an
* urgent push. Every other notification (presence/connection-request
* refreshes carrying an empty pushType, value expirations, etc.) is sent at
* normal priority so it does not saturate the mobile platform's adaptive
* high-priority quota; once that quota is tripped, the platform silently
* drops genuine call pushes.
*
* @param values the values being notified to the listener.
* @param expired whether this notification is for expired (removed) values.
* @return true if the notification must be sent at high priority.
*/
static bool isHighPriorityPush(const std::vector<std::shared_ptr<Value>>& values, bool expired) noexcept;
#endif // OPENDHT_PUSH_NOTIFICATIONS
private:
class ConnectionListener;
struct RestRouterTraitsTls;
+8 -14
View File
@@ -51,11 +51,11 @@ using ConnectHandlerCb = std::function<void(const asio::error_code& ec, const as
using ssl_socket_t = restinio::impl::tls_socket_t;
using socket_t = asio::ip::tcp::socket;
class OPENDHT_PUBLIC Url
struct OPENDHT_PUBLIC Url
{
public:
Url() = default;
Url(std::string_view url);
Url(std::string url);
std::string url;
std::string protocol {"http"};
std::string_view user;
@@ -149,7 +149,7 @@ public:
using ResolverCb
= std::function<void(const asio::error_code& ec, const std::vector<asio::ip::tcp::endpoint>& endpoints)>;
Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
Resolver(asio::io_context& ctx, std::string url, std::shared_ptr<log::Logger> logger = {});
Resolver(asio::io_context& ctx,
std::string_view host,
std::string_view service,
@@ -161,10 +161,6 @@ public:
std::vector<asio::ip::tcp::endpoint> endpoints,
const bool ssl = false,
std::shared_ptr<log::Logger> logger = {});
Resolver(asio::io_context& ctx,
const std::string& url,
std::vector<asio::ip::tcp::endpoint> endpoints,
std::shared_ptr<log::Logger> logger = {});
~Resolver();
@@ -216,19 +212,19 @@ public:
// resolves implicitly
Request(asio::io_context& ctx,
const std::string& url,
std::string url,
const Json::Value& json,
OnJsonCb jsoncb,
std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, std::string url, OnJsonCb jsoncb, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, std::string url, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx,
std::string_view host,
std::string_view service,
const bool ssl = false,
std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
Request(asio::io_context& ctx, std::string url, OnDoneCb onDone, std::shared_ptr<log::Logger> logger = {});
// user defined resolver
Request(asio::io_context& ctx, std::shared_ptr<Resolver> resolver, sa_family_t family = AF_UNSPEC);
@@ -344,8 +340,6 @@ private:
dht::crypto::Identity client_identity_;
std::shared_ptr<dht::crypto::Certificate> server_ca_;
std::string service_;
std::string host_;
unsigned int id_;
static std::atomic_uint ids_;
+1 -11
View File
@@ -28,18 +28,8 @@ install(CODE [[
if(NOT _pip_check EQUAL 0)
message(WARNING "Skipping Python package install: pip not available for ${Python3_EXECUTABLE}.")
else()
set(_pip_install_args
install
--no-deps
--no-build-isolation
--break-system-packages
--prefix ${CMAKE_INSTALL_PREFIX})
if(DEFINED ENV{DESTDIR} AND NOT "$ENV{DESTDIR}" STREQUAL "")
list(APPEND _pip_install_args --root "$ENV{DESTDIR}")
endif()
list(APPEND _pip_install_args .)
execute_process(
COMMAND ${Python3_EXECUTABLE} -m pip ${_pip_install_args}
COMMAND ${Python3_EXECUTABLE} -m pip install --no-deps --break-system-packages .
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/python
OUTPUT_VARIABLE _pip_output
ERROR_VARIABLE _pip_error
+15
View File
@@ -36,4 +36,19 @@ base64_decode(std::string_view str)
return buffer;
}
std::vector<unsigned char>
base64_default_or_url_decode(std::string_view str)
{
std::vector<unsigned char> buffer(simdutf::maximal_binary_length_from_base64(str.data(), str.size()));
simdutf::result r = simdutf::base64_to_binary(str.data(),
str.size(),
(char*) buffer.data(),
simdutf::base64_options::base64_default_or_url);
if (r.error) {
throw std::invalid_argument(concat("Invalid base64 input: "sv, simdutf::error_to_string(r.error)));
} else {
buffer.resize(r.count);
}
return buffer;
}
} // namespace dht
+8 -1
View File
@@ -21,5 +21,12 @@ std::string base64_encode(const std::vector<unsigned char>& str);
* @return a base64-decoded buffer
*/
std::vector<unsigned char> base64_decode(std::string_view str);
/**
* Decode a buffer in base64 or url style base64 (including '-' '_' ).
*
* @param str the input buffer
* @return a base64-decoded buffer
*/
std::vector<unsigned char> base64_default_or_url_decode(std::string_view str);
} // namespace dht
} // namespace dht
+31 -16
View File
@@ -1134,6 +1134,25 @@ DhtProxyServer::handleCancelPushListen(const asio::error_code& ec,
pushListeners_.erase(pushListener);
}
bool
DhtProxyServer::isHighPriorityPush(const std::vector<std::shared_ptr<Value>>& values, bool expired) noexcept
{
// Expired-value notifications (de-registration) are never time-critical.
if (expired)
return false;
// Promote to high (urgent) push priority only for real-time call signaling.
// Background traffic — most notably connection-request refreshes, which
// carry an empty pushType — stays at normal priority so it does not trip the
// mobile platform's adaptive high-priority quota, which would otherwise
// silently drop genuine call pushes once the high-priority noise ratio gets
// too high.
for (const auto& v : values) {
if (v->priority == 0 and (v->pushType == "videoCall" or v->pushType == "audioCall"))
return true;
}
return false;
}
bool
DhtProxyServer::handlePushListen(const InfoHash& infoHash,
const std::string& pushToken,
@@ -1176,20 +1195,23 @@ DhtProxyServer::handlePushListen(const InfoHash& infoHash,
json["exp"] = json["ids"];
}
auto minPriority = 1000u;
for (const auto& v : values)
minPriority = std::min(minPriority, v->priority);
const bool highPriority = isHighPriorityPush(values, expired);
if (logger_)
logger_->debug("[proxy:server] [listen {}] [client {}] [session {}] [expired {}] [priority {}] [values {}]",
if (logger_) {
auto minPriority = 1000u;
for (const auto& v : values)
minPriority = std::min(minPriority, v->priority);
logger_->debug("[proxy:server] [listen {}] [client {}] [session {}] [expired {}] [priority {}] [high {}] [values {}]",
infoHash,
clientId,
sessionCtx->sessionId,
expired,
minPriority,
highPriority,
values.size());
}
sendPushNotification(pushToken, std::move(json), type, !expired and minPriority == 0, topic);
sendPushNotification(pushToken, std::move(json), type, highPriority, topic);
return true;
}
@@ -1205,14 +1227,7 @@ DhtProxyServer::sendPushNotification(
try {
std::shared_ptr<http::Request> request;
if (type == PushType::UnifiedPush) {
http::Url tokenUrl(token);
request = std::make_shared<http::Request>(io_context(),
concat(tokenUrl.protocol, "://"sv, tokenUrl.host),
tokenUrl.service,
tokenUrl.protocol.find("https") == 0,
logger_);
request->set_target(tokenUrl.target);
request->set_header_field(restinio::http_field_t::host, std::string(tokenUrl.host));
request = std::make_shared<http::Request>(io_context(), token, logger_);
} else {
request = std::make_shared<http::Request>(io_context(),
pushHostPort_.first,
@@ -1231,8 +1246,8 @@ DhtProxyServer::sendPushNotification(
Blob auth; ///< Auth secret
auto delim = topicView.find('|');
if (delim != std::string_view::npos) {
pubKey = base64_decode(topicView.substr(0, delim));
auth = base64_decode(topicView.substr(delim + 1));
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");
+12 -13
View File
@@ -42,8 +42,8 @@ constexpr std::string_view ORIGIN_PROTOCOL {"//"sv};
constexpr const char HTTP_HEADER_DELIM[] = "\r\n\r\n";
constexpr unsigned MAX_REDIRECTS {5};
Url::Url(std::string_view url_str)
: url(url_str)
Url::Url(std::string url_str)
: url(std::move(url_str))
{
std::string_view u = url;
if (u.empty())
@@ -830,8 +830,8 @@ Connection::timeout(const std::chrono::seconds& timeout, HandlerCb cb)
// Resolver
Resolver::Resolver(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
: url_(url)
Resolver::Resolver(asio::io_context& ctx, std::string url, std::shared_ptr<dht::Logger> logger)
: url_(std::move(url))
, resolver_(ctx)
, destroyed_(std::make_shared<bool>(false))
, logger_(logger)
@@ -967,14 +967,14 @@ Resolver::resolve(std::string_view host, std::string_view service)
std::atomic_uint Request::ids_ {1};
Request::Request(asio::io_context& ctx,
const std::string& url,
std::string url,
const Json::Value& json,
OnJsonCb jsoncb,
std::shared_ptr<dht::Logger> logger)
: logger_(std::move(logger))
, id_(Request::ids_++)
, ctx_(ctx)
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
{
init_default_headers();
set_header_field(restinio::http_field_t::content_type, std::string(HTTP_HEADER_CONTENT_TYPE_JSON));
@@ -997,11 +997,11 @@ Request::Request(asio::io_context& ctx,
});
}
Request::Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb, std::shared_ptr<dht::Logger> logger)
Request::Request(asio::io_context& ctx, std::string url, OnJsonCb jsoncb, std::shared_ptr<dht::Logger> logger)
: logger_(std::move(logger))
, id_(Request::ids_++)
, ctx_(ctx)
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
{
init_default_headers();
set_header_field(restinio::http_field_t::accept, std::string(HTTP_HEADER_CONTENT_TYPE_JSON));
@@ -1022,20 +1022,20 @@ Request::Request(asio::io_context& ctx, const std::string& url, OnJsonCb jsoncb,
});
}
Request::Request(asio::io_context& ctx, const std::string& url, std::shared_ptr<dht::Logger> logger)
Request::Request(asio::io_context& ctx, std::string url, std::shared_ptr<dht::Logger> logger)
: logger_(logger)
, id_(Request::ids_++)
, ctx_(ctx)
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
{
init_default_headers();
}
Request::Request(asio::io_context& ctx, const std::string& url, OnDoneCb onDone, std::shared_ptr<dht::Logger> logger)
Request::Request(asio::io_context& ctx, std::string url, OnDoneCb onDone, std::shared_ptr<dht::Logger> logger)
: logger_(logger)
, id_(Request::ids_++)
, ctx_(ctx)
, resolver_(std::make_shared<Resolver>(ctx, url, logger))
, resolver_(std::make_shared<Resolver>(ctx, std::move(url), logger))
{
init_default_headers();
add_on_done_callback(std::move(onDone));
@@ -1086,7 +1086,6 @@ Request::Request(asio::io_context& ctx,
, family_(family)
, resolver_(resolver)
{
set_header_field(restinio::http_field_t::host, concat(get_url().host, ":", get_url().service));
set_target(Url(target).target);
}
+35
View File
@@ -327,4 +327,39 @@ DhtProxyTester::testShutdownStop()
CPPUNIT_ASSERT_EQUAL(2 * C, callback_count.load());
}
#ifdef OPENDHT_PUSH_NOTIFICATIONS
void
DhtProxyTester::testPushNotificationPriority()
{
auto mkValue = [](unsigned priority, const std::string& pushType) {
auto v = std::make_shared<dht::Value>();
v->priority = priority;
v->pushType = pushType;
return v;
};
// Real-time call signaling at high priority => urgent push.
CPPUNIT_ASSERT(dht::DhtProxyServer::isHighPriorityPush({mkValue(0, "videoCall")}, false));
CPPUNIT_ASSERT(dht::DhtProxyServer::isHighPriorityPush({mkValue(0, "audioCall")}, false));
// Connection-request refresh (empty pushType) is noise => normal push.
CPPUNIT_ASSERT(!dht::DhtProxyServer::isHighPriorityPush({mkValue(0, "")}, false));
// Any other pushType is not a call => normal push.
CPPUNIT_ASSERT(!dht::DhtProxyServer::isHighPriorityPush({mkValue(0, "message")}, false));
// Expired (de-registration) notifications are never urgent, even for calls.
CPPUNIT_ASSERT(!dht::DhtProxyServer::isHighPriorityPush({mkValue(0, "videoCall")}, true));
// A call value explicitly published at normal priority stays normal.
CPPUNIT_ASSERT(!dht::DhtProxyServer::isHighPriorityPush({mkValue(1, "videoCall")}, false));
// Mixed batch: a single high-priority call value is enough to make it urgent.
CPPUNIT_ASSERT(dht::DhtProxyServer::isHighPriorityPush({mkValue(0, ""), mkValue(0, "videoCall")}, false));
// Empty batch => normal.
CPPUNIT_ASSERT(!dht::DhtProxyServer::isHighPriorityPush({}, false));
}
#endif // OPENDHT_PUSH_NOTIFICATIONS
} // namespace test
+12
View File
@@ -21,6 +21,9 @@ class DhtProxyTester : public CppUnit::TestFixture
CPPUNIT_TEST(testPutGet40KChars);
CPPUNIT_TEST(testFuzzy);
CPPUNIT_TEST(testShutdownStop);
#ifdef OPENDHT_PUSH_NOTIFICATIONS
CPPUNIT_TEST(testPushNotificationPriority);
#endif
CPPUNIT_TEST_SUITE_END();
public:
@@ -55,6 +58,15 @@ public:
void testShutdownStop();
#ifdef OPENDHT_PUSH_NOTIFICATIONS
/**
* The proxy must only request a high-priority push for real-time call
* signaling (audioCall/videoCall at priority 0); everything else stays at
* normal priority.
*/
void testPushNotificationPriority();
#endif
private:
dht::DhtRunner::Config clientConfig {};
dht::DhtRunner nodePeer;