From cbf9457ae707ffadb8035854a51669fd95cbc68a Mon Sep 17 00:00:00 2001
From: Adrien Beraud <adrien.beraud@savoirfairelinux.com>
Date: Thu, 16 Jan 2025 16:54:47 -0500
Subject: [PATCH] name directory: allow unicode names

GitLab: #1097
Change-Id: I742d1306007b3ae4908ee2d46bd5275d1ce2c99a
---
 .../cx.ring.Ring.ConfigurationManager.xml     |   2 +
 bin/dbus/dbusconfigurationmanager.hpp         |   2 +-
 bin/jni/configurationmanager.i                |   4 +-
 bin/jni/jni_interface.i                       |   2 +-
 bin/nodejs/callback.h                         |   6 +-
 bin/nodejs/configurationmanager.i             |   4 +-
 bin/nodejs/nodejs_interface.i                 |   2 +-
 src/client/configurationmanager.cpp           |  11 +-
 src/jami/configurationmanager_interface.h     |   1 +
 src/jamidht/jamiaccount.cpp                   |  27 +++--
 src/jamidht/namedirectory.cpp                 | 111 +++++++++---------
 src/jamidht/namedirectory.h                   |  15 ++-
 src/string_utils.cpp                          |  29 +++++
 src/string_utils.h                            |   8 ++
 test/unitTest/namedirectory/namedirectory.cpp |   7 ++
 15 files changed, 140 insertions(+), 91 deletions(-)

diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
index ebcbfa723..ad5c740c3 100644
--- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
+++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml
@@ -367,6 +367,8 @@
             </tp:docstring>
             <arg type="s" name="accountId">
             </arg>
+            <arg type="s" name="requestName">
+            </arg>
             <arg type="i" name="status">
                 <tp:docstring>
                   Status code: 0 for success
diff --git a/bin/dbus/dbusconfigurationmanager.hpp b/bin/dbus/dbusconfigurationmanager.hpp
index 7ac259703..5eb11d178 100644
--- a/bin/dbus/dbusconfigurationmanager.hpp
+++ b/bin/dbus/dbusconfigurationmanager.hpp
@@ -1111,7 +1111,7 @@ private:
             exportable_serialized_callback<ConfigurationSignal::UserSearchEnded>(
                 std::bind(&DBusConfigurationManager::emitUserSearchEnded, this, _1, _2, _3, _4)),
             exportable_serialized_callback<ConfigurationSignal::RegisteredNameFound>(
-                std::bind(&DBusConfigurationManager::emitRegisteredNameFound, this, _1, _2, _3, _4)),
+                std::bind(&DBusConfigurationManager::emitRegisteredNameFound, this, _1, _2, _3, _4, _5)),
             exportable_serialized_callback<ConfigurationSignal::DeviceRevocationEnded>(
                 std::bind(&DBusConfigurationManager::emitDeviceRevocationEnded, this, _1, _2, _3)),
             exportable_serialized_callback<ConfigurationSignal::AccountProfileReceived>(
diff --git a/bin/jni/configurationmanager.i b/bin/jni/configurationmanager.i
index 87f0c21ad..c499fe52b 100644
--- a/bin/jni/configurationmanager.i
+++ b/bin/jni/configurationmanager.i
@@ -53,7 +53,7 @@ public:
     virtual void getDeviceName(std::vector<std::string>* /*name_ret*/){}
 
     virtual void nameRegistrationEnded(const std::string& /*account_id*/, int state, const std::string& /*name*/){}
-    virtual void registeredNameFound(const std::string& /*account_id*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
+    virtual void registeredNameFound(const std::string& /*account_id*/, const std::string& /*query*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
     virtual void userSearchEnded(const std::string& /*account_id*/, int state, const std::string& /*query*/, const std::vector<std::map<std::string, std::string>>& /*results*/){}
 
     virtual void migrationEnded(const std::string& /*accountId*/, const std::string& /*state*/){}
@@ -275,7 +275,7 @@ public:
     virtual void getDeviceName(std::vector<std::string>* /*name_ret*/){}
 
     virtual void nameRegistrationEnded(const std::string& /*account_id*/, int state, const std::string& /*name*/){}
-    virtual void registeredNameFound(const std::string& /*account_id*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
+    virtual void registeredNameFound(const std::string& /*account_id*/, const std::string& /*query*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
     virtual void userSearchEnded(const std::string& /*account_id*/, int state, const std::string& /*query*/, const std::vector<std::map<std::string, std::string>>& /*results*/){}
 
     virtual void migrationEnded(const std::string& /*accountId*/, const std::string& /*state*/){}
diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i
index 4a0df09dc..74c83c88d 100644
--- a/bin/jni/jni_interface.i
+++ b/bin/jni/jni_interface.i
@@ -287,7 +287,7 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM
         exportable_callback<ConfigurationSignal::GetHardwareAudioFormat>(bind(&ConfigurationCallback::getHardwareAudioFormat, confM, _1 )),
         exportable_callback<ConfigurationSignal::GetAppDataPath>(bind(&ConfigurationCallback::getAppDataPath, confM, _1, _2 )),
         exportable_callback<ConfigurationSignal::GetDeviceName>(bind(&ConfigurationCallback::getDeviceName, confM, _1 )),
-        exportable_callback<ConfigurationSignal::RegisteredNameFound>(bind(&ConfigurationCallback::registeredNameFound, confM, _1, _2, _3, _4 )),
+        exportable_callback<ConfigurationSignal::RegisteredNameFound>(bind(&ConfigurationCallback::registeredNameFound, confM, _1, _2, _3, _4, _5 )),
         exportable_callback<ConfigurationSignal::NameRegistrationEnded>(bind(&ConfigurationCallback::nameRegistrationEnded, confM, _1, _2, _3 )),
         exportable_callback<ConfigurationSignal::UserSearchEnded>(bind(&ConfigurationCallback::userSearchEnded, confM, _1, _2, _3, _4 )),
         exportable_callback<ConfigurationSignal::MigrationEnded>(bind(&ConfigurationCallback::migrationEnded, confM, _1, _2)),
diff --git a/bin/nodejs/callback.h b/bin/nodejs/callback.h
index 325cba491..0d5cdebd5 100644
--- a/bin/nodejs/callback.h
+++ b/bin/nodejs/callback.h
@@ -448,19 +448,21 @@ nameRegistrationEnded(const std::string& accountId, int state, const std::string
 
 void
 registeredNameFound(const std::string& accountId,
+                    const std::string& requestName,
                     int state,
                     const std::string& address,
                     const std::string& name)
 {
     std::lock_guard lock(pendingSignalsLock);
-    pendingSignals.emplace([accountId, state, address, name]() {
+    pendingSignals.emplace([accountId, requestName, state, address, name]() {
         Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), registeredNameFoundCb);
         if (!func.IsEmpty()) {
             SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(accountId),
+                                            V8_STRING_NEW_LOCAL(requestName),
                                             SWIGV8_INTEGER_NEW(state),
                                             V8_STRING_NEW_LOCAL(address),
                                             V8_STRING_NEW_LOCAL(name)};
-            func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 4, callback_args);
+            func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 5, callback_args);
         }
     });
 
diff --git a/bin/nodejs/configurationmanager.i b/bin/nodejs/configurationmanager.i
index a1d034f07..2d2108d21 100644
--- a/bin/nodejs/configurationmanager.i
+++ b/bin/nodejs/configurationmanager.i
@@ -50,7 +50,7 @@ public:
     virtual void errorAlert(int alert){}
 
     virtual void nameRegistrationEnded(const std::string& /*account_id*/, int state, const std::string& /*name*/){}
-    virtual void registeredNameFound(const std::string& /*account_id*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
+    virtual void registeredNameFound(const std::string& /*account_id*/, const std::string& /*query*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
     virtual void userSearchEnded(const std::string& /*account_id*/, int state, const std::string& /*query*/, const std::vector<std::map<std::string, std::string>>& /*results*/){}
 
     virtual void migrationEnded(const std::string& /*accountId*/, const std::string& /*state*/){}
@@ -263,7 +263,7 @@ public:
     virtual void errorAlert(int alert){}
 
     virtual void nameRegistrationEnded(const std::string& /*account_id*/, int state, const std::string& /*name*/){}
-    virtual void registeredNameFound(const std::string& /*account_id*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
+    virtual void registeredNameFound(const std::string& /*account_id*/, const std::string& /*query*/, int state, const std::string& /*address*/, const std::string& /*name*/){}
     virtual void userSearchEnded(const std::string& /*account_id*/, int state, const std::string& /*query*/, const std::vector<std::map<std::string, std::string>>& /*results*/){}
 
     virtual void migrationEnded(const std::string& /*accountId*/, const std::string& /*state*/){}
diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i
index b486c7b1d..e9c15e224 100644
--- a/bin/nodejs/nodejs_interface.i
+++ b/bin/nodejs/nodejs_interface.i
@@ -133,7 +133,7 @@ void init(const SWIGV8_VALUE& funcMap){
         exportable_callback<ConfigurationSignal::ContactRemoved>(bind(&contactRemoved, _1, _2, _3 )),
         exportable_callback<ConfigurationSignal::ExportOnRingEnded>(bind(&exportOnRingEnded, _1, _2, _3 )),
         exportable_callback<ConfigurationSignal::NameRegistrationEnded>(bind(&nameRegistrationEnded, _1, _2, _3 )),
-        exportable_callback<ConfigurationSignal::RegisteredNameFound>(bind(&registeredNameFound, _1, _2, _3, _4 )),
+        exportable_callback<ConfigurationSignal::RegisteredNameFound>(bind(&registeredNameFound, _1, _2, _3, _4, _5 )),
         exportable_callback<ConfigurationSignal::VolatileDetailsChanged>(bind(&volatileDetailsChanged, _1, _2)),
         exportable_callback<ConfigurationSignal::KnownDevicesChanged>(bind(&knownDevicesChanged, _1, _2 )),
         exportable_callback<ConfigurationSignal::IncomingAccountMessage>(bind(&incomingAccountMessage, _1, _2, _3, _4 )),
diff --git a/src/client/configurationmanager.cpp b/src/client/configurationmanager.cpp
index 7110b07af..a5dfa0305 100644
--- a/src/client/configurationmanager.cpp
+++ b/src/client/configurationmanager.cpp
@@ -1002,11 +1002,12 @@ lookupName(const std::string& account, const std::string& nameserver, const std:
 {
 #if HAVE_RINGNS
     if (account.empty()) {
-        auto cb = [name](const std::string& result, jami::NameDirectory::Response response) {
+        auto cb = [name](const std::string& regName, const std::string& address, jami::NameDirectory::Response response) {
             jami::emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>("",
+                                                                                name,
                                                                                 (int) response,
-                                                                                result,
-                                                                                name);
+                                                                                address,
+                                                                                regName);
         };
         if (nameserver.empty())
             jami::NameDirectory::lookupUri(name, "", cb);
@@ -1028,10 +1029,10 @@ lookupAddress(const std::string& account, const std::string& nameserver, const s
     if (account.empty()) {
         jami::NameDirectory::instance(nameserver)
             .lookupAddress(address,
-                           [address](const std::string& result,
+                           [address](const std::string& regName, const std::string& addr,
                                      jami::NameDirectory::Response response) {
                                jami::emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(
-                                   "", (int) response, address, result);
+                                   "", address, (int) response, regName, addr);
                            });
         return true;
     } else if (auto acc = jami::Manager::instance().getAccount<JamiAccount>(account)) {
diff --git a/src/jami/configurationmanager_interface.h b/src/jami/configurationmanager_interface.h
index c9542a6f3..156489812 100644
--- a/src/jami/configurationmanager_interface.h
+++ b/src/jami/configurationmanager_interface.h
@@ -473,6 +473,7 @@ struct LIBJAMI_PUBLIC ConfigurationSignal
     {
         constexpr static const char* name = "RegisteredNameFound";
         using cb_type = void(const std::string& /*account_id*/,
+                             const std::string& /*request_name*/,
                              int state,
                              const std::string& /*address*/,
                              const std::string& /*name*/);
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index 3c30fa561..3979c72fa 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -396,18 +396,18 @@ JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const U
         auto suffix = stripPrefix(uri.toString());
         NameDirectory::lookupUri(suffix,
                                  config().nameServer,
-                                 [wthis_ = weak(), call](const std::string& result,
+                                 [wthis_ = weak(), call](const std::string& regName, const std::string& address,
                                                          NameDirectory::Response response) {
                                      // we may run inside an unknown thread, but following code must
                                      // be called in main thread
-                                     runOnMainThread([wthis_, result, response, call]() {
+                                     runOnMainThread([wthis_, regName, address, response, call]() {
                                          if (response != NameDirectory::Response::found) {
                                              call->onFailure(EINVAL);
                                              return;
                                          }
                                          if (auto sthis = wthis_.lock()) {
                                              try {
-                                                 sthis->startOutgoingCall(call, result);
+                                                 sthis->startOutgoingCall(call, regName);
                                              } catch (...) {
                                                  call->onFailure(ENOENT);
                                              }
@@ -590,11 +590,11 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std::
 
 #if HAVE_RINGNS
     accountManager_->lookupAddress(toUri,
-                                   [wCall](const std::string& result,
+                                   [wCall](const std::string& regName, const std::string& address,
                                            const NameDirectory::Response& response) {
                                        if (response == NameDirectory::Response::found)
                                            if (auto call = wCall.lock()) {
-                                               call->setPeerRegisteredName(result);
+                                               call->setPeerRegisteredName(regName);
                                            }
                                    });
 #endif
@@ -1441,10 +1441,11 @@ JamiAccount::lookupName(const std::string& name)
     if (accountManager_)
         accountManager_->lookupUri(name,
                                    config().nameServer,
-                                   [acc = getAccountID(), name](const std::string& result,
+                                   [acc = getAccountID(), name](const std::string& regName,
+                                                                const std::string& address,
                                                                 NameDirectory::Response response) {
                                        emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(
-                                           acc, (int) response, result, name);
+                                           acc, name, (int) response, address, regName);
                                    });
 }
 
@@ -1455,11 +1456,11 @@ JamiAccount::lookupAddress(const std::string& addr)
     auto acc = getAccountID();
     if (accountManager_)
         accountManager_->lookupAddress(
-            addr, [acc, addr](const std::string& result, NameDirectory::Response response) {
-                emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
+            addr, [acc, addr](const std::string& regName, const std::string& address, NameDirectory::Response response) {
+                emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc, addr,
                                                                               (int) response,
-                                                                              addr,
-                                                                              result);
+                                                                              address,
+                                                                              regName);
             });
 }
 
@@ -1848,11 +1849,11 @@ JamiAccount::doRegister_()
         // Look for registered name
         accountManager_->lookupAddress(
             accountManager_->getInfo()->accountId,
-            [w = weak()](const std::string& result, const NameDirectory::Response& response) {
+            [w = weak()](const std::string& regName, const std::string& address, const NameDirectory::Response& response) {
                 if (auto this_ = w.lock()) {
                     if (response == NameDirectory::Response::found
                         or response == NameDirectory::Response::notFound) {
-                        const auto& nameResult = response == NameDirectory::Response::found ? result
+                        const auto& nameResult = response == NameDirectory::Response::found ? regName
                                                                                             : "";
                         if (this_->setRegisteredName(nameResult)) {
                             this_->editConfig([&](JamiAccountConfig& config) {
diff --git a/src/jamidht/namedirectory.cpp b/src/jamidht/namedirectory.cpp
index 6d8ae56e5..94a0ab2ca 100644
--- a/src/jamidht/namedirectory.cpp
+++ b/src/jamidht/namedirectory.cpp
@@ -55,10 +55,20 @@ constexpr const char DEFAULT_SERVER_HOST[] = "https://ns.jami.net";
 const std::string HEX_PREFIX = "0x";
 constexpr std::chrono::seconds SAVE_INTERVAL {5};
 
-/** Parser for URIs.         ( protocol        )    ( username         ) ( hostname ) */
+/**
+ Parser for URIs.         ( protocol        )    ( username         ) ( hostname )
+ - Requires "@" if a username is present (e.g., "user@domain.com").
+ - Allows common URL-safe special characters in usernames and domains.
+
+ Regex breakdown:
+ 1. `([a-zA-Z]+:(?://)?)?` → Optional scheme ("http://", "ftp://").
+ 2. `(?:([^\s@]{1,64})@)?` → Optional username (max 64 chars, Unicode allowed).
+ 3. `([^\s@]+)` → Domain or standalone name (Unicode allowed, no spaces or "@").
+
+ */
 const std::regex URI_VALIDATOR {
-    "^([a-zA-Z]+:(?://)?)?(?:([a-z0-9-_]{1,64})@)?([a-zA-Z0-9\\-._~%!$&'()*+,;=:\\[\\]]+)"};
-const std::regex NAME_VALIDATOR {"^[a-zA-Z0-9-_]{3,32}$"};
+    R"(^([a-zA-Z]+:(?://)?)?(?:([\w\-.~%!$&'()*+,;=]{1,64}|[^\s@]{1,64})@)?([^\s@]+)$)"
+};
 
 constexpr size_t MAX_RESPONSE_SIZE {1024ul * 1024};
 
@@ -91,7 +101,7 @@ NameDirectory::lookupUri(std::string_view uri, const std::string& default_server
         }
     }
     JAMI_ERROR("Unable to parse URI: {}", uri);
-    cb("", Response::invalidResponse);
+    cb("", "", Response::invalidResponse);
 }
 
 NameDirectory::NameDirectory(const std::string& serverUrl, std::shared_ptr<dht::Logger> l)
@@ -163,8 +173,8 @@ void
 NameDirectory::lookupAddress(const std::string& addr, LookupCallback cb)
 {
     auto cacheResult = nameCache(addr);
-    if (not cacheResult.empty()) {
-        cb(cacheResult, Response::found);
+    if (not cacheResult.first.empty()) {
+        cb(cacheResult.first, cacheResult.second, Response::found);
         return;
     }
     auto request = std::make_shared<Request>(*httpContext_,
@@ -175,16 +185,16 @@ NameDirectory::lookupAddress(const std::string& addr, LookupCallback cb)
         setHeaderFields(*request);
         request->add_on_done_callback(
             [this, cb = std::move(cb), addr](const dht::http::Response& response) {
-                if (response.status_code >= 400 && response.status_code < 500) {
+                if (response.status_code > 400 && response.status_code < 500) {
                     auto cacheResult = nameCache(addr);
-                    if (not cacheResult.empty())
-                        cb(cacheResult, Response::found);
+                    if (not cacheResult.first.empty())
+                        cb(cacheResult.first, cacheResult.second, Response::found);
                     else
-                        cb("", Response::notFound);
+                        cb("", "", Response::notFound);
                 } else if (response.status_code != 200) {
                     JAMI_ERROR("Address lookup for {} on {} failed with code={}",
                                addr, serverUrl_, response.status_code);
-                    cb("", Response::error);
+                    cb("", "", Response::error);
                 } else {
                     try {
                         Json::Value json;
@@ -198,25 +208,25 @@ NameDirectory::lookupAddress(const std::string& addr, LookupCallback cb)
                             JAMI_DBG("Address lookup for %s: Unable to parse server response: %s",
                                      addr.c_str(),
                                      response.body.c_str());
-                            cb("", Response::error);
+                            cb("", "", Response::error);
                             return;
                         }
                         auto name = json["name"].asString();
                         if (name.empty()) {
-                            cb(name, Response::notFound);
+                            cb(name, addr, Response::notFound);
                             return;
                         }
                         JAMI_DEBUG("Found name for {}: {}", addr, name);
                         {
                             std::lock_guard l(cacheLock_);
-                            addrCache_.emplace(name, addr);
-                            nameCache_.emplace(addr, name);
+                            addrCache_.emplace(name, std::pair(name, addr));
+                            nameCache_.emplace(addr, std::pair(name, addr));
                         }
-                        cb(name, Response::found);
+                        cb(name, addr, Response::found);
                         scheduleCacheSave();
                     } catch (const std::exception& e) {
                         JAMI_ERROR("Error when performing address lookup: {}", e.what());
-                        cb("", Response::error);
+                        cb("", "", Response::error);
                     }
                 }
                 std::lock_guard lk(requestsMtx_);
@@ -246,33 +256,28 @@ NameDirectory::verify(const std::string& name,
 }
 
 void
-NameDirectory::lookupName(const std::string& n, LookupCallback cb)
+NameDirectory::lookupName(const std::string& name, LookupCallback cb)
 {
-    std::string name {n};
-    if (not validateName(name)) {
-        cb("", Response::invalidResponse);
-        return;
-    }
-    toLower(name);
-    std::string cacheResult = addrCache(name);
-    if (not cacheResult.empty()) {
-        cb(cacheResult, Response::found);
+    auto cacheResult = addrCache(name);
+    if (not cacheResult.first.empty()) {
+        cb(cacheResult.first, cacheResult.second, Response::found);
         return;
     }
+    auto encodedName = urlEncode(name);
     auto request = std::make_shared<Request>(*httpContext_,
                                              resolver_,
-                                             serverUrl_ + QUERY_NAME + name);
+                                             serverUrl_ + QUERY_NAME + encodedName);
     try {
         request->set_method(restinio::http_method_get());
         setHeaderFields(*request);
         request->add_on_done_callback([this, name, cb = std::move(cb)](
                                           const dht::http::Response& response) {
-            if (response.status_code >= 400 && response.status_code < 500)
-                cb("", Response::notFound);
+            if (response.status_code > 400 && response.status_code < 500)
+                cb("", "", Response::notFound);
             else if (response.status_code < 200 || response.status_code > 299) {
                 JAMI_ERROR("Name lookup for {} on {} failed with code={}",
                            name, serverUrl_, response.status_code);
-                cb("", Response::error);
+                cb("", "", Response::error);
             } else {
                 try {
                     Json::Value json;
@@ -285,9 +290,10 @@ NameDirectory::lookupName(const std::string& n, LookupCallback cb)
                                        &err)) {
                         JAMI_ERROR("Name lookup for {}: Unable to parse server response: {}",
                                  name, response.body);
-                        cb("", Response::error);
+                        cb("", "", Response::error);
                         return;
                     }
+                    auto nameResult = json["name"].asString();
                     auto addr = json["addr"].asString();
                     auto publickey = json["publickey"].asString();
                     auto signature = json["signature"].asString();
@@ -295,32 +301,33 @@ NameDirectory::lookupName(const std::string& n, LookupCallback cb)
                     if (!addr.compare(0, HEX_PREFIX.size(), HEX_PREFIX))
                         addr = addr.substr(HEX_PREFIX.size());
                     if (addr.empty()) {
-                        cb("", Response::notFound);
+                        cb("", "", Response::notFound);
                         return;
                     }
                     if (not publickey.empty() and not signature.empty()) {
                         try {
                             auto pk = dht::crypto::PublicKey(base64::decode(publickey));
-                            if (pk.getId().toString() != addr or not verify(name, pk, signature)) {
-                                cb("", Response::invalidResponse);
+                            if (pk.getId().toString() != addr or not verify(nameResult, pk, signature)) {
+                                cb("", "", Response::invalidResponse);
                                 return;
                             }
                         } catch (const std::exception& e) {
-                            cb("", Response::invalidResponse);
+                            cb("", "", Response::invalidResponse);
                             return;
                         }
                     }
                     JAMI_DEBUG("Found address for {}: {}", name, addr);
                     {
                         std::lock_guard l(cacheLock_);
-                        addrCache_.emplace(name, addr);
-                        nameCache_.emplace(addr, name);
+                        addrCache_.emplace(name, std::pair(nameResult, addr));
+                        addrCache_.emplace(nameResult, std::pair(nameResult, addr));
+                        nameCache_.emplace(addr, std::pair(nameResult, addr));
                     }
-                    cb(addr, Response::found);
+                    cb(nameResult, addr, Response::found);
                     scheduleCacheSave();
                 } catch (const std::exception& e) {
                     JAMI_ERROR("Error when performing name lookup: {}", e.what());
-                    cb("", Response::error);
+                    cb("", "", Response::error);
                 }
             }
             if (auto req = response.request.lock())
@@ -339,12 +346,6 @@ NameDirectory::lookupName(const std::string& n, LookupCallback cb)
     }
 }
 
-bool
-NameDirectory::validateName(const std::string& name) const
-{
-    return std::regex_match(name, NAME_VALIDATOR);
-}
-
 using Blob = std::vector<uint8_t>;
 void
 NameDirectory::registerName(const std::string& addr,
@@ -355,14 +356,10 @@ NameDirectory::registerName(const std::string& addr,
                             const std::string& publickey)
 {
     std::string name {n};
-    if (not validateName(name)) {
-        cb(RegistrationResponse::invalidName, name);
-        return;
-    }
     toLower(name);
     auto cacheResult = addrCache(name);
-    if (not cacheResult.empty()) {
-        if (cacheResult == addr)
+    if (not cacheResult.first.empty()) {
+        if (cacheResult.second == addr)
             cb(RegistrationResponse::success, name);
         else
             cb(RegistrationResponse::alreadyTaken, name);
@@ -381,9 +378,11 @@ NameDirectory::registerName(const std::string& addr,
         owner,
         signedname,
         base64::encode(publickey));
+
+    auto encodedName = urlEncode(name);
     auto request = std::make_shared<Request>(*httpContext_,
                                              resolver_,
-                                             serverUrl_ + QUERY_NAME + name);
+                                             serverUrl_ + QUERY_NAME + encodedName);
     try {
         request->set_method(restinio::http_method_post());
         setHeaderFields(*request);
@@ -433,8 +432,8 @@ NameDirectory::registerName(const std::string& addr,
                              name, addr, success ? "success" : "failure");
                     if (success) {
                         std::lock_guard l(cacheLock_);
-                        addrCache_.emplace(name, addr);
-                        nameCache_.emplace(addr, name);
+                        addrCache_.emplace(name, std::pair(name, addr));
+                        nameCache_.emplace(addr, std::pair(name, addr));
                     }
                     cb(success ? RegistrationResponse::success : RegistrationResponse::error, name);
                 }
@@ -512,7 +511,7 @@ NameDirectory::loadCache()
     if (pac.next(oh))
         oh.get().convert(nameCache_);
     for (const auto& m : nameCache_)
-        addrCache_.emplace(m.second, m.first);
+        addrCache_.emplace(m.second.second, m.second);
     JAMI_DEBUG("Loaded {:d} name-address mappings", nameCache_.size());
 }
 
diff --git a/src/jamidht/namedirectory.h b/src/jamidht/namedirectory.h
index a7ebb18e7..fda66953a 100644
--- a/src/jamidht/namedirectory.h
+++ b/src/jamidht/namedirectory.h
@@ -64,7 +64,7 @@ public:
         unsupported
     };
 
-    using LookupCallback = std::function<void(const std::string& result, Response response)>;
+    using LookupCallback = std::function<void(const std::string& name, const std::string& address, Response response)>;
     using SearchResult = std::vector<std::map<std::string, std::string>>;
     using SearchCallback = std::function<void(const SearchResult& result, Response response)>;
     using RegistrationCallback = std::function<void(RegistrationResponse response, const std::string& name)>;
@@ -117,27 +117,26 @@ private:
 
     std::map<std::string, std::string> pendingRegistrations_ {};
 
-    std::map<std::string, std::string> nameCache_ {};
-    std::map<std::string, std::string> addrCache_ {};
+    std::map<std::string, std::pair<std::string, std::string>> nameCache_ {};
+    std::map<std::string, std::pair<std::string, std::string>> addrCache_ {};
 
     std::weak_ptr<Task> saveTask_;
 
     void setHeaderFields(dht::http::Request& request);
 
-    std::string nameCache(const std::string& addr)
+    std::pair<std::string, std::string> nameCache(const std::string& addr)
     {
         std::lock_guard l(cacheLock_);
         auto cacheRes = nameCache_.find(addr);
-        return cacheRes != nameCache_.end() ? cacheRes->second : std::string {};
+        return cacheRes != nameCache_.end() ? cacheRes->second : std::pair<std::string, std::string> {};
     }
-    std::string addrCache(const std::string& name)
+    std::pair<std::string, std::string> addrCache(const std::string& name)
     {
         std::lock_guard l(cacheLock_);
         auto cacheRes = addrCache_.find(name);
-        return cacheRes != addrCache_.end() ? cacheRes->second : std::string {};
+        return cacheRes != addrCache_.end() ? cacheRes->second : std::pair<std::string, std::string> {};
     }
 
-    bool validateName(const std::string& name) const;
     static bool verify(const std::string& name,
                        const dht::crypto::PublicKey& publickey,
                        const std::string& signature);
diff --git a/src/string_utils.cpp b/src/string_utils.cpp
index 5a640627c..fb11b0f44 100644
--- a/src/string_utils.cpp
+++ b/src/string_utils.cpp
@@ -170,4 +170,33 @@ string_split_set(std::string& str, std::string_view separator)
     return output;
 }
 
+std::string urlEncode(std::string_view input)
+{
+    if (input.empty()) {
+        return {};
+    }
+
+    auto isAsciiAlnum = [](unsigned char c) {
+        return (c >= '0' && c <= '9')
+        || (c >= 'A' && c <= 'Z')
+        || (c >= 'a' && c <= 'z');
+    };
+
+    std::ostringstream encoded;
+    // Use uppercase for hex digits
+    encoded << std::uppercase << std::hex;
+
+    for (unsigned char c : input) {
+        // If character is unreserved per RFC 3986, keep it as-is
+        if (isAsciiAlnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
+            encoded << c;
+        } else {
+            // Otherwise, percent-encode
+            encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(c);
+        }
+    }
+
+    return encoded.str();
+}
+
 } // namespace jami
diff --git a/src/string_utils.h b/src/string_utils.h
index b6bb7cfe2..5b74d8db9 100644
--- a/src/string_utils.h
+++ b/src/string_utils.h
@@ -207,6 +207,14 @@ std::string string_join(const std::set<std::string>& set, std::string_view separ
 
 std::set<std::string> string_split_set(std::string& str, std::string_view separator = "/");
 
+/**
+ * Percent-encode a string according to RFC 3986 unreserved characters.
+ *
+ * Only [0-9A-Za-z], '-' , '_' , '.' , '~' remain unencoded.
+ * Everything else (including non-ASCII) becomes '%XX'.
+ */
+std::string urlEncode(std::string_view input);
+
 } // namespace jami
 
 // Add string operators crucially missing from standard
diff --git a/test/unitTest/namedirectory/namedirectory.cpp b/test/unitTest/namedirectory/namedirectory.cpp
index a87023890..45a8001f5 100644
--- a/test/unitTest/namedirectory/namedirectory.cpp
+++ b/test/unitTest/namedirectory/namedirectory.cpp
@@ -190,6 +190,7 @@ NameDirectoryTest::testRegisterName()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::NameRegistrationEnded>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&) {
             nameRegistered = status == 0;
@@ -211,6 +212,7 @@ NameDirectoryTest::testLookupName()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::RegisteredNameFound>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&,
             const std::string&) {
@@ -233,6 +235,7 @@ NameDirectoryTest::testLookupNameInvalid()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::RegisteredNameFound>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&,
             const std::string&) {
@@ -255,6 +258,7 @@ NameDirectoryTest::testLookupNameNotFound()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::RegisteredNameFound>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&,
             const std::string&) {
@@ -277,6 +281,7 @@ NameDirectoryTest::testLookupAddr()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::RegisteredNameFound>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&,
             const std::string&) {
@@ -299,6 +304,7 @@ NameDirectoryTest::testLookupAddrInvalid()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::RegisteredNameFound>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&,
             const std::string&) {
@@ -321,6 +327,7 @@ NameDirectoryTest::testLookupAddrNotFound()
     // Watch signals
     confHandlers.insert(libjami::exportable_callback<libjami::ConfigurationSignal::RegisteredNameFound>(
         [&](const std::string&,
+            const std::string&,
             int status,
             const std::string&,
             const std::string&) {
-- 
GitLab