Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 : #include "server_account_manager.h"
18 : #include "base64.h"
19 : #include "jami/account_const.h"
20 : #include "fileutils.h"
21 : #include "logger.h"
22 :
23 : #include <opendht/http.h>
24 : #include <opendht/log.h>
25 : #include <opendht/thread_pool.h>
26 :
27 : #include "conversation_module.h"
28 : #include "jamiaccount.h"
29 : #include "manager.h"
30 :
31 : #include <algorithm>
32 : #include <string_view>
33 :
34 : using namespace std::literals;
35 :
36 : namespace jami {
37 :
38 : using Request = dht::http::Request;
39 :
40 : #define JAMI_PATH_LOGIN "/api/login"
41 : #define JAMI_PATH_AUTH "/api/auth"
42 : constexpr std::string_view PATH_DEVICE = JAMI_PATH_AUTH "/device";
43 : constexpr std::string_view PATH_DEVICES = JAMI_PATH_AUTH "/devices";
44 : constexpr std::string_view PATH_SEARCH = JAMI_PATH_AUTH "/directory/search";
45 : constexpr std::string_view PATH_CONTACTS = JAMI_PATH_AUTH "/contacts";
46 : constexpr std::string_view PATH_CONVERSATIONS = JAMI_PATH_AUTH "/conversations";
47 : constexpr std::string_view PATH_CONVERSATIONS_REQUESTS = JAMI_PATH_AUTH "/conversationRequests";
48 : constexpr std::string_view PATH_BLUEPRINT = JAMI_PATH_AUTH "/policyData";
49 :
50 0 : ServerAccountManager::ServerAccountManager(const std::string& accountId,
51 : const std::filesystem::path& path,
52 : const std::string& managerHostname,
53 0 : const std::string& nameServer)
54 : : AccountManager(accountId, path, nameServer)
55 0 : , managerHostname_(managerHostname)
56 0 : , logger_(Logger::dhtLogger())
57 0 : {}
58 :
59 : void
60 0 : ServerAccountManager::setAuthHeaderFields(Request& request) const
61 : {
62 0 : request.set_header_field(restinio::http_field_t::authorization, "Bearer " + token_);
63 0 : }
64 :
65 : void
66 0 : ServerAccountManager::initAuthentication(std::string deviceName,
67 : std::unique_ptr<AccountCredentials> credentials,
68 : AuthSuccessCallback onSuccess,
69 : AuthFailureCallback onFailure,
70 : const OnChangeCallback& onChange)
71 : {
72 0 : auto ctx = std::make_shared<AuthContext>();
73 0 : ctx->accountId = accountId_;
74 0 : ctx->deviceName = std::move(deviceName);
75 0 : ctx->credentials = dynamic_unique_cast<ServerAccountCredentials>(std::move(credentials));
76 0 : ctx->onSuccess = std::move(onSuccess);
77 0 : ctx->onFailure = std::move(onFailure);
78 0 : if (not ctx->credentials or ctx->credentials->username.empty()) {
79 0 : ctx->onFailure(AuthError::INVALID_ARGUMENTS, "invalid credentials");
80 0 : return;
81 : }
82 :
83 : // Reuse the existing private key if there is one.
84 0 : if (auto privateKey = ctx->credentials->identity.first) {
85 0 : JAMI_LOG("[Account {}] [Auth] Authenticating with existing device private key", accountId_);
86 0 : std::promise<std::shared_ptr<dht::crypto::PrivateKey>> keyPromise;
87 0 : keyPromise.set_value(privateKey);
88 0 : ctx->key = keyPromise.get_future();
89 0 : } else {
90 0 : JAMI_LOG("[Account {}] [Auth] Authenticating with new device private key", accountId_);
91 0 : ctx->key = dht::ThreadPool::computation().getShared<std::shared_ptr<dht::crypto::PrivateKey>>(
92 0 : []() { return std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate()); });
93 0 : }
94 0 : ctx->request = buildRequest(ctx->key);
95 :
96 0 : onChange_ = std::move(onChange);
97 0 : const std::string url = managerHostname_ + PATH_DEVICE;
98 0 : JAMI_WARNING("[Account {}] [Auth] Authentication with: {} to {}", accountId_, ctx->credentials->username, url);
99 :
100 0 : dht::ThreadPool::computation().run([ctx, url, w = weak_from_this()] {
101 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
102 0 : if (not this_)
103 0 : return;
104 0 : Json::Value body;
105 : {
106 0 : auto csr = ctx->request.get()->toString();
107 0 : body["csr"] = csr;
108 0 : body["deviceName"] = ctx->deviceName;
109 0 : }
110 : auto request = std::make_shared<Request>(
111 0 : *Manager::instance().ioContext(),
112 0 : url,
113 : body,
114 0 : [ctx, w](Json::Value json, const dht::http::Response& response) {
115 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
116 0 : JAMI_DEBUG("[Auth] Got request callback with status code={}", response.status_code);
117 0 : if (response.status_code == 0 || this_ == nullptr)
118 0 : ctx->onFailure(AuthError::SERVER_ERROR, "Unable to connect to server");
119 0 : else if (response.status_code >= 400 && response.status_code < 500)
120 0 : ctx->onFailure(AuthError::INVALID_ARGUMENTS, "Invalid credentials provided!");
121 0 : else if (response.status_code < 200 || response.status_code > 299)
122 0 : ctx->onFailure(AuthError::INVALID_ARGUMENTS, "");
123 : else {
124 : do {
125 : try {
126 0 : JAMI_WARNING("[Account {}] [Auth] Got server response: {} bytes",
127 : this_->accountId_,
128 : response.body.size());
129 0 : auto cert = std::make_shared<dht::crypto::Certificate>(json["certificateChain"].asString());
130 0 : auto accountCert = cert->issuer;
131 0 : if (not accountCert) {
132 0 : JAMI_ERROR("[Auth] Unable to parse certificate: no issuer");
133 0 : ctx->onFailure(AuthError::SERVER_ERROR, "Invalid certificate from server");
134 0 : break;
135 : }
136 0 : auto receipt = json["deviceReceipt"].asString();
137 0 : Json::Value receiptJson;
138 0 : std::string err;
139 : auto receiptReader = std::unique_ptr<Json::CharReader>(
140 0 : Json::CharReaderBuilder {}.newCharReader());
141 0 : if (!receiptReader->parse(receipt.data(),
142 0 : receipt.data() + receipt.size(),
143 : &receiptJson,
144 : &err)) {
145 0 : JAMI_ERROR("[Auth] Unable to parse receipt from server: {}", err);
146 0 : ctx->onFailure(AuthError::SERVER_ERROR, "Unable to parse receipt from server");
147 0 : break;
148 : }
149 0 : auto receiptSignature = base64::decode(json["receiptSignature"].asString());
150 :
151 0 : auto info = std::make_unique<AccountInfo>();
152 0 : info->identity.first = ctx->key.get();
153 0 : info->identity.second = cert;
154 0 : info->devicePk = cert->getSharedPublicKey();
155 0 : info->deviceId = info->devicePk->getLongId().toString();
156 0 : info->accountId = accountCert->getId().toString();
157 0 : info->contacts = std::make_unique<ContactList>(ctx->accountId,
158 : accountCert,
159 0 : this_->path_,
160 0 : this_->onChange_);
161 0 : info->contacts->load();
162 0 : if (ctx->deviceName.empty())
163 0 : ctx->deviceName = info->deviceId.substr(8);
164 0 : info->contacts->foundAccountDevice(cert, ctx->deviceName, clock::now());
165 0 : info->ethAccount = receiptJson["eth"].asString();
166 0 : info->announce = parseAnnounce(receiptJson["announce"].asString(),
167 0 : info->accountId,
168 0 : info->devicePk->getId().toString(),
169 0 : info->devicePk->getLongId().toString());
170 0 : if (not info->announce) {
171 0 : ctx->onFailure(AuthError::SERVER_ERROR, "Unable to parse announce from server");
172 0 : return;
173 : }
174 0 : info->username = ctx->credentials->username;
175 :
176 0 : this_->creds_ = std::move(ctx->credentials);
177 0 : this_->info_ = std::move(info);
178 0 : std::map<std::string, std::string> config;
179 0 : for (auto itr = json.begin(); itr != json.end(); ++itr) {
180 0 : const auto& name = itr.name();
181 0 : if (name == "nameServer"sv) {
182 0 : auto nameServer = json["nameServer"].asString();
183 0 : if (!nameServer.empty() && nameServer[0] == '/')
184 0 : nameServer = this_->managerHostname_ + nameServer;
185 0 : this_->nameDir_ = NameDirectory::instance(nameServer);
186 0 : config.emplace(libjami::Account::ConfProperties::Nameserver::URI,
187 0 : std::move(nameServer));
188 0 : } else if (name == "userPhoto"sv) {
189 0 : this_->info_->photo = json["userPhoto"].asString();
190 0 : } else if (name == libjami::Account::ConfProperties::DISPLAYNAME) {
191 0 : this_->info_->displayName = json[libjami::Account::ConfProperties::DISPLAYNAME]
192 0 : .asString();
193 : } else {
194 0 : config.emplace(name, itr->asString());
195 : }
196 0 : }
197 :
198 0 : ctx->onSuccess(*this_->info_,
199 0 : std::move(config),
200 0 : std::move(receipt),
201 0 : std::move(receiptSignature));
202 0 : } catch (const std::exception& e) {
203 0 : JAMI_ERROR("[Account {}] [Auth] Error when loading account: {}",
204 : this_->accountId_,
205 : e.what());
206 0 : ctx->onFailure(AuthError::NETWORK, "");
207 0 : }
208 : } while (false);
209 : }
210 :
211 0 : if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
212 0 : this_->clearRequest(response.request);
213 0 : },
214 0 : this_->logger_);
215 0 : request->set_auth(ctx->credentials->username, ctx->credentials->password);
216 0 : this_->sendRequest(request);
217 0 : });
218 0 : }
219 :
220 : void
221 0 : ServerAccountManager::onAuthEnded(const Json::Value& json, const dht::http::Response& response, TokenScope expectedScope)
222 : {
223 0 : if (response.status_code >= 200 && response.status_code < 300) {
224 0 : auto scopeStr = json["scope"].asString();
225 0 : auto scope = scopeStr == "DEVICE"sv ? TokenScope::Device
226 0 : : (scopeStr == "USER"sv ? TokenScope::User : TokenScope::None);
227 0 : auto expires_in = json["expires_in"].asLargestUInt();
228 0 : auto expiration = std::chrono::steady_clock::now() + std::chrono::seconds(expires_in);
229 0 : JAMI_WARNING("[Account {}] [Auth] Got server response: {} ({} bytes)",
230 : accountId_,
231 : response.status_code,
232 : response.body.size());
233 0 : setToken(json["access_token"].asString(), scope, expiration);
234 0 : } else {
235 0 : JAMI_WARNING("[Account {}] [Auth] Got server response: {} ({} bytes)",
236 : accountId_,
237 : response.status_code,
238 : response.body.size());
239 0 : authFailed(expectedScope, response.status_code);
240 : }
241 0 : clearRequest(response.request);
242 0 : }
243 :
244 : void
245 0 : ServerAccountManager::authenticateDevice()
246 : {
247 0 : if (not info_) {
248 0 : authFailed(TokenScope::Device, 0);
249 : }
250 0 : const std::string url = managerHostname_ + JAMI_PATH_LOGIN;
251 0 : JAMI_WARNING("[Account {}] [Auth] Getting a device token: {}", accountId_, url);
252 : auto request = std::make_shared<Request>(
253 0 : *Manager::instance().ioContext(),
254 : url,
255 0 : Json::Value {Json::objectValue},
256 0 : [w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
257 0 : if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
258 0 : this_->onAuthEnded(json, response, TokenScope::Device);
259 0 : },
260 0 : logger_);
261 0 : request->set_identity(info_->identity);
262 : // request->set_certificate_authority(info_->identity.second->issuer->issuer);
263 0 : sendRequest(request);
264 0 : }
265 :
266 : void
267 0 : ServerAccountManager::authenticateAccount(const std::string& username, const std::string& password)
268 : {
269 0 : const std::string url = managerHostname_ + JAMI_PATH_LOGIN;
270 0 : JAMI_WARNING("[Account {}] [Auth] Getting a user token: {}", accountId_, url);
271 : auto request = std::make_shared<Request>(
272 0 : *Manager::instance().ioContext(),
273 : url,
274 0 : Json::Value {Json::objectValue},
275 0 : [w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
276 0 : if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock()))
277 0 : this_->onAuthEnded(json, response, TokenScope::User);
278 0 : },
279 0 : logger_);
280 0 : request->set_auth(username, password);
281 0 : sendRequest(request);
282 0 : }
283 :
284 : void
285 0 : ServerAccountManager::sendRequest(const std::shared_ptr<dht::http::Request>& request)
286 : {
287 0 : request->set_header_field(restinio::http_field_t::user_agent, userAgent());
288 : {
289 0 : std::lock_guard lock(requestLock_);
290 0 : requests_.emplace(request);
291 0 : }
292 0 : request->send();
293 0 : }
294 :
295 : void
296 0 : ServerAccountManager::clearRequest(const std::weak_ptr<dht::http::Request>& request)
297 : {
298 0 : asio::post(*Manager::instance().ioContext(), [w = weak_from_this(), request] {
299 0 : if (auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock())) {
300 0 : if (auto req = request.lock()) {
301 0 : std::lock_guard lock(this_->requestLock_);
302 0 : this_->requests_.erase(req);
303 0 : }
304 0 : }
305 0 : });
306 0 : }
307 :
308 : void
309 0 : ServerAccountManager::authFailed(TokenScope scope, int code)
310 : {
311 0 : RequestQueue requests;
312 : {
313 0 : std::lock_guard lock(tokenLock_);
314 0 : requests = std::move(getRequestQueue(scope));
315 0 : }
316 0 : JAMI_DEBUG("[Auth] Failed auth with scope {}, ending {} pending requests", (int) scope, requests.size());
317 0 : if (code == 401) {
318 : // NOTE: we do not login every time to the server but retrieve a device token to use the
319 : // account If authentificate device fails with 401 it means that the device is revoked
320 0 : if (onDeviceRevoked_)
321 0 : onDeviceRevoked_();
322 : }
323 0 : while (not requests.empty()) {
324 0 : auto req = std::move(requests.front());
325 0 : requests.pop();
326 0 : req->terminate(code == 0 ? asio::error::not_connected : asio::error::access_denied);
327 0 : }
328 0 : }
329 :
330 : void
331 0 : ServerAccountManager::authError(TokenScope scope)
332 : {
333 : {
334 0 : std::lock_guard lock(tokenLock_);
335 0 : if (scope <= tokenScope_) {
336 0 : token_ = {};
337 0 : tokenScope_ = TokenScope::None;
338 : }
339 0 : }
340 0 : if (scope == TokenScope::Device)
341 0 : authenticateDevice();
342 0 : }
343 :
344 : void
345 0 : ServerAccountManager::setToken(std::string token, TokenScope scope, std::chrono::steady_clock::time_point expiration)
346 : {
347 0 : std::lock_guard lock(tokenLock_);
348 0 : token_ = std::move(token);
349 0 : tokenScope_ = scope;
350 0 : tokenExpire_ = expiration;
351 :
352 0 : nameDir_.get().setToken(token_);
353 0 : if (not token_.empty() and scope != TokenScope::None) {
354 0 : auto& reqQueue = getRequestQueue(scope);
355 0 : JAMI_DEBUG("[Account {}] [Auth] Got token with scope {}, handling {} pending requests",
356 : accountId_,
357 : (int) scope,
358 : reqQueue.size());
359 0 : while (not reqQueue.empty()) {
360 0 : auto req = std::move(reqQueue.front());
361 0 : reqQueue.pop();
362 0 : setAuthHeaderFields(*req);
363 0 : sendRequest(req);
364 0 : }
365 : }
366 0 : }
367 :
368 : void
369 0 : ServerAccountManager::sendDeviceRequest(const std::shared_ptr<dht::http::Request>& req)
370 : {
371 0 : std::lock_guard lock(tokenLock_);
372 0 : if (hasAuthorization(TokenScope::Device)) {
373 0 : setAuthHeaderFields(*req);
374 0 : sendRequest(req);
375 : } else {
376 0 : auto& rQueue = getRequestQueue(TokenScope::Device);
377 0 : if (rQueue.empty())
378 0 : authenticateDevice();
379 0 : rQueue.emplace(req);
380 : }
381 0 : }
382 :
383 : void
384 0 : ServerAccountManager::sendAccountRequest(const std::shared_ptr<dht::http::Request>& req, const std::string& pwd)
385 : {
386 0 : std::lock_guard lock(tokenLock_);
387 0 : if (hasAuthorization(TokenScope::User)) {
388 0 : setAuthHeaderFields(*req);
389 0 : sendRequest(req);
390 : } else {
391 0 : auto& rQueue = getRequestQueue(TokenScope::User);
392 0 : if (rQueue.empty())
393 0 : authenticateAccount(info_->username, pwd);
394 0 : rQueue.emplace(req);
395 : }
396 0 : }
397 :
398 : void
399 0 : ServerAccountManager::syncDevices()
400 : {
401 0 : const std::string urlDevices = managerHostname_ + PATH_DEVICES;
402 0 : const std::string urlContacts = managerHostname_ + PATH_CONTACTS;
403 0 : const std::string urlConversations = managerHostname_ + PATH_CONVERSATIONS;
404 0 : const std::string urlConversationsRequests = managerHostname_ + PATH_CONVERSATIONS_REQUESTS;
405 :
406 0 : JAMI_WARNING("[Account {}] [Auth] Sync conversations {}", accountId_, urlConversations);
407 0 : Json::Value jsonConversations(Json::arrayValue);
408 0 : for (const auto& [key, convInfo] : ConversationModule::convInfos(accountId_)) {
409 0 : jsonConversations.append(convInfo.toJson());
410 0 : }
411 0 : sendDeviceRequest(std::make_shared<Request>(
412 0 : *Manager::instance().ioContext(),
413 : urlConversations,
414 : jsonConversations,
415 0 : [w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
416 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
417 0 : if (!this_)
418 0 : return;
419 0 : JAMI_DEBUG("[Account {}] [Auth] Got conversation sync request callback with status code={}",
420 : this_->accountId_,
421 : response.status_code);
422 0 : if (response.status_code >= 200 && response.status_code < 300) {
423 : try {
424 0 : JAMI_WARNING("[Account {}] [Auth] Got server response: {} bytes",
425 : this_->accountId_,
426 : response.body.size());
427 0 : if (not json.isArray()) {
428 0 : JAMI_ERROR("[Account {}] [Auth] Unable to parse server response: not an array",
429 : this_->accountId_);
430 : } else {
431 0 : SyncMsg convs;
432 0 : for (unsigned i = 0, n = json.size(); i < n; i++) {
433 0 : const auto& e = json[i];
434 0 : auto ci = ConvInfo(e);
435 0 : convs.c[ci.id] = std::move(ci);
436 0 : }
437 0 : dht::ThreadPool::io().run([accountId = this_->accountId_, convs] {
438 0 : if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
439 0 : acc->convModule()->onSyncData(convs, "", "");
440 0 : }
441 0 : });
442 0 : }
443 0 : } catch (const std::exception& e) {
444 0 : JAMI_ERROR("[Account {}] Error when iterating conversation list: {}", this_->accountId_, e.what());
445 0 : }
446 0 : } else if (response.status_code == 401)
447 0 : this_->authError(TokenScope::Device);
448 :
449 0 : this_->clearRequest(response.request);
450 0 : },
451 0 : logger_));
452 :
453 0 : JAMI_WARNING("[Account {}] [Auth] Sync conversations requests {}", accountId_, urlConversationsRequests);
454 0 : Json::Value jsonConversationsRequests(Json::arrayValue);
455 0 : for (const auto& [key, convRequest] : ConversationModule::convRequests(accountId_)) {
456 0 : auto jsonConversation = convRequest.toJson();
457 0 : jsonConversationsRequests.append(std::move(jsonConversation));
458 0 : }
459 0 : sendDeviceRequest(std::make_shared<Request>(
460 0 : *Manager::instance().ioContext(),
461 : urlConversationsRequests,
462 : jsonConversationsRequests,
463 0 : [w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
464 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
465 0 : if (!this_)
466 0 : return;
467 0 : JAMI_DEBUG("[Account {}] [Auth] Got conversations requests sync request callback with "
468 : "status code={}",
469 : this_->accountId_,
470 : response.status_code);
471 0 : if (response.status_code >= 200 && response.status_code < 300) {
472 : try {
473 0 : JAMI_WARNING("[Account {}] [Auth] Got server response: {}", this_->accountId_, response.body);
474 0 : if (not json.isArray()) {
475 0 : JAMI_ERROR("[Account {}] [Auth] Unable to parse server response: not an array",
476 : this_->accountId_);
477 : } else {
478 0 : SyncMsg convReqs;
479 0 : for (unsigned i = 0, n = json.size(); i < n; i++) {
480 0 : const auto& e = json[i];
481 0 : auto cr = ConversationRequest(e);
482 0 : convReqs.cr[cr.conversationId] = std::move(cr);
483 0 : }
484 0 : dht::ThreadPool::io().run([accountId = this_->accountId_, convReqs] {
485 0 : if (auto acc = Manager::instance().getAccount<JamiAccount>(accountId)) {
486 0 : acc->convModule()->onSyncData(convReqs, "", "");
487 0 : }
488 0 : });
489 0 : }
490 0 : } catch (const std::exception& e) {
491 0 : JAMI_ERROR("[Account {}] Error when iterating conversations requests list: {}",
492 : this_->accountId_,
493 : e.what());
494 0 : }
495 0 : } else if (response.status_code == 401)
496 0 : this_->authError(TokenScope::Device);
497 :
498 0 : this_->clearRequest(response.request);
499 0 : },
500 0 : logger_));
501 :
502 0 : JAMI_WARNING("[Account {}] [Auth] syncContacts {}", accountId_, urlContacts);
503 0 : Json::Value jsonContacts(Json::arrayValue);
504 0 : for (const auto& contact : info_->contacts->getContacts()) {
505 0 : auto jsonContact = contact.second.toJson();
506 0 : jsonContact["uri"] = contact.first.toString();
507 0 : jsonContacts.append(std::move(jsonContact));
508 0 : }
509 0 : sendDeviceRequest(std::make_shared<Request>(
510 0 : *Manager::instance().ioContext(),
511 : urlContacts,
512 : jsonContacts,
513 0 : [w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
514 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
515 0 : if (!this_)
516 0 : return;
517 0 : JAMI_DEBUG("[Account {}] [Auth] Got contact sync request callback with status code={}",
518 : this_->accountId_,
519 : response.status_code);
520 0 : if (response.status_code >= 200 && response.status_code < 300) {
521 : try {
522 0 : JAMI_WARNING("[Account {}] [Auth] Got server response: {} bytes",
523 : this_->accountId_,
524 : response.body.size());
525 0 : if (not json.isArray()) {
526 0 : JAMI_ERROR("[Auth] Unable to parse server response: not an array");
527 : } else {
528 0 : for (unsigned i = 0, n = json.size(); i < n; i++) {
529 0 : const auto& e = json[i];
530 0 : Contact contact(e);
531 0 : this_->info_->contacts->updateContact(dht::InfoHash {e["uri"].asString()}, contact);
532 0 : }
533 0 : this_->info_->contacts->saveContacts();
534 : }
535 0 : } catch (const std::exception& e) {
536 0 : JAMI_ERROR("Error when iterating contact list: {}", e.what());
537 0 : }
538 0 : } else if (response.status_code == 401)
539 0 : this_->authError(TokenScope::Device);
540 :
541 0 : this_->clearRequest(response.request);
542 0 : },
543 0 : logger_));
544 :
545 0 : JAMI_WARNING("[Account {}] [Auth] syncDevices {}", accountId_, urlDevices);
546 0 : sendDeviceRequest(std::make_shared<Request>(
547 0 : *Manager::instance().ioContext(),
548 : urlDevices,
549 0 : [w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
550 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
551 0 : if (!this_)
552 0 : return;
553 0 : JAMI_DEBUG("[Account {}] [Auth] Got request callback with status code={}",
554 : this_->accountId_,
555 : response.status_code);
556 0 : if (response.status_code >= 200 && response.status_code < 300) {
557 : try {
558 0 : JAMI_LOG("[Account {}] [Auth] Got server response: {} bytes",
559 : this_->accountId_,
560 : response.body.size());
561 0 : if (not json.isArray()) {
562 0 : JAMI_ERROR("[Auth] Unable to parse server response: not an array");
563 : } else {
564 0 : for (unsigned i = 0, n = json.size(); i < n; i++) {
565 0 : const auto& e = json[i];
566 0 : const bool revoked = e["revoked"].asBool();
567 0 : dht::PkId deviceId(e["deviceId"].asString());
568 0 : if (!deviceId) {
569 0 : continue;
570 : }
571 0 : if (!revoked) {
572 0 : this_->info_->contacts->foundAccountDevice(deviceId,
573 0 : e["alias"].asString(),
574 0 : clock::now());
575 : } else {
576 0 : this_->info_->contacts->removeAccountDevice(deviceId);
577 : }
578 : }
579 : }
580 0 : } catch (const std::exception& e) {
581 0 : JAMI_ERROR("Error when iterating device list: {}", e.what());
582 0 : }
583 0 : } else if (response.status_code == 401)
584 0 : this_->authError(TokenScope::Device);
585 :
586 0 : this_->clearRequest(response.request);
587 0 : },
588 0 : logger_));
589 0 : }
590 :
591 : void
592 0 : ServerAccountManager::syncBlueprintConfig(SyncBlueprintCallback onSuccess)
593 : {
594 0 : auto syncBlueprintCallback = std::make_shared<SyncBlueprintCallback>(onSuccess);
595 0 : const std::string urlBlueprints = managerHostname_ + PATH_BLUEPRINT;
596 0 : JAMI_DEBUG("[Auth] Synchronize blueprint configuration {}", urlBlueprints);
597 0 : sendDeviceRequest(std::make_shared<Request>(
598 0 : *Manager::instance().ioContext(),
599 : urlBlueprints,
600 0 : [syncBlueprintCallback, w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
601 0 : JAMI_DEBUG("[Auth] Got sync request callback with status code={}", response.status_code);
602 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
603 0 : if (!this_)
604 0 : return;
605 0 : if (response.status_code >= 200 && response.status_code < 300) {
606 : try {
607 0 : std::map<std::string, std::string> config;
608 0 : for (auto itr = json.begin(); itr != json.end(); ++itr) {
609 0 : const auto& name = itr.name();
610 0 : config.emplace(name, itr->asString());
611 0 : }
612 0 : (*syncBlueprintCallback)(config);
613 0 : } catch (const std::exception& e) {
614 0 : JAMI_ERROR("Error when iterating blueprint config json: {}", e.what());
615 0 : }
616 0 : } else if (response.status_code == 401)
617 0 : this_->authError(TokenScope::Device);
618 0 : this_->clearRequest(response.request);
619 0 : },
620 0 : logger_));
621 0 : }
622 :
623 : bool
624 0 : ServerAccountManager::revokeDevice(const std::string& device,
625 : std::string_view scheme,
626 : const std::string& password,
627 : RevokeDeviceCallback cb)
628 : {
629 0 : if (not info_ || scheme != fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD) {
630 0 : if (cb)
631 0 : cb(RevokeDeviceResult::ERROR_CREDENTIALS);
632 0 : return false;
633 : }
634 0 : const std::string url = managerHostname_ + PATH_DEVICE + "/" + device;
635 0 : JAMI_WARNING("[Revoke] Revoking device of {} at {}", info_->username, url);
636 : auto request = std::make_shared<Request>(
637 0 : *Manager::instance().ioContext(),
638 : url,
639 0 : [cb, w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
640 0 : JAMI_DEBUG("[Revoke] Got request callback with status code={}", response.status_code);
641 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
642 0 : if (!this_)
643 0 : return;
644 0 : if (response.status_code >= 200 && response.status_code < 300) {
645 : try {
646 0 : JAMI_WARNING("[Revoke] Got server response");
647 0 : if (json["errorDetails"].empty()) {
648 0 : if (cb)
649 0 : cb(RevokeDeviceResult::SUCCESS);
650 0 : this_->syncDevices(); // this will remove the devices from the known devices
651 : }
652 0 : } catch (const std::exception& e) {
653 0 : JAMI_ERROR("Error when loading device list: {}", e.what());
654 0 : }
655 0 : } else if (cb)
656 0 : cb(RevokeDeviceResult::ERROR_NETWORK);
657 0 : this_->clearRequest(response.request);
658 0 : },
659 0 : logger_);
660 0 : request->set_method(restinio::http_method_delete());
661 0 : sendAccountRequest(request, password);
662 0 : return true;
663 0 : }
664 :
665 : void
666 0 : ServerAccountManager::registerName(const std::string& name,
667 : std::string_view /*scheme*/,
668 : const std::string&,
669 : RegistrationCallback cb)
670 : {
671 0 : cb(NameDirectory::RegistrationResponse::unsupported, name);
672 0 : }
673 :
674 : bool
675 0 : ServerAccountManager::searchUser(const std::string& query, SearchCallback cb)
676 : {
677 0 : const std::string url = managerHostname_ + PATH_SEARCH + "?queryString=" + query;
678 0 : JAMI_WARNING("[Search] Searching user {} at {}", query, url);
679 0 : sendDeviceRequest(std::make_shared<Request>(
680 0 : *Manager::instance().ioContext(),
681 : url,
682 0 : [cb, w = weak_from_this()](Json::Value json, const dht::http::Response& response) {
683 0 : JAMI_DEBUG("[Search] Got request callback with status code={}", response.status_code);
684 0 : auto this_ = std::static_pointer_cast<ServerAccountManager>(w.lock());
685 0 : if (!this_)
686 0 : return;
687 0 : if (response.status_code >= 200 && response.status_code < 300) {
688 : try {
689 0 : const auto& profiles = json["profiles"];
690 0 : Json::Value::ArrayIndex rcount = profiles.size();
691 0 : std::vector<std::map<std::string, std::string>> results;
692 0 : results.reserve(rcount);
693 0 : JAMI_WARNING("[Search] Got server response: {} bytes", response.body.size());
694 0 : for (Json::Value::ArrayIndex i = 0; i < rcount; i++) {
695 0 : const auto& ruser = profiles[i];
696 0 : std::map<std::string, std::string> user;
697 0 : for (const auto& member : ruser.getMemberNames()) {
698 0 : const auto& rmember = ruser[member];
699 0 : if (rmember.isString())
700 0 : user[member] = rmember.asString();
701 0 : }
702 0 : results.emplace_back(std::move(user));
703 0 : }
704 0 : if (cb)
705 0 : cb(results, SearchResponse::found);
706 0 : } catch (const std::exception& e) {
707 0 : JAMI_ERROR("[Search] Error during search: {}", e.what());
708 0 : }
709 0 : } else {
710 0 : if (response.status_code == 401)
711 0 : this_->authError(TokenScope::Device);
712 0 : if (cb)
713 0 : cb({}, SearchResponse::error);
714 : }
715 0 : this_->clearRequest(response.request);
716 0 : },
717 0 : logger_));
718 0 : return true;
719 0 : }
720 :
721 : } // namespace jami
|