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