22#include <opendht/http.h>
23#include <opendht/log.h>
24#include <opendht/thread_pool.h>
33using namespace std::literals;
37using Request = dht::http::Request;
39#define JAMI_PATH_LOGIN "/api/login"
40#define JAMI_PATH_AUTH "/api/auth"
50 const std::filesystem::path& path,
52 const std::string& nameServer)
55 , logger_(
Logger::dhtLogger()) {}
58ServerAccountManager::setAuthHeaderFields(
Request& request)
const
60 request.set_header_field(restinio::http_field_t::authorization,
"Bearer " + token_);
65 std::string deviceName,
66 std::unique_ptr<AccountCredentials> credentials,
71 auto ctx = std::make_shared<AuthContext>();
75 ctx->deviceName = std::move(deviceName);
77 ctx->onSuccess = std::move(onSuccess);
78 ctx->onFailure = std::move(onFailure);
79 if (
not ctx->credentials
or ctx->credentials->username.empty()) {
85 const std::string url = managerHostname_ +
PATH_DEVICE;
89 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
93 auto csr =
ctx->request.get()->toString();
95 body[
"deviceName"] =
ctx->deviceName;
97 auto request = std::make_shared<Request>(
101 [
ctx, w](Json::Value json,
const dht::http::Response& response) {
102 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
103 JAMI_DEBUG(
"[Auth] Got request callback with status code={}",
104 response.status_code);
105 if (response.status_code == 0 ||
this_ ==
nullptr)
107 else if (response.status_code >= 400 && response.status_code < 500)
109 else if (response.status_code < 200 || response.status_code > 299)
114 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} bytes",
this_->accountId_, response.body.size());
115 auto cert = std::make_shared<dht::crypto::Certificate>(
116 json[
"certificateChain"].asString());
119 JAMI_ERR(
"[Auth] Unable to parse certificate: no issuer");
121 "Invalid certificate from server");
124 auto receipt = json[
"deviceReceipt"].asString();
128 Json::CharReaderBuilder {}.newCharReader());
130 receipt.data() + receipt.size(),
133 JAMI_ERR(
"[Auth] Unable to parse receipt from server: %s",
136 "Unable to parse receipt from server");
140 json[
"receiptSignature"].asString());
142 auto info = std::make_unique<AccountInfo>();
143 info->identity.first =
ctx->key.get();
144 info->identity.second =
cert;
145 info->devicePk =
cert->getSharedPublicKey();
146 info->deviceId = info->devicePk->getLongId().toString();
148 info->contacts = std::make_unique<ContactList>(
ctx->accountId,
153 if (
ctx->deviceName.empty())
154 ctx->deviceName = info->deviceId.substr(8);
155 info->contacts->foundAccountDevice(
cert,
162 info->devicePk->getId().toString(),
163 info->devicePk->getLongId().toString());
164 if (
not info->announce) {
166 "Unable to parse announce from server");
169 info->username =
ctx->credentials->username;
171 this_->creds_ = std::move(
ctx->credentials);
172 this_->info_ = std::move(info);
173 std::map<std::string, std::string> config;
174 for (
auto itr = json.begin();
itr != json.end(); ++
itr) {
175 const auto& name =
itr.name();
176 if (name ==
"nameServer"sv) {
177 auto nameServer = json[
"nameServer"].asString();
178 if (!nameServer.empty() && nameServer[0] ==
'/')
179 nameServer =
this_->managerHostname_ + nameServer;
183 std::move(nameServer));
184 }
else if (name ==
"userPhoto"sv) {
185 this_->info_->photo = json[
"userPhoto"].asString();
187 config.emplace(name,
itr->asString());
194 std::move(receiptSignature));
195 }
catch (
const std::exception&
e) {
196 JAMI_ERROR(
"[Account {}] [Auth] Error when loading account: {}",
this_->accountId_,
e.what());
202 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
203 this_->clearRequest(response.request);
206 request->set_auth(
ctx->credentials->username,
ctx->credentials->password);
207 this_->sendRequest(request);
212ServerAccountManager::onAuthEnded(
const Json::Value& json,
213 const dht::http::Response& response,
216 if (response.status_code >= 200 && response.status_code < 300) {
217 auto scopeStr = json[
"scope"].asString();
220 : (
scopeStr ==
"USER"sv ? TokenScope::User : TokenScope::None);
221 auto expires_in = json[
"expires_in"].asLargestUInt();
223 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} ({} bytes)",
accountId_, response.status_code, response.body.size());
226 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} ({} bytes)",
accountId_, response.status_code, response.body.size());
229 clearRequest(response.request);
233ServerAccountManager::authenticateDevice()
236 authFailed(TokenScope::Device, 0);
240 auto request = std::make_shared<Request>(
243 Json::Value {Json::objectValue},
244 [w=
weak_from_this()](Json::Value json,
const dht::http::Response& response) {
245 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
246 this_->onAuthEnded(json, response, TokenScope::Device);
249 request->set_identity(
info_->identity);
251 sendRequest(request);
255ServerAccountManager::authenticateAccount(
const std::string& username,
const std::string& password)
259 auto request = std::make_shared<Request>(
262 Json::Value {Json::objectValue},
263 [w=
weak_from_this()](Json::Value json,
const dht::http::Response& response) {
264 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
265 this_->onAuthEnded(json, response, TokenScope::User);
268 request->set_auth(username, password);
269 sendRequest(request);
273ServerAccountManager::sendRequest(
const std::shared_ptr<dht::http::Request>& request)
275 request->set_header_field(restinio::http_field_t::user_agent,
userAgent());
277 std::lock_guard lock(requestLock_);
278 requests_.emplace(request);
284ServerAccountManager::clearRequest(
const std::weak_ptr<dht::http::Request>& request)
287 if (
auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock())) {
288 if (auto req = request.lock()) {
289 std::lock_guard lock(this_->requestLock_);
290 this_->requests_.erase(req);
297ServerAccountManager::authFailed(TokenScope scope,
int code)
299 RequestQueue requests;
301 std::lock_guard lock(tokenLock_);
302 requests = std::move(getRequestQueue(scope));
304 JAMI_DEBUG(
"[Auth] Failed auth with scope {}, ending {} pending requests",
311 if (onNeedsMigration_)
314 while (not requests.empty()) {
315 auto req = std::move(requests.front());
317 req->terminate(code == 0 ? asio::error::not_connected :
asio::error::access_denied);
322ServerAccountManager::authError(TokenScope scope)
325 std::lock_guard lock(tokenLock_);
326 if (scope <= tokenScope_) {
328 tokenScope_ = TokenScope::None;
331 if (scope == TokenScope::Device)
332 authenticateDevice();
336ServerAccountManager::setToken(std::string token,
338 std::chrono::steady_clock::time_point expiration)
340 std::lock_guard lock(tokenLock_);
341 token_ = std::move(token);
343 tokenExpire_ = expiration;
345 nameDir_.get().setToken(token_);
346 if (not token_.empty() and scope != TokenScope::None) {
347 auto& reqQueue = getRequestQueue(scope);
348 JAMI_DEBUG(
"[Account {}] [Auth] Got token with scope {}, handling {} pending requests",
352 while (not reqQueue.empty()) {
353 auto req = std::move(reqQueue.front());
355 setAuthHeaderFields(*req);
362ServerAccountManager::sendDeviceRequest(
const std::shared_ptr<dht::http::Request>& req)
364 std::lock_guard lock(tokenLock_);
365 if (hasAuthorization(TokenScope::Device)) {
366 setAuthHeaderFields(*req);
369 auto& rQueue = getRequestQueue(TokenScope::Device);
371 authenticateDevice();
377ServerAccountManager::sendAccountRequest(
const std::shared_ptr<dht::http::Request>& req,
378 const std::string& pwd)
380 std::lock_guard lock(tokenLock_);
381 if (hasAuthorization(TokenScope::User)) {
382 setAuthHeaderFields(*req);
385 auto& rQueue = getRequestQueue(TokenScope::User);
387 authenticateAccount(info_->username, pwd);
393ServerAccountManager::syncDevices()
395 const std::string urlDevices = managerHostname_ +
PATH_DEVICES;
396 const std::string urlContacts = managerHostname_ +
PATH_CONTACTS;
400 JAMI_WARNING(
"[Account {}] [Auth] Sync conversations {}", accountId_, urlConversations);
401 Json::Value jsonConversations(Json::arrayValue);
402 for (
const auto& [key, convInfo] : ConversationModule::convInfos(accountId_)) {
403 jsonConversations.append(convInfo.toJson());
405 sendDeviceRequest(std::make_shared<Request>(
406 *Manager::instance().ioContext(),
409 [w=weak_from_this()](Json::Value json,
const dht::http::Response& response) {
410 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
412 JAMI_DEBUG(
"[Account {}] [Auth] Got conversation sync request callback with status code={}",
414 response.status_code);
415 if (response.status_code >= 200 && response.status_code < 300) {
417 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
418 if (not json.isArray()) {
419 JAMI_ERROR(
"[Account {}] [Auth] Unable to parse server response: not an array", this_->accountId_);
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());
456 JAMI_DEBUG(
"[Account {}] [Auth] Got conversations requests sync request callback with status code={}",
457 this_->accountId_, response.status_code);
458 if (response.status_code >= 200 && response.status_code < 300) {
460 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {}", this_->accountId_, response.body);
461 if (not json.isArray()) {
462 JAMI_ERROR(
"[Account {}] [Auth] Unable to parse server response: not an array", this_->accountId_);
465 for (
unsigned i = 0, n = json.size(); i < n; i++) {
466 const auto& e = json[i];
467 auto cr = ConversationRequest(e);
468 convReqs.cr[cr.conversationId] = std::move(cr);
470 dht::ThreadPool::io().run([accountId=this_->accountId_, convReqs] {
471 if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
472 acc->convModule()->onSyncData(convReqs,
"",
"");
476 }
catch (
const std::exception& e) {
477 JAMI_ERROR(
"[Account {}] Error when iterating conversations requests list: {}", this_->accountId_, e.what());
479 }
else if (response.status_code == 401)
480 this_->authError(TokenScope::Device);
482 this_->clearRequest(response.request);
486 JAMI_WARNING(
"[Account {}] [Auth] syncContacts {}", accountId_, urlContacts);
487 Json::Value jsonContacts(Json::arrayValue);
488 for (
const auto& contact : info_->contacts->
getContacts()) {
489 auto jsonContact = contact.second.toJson();
490 jsonContact[
"uri"] = contact.first.toString();
491 jsonContacts.append(std::move(jsonContact));
493 sendDeviceRequest(std::make_shared<Request>(
494 *Manager::instance().ioContext(),
497 [w=weak_from_this()](Json::Value json,
const dht::http::Response& response) {
498 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
500 JAMI_DEBUG(
"[Account {}] [Auth] Got contact sync request callback with status code={}",
501 this_->accountId_, response.status_code);
502 if (response.status_code >= 200 && response.status_code < 300) {
504 JAMI_WARNING(
"[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
505 if (not json.isArray()) {
506 JAMI_ERROR(
"[Auth] Unable to parse server response: not an array");
508 for (
unsigned i = 0, n = json.size(); i < n; i++) {
509 const auto& e = json[i];
511 this_->info_->contacts
512 ->updateContact(dht::InfoHash {e[
"uri"].asString()}, contact);
514 this_->info_->contacts->saveContacts();
516 }
catch (
const std::exception& e) {
517 JAMI_ERROR(
"Error when iterating contact list: {}", e.what());
519 }
else if (response.status_code == 401)
520 this_->authError(TokenScope::Device);
522 this_->clearRequest(response.request);
526 JAMI_WARNING(
"[Account {}] [Auth] syncDevices {}", accountId_, urlDevices);
527 sendDeviceRequest(std::make_shared<Request>(
528 *Manager::instance().ioContext(),
530 [w=weak_from_this()](Json::Value json,
const dht::http::Response& response) {
531 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
533 JAMI_DEBUG(
"[Account {}] [Auth] Got request callback with status code={}", this_->accountId_, response.status_code);
534 if (response.status_code >= 200 && response.status_code < 300) {
536 JAMI_LOG(
"[Account {}] [Auth] Got server response: {} bytes", this_->accountId_, response.body.size());
537 if (not json.isArray()) {
538 JAMI_ERROR(
"[Auth] Unable to parse server response: not an array");
540 for (
unsigned i = 0, n = json.size(); i < n; i++) {
541 const auto& e = json[i];
542 const bool revoked = e[
"revoked"].asBool();
543 dht::PkId deviceId(e[
"deviceId"].
asString());
548 this_->info_->contacts->foundAccountDevice(deviceId,
553 this_->info_->contacts->removeAccountDevice(deviceId);
557 }
catch (
const std::exception& e) {
558 JAMI_ERROR(
"Error when iterating device list: {}", e.what());
560 }
else if (response.status_code == 401)
561 this_->authError(TokenScope::Device);
563 this_->clearRequest(response.request);
571 auto syncBlueprintCallback = std::make_shared<SyncBlueprintCallback>(onSuccess);
572 const std::string urlBlueprints = managerHostname_ +
PATH_BLUEPRINT;
573 JAMI_DEBUG(
"[Auth] Synchronize blueprint configuration {}", urlBlueprints);
574 sendDeviceRequest(std::make_shared<Request>(
575 *Manager::instance().ioContext(),
577 [syncBlueprintCallback, w=weak_from_this()](Json::Value json,
const dht::http::Response& response) {
578 JAMI_DEBUG(
"[Auth] Got sync request callback with status code={}", response.status_code);
579 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
581 if (response.status_code >= 200 && response.status_code < 300) {
583 std::map<std::string, std::string> config;
584 for (
auto itr = json.begin(); itr != json.end(); ++itr) {
585 const auto& name = itr.name();
586 config.emplace(name, itr->asString());
588 (*syncBlueprintCallback)(config);
589 }
catch (
const std::exception& e) {
590 JAMI_ERROR(
"Error when iterating blueprint config json: {}", e.what());
592 }
else if (response.status_code == 401)
593 this_->authError(TokenScope::Device);
594 this_->clearRequest(response.request);
600ServerAccountManager::revokeDevice(
const std::string& device,
601 std::string_view scheme,
const std::string& password,
604 if (not info_ || scheme != fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD) {
606 cb(RevokeDeviceResult::ERROR_CREDENTIALS);
609 const std::string url = managerHostname_ +
PATH_DEVICE +
"/" + device;
610 JAMI_WARNING(
"[Revoke] Revoking device of {} at {}", info_->username, url);
611 auto request = std::make_shared<Request>(
612 *Manager::instance().ioContext(),
614 [cb, w=weak_from_this()](Json::Value json,
const dht::http::Response& response) {
615 JAMI_DEBUG(
"[Revoke] Got request callback with status code={}", response.status_code);
616 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
618 if (response.status_code >= 200 && response.status_code < 300) {
621 if (json[
"errorDetails"].empty()) {
623 cb(RevokeDeviceResult::SUCCESS);
624 this_->syncDevices();
626 }
catch (
const std::exception& e) {
627 JAMI_ERROR(
"Error when loading device list: {}", e.what());
630 cb(RevokeDeviceResult::ERROR_NETWORK);
631 this_->clearRequest(response.request);
634 request->set_method(restinio::http_method_delete());
635 sendAccountRequest(request, password);
640ServerAccountManager::registerName(
const std::string& name, std::string_view scheme,
const std::string&,
RegistrationCallback cb)
642 cb(NameDirectory::RegistrationResponse::unsupported, name);
648 const std::string url = managerHostname_ +
PATH_SEARCH +
"?queryString=" + query;
649 JAMI_WARNING(
"[Search] Searching user {} at {}", query, url);
650 sendDeviceRequest(std::make_shared<Request>(
651 *Manager::instance().ioContext(),
653 [cb, w=weak_from_this()](Json::Value json,
const dht::http::Response& response) {
654 JAMI_DEBUG(
"[Search] Got request callback with status code={}", response.status_code);
655 auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
657 if (response.status_code >= 200 && response.status_code < 300) {
659 const auto& profiles = json[
"profiles"];
660 Json::Value::ArrayIndex rcount = profiles.size();
661 std::vector<std::map<std::string, std::string>> results;
662 results.reserve(rcount);
663 JAMI_WARNING(
"[Search] Got server response: {} bytes", response.body.size());
664 for (Json::Value::ArrayIndex i = 0; i < rcount; i++) {
665 const auto& ruser = profiles[i];
666 std::map<std::string, std::string> user;
667 for (
const auto& member : ruser.getMemberNames()) {
668 const auto& rmember = ruser[member];
669 if (rmember.isString())
670 user[member] = rmember.asString();
672 results.emplace_back(std::move(user));
675 cb(results, SearchResponse::found);
676 }
catch (
const std::exception& e) {
677 JAMI_ERROR(
"[Search] Error during search: {}", e.what());
680 if (response.status_code == 401)
681 this_->authError(TokenScope::Device);
683 cb({}, SearchResponse::error);
685 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()
std::shared_ptr< asio::io_context > ioContext() const
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< uint8_t > 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[]
std::vector< std::map< std::string, std::string > > getContacts(const std::string &accountId)
std::map< std::string, ConvInfo > c