23#include <opendht/http.h>
24#include <opendht/log.h>
25#include <opendht/thread_pool.h>
34using namespace std::literals;
38using Request = dht::http::Request;
40#define JAMI_PATH_LOGIN "/api/login"
41#define JAMI_PATH_AUTH "/api/auth"
51 const std::filesystem::path& path,
53 const std::string& nameServer)
56 , logger_(
Logger::dhtLogger())
60ServerAccountManager::setAuthHeaderFields(
Request& request)
const
62 request.set_header_field(restinio::http_field_t::authorization,
"Bearer " + token_);
67 std::string deviceName,
68 std::unique_ptr<AccountCredentials> credentials,
73 auto ctx = std::make_shared<AuthContext>();
77 ctx->deviceName = std::move(deviceName);
79 ctx->onSuccess = std::move(onSuccess);
80 ctx->onFailure = std::move(onFailure);
81 if (
not ctx->credentials
or ctx->credentials->username.empty()) {
87 const std::string url = managerHostname_ +
PATH_DEVICE;
91 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
96 auto csr =
ctx->request.get()->toString();
98 body[
"deviceName"] =
ctx->deviceName;
100 auto request = std::make_shared<Request>(
104 [
ctx, w](Json::Value json,
const dht::http::Response& response) {
105 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
106 JAMI_DEBUG(
"[Auth] Got request callback with status code={}", response.status_code);
107 if (response.status_code == 0 ||
this_ ==
nullptr)
109 else if (response.status_code >= 400 && response.status_code < 500)
111 else if (response.status_code < 200 || response.status_code > 299)
116 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} bytes",
118 response.body.size());
119 auto cert = std::make_shared<dht::crypto::Certificate>(json[
"certificateChain"].asString());
122 JAMI_ERROR(
"[Auth] Unable to parse certificate: no issuer");
126 auto receipt = json[
"deviceReceipt"].asString();
130 Json::CharReaderBuilder {}.newCharReader());
132 receipt.data() + receipt.size(),
135 JAMI_ERROR(
"[Auth] Unable to parse receipt from server: {}",
err);
139 auto receiptSignature =
base64::decode(json[
"receiptSignature"].asString());
141 auto info = std::make_unique<AccountInfo>();
142 info->identity.first =
ctx->key.get();
143 info->identity.second =
cert;
144 info->devicePk =
cert->getSharedPublicKey();
145 info->deviceId = info->devicePk->getLongId().toString();
147 info->contacts = std::make_unique<ContactList>(
ctx->accountId,
152 if (
ctx->deviceName.empty())
153 ctx->deviceName = info->deviceId.substr(8);
154 info->contacts->foundAccountDevice(
cert,
ctx->deviceName, clock::now());
158 info->devicePk->getId().toString(),
159 info->devicePk->getLongId().toString());
160 if (
not info->announce) {
164 info->username =
ctx->credentials->username;
166 this_->creds_ = std::move(
ctx->credentials);
167 this_->info_ = std::move(info);
168 std::map<std::string, std::string> config;
169 for (
auto itr = json.begin();
itr != json.end(); ++
itr) {
170 const auto& name =
itr.name();
171 if (name ==
"nameServer"sv) {
172 auto nameServer = json[
"nameServer"].asString();
173 if (!nameServer.empty() && nameServer[0] ==
'/')
174 nameServer =
this_->managerHostname_ + nameServer;
177 std::move(nameServer));
178 }
else if (name ==
"userPhoto"sv) {
179 this_->info_->photo = json[
"userPhoto"].asString();
184 config.emplace(name,
itr->asString());
191 std::move(receiptSignature));
192 }
catch (
const std::exception&
e) {
193 JAMI_ERROR(
"[Account {}] [Auth] Error when loading account: {}",
201 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
202 this_->clearRequest(response.request);
205 request->set_auth(
ctx->credentials->username,
ctx->credentials->password);
206 this_->sendRequest(request);
211ServerAccountManager::onAuthEnded(
const Json::Value& json,
const dht::http::Response& response, TokenScope
expectedScope)
213 if (response.status_code >= 200 && response.status_code < 300) {
214 auto scopeStr = json[
"scope"].asString();
216 : (
scopeStr ==
"USER"sv ? TokenScope::User : TokenScope::None);
217 auto expires_in = json[
"expires_in"].asLargestUInt();
219 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} ({} bytes)",
221 response.status_code,
222 response.body.size());
225 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} ({} bytes)",
227 response.status_code,
228 response.body.size());
231 clearRequest(response.request);
235ServerAccountManager::authenticateDevice()
238 authFailed(TokenScope::Device, 0);
242 auto request = std::make_shared<Request>(
245 Json::Value {Json::objectValue},
246 [w =
weak_from_this()](Json::Value json,
const dht::http::Response& response) {
247 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
248 this_->onAuthEnded(json, response, TokenScope::Device);
251 request->set_identity(
info_->identity);
253 sendRequest(request);
257ServerAccountManager::authenticateAccount(
const std::string& username,
const std::string& password)
261 auto request = std::make_shared<Request>(
264 Json::Value {Json::objectValue},
265 [w =
weak_from_this()](Json::Value json,
const dht::http::Response& response) {
266 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
267 this_->onAuthEnded(json, response, TokenScope::User);
270 request->set_auth(username, password);
271 sendRequest(request);
275ServerAccountManager::sendRequest(
const std::shared_ptr<dht::http::Request>& request)
277 request->set_header_field(restinio::http_field_t::user_agent,
userAgent());
279 std::lock_guard lock(requestLock_);
280 requests_.emplace(request);
286ServerAccountManager::clearRequest(
const std::weak_ptr<dht::http::Request>& request)
289 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock())) {
290 if (auto req = request.lock()) {
291 std::lock_guard lock(this_->requestLock_);
292 this_->requests_.erase(req);
299ServerAccountManager::authFailed(TokenScope scope,
int code)
301 RequestQueue requests;
303 std::lock_guard lock(tokenLock_);
304 requests = std::move(getRequestQueue(scope));
306 JAMI_DEBUG(
"[Auth] Failed auth with scope {}, ending {} pending requests", (
int) scope, requests.size());
310 if (onNeedsMigration_)
313 while (not requests.empty()) {
314 auto req = std::move(requests.front());
316 req->terminate(code == 0 ? asio::error::not_connected :
asio::error::access_denied);
321ServerAccountManager::authError(TokenScope scope)
324 std::lock_guard lock(tokenLock_);
325 if (scope <= tokenScope_) {
327 tokenScope_ = TokenScope::None;
330 if (scope == TokenScope::Device)
331 authenticateDevice();
335ServerAccountManager::setToken(std::string token, TokenScope scope, std::chrono::steady_clock::time_point expiration)
337 std::lock_guard lock(tokenLock_);
338 token_ = std::move(token);
340 tokenExpire_ = expiration;
342 nameDir_.get().setToken(token_);
343 if (not token_.empty() and scope != TokenScope::None) {
344 auto& reqQueue = getRequestQueue(scope);
345 JAMI_DEBUG(
"[Account {}] [Auth] Got token with scope {}, handling {} pending requests",
349 while (not reqQueue.empty()) {
350 auto req = std::move(reqQueue.front());
352 setAuthHeaderFields(*req);
359ServerAccountManager::sendDeviceRequest(
const std::shared_ptr<dht::http::Request>& req)
361 std::lock_guard lock(tokenLock_);
362 if (hasAuthorization(TokenScope::Device)) {
363 setAuthHeaderFields(*req);
366 auto& rQueue = getRequestQueue(TokenScope::Device);
368 authenticateDevice();
374ServerAccountManager::sendAccountRequest(
const std::shared_ptr<dht::http::Request>& req,
const std::string& pwd)
376 std::lock_guard lock(tokenLock_);
377 if (hasAuthorization(TokenScope::User)) {
378 setAuthHeaderFields(*req);
381 auto& rQueue = getRequestQueue(TokenScope::User);
383 authenticateAccount(info_->username, pwd);
389ServerAccountManager::syncDevices()
391 const std::string urlDevices = managerHostname_ +
PATH_DEVICES;
392 const std::string urlContacts = managerHostname_ +
PATH_CONTACTS;
396 JAMI_WARNING(
"[Account {}] [Auth] Sync conversations {}", accountId_, urlConversations);
397 Json::Value jsonConversations(Json::arrayValue);
398 for (
const auto& [key, convInfo] : ConversationModule::convInfos(accountId_)) {
399 jsonConversations.append(convInfo.toJson());
401 sendDeviceRequest(std::make_shared<Request>(
402 *Manager::instance().ioContext(),
405 [w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
406 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
409 JAMI_DEBUG(
"[Account {}] [Auth] Got conversation sync request callback with status code={}",
411 response.status_code);
412 if (response.status_code >= 200 && response.status_code < 300) {
414 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} bytes",
416 response.body.size());
417 if (not json.isArray()) {
418 JAMI_ERROR(
"[Account {}] [Auth] Unable to parse server response: not an array",
422 for (
unsigned i = 0, n = json.size(); i < n; i++) {
423 const auto& e = json[i];
425 convs.
c[ci.id] = std::move(ci);
427 dht::ThreadPool::io().run([accountId = this_->accountId_, convs] {
428 if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
429 acc->convModule()->onSyncData(convs,
"",
"");
433 }
catch (
const std::exception& e) {
434 JAMI_ERROR(
"[Account {}] Error when iterating conversation list: {}", this_->accountId_, e.what());
436 }
else if (response.status_code == 401)
437 this_->authError(TokenScope::Device);
439 this_->clearRequest(response.request);
443 JAMI_WARNING(
"[Account {}] [Auth] Sync conversations requests {}", accountId_, urlConversationsRequests);
444 Json::Value jsonConversationsRequests(Json::arrayValue);
446 auto jsonConversation = convRequest.toJson();
447 jsonConversationsRequests.append(std::move(jsonConversation));
449 sendDeviceRequest(std::make_shared<Request>(
450 *Manager::instance().ioContext(),
451 urlConversationsRequests,
452 jsonConversationsRequests,
453 [w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
454 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
457 JAMI_DEBUG(
"[Account {}] [Auth] Got conversations requests sync request callback with "
460 response.status_code);
461 if (response.status_code >= 200 && response.status_code < 300) {
463 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {}", this_->accountId_, response.body);
464 if (not json.isArray()) {
465 JAMI_ERROR(
"[Account {}] [Auth] Unable to parse server response: not an array",
469 for (
unsigned i = 0, n = json.size(); i < n; i++) {
470 const auto& e = json[i];
471 auto cr = ConversationRequest(e);
472 convReqs.cr[cr.conversationId] = std::move(cr);
474 dht::ThreadPool::io().run([accountId = this_->accountId_, convReqs] {
475 if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
476 acc->convModule()->onSyncData(convReqs,
"",
"");
480 }
catch (
const std::exception& e) {
481 JAMI_ERROR(
"[Account {}] Error when iterating conversations requests list: {}",
485 }
else if (response.status_code == 401)
486 this_->authError(TokenScope::Device);
488 this_->clearRequest(response.request);
492 JAMI_WARNING(
"[Account {}] [Auth] syncContacts {}", accountId_, urlContacts);
493 Json::Value jsonContacts(Json::arrayValue);
494 for (
const auto& contact : info_->contacts->
getContacts()) {
495 auto jsonContact = contact.second.toJson();
496 jsonContact[
"uri"] = contact.first.toString();
497 jsonContacts.append(std::move(jsonContact));
499 sendDeviceRequest(std::make_shared<Request>(
500 *Manager::instance().ioContext(),
503 [w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
504 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
507 JAMI_DEBUG(
"[Account {}] [Auth] Got contact sync request callback with status code={}",
509 response.status_code);
510 if (response.status_code >= 200 && response.status_code < 300) {
512 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} bytes",
514 response.body.size());
515 if (not json.isArray()) {
516 JAMI_ERROR(
"[Auth] Unable to parse server response: not an array");
518 for (
unsigned i = 0, n = json.size(); i < n; i++) {
519 const auto& e = json[i];
521 this_->info_->contacts->updateContact(dht::InfoHash {e[
"uri"].asString()}, contact);
523 this_->info_->contacts->saveContacts();
525 }
catch (
const std::exception& e) {
526 JAMI_ERROR(
"Error when iterating contact list: {}", e.what());
528 }
else if (response.status_code == 401)
529 this_->authError(TokenScope::Device);
531 this_->clearRequest(response.request);
535 JAMI_WARNING(
"[Account {}] [Auth] syncDevices {}", accountId_, urlDevices);
536 sendDeviceRequest(std::make_shared<Request>(
537 *Manager::instance().ioContext(),
539 [w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
540 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
543 JAMI_DEBUG(
"[Account {}] [Auth] Got request callback with status code={}",
545 response.status_code);
546 if (response.status_code >= 200 && response.status_code < 300) {
548 JAMI_LOG(
"[Account {}] [Auth] Got server response: {} bytes",
550 response.body.size());
551 if (not json.isArray()) {
552 JAMI_ERROR(
"[Auth] Unable to parse server response: not an array");
554 for (
unsigned i = 0, n = json.size(); i < n; i++) {
555 const auto& e = json[i];
556 const bool revoked = e[
"revoked"].asBool();
557 dht::PkId deviceId(e[
"deviceId"].
asString());
562 this_->info_->contacts->foundAccountDevice(deviceId,
566 this_->info_->contacts->removeAccountDevice(deviceId);
570 }
catch (
const std::exception& e) {
571 JAMI_ERROR(
"Error when iterating device list: {}", e.what());
573 }
else if (response.status_code == 401)
574 this_->authError(TokenScope::Device);
576 this_->clearRequest(response.request);
584 auto syncBlueprintCallback = std::make_shared<SyncBlueprintCallback>(onSuccess);
585 const std::string urlBlueprints = managerHostname_ +
PATH_BLUEPRINT;
586 JAMI_DEBUG(
"[Auth] Synchronize blueprint configuration {}", urlBlueprints);
587 sendDeviceRequest(std::make_shared<Request>(
588 *Manager::instance().ioContext(),
590 [syncBlueprintCallback, w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
591 JAMI_DEBUG(
"[Auth] Got sync request callback with status code={}", response.status_code);
592 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
595 if (response.status_code >= 200 && response.status_code < 300) {
597 std::map<std::string, std::string> config;
598 for (
auto itr = json.begin(); itr != json.end(); ++itr) {
599 const auto& name = itr.name();
600 config.emplace(name, itr->asString());
602 (*syncBlueprintCallback)(config);
603 }
catch (
const std::exception& e) {
604 JAMI_ERROR(
"Error when iterating blueprint config json: {}", e.what());
606 }
else if (response.status_code == 401)
607 this_->authError(TokenScope::Device);
608 this_->clearRequest(response.request);
614ServerAccountManager::revokeDevice(
const std::string& device,
615 std::string_view scheme,
616 const std::string& password,
619 if (not info_ || scheme != fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD) {
621 cb(RevokeDeviceResult::ERROR_CREDENTIALS);
624 const std::string url = managerHostname_ +
PATH_DEVICE +
"/" + device;
625 JAMI_WARNING(
"[Revoke] Revoking device of {} at {}", info_->username, url);
626 auto request = std::make_shared<Request>(
627 *Manager::instance().ioContext(),
629 [cb, w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
630 JAMI_DEBUG(
"[Revoke] Got request callback with status code={}", response.status_code);
631 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
634 if (response.status_code >= 200 && response.status_code < 300) {
637 if (json[
"errorDetails"].empty()) {
639 cb(RevokeDeviceResult::SUCCESS);
640 this_->syncDevices();
642 }
catch (
const std::exception& e) {
643 JAMI_ERROR(
"Error when loading device list: {}", e.what());
646 cb(RevokeDeviceResult::ERROR_NETWORK);
647 this_->clearRequest(response.request);
650 request->set_method(restinio::http_method_delete());
651 sendAccountRequest(request, password);
656ServerAccountManager::registerName(
const std::string& name,
661 cb(NameDirectory::RegistrationResponse::unsupported, name);
667 const std::string url = managerHostname_ +
PATH_SEARCH +
"?queryString=" + query;
668 JAMI_WARNING(
"[Search] Searching user {} at {}", query, url);
669 sendDeviceRequest(std::make_shared<Request>(
670 *Manager::instance().ioContext(),
672 [cb, w = weak_from_this()](Json::Value json,
const dht::http::Response& response) {
673 JAMI_DEBUG(
"[Search] Got request callback with status code={}", response.status_code);
674 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
677 if (response.status_code >= 200 && response.status_code < 300) {
679 const auto& profiles = json[
"profiles"];
680 Json::Value::ArrayIndex rcount = profiles.size();
681 std::vector<std::map<std::string, std::string>> results;
682 results.reserve(rcount);
683 JAMI_WARNING(
"[Search] Got server response: {} bytes", response.body.size());
684 for (Json::Value::ArrayIndex i = 0; i < rcount; i++) {
685 const auto& ruser = profiles[i];
686 std::map<std::string, std::string> user;
687 for (
const auto& member : ruser.getMemberNames()) {
688 const auto& rmember = ruser[member];
689 if (rmember.isString())
690 user[member] = rmember.asString();
692 results.emplace_back(std::move(user));
695 cb(results, SearchResponse::found);
696 }
catch (
const std::exception& e) {
697 JAMI_ERROR(
"[Search] Error during search: {}", e.what());
700 if (response.status_code == 401)
701 this_->authError(TokenScope::Device);
703 cb({}, SearchResponse::error);
705 this_->clearRequest(response.request);
NameDirectory::RegistrationCallback RegistrationCallback
const std::string accountId_
OnChangeCallback onChange_
CertRequest buildRequest(PrivateKey fDeviceKey)
std::unique_ptr< AccountInfo > info_
static std::shared_ptr< dht::Value > parseAnnounce(const std::string &announceBase64, const std::string &accountId, const std::string &deviceSha1, const std::string &deviceSha256)
std::shared_future< std::shared_ptr< dht::crypto::PrivateKey > > PrivateKey
std::function< void(RevokeDeviceResult)> RevokeDeviceCallback
std::function< void(AuthError error, const std::string &message)> AuthFailureCallback
NameDirectory::SearchCallback SearchCallback
std::function< void(const AccountInfo &info, const std::map< std::string, std::string > &config, std::string &&receipt, std::vector< uint8_t > &&receipt_signature)> AuthSuccessCallback
Level-driven logging class that support printf and C++ stream logging fashions.
static LIBJAMI_TEST_EXPORT Manager & instance()
static NameDirectory & instance()
void initAuthentication(PrivateKey request, std::string deviceName, std::unique_ptr< AccountCredentials > credentials, AuthSuccessCallback onSuccess, AuthFailureCallback onFailure, const OnChangeCallback &onChange) override
std::function< void(const std::map< std::string, std::string > &config)> SyncBlueprintCallback
ServerAccountManager(const std::string &accountId, const std::filesystem::path &path, const std::string &managerHostname, const std::string &nameServer)
#define JAMI_ERROR(formatstr,...)
#define JAMI_DEBUG(formatstr,...)
#define JAMI_WARNING(formatstr,...)
#define JAMI_LOG(formatstr,...)
std::string asString(bytes const &_b)
Converts byte array to a string containing the same (binary) data.
std::vector< unsigned char > decode(std::string_view str)
constexpr std::string_view PATH_CONVERSATIONS
constexpr std::string_view PATH_BLUEPRINT
void emitSignal(Args... args)
dht::http::Request Request
constexpr std::string_view PATH_CONVERSATIONS_REQUESTS
const std::string & userAgent()
constexpr std::string_view PATH_CONTACTS
constexpr std::string_view PATH_DEVICES
constexpr std::string_view PATH_DEVICE
constexpr std::string_view PATH_SEARCH
static constexpr const char URI[]
static constexpr const char DISPLAYNAME[]
std::vector< std::map< std::string, std::string > > getContacts(const std::string &accountId)
std::map< std::string, ConvInfo > c