diff --git a/src/jamidht/account_manager.h b/src/jamidht/account_manager.h index 5634fdafad9c68226947c0d7fa08965b53db545d..d1e129c34e7c7cec26603e12121e1177066c5e74 100644 --- a/src/jamidht/account_manager.h +++ b/src/jamidht/account_manager.h @@ -121,7 +121,7 @@ public: virtual void syncDevices() = 0; - virtual bool isPasswordValid(const std::string& password) { return false; }; + virtual bool isPasswordValid(const std::string& /*password*/) { return false; }; dht::crypto::Identity loadIdentity(const std::string& crt_path, const std::string& key_path, const std::string& key_pwd) const; diff --git a/src/jamidht/namedirectory.cpp b/src/jamidht/namedirectory.cpp index 965ce49ac187ed46a34b73fd22dc38d3bff7b2d6..6fa926b461e1a6aa82d30c0cb8b6df054af5ced7 100644 --- a/src/jamidht/namedirectory.cpp +++ b/src/jamidht/namedirectory.cpp @@ -50,6 +50,7 @@ namespace jami { constexpr const char* const QUERY_NAME {"/name/"}; constexpr const char* const QUERY_ADDR {"/addr/"}; constexpr const char* const CACHE_DIRECTORY {"namecache"}; +constexpr const char DEFAULT_SERVER_HOST[] = "https://ns.jami.net"; const std::string HEX_PREFIX = "0x"; constexpr std::chrono::seconds SAVE_INTERVAL {5}; @@ -68,6 +69,9 @@ toLower(std::string& string) std::transform(string.begin(), string.end(), string.begin(), ::tolower); } +NameDirectory& +NameDirectory::instance() { return instance(DEFAULT_SERVER_HOST); } + void NameDirectory::lookupUri(const std::string& uri, const std::string& default_server, LookupCallback cb) diff --git a/src/jamidht/namedirectory.h b/src/jamidht/namedirectory.h index 8b70b9132d4eef54c4c2bb1f377c7d5eb46941bc..186c771a399bda9c64f89aab78613d69862a751c 100644 --- a/src/jamidht/namedirectory.h +++ b/src/jamidht/namedirectory.h @@ -67,7 +67,11 @@ public: void load(); static NameDirectory& instance(const std::string& serverUrl, std::shared_ptr<dht::Logger> l = {}); - static NameDirectory& instance() { return instance(DEFAULT_SERVER_HOST); } + static NameDirectory& instance(); + + void setToken(std::string token) { + serverToken_ = std::move(token); + } static void lookupUri(const std::string& uri, const std::string& default_server, LookupCallback cb); @@ -83,9 +87,9 @@ private: NON_COPYABLE(NameDirectory); NameDirectory(NameDirectory&&) = delete; NameDirectory& operator=(NameDirectory&&) = delete; - constexpr static const char* const DEFAULT_SERVER_HOST = "https://ns.jami.net"; std::string serverUrl_; + std::string serverToken_; std::string cachePath_; std::mutex cacheLock_ {}; diff --git a/src/jamidht/server_account_manager.cpp b/src/jamidht/server_account_manager.cpp index f2b298c5fab59bc1a38c68358fb47560eece0e56..cc342e2116245548a4650b5ab440b38b50bda38c 100644 --- a/src/jamidht/server_account_manager.cpp +++ b/src/jamidht/server_account_manager.cpp @@ -32,11 +32,11 @@ namespace jami { using Request = dht::http::Request; -static const std::string PATH_AUTH = "/api/auth"; +static const std::string PATH_LOGIN = "/api/login"; +static const std::string PATH_AUTH = "/api/auth"; static const std::string PATH_DEVICE = PATH_AUTH + "/device"; static const std::string PATH_DEVICES = PATH_AUTH + "/devices"; - -constexpr const char* const HTTPS_PROTO {"https"}; +static const std::string PATH_SEARCH = PATH_AUTH + "/directory/search"; ServerAccountManager::ServerAccountManager( const std::string& path, @@ -52,10 +52,8 @@ ServerAccountManager::ServerAccountManager( {}; void -ServerAccountManager::setHeaderFields(Request& request){ - request.set_header_field(restinio::http_field_t::user_agent, "Jami"); - request.set_header_field(restinio::http_field_t::accept, "application/json"); - request.set_header_field(restinio::http_field_t::content_type, "application/json"); +ServerAccountManager::setAuthHeaderFields(Request& request) const { + request.set_header_field(restinio::http_field_t::authorization, "Bearer " + token_); } void @@ -81,27 +79,19 @@ ServerAccountManager::initAuthentication( onChange_ = std::move(onChange); const std::string url = managerHostname_ + PATH_DEVICE; JAMI_WARN("[Auth] authentication with: %s to %s", ctx->credentials->username.c_str(), url.c_str()); - auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_); - auto reqid = request->id(); - request->set_method(restinio::http_method_post()); - request->set_auth(ctx->credentials->username, ctx->credentials->password); - requests_[reqid] = request; - dht::ThreadPool::computation().run([onAsync = onAsync_, ctx, request, reqid]{ - onAsync([ctx, request, reqid, onAsync](AccountManager& accountManager){ + dht::ThreadPool::computation().run([onAsync = onAsync_, ctx, url]{ + onAsync([=](AccountManager& accountManager){ auto& this_ = *static_cast<ServerAccountManager*>(&accountManager); + Json::Value body; { std::stringstream ss; auto csr = ctx->request.get()->toString(); - string_replace(csr, "\n", "\\n"); - string_replace(csr, "\r", "\\r"); - ss << "{\"csr\":\"" << csr << "\", \"deviceName\":\"" << ctx->deviceName << "\"}"; - JAMI_WARN("[Auth] Sending request: %s", csr.c_str()); - request->set_body(ss.str()); + body["csr"] = csr; + body["deviceName"] = ctx->deviceName; } - this_.setHeaderFields(*request); - request->add_on_done_callback([reqid, request, ctx, onAsync] - (const dht::http::Response& response){ + auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, body, [ctx, onAsync] + (Json::Value json, const dht::http::Response& response){ JAMI_DBG("[Auth] Got request callback with status code=%u", response.status_code); if (response.status_code == 0) ctx->onFailure(AuthError::SERVER_ERROR, "Can't connect to server"); @@ -112,22 +102,8 @@ ServerAccountManager::initAuthentication( else { do { try { - Json::Value json; - std::string err; - Json::CharReaderBuilder rbuilder; - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){ - JAMI_ERR("[Auth] Can't parse server response: %s", err.c_str()); - ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse server response"); - break; - } JAMI_WARN("[Auth] Got server response: %s", response.body.c_str()); - - auto certStr = json["certificateChain"].asString(); - string_replace(certStr, "\\n", "\n"); - string_replace(certStr, "\\r", "\r"); - auto cert = std::make_shared<dht::crypto::Certificate>(certStr); - + auto cert = std::make_shared<dht::crypto::Certificate>(json["certificateChain"].asString()); auto accountCert = cert->issuer; if (not accountCert) { JAMI_ERR("[Auth] Can't parse certificate: no issuer"); @@ -136,18 +112,14 @@ ServerAccountManager::initAuthentication( } auto receipt = json["deviceReceipt"].asString(); Json::Value receiptJson; - auto receiptReader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + std::string err; + auto receiptReader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder{}.newCharReader()); if (!receiptReader->parse(receipt.data(), receipt.data() + receipt.size(), &receiptJson, &err)){ JAMI_ERR("[Auth] Can't parse receipt from server: %s", err.c_str()); ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse receipt from server"); break; } - onAsync([reqid, ctx, - json=std::move(json), - receiptJson=std::move(receiptJson), - cert, - accountCert, - receipt=std::move(receipt)] (AccountManager& accountManager) mutable + onAsync([=] (AccountManager& accountManager) mutable { auto& this_ = *static_cast<ServerAccountManager*>(&accountManager); auto receiptSignature = base64::decode(json["receiptSignature"].asString()); @@ -195,42 +167,131 @@ ServerAccountManager::initAuthentication( } } while (false); } - onAsync([reqid](AccountManager& accountManager){ + + onAsync([response](AccountManager& accountManager){ auto& this_ = *static_cast<ServerAccountManager*>(&accountManager); - this_.requests_.erase(reqid); + this_.clearRequest(response.request); }); - }); - request->send(); + }, this_.logger_); + request->set_auth(ctx->credentials->username, ctx->credentials->password); + this_.sendRequest(request); }); }); } +void +ServerAccountManager::authenticateDevice() { + const std::string url = managerHostname_ + PATH_LOGIN; + JAMI_WARN("[Auth] getting a device token: %s", url.c_str()); + auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, Json::Value{Json::objectValue}, [onAsync = onAsync_] (Json::Value json, const dht::http::Response& response){ + onAsync([=] (AccountManager& accountManager) { + auto& this_ = *static_cast<ServerAccountManager*>(&accountManager); + if (response.status_code >= 200 && response.status_code < 300) { + auto scopeStr = json["scope"].asString(); + auto scope = scopeStr == "DEVICE" ? TokenScope::Device + : (scopeStr == "USER" ? TokenScope::User + : TokenScope::None); + JAMI_WARN("[Auth] Got server response: %d %s", response.status_code, response.body.c_str()); + this_.setToken(json["access_token"].asString(), scope); + } else { + this_.authFailed(TokenScope::Device, response.status_code); + } + this_.clearRequest(response.request); + }); + }, logger_); + request->set_identity(info_->identity); + // request->set_certificate_authority(info_->identity.second->issuer->issuer); + sendRequest(request); +} + +void +ServerAccountManager::sendRequest(const std::shared_ptr<dht::http::Request>& request) { + request->set_header_field(restinio::http_field_t::user_agent, "Jami"); + { + std::lock_guard<std::mutex> lock(requestLock_); + requests_.emplace(request); + } + request->send(); +} + +void +ServerAccountManager::clearRequest(const std::weak_ptr<dht::http::Request>& request) { + if (auto req = request.lock()) { + std::lock_guard<std::mutex> lock(requestLock_); + requests_.erase(req); + } +} + +void +ServerAccountManager::authFailed(TokenScope scope, int code) +{ + RequestQueue requests; + { + std::lock_guard<std::mutex> lock(tokenLock_); + requests = std::move(getRequestQueue(scope)); + } + JAMI_DBG("[Auth] Failed auth with scope %d, ending %zu pending requests", (int)scope, requests.size()); + while (not requests.empty()) { + auto req = std::move(requests.front()); + requests.pop(); + req->terminate(code == 0 ? asio::error::not_connected : asio::error::access_denied); + } +} + +void +ServerAccountManager::setToken(std::string token, TokenScope scope) +{ + std::lock_guard<std::mutex> lock(tokenLock_); + token_ = std::move(token); + nameDir_.get().setToken(token_); + if (not token_.empty()) { + auto& reqQueue = getRequestQueue(scope); + JAMI_DBG("[Auth] Got token with scope %d, handling %zu pending requests", (int)scope, reqQueue.size()); + while (not reqQueue.empty()) { + auto req = std::move(reqQueue.front()); + reqQueue.pop(); + setAuthHeaderFields(*req); + sendRequest(req); + } + } +} + +void ServerAccountManager::sendDeviceRequest(const std::shared_ptr<dht::http::Request>& req) +{ + std::lock_guard<std::mutex> lock(tokenLock_); + if (hasAuthorization(TokenScope::Device)) { + setAuthHeaderFields(*req); + sendRequest(req); + } else { + auto& rQueue = getRequestQueue(TokenScope::Device); + if (rQueue.empty()) + authenticateDevice(); + rQueue.emplace(req); + } +} + +void ServerAccountManager::sendAccountRequest(const std::shared_ptr<dht::http::Request>& req) +{ + std::lock_guard<std::mutex> lock(tokenLock_); + if (hasAuthorization(TokenScope::User)) { + setAuthHeaderFields(*req); + sendRequest(req); + } else { + pendingAccountRequests_.emplace(req); + } +} + void ServerAccountManager::syncDevices() { - if (not creds_) - return; - const std::string url = managerHostname_ + PATH_DEVICES + "?username=" + creds_->username; - JAMI_WARN("[Auth] syncDevices with: %s to %s", creds_->username.c_str(), url.c_str()); - auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_); - auto reqid = request->id(); - request->set_method(restinio::http_method_get()); - request->set_auth(creds_->username, creds_->password); - setHeaderFields(*request); - request->add_on_done_callback([reqid, onAsync = onAsync_] (const dht::http::Response& response){ - onAsync([reqid, response] (AccountManager& accountManager) { + const std::string url = managerHostname_ + PATH_DEVICES; + JAMI_WARN("[Auth] syncDevices %s", url.c_str()); + sendDeviceRequest(std::make_shared<Request>(*Manager::instance().ioContext(), url, [onAsync = onAsync_] (Json::Value json, const dht::http::Response& response){ + onAsync([=] (AccountManager& accountManager) { JAMI_DBG("[Auth] Got request callback with status code=%u", response.status_code); auto& this_ = *static_cast<ServerAccountManager*>(&accountManager); - if (response.status_code >= 200 || response.status_code < 300) { + if (response.status_code >= 200 && response.status_code < 300) { try { - Json::Value json; - std::string err; - Json::CharReaderBuilder rbuilder; - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){ - JAMI_ERR("[Auth] Can't parse server response: %s", err.c_str()); - return; - } JAMI_WARN("[Auth] Got server response: %s", response.body.c_str()); if (not json.isArray()) { JAMI_ERR("[Auth] Can't parse server response: not an array"); @@ -248,11 +309,9 @@ ServerAccountManager::syncDevices() JAMI_ERR("Error when loading device list: %s", e.what()); } } - this_.requests_.erase(reqid); + this_.clearRequest(response.request); }); - }); - request->send(); - requests_[reqid] = std::move(request); + }, logger_)); } bool @@ -265,27 +324,13 @@ ServerAccountManager::revokeDevice(const std::string& password, const std::strin } const std::string url = managerHostname_ + PATH_DEVICE + "?deviceId=" + device; JAMI_WARN("[Revoke] Removing device of %s at %s", info_->username.c_str(), url.c_str()); - auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_); - auto reqid = request->id(); - request->set_method(restinio::http_method_delete()); - request->set_auth(info_->username, password); - setHeaderFields(*request); - request->add_on_done_callback([reqid, cb, onAsync = onAsync_] (const dht::http::Response& response){ - onAsync([reqid, cb, response] (AccountManager& accountManager) { + auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, [cb, onAsync = onAsync_] (Json::Value json, const dht::http::Response& response){ + onAsync([=] (AccountManager& accountManager) { JAMI_DBG("[Revoke] Got request callback with status code=%u", response.status_code); auto& this_ = *static_cast<ServerAccountManager*>(&accountManager); - if (response.status_code >= 200 || response.status_code < 300) { - if (response.body.empty()) - return; + if (response.status_code >= 200 && response.status_code < 300) { try { - Json::Value json; - std::string err; - Json::CharReaderBuilder rbuilder; - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){ - JAMI_ERR("[Revoke] Can't parse server response: %s", err.c_str()); - } - JAMI_WARN("[Revoke] Got server response: %s", response.body.c_str()); + JAMI_WARN("[Revoke] Got server response"); if (json["errorDetails"].empty()){ if (cb) cb(RevokeDeviceResult::SUCCESS); @@ -298,12 +343,13 @@ ServerAccountManager::revokeDevice(const std::string& password, const std::strin } else if (cb) cb(RevokeDeviceResult::ERROR_NETWORK); - this_.requests_.erase(reqid); + this_.clearRequest(response.request); }); - }); + }, logger_); + request->set_method(restinio::http_method_delete()); + request->set_auth(info_->username, password); JAMI_DBG("[Revoke] Sending revoke device '%s' to JAMS", device.c_str()); - request->send(); - requests_[reqid] = std::move(request); + sendRequest(request); return false; } diff --git a/src/jamidht/server_account_manager.h b/src/jamidht/server_account_manager.h index bbebf8ad30040903983f65b4db9d7f447eed8a61..60c419a9cf890c259a0981dacf99ca8da29a0035 100644 --- a/src/jamidht/server_account_manager.h +++ b/src/jamidht/server_account_manager.h @@ -20,6 +20,9 @@ #include "account_manager.h" +#include <queue> +#include <set> + namespace jami { class ServerAccountManager : public AccountManager { @@ -64,10 +67,42 @@ private: const std::string managerHostname_; std::shared_ptr<dht::Logger> logger_; - std::map<unsigned int /*id*/, std::shared_ptr<dht::http::Request>> requests_; + + std::mutex requestLock_; + std::set<std::shared_ptr<dht::http::Request>> requests_; std::unique_ptr<ServerAccountCredentials> creds_; - void setHeaderFields(dht::http::Request& request); + void sendRequest(const std::shared_ptr<dht::http::Request>& request); + void clearRequest(const std::weak_ptr<dht::http::Request>& request); + + enum class TokenScope : unsigned { + None = 0, + Device, + User, + Admin + }; + std::mutex tokenLock_; + TokenScope tokenScope_ {}; + std::string token_ {}; + using RequestQueue = std::queue<std::shared_ptr<dht::http::Request>>; + RequestQueue pendingDeviceRequests_; + RequestQueue pendingAccountRequests_; + RequestQueue& getRequestQueue(TokenScope scope) { + return scope == TokenScope::Device ? pendingDeviceRequests_ : pendingAccountRequests_; + } + bool hasAuthorization(TokenScope scope) const { + return not token_.empty() and tokenScope_ >= scope; + } + void setAuthHeaderFields(dht::http::Request& request) const; + + void sendDeviceRequest(const std::shared_ptr<dht::http::Request>& req); + void sendAccountRequest(const std::shared_ptr<dht::http::Request>& req); + + void authenticateDevice(); + void authenticateAccount(); + void authFailed(TokenScope scope, int code); + + void setToken(std::string token, TokenScope scope); }; }