• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

realm / realm-core / 2604

05 Sep 2024 08:32AM UTC coverage: 91.108% (-0.04%) from 91.149%
2604

push

Evergreen

jedelbo
Add failing test

102796 of 181478 branches covered (56.64%)

8 of 8 new or added lines in 1 file covered. (100.0%)

122 existing lines in 17 files now uncovered.

217184 of 238381 relevant lines covered (91.11%)

5669407.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.68
/src/realm/object-store/sync/app.cpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2020 Realm Inc.
4
//
5
// Licensed under the Apache License, Version 2.0 (the "License");
6
// you may not use this file except in compliance with the License.
7
// You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing, software
12
// distributed under the License is distributed on an "AS IS" BASIS,
13
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or utilied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#include <realm/object-store/sync/app.hpp>
20

21
#include <realm/sync/network/http.hpp>
22
#include <realm/util/base64.hpp>
23
#include <realm/util/flat_map.hpp>
24
#include <realm/util/platform_info.hpp>
25
#include <realm/util/uri.hpp>
26
#include <realm/object-store/sync/app_user.hpp>
27
#include <realm/object-store/sync/app_utils.hpp>
28
#include <realm/object-store/sync/impl/app_metadata.hpp>
29
#include <realm/object-store/sync/impl/sync_file.hpp>
30
#include <realm/object-store/sync/sync_manager.hpp>
31

32
#ifdef __EMSCRIPTEN__
33
#include <realm/object-store/sync/impl/emscripten/network_transport.hpp>
34
#endif
35

36
#include <external/json/json.hpp>
37
#include <sstream>
38
#include <string>
39

40
using namespace realm;
41
using namespace realm::app;
42
using namespace bson;
43
using util::Optional;
44
using util::UniqueFunction;
45

46
namespace {
47
// MARK: - Helpers
48

49
REALM_COLD
50
REALM_NOINLINE
51
REALM_NORETURN
52
void throw_json_error(ErrorCodes::Error ec, std::string_view message)
53
{
2✔
54
    throw AppError(ec, std::string(message));
2✔
55
}
2✔
56

57
template <typename T>
58
T as(const Bson& bson)
59
{
67,234✔
60
    if (holds_alternative<T>(bson)) {
67,234✔
61
        return static_cast<T>(bson);
67,234✔
62
    }
67,234✔
63
    throw_json_error(ErrorCodes::MalformedJson, "?");
×
64
}
×
65

66
template <typename T>
67
T get(const BsonDocument& doc, const std::string& key)
68
{
47,985✔
69
    if (auto val = doc.find(key)) {
47,985✔
70
        return as<T>(*val);
47,985✔
71
    }
47,985✔
72
    throw_json_error(ErrorCodes::MissingJsonKey, key);
×
73
    return {};
×
74
}
47,985✔
75

76
template <typename T>
77
void read_field(const BsonDocument& data, const std::string& key, T& value)
78
{
64✔
79
    if (auto val = data.find(key)) {
64✔
80
        value = as<T>(*val);
64✔
81
    }
64✔
82
    else {
×
83
        throw_json_error(ErrorCodes::MissingJsonKey, key);
×
84
    }
×
85
}
64✔
86

87
template <>
88
void read_field(const BsonDocument& data, const std::string& key, ObjectId& value)
89
{
32✔
90
    value = ObjectId(get<std::string>(data, key).c_str());
32✔
91
}
32✔
92

93
template <typename T>
94
void read_field(const BsonDocument& data, const std::string& key, Optional<T>& value)
95
{
4,418✔
96
    if (auto val = data.find(key)) {
4,418✔
97
        value = as<T>(*val);
16✔
98
    }
16✔
99
}
4,418✔
100

101
template <typename T>
102
T parse(std::string_view str)
103
{
14,241✔
104
    try {
14,241✔
105
        return as<T>(bson::parse(str));
14,241✔
106
    }
14,241✔
107
    catch (const std::exception& e) {
14,241✔
108
        throw_json_error(ErrorCodes::MalformedJson, e.what());
2✔
109
    }
2✔
110
}
14,241✔
111

112
struct UserAPIKeyResponseHandler {
113
    UniqueFunction<void(App::UserAPIKey&&, Optional<AppError>)> completion;
114
    void operator()(const Response& response)
115
    {
44✔
116
        if (auto error = AppUtils::check_for_errors(response)) {
44✔
117
            return completion({}, std::move(error));
22✔
118
        }
22✔
119

120
        try {
22✔
121
            auto json = parse<BsonDocument>(response.body);
22✔
122
            completion(read_user_api_key(json), {});
22✔
123
        }
22✔
124
        catch (AppError& e) {
22✔
125
            completion({}, std::move(e));
×
126
        }
×
127
    }
22✔
128

129
    static App::UserAPIKey read_user_api_key(const BsonDocument& doc)
130
    {
32✔
131
        App::UserAPIKey user_api_key;
32✔
132
        read_field(doc, "_id", user_api_key.id);
32✔
133
        read_field(doc, "key", user_api_key.key);
32✔
134
        read_field(doc, "name", user_api_key.name);
32✔
135
        read_field(doc, "disabled", user_api_key.disabled);
32✔
136
        return user_api_key;
32✔
137
    }
32✔
138
};
139

140
// generate the request headers for a HTTP call, by default it will generate
141
// headers with a refresh token if a user is passed
142
HttpHeaders get_request_headers(const std::shared_ptr<User>& user, RequestTokenType token_type)
143
{
15,477✔
144
    HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}};
15,477✔
145
    if (user) {
15,477✔
146
        switch (token_type) {
5,723✔
147
            case RequestTokenType::NoAuth:
✔
148
                break;
×
149
            case RequestTokenType::AccessToken:
5,502✔
150
                headers.insert({"Authorization", util::format("Bearer %1", user->access_token())});
5,502✔
151
                break;
5,502✔
152
            case RequestTokenType::RefreshToken:
221✔
153
                headers.insert({"Authorization", util::format("Bearer %1", user->refresh_token())});
221✔
154
                break;
221✔
155
        }
5,723✔
156
    }
5,723✔
157
    return headers;
15,477✔
158
}
15,477✔
159

160
std::string trim_base_url(std::string base_url)
161
{
4,987✔
162
    while (!base_url.empty() && base_url.back() == '/') {
4,989✔
163
        base_url.pop_back();
2✔
164
    }
2✔
165

166
    return base_url;
4,987✔
167
}
4,987✔
168

169
std::string base_url_from_app_config(const AppConfig& app_config)
170
{
4,407✔
171
    if (!app_config.base_url) {
4,407✔
172
        return std::string{App::default_base_url()};
3,808✔
173
    }
3,808✔
174

175
    return trim_base_url(*app_config.base_url);
599✔
176
}
4,407✔
177

178
UniqueFunction<void(const Response&)> handle_default_response(UniqueFunction<void(Optional<AppError>)>&& completion)
179
{
24✔
180
    return [completion = std::move(completion)](const Response& response) {
24✔
181
        completion(AppUtils::check_for_errors(response));
24✔
182
    };
24✔
183
}
24✔
184

185
constexpr static std::string_view s_base_path = "/api/client/v2.0";
186
constexpr static std::string_view s_app_path = "/app";
187
constexpr static std::string_view s_auth_path = "/auth";
188
constexpr static std::string_view s_sync_path = "/realm-sync";
189
constexpr static uint64_t s_default_timeout_ms = 60000;
190
constexpr static std::string_view s_username_password_provider_key = "local-userpass";
191
constexpr static std::string_view s_user_api_key_provider_key_path = "api_keys";
192
static util::FlatMap<std::string, util::FlatMap<std::string, SharedApp>> s_apps_cache; // app_id -> base_url -> app
193
std::mutex s_apps_mutex;
194
} // anonymous namespace
195

196
namespace realm::app {
197

198
std::string_view App::default_base_url()
199
{
3,890✔
200
    return "https://services.cloud.mongodb.com";
3,890✔
201
}
3,890✔
202

203
// NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive.
204
// "Calling function configure requires negative capability '!app->m_route_mutex'"
205
// But 'app' is an object just created in this static method so it is not possible to annotate this in the header.
206
SharedApp App::get_app(CacheMode mode, const AppConfig& config) NO_THREAD_SAFETY_ANALYSIS
207
{
4,401✔
208
    if (mode == CacheMode::Enabled) {
4,401✔
209
        std::lock_guard lock(s_apps_mutex);
10✔
210
        auto& app = s_apps_cache[config.app_id][base_url_from_app_config(config)];
10✔
211
        if (!app) {
10✔
212
            app = App::make_app(config);
6✔
213
        }
6✔
214
        return app;
10✔
215
    }
10✔
216
    REALM_ASSERT(mode == CacheMode::Disabled);
4,391✔
217
    return App::make_app(config);
4,391✔
218
}
4,401✔
219

220
SharedApp App::make_app(const AppConfig& config)
221
{
4,397✔
222
#ifdef __EMSCRIPTEN__
223
    if (!config.transport) {
224
        // Make a copy and provide a default transport if not provided
225
        AppConfig config_copy = config;
226
        config_copy.transport = std::make_shared<_impl::EmscriptenNetworkTransport>();
227
        return std::make_shared<App>(Private(), config_copy);
228
    }
229
    return std::make_shared<App>(Private(), config);
230
#else
231
    return std::make_shared<App>(Private(), config);
4,397✔
232
#endif
4,397✔
233
}
4,397✔
234

235
SharedApp App::get_cached_app(const std::string& app_id, const std::optional<std::string>& base_url)
236
{
10✔
237
    std::lock_guard lock(s_apps_mutex);
10✔
238
    if (auto it = s_apps_cache.find(app_id); it != s_apps_cache.end()) {
10✔
239
        const auto& apps_by_url = it->second;
10✔
240

241
        auto app_it = base_url ? apps_by_url.find(trim_base_url(*base_url)) : apps_by_url.begin();
10✔
242
        if (app_it != apps_by_url.end()) {
10✔
243
            return app_it->second;
8✔
244
        }
8✔
245
    }
10✔
246

247
    return nullptr;
2✔
248
}
10✔
249

250
void App::clear_cached_apps()
251
{
4,573✔
252
    std::lock_guard lock(s_apps_mutex);
4,573✔
253
    s_apps_cache.clear();
4,573✔
254
}
4,573✔
255

256
void App::close_all_sync_sessions()
UNCOV
257
{
×
258
    std::lock_guard lock(s_apps_mutex);
×
259
    for (auto& apps_by_url : s_apps_cache) {
×
260
        for (auto& app : apps_by_url.second) {
×
261
            app.second->sync_manager()->close_all_sessions();
×
262
        }
×
263
    }
×
264
}
×
265

266
App::App(Private, const AppConfig& config)
267
    : m_config(config)
2,190✔
268
    , m_base_url(base_url_from_app_config(m_config))
2,190✔
269
    , m_request_timeout_ms(m_config.default_request_timeout_ms.value_or(s_default_timeout_ms))
2,190✔
270
    , m_file_manager(std::make_unique<SyncFileManager>(config))
2,190✔
271
    , m_metadata_store(create_metadata_store(config, *m_file_manager))
2,190✔
272
    , m_sync_manager(SyncManager::create(config.sync_client_config))
2,190✔
273
{
4,397✔
274
    REALM_ASSERT(m_config.transport);
4,397✔
275

276
    // if a base url is provided, then verify the value
277
    if (m_config.base_url) {
4,397✔
278
        util::Uri::parse(*m_config.base_url);
595✔
279
    }
595✔
280
    // Setup a baseline set of routes using the provided or default base url
281
    // These will be updated when the location info is refreshed prior to sending the
282
    // first AppServices HTTP request.
283
    configure_route(m_base_url, "");
4,397✔
284
    m_sync_manager->set_sync_route(make_sync_route(), false);
4,397✔
285

286
    if (m_config.device_info.platform_version.empty()) {
4,397✔
UNCOV
287
        throw InvalidArgument("You must specify the Platform Version in App::Config::device_info");
×
288
    }
×
289

290
    if (m_config.device_info.sdk.empty()) {
4,397✔
UNCOV
291
        throw InvalidArgument("You must specify the SDK Name in App::Config::device_info");
×
292
    }
×
293

294
    if (m_config.device_info.sdk_version.empty()) {
4,397✔
UNCOV
295
        throw InvalidArgument("You must specify the SDK Version in App::Config::device_info");
×
296
    }
×
297
}
4,397✔
298

299
App::~App() {}
4,397✔
300

301
bool App::init_logger()
302
{
46,442✔
303
    if (!m_logger_ptr) {
46,442✔
304
        m_logger_ptr = m_sync_manager->get_logger();
4,389✔
305
        if (!m_logger_ptr) {
4,389✔
UNCOV
306
            m_logger_ptr = util::Logger::get_default_logger();
×
307
        }
×
308
    }
4,389✔
309
    return bool(m_logger_ptr);
46,442✔
310
}
46,442✔
311

312
bool App::would_log(util::Logger::Level level)
313
{
6,070✔
314
    return init_logger() && m_logger_ptr->would_log(util::LogCategory::app, level);
6,070✔
315
}
6,070✔
316

317
template <class... Params>
318
void App::log_debug(const char* message, Params&&... params)
319
{
40,238✔
320
    if (init_logger()) {
40,238✔
321
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::debug, message,
40,238✔
322
                          std::forward<Params>(params)...);
40,238✔
323
    }
40,238✔
324
}
40,238✔
325

326
template <class... Params>
327
void App::log_error(const char* message, Params&&... params)
328
{
134✔
329
    if (init_logger()) {
134✔
330
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::error, message,
134✔
331
                          std::forward<Params>(params)...);
134✔
332
    }
134✔
333
}
134✔
334

335
std::string App::auth_route()
336
{
9,744✔
337
    util::CheckedLockGuard guard(m_route_mutex);
9,744✔
338
    return m_auth_route;
9,744✔
339
}
9,744✔
340

341
std::string App::base_url()
UNCOV
342
{
×
343
    util::CheckedLockGuard guard(m_route_mutex);
×
344
    return m_base_url;
×
345
}
×
346

347
std::string App::get_host_url()
348
{
4,448✔
349
    util::CheckedLockGuard guard(m_route_mutex);
4,448✔
350
    return m_host_url;
4,448✔
351
}
4,448✔
352

353
std::string App::get_ws_host_url()
354
{
72✔
355
    util::CheckedLockGuard guard(m_route_mutex);
72✔
356
    return m_ws_host_url;
72✔
357
}
72✔
358

359
std::string App::make_sync_route(Optional<std::string> ws_host_url)
360
{
8,775✔
361
    return util::format("%1%2%3/%4%5", ws_host_url.value_or(m_ws_host_url), s_base_path, s_app_path, m_config.app_id,
8,775✔
362
                        s_sync_path);
8,775✔
363
}
8,775✔
364

365
void App::configure_route(const std::string& host_url, const std::string& ws_host_url)
366
{
8,783✔
367
    m_host_url = host_url;
8,783✔
368
    m_ws_host_url = ws_host_url;
8,783✔
369
    if (m_ws_host_url.empty())
8,783✔
370
        m_ws_host_url = App::create_ws_host_url(m_host_url);
4,397✔
371

372
    // host_url is the url to the server: e.g., https://services.cloud.mongodb.com or https://localhost:9090
373
    // base_route is the baseline client api path: e.g. <host_url>/api/client/v2.0
374
    m_base_route = util::format("%1%2", m_host_url, s_base_path);
8,783✔
375
    // app_route is the cloud app URL: <host_url>/api/client/v2.0/app/<app_id>
376
    m_app_route = util::format("%1%2/%3", m_base_route, s_app_path, m_config.app_id);
8,783✔
377
    // auth_route is cloud app auth URL: <host_url>/api/client/v2.0/app/<app_id>/auth
378
    m_auth_route = util::format("%1%2", m_app_route, s_auth_path);
8,783✔
379
}
8,783✔
380

381
// Create a temporary websocket URL domain using the given host URL
382
// This updates the URL based on the following assumptions:
383
// If the URL doesn't start with 'http' => <host-url>
384
// http[s]://[region-prefix]realm.mongodb.com => ws[s]://ws.[region-prefix]realm.mongodb.com
385
// http[s]://[region-prefix]services.cloud.mongodb.com => ws[s]://[region-prefix].ws.services.cloud.mongodb.com
386
// All others => http[s]://<host-url> => ws[s]://<host-url>
387
std::string App::create_ws_host_url(std::string_view host_url)
388
{
4,591✔
389
    constexpr static std::string_view old_base_domain = "realm.mongodb.com";
4,591✔
390
    constexpr static std::string_view new_base_domain = "services.cloud.mongodb.com";
4,591✔
391
    const size_t base_len = std::char_traits<char>::length("http://");
4,591✔
392

393
    // Doesn't contain 7 or more characters (length of 'http://') or start with http,
394
    // just return provided string
395
    if (host_url.length() < base_len || host_url.substr(0, 4) != "http") {
4,591✔
396
        return std::string(host_url);
2✔
397
    }
2✔
398
    // If it starts with 'https' then ws url will start with 'wss'
399
    bool https = host_url[4] == 's';
4,589✔
400
    size_t prefix_len = base_len + (https ? 1 : 0);
4,589✔
401
    std::string_view prefix = https ? "wss://" : "ws://";
4,589✔
402

403
    // http[s]://[<region-prefix>]realm.mongodb.com[/<path>] =>
404
    //     ws[s]://ws.[<region-prefix>]realm.mongodb.com[/<path>]
405
    if (host_url.find(old_base_domain) != std::string_view::npos) {
4,589✔
406
        return util::format("%1ws.%2", prefix, host_url.substr(prefix_len));
12✔
407
    }
12✔
408
    // http[s]://[<region-prefix>]services.cloud.mongodb.com[/<path>] =>
409
    //     ws[s]://[<region-prefix>].ws.services.cloud.mongodb.com[/<path>]
410
    if (auto start = host_url.find(new_base_domain); start != std::string_view::npos) {
4,577✔
411
        return util::format("%1%2ws.%3", prefix, host_url.substr(prefix_len, start - prefix_len),
3,850✔
412
                            host_url.substr(start));
3,850✔
413
    }
3,850✔
414

415
    // All others => http[s]://<host-url>[/<path>] => ws[s]://<host-url>[/<path>]
416
    return util::format("ws%1", host_url.substr(4));
727✔
417
}
4,577✔
418

419
void App::update_hostname(const std::string& host_url, const std::string& ws_host_url,
420
                          const std::string& new_base_url)
421
{
4,386✔
422
    log_debug("App: update_hostname: %1 | %2 | %3", host_url, ws_host_url, new_base_url);
4,386✔
423
    m_base_url = trim_base_url(new_base_url);
4,386✔
424
    // If a new host url was returned from the server, use it to configure the routes
425
    // Otherwise, use the m_base_url value
426
    std::string base_url = host_url.length() > 0 ? host_url : m_base_url;
4,386✔
427
    configure_route(base_url, ws_host_url);
4,386✔
428
}
4,386✔
429

430
// MARK: - Template specializations
431

432
template <>
433
App::UsernamePasswordProviderClient App::provider_client<App::UsernamePasswordProviderClient>()
434
{
4,816✔
435
    return App::UsernamePasswordProviderClient(shared_from_this());
4,816✔
436
}
4,816✔
437

438
template <>
439
App::UserAPIKeyProviderClient App::provider_client<App::UserAPIKeyProviderClient>()
440
{
20✔
441
    return App::UserAPIKeyProviderClient(*this);
20✔
442
}
20✔
443

444
// MARK: - UsernamePasswordProviderClient
445

446
void App::UsernamePasswordProviderClient::register_email(const std::string& email, const std::string& password,
447
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
448
{
4,820✔
449
    m_parent->log_debug("App: register_email: %1", email);
4,820✔
450
    m_parent->post(util::format("%1/providers/%2/register", m_parent->auth_route(), s_username_password_provider_key),
4,820✔
451
                   std::move(completion), {{"email", email}, {"password", password}});
4,820✔
452
}
4,820✔
453

454
void App::UsernamePasswordProviderClient::confirm_user(const std::string& token, const std::string& token_id,
455
                                                       UniqueFunction<void(Optional<AppError>)>&& completion)
456
{
2✔
457
    m_parent->log_debug("App: confirm_user");
2✔
458
    m_parent->post(util::format("%1/providers/%2/confirm", m_parent->auth_route(), s_username_password_provider_key),
2✔
459
                   std::move(completion), {{"token", token}, {"tokenId", token_id}});
2✔
460
}
2✔
461

462
void App::UsernamePasswordProviderClient::resend_confirmation_email(
463
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
464
{
2✔
465
    m_parent->log_debug("App: resend_confirmation_email: %1", email);
2✔
466
    m_parent->post(
2✔
467
        util::format("%1/providers/%2/confirm/send", m_parent->auth_route(), s_username_password_provider_key),
2✔
468
        std::move(completion), {{"email", email}});
2✔
469
}
2✔
470

471
void App::UsernamePasswordProviderClient::retry_custom_confirmation(
472
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
473
{
4✔
474
    m_parent->log_debug("App: retry_custom_confirmation: %1", email);
4✔
475
    m_parent->post(
4✔
476
        util::format("%1/providers/%2/confirm/call", m_parent->auth_route(), s_username_password_provider_key),
4✔
477
        std::move(completion), {{"email", email}});
4✔
478
}
4✔
479

480
void App::UsernamePasswordProviderClient::send_reset_password_email(
481
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
UNCOV
482
{
×
483
    m_parent->log_debug("App: send_reset_password_email: %1", email);
×
484
    m_parent->post(
×
485
        util::format("%1/providers/%2/reset/send", m_parent->auth_route(), s_username_password_provider_key),
×
486
        std::move(completion), {{"email", email}});
×
487
}
×
488

489
void App::UsernamePasswordProviderClient::reset_password(const std::string& password, const std::string& token,
490
                                                         const std::string& token_id,
491
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
492
{
2✔
493
    m_parent->log_debug("App: reset_password");
2✔
494
    m_parent->post(util::format("%1/providers/%2/reset", m_parent->auth_route(), s_username_password_provider_key),
2✔
495
                   std::move(completion), {{"password", password}, {"token", token}, {"tokenId", token_id}});
2✔
496
}
2✔
497

498
void App::UsernamePasswordProviderClient::call_reset_password_function(
499
    const std::string& email, const std::string& password, const BsonArray& args,
500
    UniqueFunction<void(Optional<AppError>)>&& completion)
501
{
6✔
502
    m_parent->log_debug("App: call_reset_password_function: %1", email);
6✔
503
    m_parent->post(
6✔
504
        util::format("%1/providers/%2/reset/call", m_parent->auth_route(), s_username_password_provider_key),
6✔
505
        std::move(completion), {{"email", email}, {"password", password}, {"arguments", args}});
6✔
506
}
6✔
507

508
// MARK: - UserAPIKeyProviderClient
509

510
std::string App::UserAPIKeyProviderClient::url_for_path(const std::string& path = "") const
511
{
82✔
512
    if (!path.empty()) {
82✔
513
        return m_auth_request_client.url_for_path(
58✔
514
            util::format("%1/%2/%3", s_auth_path, s_user_api_key_provider_key_path, path));
58✔
515
    }
58✔
516

517
    return m_auth_request_client.url_for_path(util::format("%1/%2", s_auth_path, s_user_api_key_provider_key_path));
24✔
518
}
82✔
519

520
void App::UserAPIKeyProviderClient::create_api_key(
521
    const std::string& name, const std::shared_ptr<User>& user,
522
    UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
523
{
10✔
524
    m_auth_request_client.do_authenticated_request(
10✔
525
        HttpMethod::post, url_for_path(), Bson(BsonDocument{{"name", name}}).to_string(), user,
10✔
526
        RequestTokenType::RefreshToken, UserAPIKeyResponseHandler{std::move(completion)});
10✔
527
}
10✔
528

529
void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
530
                                                  UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
531
{
34✔
532
    m_auth_request_client.do_authenticated_request(HttpMethod::get, url_for_path(id.to_string()), "", user,
34✔
533
                                                   RequestTokenType::RefreshToken,
34✔
534
                                                   UserAPIKeyResponseHandler{std::move(completion)});
34✔
535
}
34✔
536

537
void App::UserAPIKeyProviderClient::fetch_api_keys(
538
    const std::shared_ptr<User>& user,
539
    UniqueFunction<void(std::vector<UserAPIKey>&&, Optional<AppError>)>&& completion)
540
{
14✔
541
    m_auth_request_client.do_authenticated_request(
14✔
542
        HttpMethod::get, url_for_path(), "", user, RequestTokenType::RefreshToken,
14✔
543
        [completion = std::move(completion)](const Response& response) {
14✔
544
            if (auto error = AppUtils::check_for_errors(response)) {
14✔
545
                return completion({}, std::move(error));
4✔
546
            }
4✔
547

548
            try {
10✔
549
                auto json = parse<BsonArray>(response.body);
10✔
550
                std::vector<UserAPIKey> keys;
10✔
551
                keys.reserve(json.size());
10✔
552
                for (auto&& api_key_json : json) {
10✔
553
                    keys.push_back(UserAPIKeyResponseHandler::read_user_api_key(as<BsonDocument>(api_key_json)));
10✔
554
                }
10✔
555
                return completion(std::move(keys), {});
10✔
556
            }
10✔
557
            catch (AppError& e) {
10✔
UNCOV
558
                completion({}, std::move(e));
×
559
            }
×
560
        });
10✔
561
}
14✔
562

563
void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
564
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
565
{
8✔
566
    m_auth_request_client.do_authenticated_request(HttpMethod::del, url_for_path(id.to_string()), "", user,
8✔
567
                                                   RequestTokenType::RefreshToken,
8✔
568
                                                   handle_default_response(std::move(completion)));
8✔
569
}
8✔
570

571
void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
572
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
573
{
8✔
574
    m_auth_request_client.do_authenticated_request(
8✔
575
        HttpMethod::put, url_for_path(util::format("%1/enable", id.to_string())), "", user,
8✔
576
        RequestTokenType::RefreshToken, handle_default_response(std::move(completion)));
8✔
577
}
8✔
578

579
void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr<User>& user,
580
                                                    UniqueFunction<void(Optional<AppError>)>&& completion)
581
{
8✔
582
    m_auth_request_client.do_authenticated_request(
8✔
583
        HttpMethod::put, url_for_path(util::format("%1/disable", id.to_string())), "", user,
8✔
584
        RequestTokenType::RefreshToken, handle_default_response(std::move(completion)));
8✔
585
}
8✔
586
// MARK: - App
587

588
// The user cache can have an expired pointer to an object if another thread is
589
// currently waiting for the mutex so that it can unregister the object, which
590
// will result in shared_from_this() throwing. We could instead do
591
// `weak_from_this().lock()`, but that is more expensive in the much more common
592
// case where the pointer is valid.
593
//
594
// Storing weak_ptrs in m_user would also avoid this problem, but would introduce
595
// a different one where the natural way to use the users could result in us
596
// trying to release the final strong reference while holding the lock, which
597
// would lead to a deadlock
598
static std::shared_ptr<User> try_lock(User& user)
599
{
3,917✔
600
    try {
3,917✔
601
        return user.shared_from_this();
3,917✔
602
    }
3,917✔
603
    catch (const std::bad_weak_ptr&) {
3,917✔
UNCOV
604
        return nullptr;
×
605
    }
×
606
}
3,917✔
607

608
std::shared_ptr<User> App::get_user_for_id(const std::string& user_id)
609
{
9,645✔
610
    if (auto& user = m_users[user_id]) {
9,645✔
611
        if (auto locked = try_lock(*user)) {
52✔
612
            return locked;
52✔
613
        }
52✔
614
    }
52✔
615
    return User::make(shared_from_this(), user_id);
9,593✔
616
}
9,645✔
617

618
void App::user_data_updated(const std::string& user_id)
619
{
4,878✔
620
    if (auto it = m_users.find(user_id); it != m_users.end()) {
4,878✔
621
        it->second->update_backing_data(m_metadata_store->get_user(user_id));
6✔
622
    }
6✔
623
}
4,878✔
624

625
std::shared_ptr<User> App::current_user()
626
{
8,614✔
627
    util::CheckedLockGuard lock(m_user_mutex);
8,614✔
628
    if (m_current_user && m_current_user->is_logged_in()) {
8,614✔
629
        if (auto user = try_lock(*m_current_user)) {
3,857✔
630
            return user;
3,857✔
631
        }
3,857✔
632
    }
3,857✔
633
    if (auto user_id = m_metadata_store->get_current_user(); !user_id.empty()) {
4,757✔
634
        auto user = get_user_for_id(user_id);
4,715✔
635
        m_current_user = user.get();
4,715✔
636
        return user;
4,715✔
637
    }
4,715✔
638
    return nullptr;
42✔
639
}
4,757✔
640

641
std::shared_ptr<User> App::get_existing_logged_in_user(std::string_view user_id)
642
{
2✔
643
    util::CheckedLockGuard lock(m_user_mutex);
2✔
644
    if (auto it = m_users.find(std::string(user_id)); it != m_users.end() && it->second->is_logged_in()) {
2!
UNCOV
645
        if (auto user = try_lock(*it->second)) {
×
646
            return user;
×
647
        }
×
648
    }
×
649
    if (m_metadata_store->has_logged_in_user(user_id)) {
2✔
650
        return User::make(shared_from_this(), user_id);
2✔
651
    }
2✔
UNCOV
652
    return nullptr;
×
653
}
2✔
654

655
std::string App::get_base_url() const
656
{
74✔
657
    util::CheckedLockGuard guard(m_route_mutex);
74✔
658
    return m_base_url;
74✔
659
}
74✔
660

661
void App::update_base_url(std::string_view new_base_url, UniqueFunction<void(Optional<AppError>)>&& completion)
662
{
14✔
663
    if (new_base_url.empty()) {
14✔
664
        // Treat an empty string the same as requesting the default base url
665
        new_base_url = App::default_base_url();
12✔
666
        log_debug("App::update_base_url: empty => %1", new_base_url);
12✔
667
    }
12✔
668
    else {
2✔
669
        log_debug("App::update_base_url: %1", new_base_url);
2✔
670
    }
2✔
671

672
    // Validate the new base_url
673
    util::Uri::parse(new_base_url);
14✔
674

675
    bool update_not_needed;
14✔
676
    {
14✔
677
        util::CheckedLockGuard guard(m_route_mutex);
14✔
678
        // Update the location if the base_url is different or a location update is already needed
679
        m_location_updated = (new_base_url == m_base_url) && m_location_updated;
14!
680
        update_not_needed = m_location_updated;
14✔
681
    }
14✔
682
    // If the new base_url is the same as the current base_url and the location has already been updated,
683
    // then we're done
684
    if (update_not_needed) {
14✔
UNCOV
685
        completion(util::none);
×
686
        return;
×
687
    }
×
688

689
    // Otherwise, request the location information at the new base URL
690
    request_location(std::move(completion), std::string(new_base_url));
14✔
691
}
14✔
692

693
std::vector<std::shared_ptr<User>> App::all_users()
694
{
82✔
695
    util::CheckedLockGuard lock(m_user_mutex);
82✔
696
    auto user_ids = m_metadata_store->get_all_users();
82✔
697
    std::vector<std::shared_ptr<User>> users;
82✔
698
    users.reserve(user_ids.size());
82✔
699
    for (auto& user_id : user_ids) {
82✔
700
        users.push_back(get_user_for_id(user_id));
52✔
701
    }
52✔
702
    return users;
82✔
703
}
82✔
704

705
void App::get_profile(const std::shared_ptr<User>& user,
706
                      UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
707
{
4,886✔
708
    do_authenticated_request(
4,886✔
709
        HttpMethod::get, url_for_path("/auth/profile"), "", user, RequestTokenType::AccessToken,
4,886✔
710
        [completion = std::move(completion), self = shared_from_this(), user,
4,886✔
711
         this](const Response& profile_response) {
4,886✔
712
            if (auto error = AppUtils::check_for_errors(profile_response)) {
4,886✔
UNCOV
713
                return completion(nullptr, std::move(error));
×
714
            }
×
715

716
            try {
4,886✔
717
                auto profile_json = parse<BsonDocument>(profile_response.body);
4,886✔
718
                auto identities_json = get<BsonArray>(profile_json, "identities");
4,886✔
719

720
                std::vector<UserIdentity> identities;
4,886✔
721
                identities.reserve(identities_json.size());
4,886✔
722
                for (auto& identity_json : identities_json) {
4,920✔
723
                    auto doc = as<BsonDocument>(identity_json);
4,920✔
724
                    identities.push_back({get<std::string>(doc, "id"), get<std::string>(doc, "provider_type")});
4,920✔
725
                }
4,920✔
726

727
                m_metadata_store->update_user(user->user_id(), [&](auto& data) {
4,886✔
728
                    data.identities = std::move(identities);
4,886✔
729
                    data.profile = UserProfile(get<BsonDocument>(profile_json, "data"));
4,886✔
730
                    user->update_backing_data(data); // FIXME
4,886✔
731
                });
4,886✔
732
            }
4,886✔
733
            catch (const AppError& err) {
4,886✔
UNCOV
734
                return completion(nullptr, err);
×
735
            }
×
736

737
            return completion(user, {});
4,886✔
738
        });
4,886✔
739
}
4,886✔
740

741
void App::attach_auth_options(BsonDocument& body)
742
{
4,908✔
743
    log_debug("App: version info: platform: %1  version: %2 - sdk: %3 - sdk version: %4 - core version: %5",
4,908✔
744
              util::get_library_platform(), m_config.device_info.platform_version, m_config.device_info.sdk,
4,908✔
745
              m_config.device_info.sdk_version, REALM_VERSION_STRING);
4,908✔
746

747
    BsonDocument options;
4,908✔
748
    options["appId"] = m_config.app_id;
4,908✔
749
    options["platform"] = util::get_library_platform();
4,908✔
750
    options["platformVersion"] = m_config.device_info.platform_version;
4,908✔
751
    options["sdk"] = m_config.device_info.sdk;
4,908✔
752
    options["sdkVersion"] = m_config.device_info.sdk_version;
4,908✔
753
    options["cpuArch"] = util::get_library_cpu_arch();
4,908✔
754
    options["deviceName"] = m_config.device_info.device_name;
4,908✔
755
    options["deviceVersion"] = m_config.device_info.device_version;
4,908✔
756
    options["frameworkName"] = m_config.device_info.framework_name;
4,908✔
757
    options["frameworkVersion"] = m_config.device_info.framework_version;
4,908✔
758
    options["coreVersion"] = REALM_VERSION_STRING;
4,908✔
759
    options["bundleId"] = m_config.device_info.bundle_id;
4,908✔
760

761
    body["options"] = BsonDocument({{"device", options}});
4,908✔
762
}
4,908✔
763

764
void App::log_in_with_credentials(const AppCredentials& credentials, const std::shared_ptr<User>& linking_user,
765
                                  UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
766
{
4,916✔
767
    if (would_log(util::Logger::Level::debug)) {
4,916✔
768
        auto app_info = util::format("app_id: %1", m_config.app_id);
4,916✔
769
        log_debug("App: log_in_with_credentials: %1", app_info);
4,916✔
770
    }
4,916✔
771
    // if we try logging in with an anonymous user while there
772
    // is already an anonymous session active, reuse it
773
    std::shared_ptr<User> anon_user;
4,916✔
774
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
4,916✔
775
        util::CheckedLockGuard lock(m_user_mutex);
86✔
776
        for (auto& [_, user] : m_users) {
86✔
777
            if (user->is_anonymous()) {
14✔
778
                anon_user = try_lock(*user);
8✔
779
                if (!anon_user)
8✔
UNCOV
780
                    continue;
×
781
                m_current_user = user;
8✔
782
                m_metadata_store->set_current_user(user->user_id());
8✔
783
                break;
8✔
784
            }
8✔
785
        }
14✔
786
    }
86✔
787

788
    if (anon_user) {
4,916✔
789
        emit_change_to_subscribers();
8✔
790
        completion(anon_user, util::none);
8✔
791
        return;
8✔
792
    }
8✔
793

794
    if (linking_user) {
4,908✔
795
        util::CheckedLockGuard lock(m_user_mutex);
10✔
796
        if (!verify_user_present(linking_user)) {
10✔
UNCOV
797
            return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found."));
×
798
        }
×
799
    }
10✔
800

801
    // construct the route
802
    std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(),
4,908✔
803
                                     linking_user ? "?link=true" : "");
4,908✔
804

805
    BsonDocument body = credentials.serialize_as_bson();
4,908✔
806
    attach_auth_options(body);
4,908✔
807

808
    do_request(
4,908✔
809
        make_request(HttpMethod::post, std::move(route), linking_user, RequestTokenType::AccessToken,
4,908✔
810
                     Bson(body).to_string()),
4,908✔
811
        [completion = std::move(completion), credentials, linking_user, self = shared_from_this(),
4,908✔
812
         this](auto&&, const Response& response) mutable {
4,908✔
813
            if (auto error = AppUtils::check_for_errors(response)) {
4,908✔
814
                log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code,
18✔
815
                          error->what());
18✔
816
                return completion(nullptr, std::move(error));
18✔
817
            }
18✔
818

819
            std::shared_ptr<User> user = linking_user;
4,890✔
820
            try {
4,890✔
821
                auto json = parse<BsonDocument>(response.body);
4,890✔
822
                if (linking_user) {
4,890✔
823
                    m_metadata_store->update_user(linking_user->user_id(), [&](auto& data) {
10✔
824
                        data.access_token = RealmJWT(get<std::string>(json, "access_token"));
10✔
825
                        // FIXME: should be powered by callback
826
                        linking_user->update_backing_data(data);
10✔
827
                    });
10✔
828
                }
10✔
829
                else {
4,880✔
830
                    auto user_id = get<std::string>(json, "user_id");
4,880✔
831
                    m_metadata_store->create_user(user_id, get<std::string>(json, "refresh_token"),
4,880✔
832
                                                  get<std::string>(json, "access_token"),
4,880✔
833
                                                  get<std::string>(json, "device_id"));
4,880✔
834
                    util::CheckedLockGuard lock(m_user_mutex);
4,880✔
835
                    user_data_updated(user_id); // FIXME: needs to be callback from metadata store
4,880✔
836
                    user = get_user_for_id(user_id);
4,880✔
837
                }
4,880✔
838
            }
4,890✔
839
            catch (const AppError& e) {
4,890✔
840
                return completion(nullptr, e);
2✔
841
            }
2✔
842
            // If the user has not been logged in, then there is a problem with the token
843
            if (!user->is_logged_in()) {
4,888✔
844
                return completion(nullptr,
2✔
845
                                  AppError(ErrorCodes::BadToken, "Could not log in user: received malformed JWT"));
2✔
846
            }
2✔
847

848
            get_profile(user, [this, completion = std::move(completion)](const std::shared_ptr<User>& user,
4,886✔
849
                                                                         Optional<AppError> error) {
4,886✔
850
                if (!error) {
4,886✔
851
                    switch_user(user);
4,886✔
852
                }
4,886✔
853
                completion(user, error);
4,886✔
854
            });
4,886✔
855
        },
4,886✔
856
        false);
4,908✔
857
}
4,908✔
858

859
void App::log_in_with_credentials(
860
    const AppCredentials& credentials,
861
    util::UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
862
{
4,906✔
863
    App::log_in_with_credentials(credentials, nullptr, std::move(completion));
4,906✔
864
}
4,906✔
865

866
void App::log_out(const std::shared_ptr<User>& user, SyncUser::State new_state,
867
                  UniqueFunction<void(Optional<AppError>)>&& completion)
868
{
52✔
869
    if (!user || user->state() == new_state || user->state() == SyncUser::State::Removed) {
52✔
870
        if (completion) {
6✔
871
            completion(util::none);
4✔
872
        }
4✔
873
        return;
6✔
874
    }
6✔
875

876
    log_debug("App: log_out(%1)", user->user_id());
46✔
877
    auto request =
46✔
878
        make_request(HttpMethod::del, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, "");
46✔
879

880
    m_metadata_store->log_out(user->user_id(), new_state);
46✔
881
    user->update_backing_data(m_metadata_store->get_user(user->user_id()));
46✔
882

883
    do_request(std::move(request),
46✔
884
               [self = shared_from_this(), completion = std::move(completion)](auto&&, const Response& response) {
46✔
885
                   auto error = AppUtils::check_for_errors(response);
46✔
886
                   if (!error) {
46✔
887
                       self->emit_change_to_subscribers();
46✔
888
                   }
46✔
889
                   if (completion) {
46✔
890
                       completion(error);
41✔
891
                   }
41✔
892
               });
46✔
893
}
46✔
894

895
void App::log_out(const std::shared_ptr<User>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
896
{
37✔
897
    auto new_state = user && user->is_anonymous() ? SyncUser::State::Removed : SyncUser::State::LoggedOut;
37✔
898
    log_out(user, new_state, std::move(completion));
37✔
899
}
37✔
900

901
void App::log_out(UniqueFunction<void(Optional<AppError>)>&& completion)
902
{
18✔
903
    log_out(current_user(), std::move(completion));
18✔
904
}
18✔
905

906
bool App::verify_user_present(const std::shared_ptr<User>& user) const
907
{
4,927✔
908
    for (auto& [_, u] : m_users) {
4,930✔
909
        if (u == user.get())
4,930✔
910
            return true;
4,927✔
911
    }
4,930✔
UNCOV
912
    return false;
×
913
}
4,927✔
914

915
void App::switch_user(const std::shared_ptr<User>& user)
916
{
4,894✔
917
    if (!user || user->state() != SyncUser::State::LoggedIn) {
4,894✔
918
        throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out");
2✔
919
    }
2✔
920
    {
4,892✔
921
        util::CheckedLockGuard lock(m_user_mutex);
4,892✔
922
        if (!verify_user_present(user)) {
4,892✔
UNCOV
923
            throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist");
×
924
        }
×
925

926
        m_current_user = user.get();
4,892✔
927
        m_metadata_store->set_current_user(user->user_id());
4,892✔
928
    }
4,892✔
UNCOV
929
    emit_change_to_subscribers();
×
930
}
4,892✔
931

932
void App::remove_user(const std::shared_ptr<User>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
933
{
21✔
934
    if (!user || user->state() == SyncUser::State::Removed) {
21✔
935
        return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed"));
4✔
936
    }
4✔
937

938
    {
17✔
939
        util::CheckedLockGuard lock(m_user_mutex);
17✔
940
        if (!verify_user_present(user)) {
17✔
UNCOV
941
            return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found"));
×
942
        }
×
943
    }
17✔
944

945
    if (user->is_logged_in()) {
17✔
946
        log_out(
15✔
947
            user, SyncUser::State::Removed,
15✔
948
            [user, completion = std::move(completion), self = shared_from_this()](const Optional<AppError>& error) {
15✔
949
                user->update_backing_data(std::nullopt);
15✔
950
                if (completion) {
15✔
951
                    completion(error);
14✔
952
                }
14✔
953
            });
15✔
954
    }
15✔
955
    else {
2✔
956
        m_metadata_store->log_out(user->user_id(), SyncUser::State::Removed);
2✔
957
        user->update_backing_data(std::nullopt);
2✔
958
        if (completion) {
2✔
959
            completion(std::nullopt);
2✔
960
        }
2✔
961
    }
2✔
962
}
17✔
963

964
void App::delete_user(const std::shared_ptr<User>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
965
{
12✔
966
    if (!user) {
12✔
UNCOV
967
        return completion(AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
×
968
    }
×
969
    if (user->state() != SyncUser::State::LoggedIn) {
12✔
970
        return completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "User must be logged in to be deleted."));
4✔
971
    }
4✔
972

973
    {
8✔
974
        util::CheckedLockGuard lock(m_user_mutex);
8✔
975
        if (!verify_user_present(user)) {
8✔
UNCOV
976
            return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found."));
×
977
        }
×
978
    }
8✔
979

980
    do_authenticated_request(HttpMethod::del, url_for_path("/auth/delete"), "", user, RequestTokenType::AccessToken,
8✔
981
                             [completion = std::move(completion), user, this](const Response& response) {
8✔
982
                                 auto error = AppUtils::check_for_errors(response);
8✔
983
                                 if (!error) {
8✔
984
                                     auto user_id = user->user_id();
8✔
985
                                     user->detach_and_tear_down();
8✔
986
                                     m_metadata_store->delete_user(*m_file_manager, user_id);
8✔
987
                                     emit_change_to_subscribers();
8✔
988
                                 }
8✔
989
                                 completion(std::move(error));
8✔
990
                             });
8✔
991
}
8✔
992

993
void App::link_user(const std::shared_ptr<User>& user, const AppCredentials& credentials,
994
                    UniqueFunction<void(const std::shared_ptr<User>&, Optional<AppError>)>&& completion)
995
{
12✔
996
    if (!user) {
12✔
UNCOV
997
        return completion(nullptr,
×
UNCOV
998
                          AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
×
999
    }
×
1000
    if (user->state() != SyncUser::State::LoggedIn) {
12✔
1001
        return completion(nullptr,
2✔
1002
                          AppError(ErrorCodes::ClientUserNotLoggedIn, "The specified user is not logged in."));
2✔
1003
    }
2✔
1004
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
10✔
UNCOV
1005
        return completion(nullptr, AppError(ErrorCodes::ClientUserAlreadyNamed,
×
UNCOV
1006
                                            "Cannot add anonymous credentials to an existing user."));
×
1007
    }
×
1008

1009
    log_in_with_credentials(credentials, user, std::move(completion));
10✔
1010
}
10✔
1011

1012
std::shared_ptr<User> App::create_fake_user_for_testing(const std::string& user_id, const std::string& access_token,
1013
                                                        const std::string& refresh_token)
UNCOV
1014
{
×
UNCOV
1015
    std::shared_ptr<User> user;
×
1016
    {
×
1017
        m_metadata_store->create_user(user_id, refresh_token, access_token, "fake_device");
×
1018
        util::CheckedLockGuard lock(m_user_mutex);
×
1019
        user_data_updated(user_id); // FIXME: needs to be callback from metadata store
×
1020
        user = get_user_for_id(user_id);
×
1021
    }
×
1022

1023
    switch_user(user);
×
UNCOV
1024
    return user;
×
1025
}
×
1026

1027

1028
void App::refresh_custom_data(const std::shared_ptr<User>& user,
1029
                              UniqueFunction<void(Optional<AppError>)>&& completion)
1030
{
14✔
1031
    refresh_access_token(user, false, std::move(completion));
14✔
1032
}
14✔
1033

1034
void App::refresh_custom_data(const std::shared_ptr<User>& user, bool update_location,
1035
                              UniqueFunction<void(Optional<AppError>)>&& completion)
1036
{
10✔
1037
    refresh_access_token(user, update_location, std::move(completion));
10✔
1038
}
10✔
1039

1040
std::string App::url_for_path(const std::string& path = "") const
1041
{
5,143✔
1042
    util::CheckedLockGuard guard(m_route_mutex);
5,143✔
1043
    return util::format("%1%2", m_base_route, path);
5,143✔
1044
}
5,143✔
1045

1046
std::string App::get_app_route(const Optional<std::string>& hostname) const
1047
{
4,420✔
1048
    if (hostname) {
4,420✔
1049
        return util::format("%1%2%3/%4", *hostname, s_base_path, s_app_path, m_config.app_id);
14✔
1050
    }
14✔
1051
    return m_app_route;
4,406✔
1052
}
4,420✔
1053

1054
void App::request_location(UniqueFunction<void(std::optional<AppError>)>&& completion,
1055
                           std::optional<std::string>&& new_hostname)
1056
{
4,420✔
1057
    // Request the new location information the original configured base_url or the new_hostname
1058
    // if the base_url is being updated. If a new_hostname has not been provided and the location
1059
    // has already been requested, this function does nothing.
1060
    std::string app_route; // The app_route for the server to query the location
4,420✔
1061
    std::string base_url;  // The configured base_url hostname used for querying the location
4,420✔
1062
    {
4,420✔
1063
        util::CheckedUniqueLock lock(m_route_mutex);
4,420✔
1064
        // Skip if the location info has already been initialized and a new hostname is not provided
1065
        if (!new_hostname && m_location_updated) {
4,420✔
1066
            // Release the lock before calling the completion function
UNCOV
1067
            lock.unlock();
×
UNCOV
1068
            completion(util::none);
×
UNCOV
1069
            return;
×
UNCOV
1070
        }
×
1071
        // If this is querying the new_hostname, then use that to query the location
1072
        if (new_hostname) {
4,420✔
1073
            base_url = *new_hostname;
14✔
1074
            app_route = get_app_route(new_hostname);
14✔
1075
        }
14✔
1076
        // Otherwise, use the current hostname
1077
        else {
4,406✔
1078
            app_route = get_app_route();
4,406✔
1079
            base_url = m_base_url;
4,406✔
1080
        }
4,406✔
1081
        REALM_ASSERT(!app_route.empty());
4,420✔
1082
    }
4,420✔
1083

UNCOV
1084
    Request req;
×
1085
    req.method = HttpMethod::get;
4,420✔
1086
    req.url = util::format("%1/location", app_route);
4,420✔
1087
    req.timeout_ms = m_request_timeout_ms;
4,420✔
1088

1089
    log_debug("App: request location: %1", req.url);
4,420✔
1090

1091
    m_config.transport->send_request_to_server(req, [self = shared_from_this(), completion = std::move(completion),
4,420✔
1092
                                                     base_url = std::move(base_url)](const Response& response) {
4,420✔
1093
        // Location request was successful - update the location info
1094
        auto error = self->update_location(response, base_url);
4,420✔
1095
        if (error) {
4,420✔
1096
            self->log_error("App: request location failed (%1%2): %3", error->code_string(),
34✔
1097
                            error->additional_status_code ? util::format(" %1", *error->additional_status_code) : "",
34✔
1098
                            error->reason());
34✔
1099
        }
34✔
1100
        completion(error);
4,420✔
1101
    });
4,420✔
1102
}
4,420✔
1103

1104
std::optional<AppError> App::update_location(const Response& response, const std::string& base_url)
1105
{
4,420✔
1106
    // Validate the location info response for errors and update the stored location info if it is
1107
    // a valid response. base_url is the new hostname or m_base_url value when request_location()
1108
    // was called.
1109

1110
    if (auto error = AppUtils::check_for_errors(response)) {
4,420✔
1111
        return error;
34✔
1112
    }
34✔
1113

1114
    // Update the location info with the data from the response
1115
    try {
4,386✔
1116
        auto json = parse<BsonDocument>(response.body);
4,386✔
1117
        auto hostname = get<std::string>(json, "hostname");
4,386✔
1118
        auto ws_hostname = get<std::string>(json, "ws_hostname");
4,386✔
1119
        std::optional<std::string> sync_route;
4,386✔
1120
        read_field(json, "sync_route", sync_route);
4,386✔
1121

1122
        util::CheckedLockGuard guard(m_route_mutex);
4,386✔
1123
        // Update the local hostname and path information
1124
        update_hostname(hostname, ws_hostname, base_url);
4,386✔
1125
        m_location_updated = true;
4,386✔
1126
        if (!sync_route) {
4,386✔
1127
            sync_route = make_sync_route();
4,378✔
1128
        }
4,378✔
1129
        m_sync_manager->set_sync_route(*sync_route, true);
4,386✔
1130
    }
4,386✔
1131
    catch (const AppError& ex) {
4,386✔
UNCOV
1132
        return ex;
×
UNCOV
1133
    }
×
1134
    return util::none;
4,386✔
1135
}
4,386✔
1136

1137
void App::update_location_and_resend(std::unique_ptr<Request>&& request, IntermediateCompletion&& completion)
1138
{
4,406✔
1139
    // Update the location information if a redirect response was received or m_location_updated == false
1140
    // and then send the request to the server with request.url updated to the new AppServices hostname.
1141
    request_location(
4,406✔
1142
        [completion = std::move(completion), request = std::move(request),
4,406✔
1143
         self = shared_from_this()](Optional<AppError> error) mutable {
4,406✔
1144
            if (error) {
4,406✔
1145
                // Operation failed, pass it up the chain
1146
                return completion(std::move(request), AppUtils::make_apperror_response(*error));
30✔
1147
            }
30✔
1148

1149
            // If the location info was updated, update the original request to point
1150
            // to the new location URL.
1151
            auto url = util::Uri::parse(request->url);
4,376✔
1152
            request->url =
4,376✔
1153
                util::format("%1%2%3%4", self->get_host_url(), url.get_path(), url.get_query(), url.get_frag());
4,376✔
1154

1155
            self->log_debug("App: send_request(after location update): %1 %2", request->method, request->url);
4,376✔
1156
            // Retry the original request with the updated url
1157
            auto& request_ref = *request;
4,376✔
1158
            self->m_config.transport->send_request_to_server(
4,376✔
1159
                request_ref,
4,376✔
1160
                [completion = std::move(completion), request = std::move(request)](const Response& response) mutable {
4,376✔
1161
                    completion(std::move(request), response);
4,376✔
1162
                });
4,376✔
1163
        },
4,376✔
1164
        // The base_url is not changing for this request
1165
        util::none);
4,406✔
1166
}
4,406✔
1167

1168
void App::post(std::string&& route, UniqueFunction<void(Optional<AppError>)>&& completion, const BsonDocument& body)
1169
{
4,836✔
1170
    do_request(
4,836✔
1171
        make_request(HttpMethod::post, std::move(route), nullptr, RequestTokenType::NoAuth, Bson(body).to_string()),
4,836✔
1172
        [completion = std::move(completion)](auto&&, const Response& response) {
4,836✔
1173
            completion(AppUtils::check_for_errors(response));
4,836✔
1174
        });
4,836✔
1175
}
4,836✔
1176

1177
void App::do_request(std::unique_ptr<Request>&& request, IntermediateCompletion&& completion, bool update_location)
1178
{
15,477✔
1179
    // NOTE: Since the calls to `send_request_to_server()` or `update_location_and_resend()` do not
1180
    // capture a shared_ptr to App as part of their callback, any function that calls `do_request()`
1181
    // or `do_authenticated_request()` needs to capture the App as `self = shared_from_this()` for
1182
    // the completion callback to ensure the lifetime of the App object is extended until the
1183
    // callback is called after the operation is complete.
1184

1185
    // Verify the request URL to make sure it is valid
1186
    if (auto valid_url = util::Uri::try_parse(request->url); !valid_url.is_ok()) {
15,477✔
UNCOV
1187
        completion(std::move(request), AppUtils::make_apperror_response(
×
UNCOV
1188
                                           AppError{valid_url.get_status().code(), valid_url.get_status().reason()}));
×
UNCOV
1189
        return;
×
UNCOV
1190
    }
×
1191

1192
    // Refresh the location info when app is created or when requested (e.g. after a websocket redirect)
1193
    // to ensure the http and websocket URL information is up to date.
1194
    {
15,477✔
1195
        util::CheckedUniqueLock lock(m_route_mutex);
15,477✔
1196
        if (update_location) {
15,477✔
1197
            // If requesting a location update, force the location to be updated before sending the request.
1198
            m_location_updated = false;
47✔
1199
        }
47✔
1200
        if (!m_location_updated) {
15,477✔
1201
            lock.unlock();
4,406✔
1202
            // Location info needs to be requested, update the location info and then send the request
1203
            update_location_and_resend(std::move(request), std::move(completion));
4,406✔
1204
            return;
4,406✔
1205
        }
4,406✔
1206
    }
15,477✔
1207

1208
    log_debug("App: do_request: %1 %2", request->method, request->url);
11,071✔
1209
    // If location info has already been updated, then send the request directly
1210
    auto& request_ref = *request;
11,071✔
1211
    m_config.transport->send_request_to_server(
11,071✔
1212
        request_ref,
11,071✔
1213
        [completion = std::move(completion), request = std::move(request)](const Response& response) mutable {
11,071✔
1214
            completion(std::move(request), response);
11,071✔
1215
        });
11,071✔
1216
}
11,071✔
1217

1218
void App::do_authenticated_request(HttpMethod method, std::string&& route, std::string&& body,
1219
                                   const std::shared_ptr<User>& user, RequestTokenType token_type,
1220
                                   util::UniqueFunction<void(const Response&)>&& completion)
1221
{
5,572✔
1222
    auto request = make_request(method, std::move(route), user, token_type, std::move(body));
5,572✔
1223
    do_request(std::move(request), [token_type, user, completion = std::move(completion), self = shared_from_this()](
5,572✔
1224
                                       std::unique_ptr<Request>&& request, const Response& response) mutable {
5,572✔
1225
        if (auto error = AppUtils::check_for_errors(response)) {
5,572✔
1226
            self->handle_auth_failure(std::move(*error), std::move(request), response, user, token_type,
66✔
1227
                                      std::move(completion));
66✔
1228
        }
66✔
1229
        else {
5,506✔
1230
            completion(response);
5,506✔
1231
        }
5,506✔
1232
    });
5,572✔
1233
}
5,572✔
1234

1235
void App::handle_auth_failure(const AppError& error, std::unique_ptr<Request>&& request, const Response& response,
1236
                              const std::shared_ptr<User>& user, RequestTokenType token_type,
1237
                              util::UniqueFunction<void(const Response&)>&& completion)
1238
{
66✔
1239
    // Only handle auth failures
1240
    if (*error.additional_status_code != 401) {
66✔
1241
        completion(response);
40✔
1242
        return;
40✔
1243
    }
40✔
1244

1245
    // If the refresh token is invalid then the user needs to be logged back
1246
    // in to be able to use it again
1247
    if (token_type == RequestTokenType::RefreshToken) {
26✔
1248
        if (user && user->is_logged_in()) {
18!
UNCOV
1249
            user->log_out();
×
UNCOV
1250
        }
×
1251
        completion(response);
18✔
1252
        return;
18✔
1253
    }
18✔
1254

1255
    // Otherwise we may be able to request a new access token and have the request succeed with that
1256
    refresh_access_token(user, false,
8✔
1257
                         [self = shared_from_this(), request = std::move(request), completion = std::move(completion),
8✔
1258
                          response = std::move(response), user](Optional<AppError>&& error) mutable {
8✔
1259
                             if (error) {
8✔
1260
                                 // pass the error back up the chain
1261
                                 completion(response);
4✔
1262
                                 return;
4✔
1263
                             }
4✔
1264

1265
                             // Reissue the request with the new access token
1266
                             request->headers = get_request_headers(user, RequestTokenType::AccessToken);
4✔
1267
                             self->do_request(std::move(request), [self = self, completion = std::move(completion)](
4✔
1268
                                                                      auto&&, auto& response) {
4✔
1269
                                 completion(response);
4✔
1270
                             });
4✔
1271
                         });
4✔
1272
}
8✔
1273

1274
/// MARK: - refresh access token
1275
void App::refresh_access_token(const std::shared_ptr<User>& user, bool update_location,
1276
                               util::UniqueFunction<void(Optional<AppError>)>&& completion)
1277
{
115✔
1278
    if (!user) {
115✔
1279
        completion(AppError(ErrorCodes::ClientUserNotFound, "No current user exists"));
2✔
1280
        return;
2✔
1281
    }
2✔
1282

1283
    if (!user->is_logged_in()) {
113✔
1284
        completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "The user is not logged in"));
2✔
1285
        return;
2✔
1286
    }
2✔
1287

1288
    log_debug("App: refresh_access_token: user_id: %1%2", user->user_id(),
111✔
1289
              update_location ? " (updating location)" : "");
111✔
1290

1291
    // If update_location is set, force the location info to be updated before sending the request
1292
    do_request(
111✔
1293
        make_request(HttpMethod::post, url_for_path("/auth/session"), user, RequestTokenType::RefreshToken, ""),
111✔
1294
        [completion = std::move(completion), self = shared_from_this(), user](auto&&, const Response& response) {
111✔
1295
            if (auto error = AppUtils::check_for_errors(response)) {
111✔
1296
                self->log_error("App: refresh_access_token: %1 -> %2 ERROR: %3", user->user_id(),
64✔
1297
                                response.http_status_code, error->what());
64✔
1298

1299
                return completion(std::move(error));
64✔
1300
            }
64✔
1301

1302
            try {
47✔
1303
                auto json = parse<BsonDocument>(response.body);
47✔
1304
                RealmJWT access_token{get<std::string>(json, "access_token")};
47✔
1305
                self->m_metadata_store->update_user(user->user_id(), [&](auto& data) {
47✔
1306
                    data.access_token = access_token;
45✔
1307
                    user->update_backing_data(data);
45✔
1308
                });
45✔
1309
            }
47✔
1310
            catch (AppError& err) {
47✔
1311
                return completion(std::move(err));
2✔
1312
            }
2✔
1313

1314
            return completion(util::none);
45✔
1315
        },
47✔
1316
        update_location);
111✔
1317
}
111✔
1318

1319
std::string App::function_call_url_path() const
1320
{
594✔
1321
    util::CheckedLockGuard guard(m_route_mutex);
594✔
1322
    return util::format("%1/functions/call", m_app_route);
594✔
1323
}
594✔
1324

1325
void App::call_function(const std::shared_ptr<User>& user, const std::string& name, std::string_view args_ejson,
1326
                        const Optional<std::string>& service_name_opt,
1327
                        UniqueFunction<void(const std::string*, Optional<AppError>)>&& completion)
1328
{
586✔
1329
    auto service_name = service_name_opt ? *service_name_opt : "<none>";
586✔
1330
    if (would_log(util::Logger::Level::debug)) {
586✔
1331
        log_debug("App: call_function: %1 service_name: %2 args_bson: %3", name, service_name, args_ejson);
586✔
1332
    }
586✔
1333

1334
    auto args = util::format("{\"arguments\":%1,\"name\":%2%3}", args_ejson, nlohmann::json(name).dump(),
586✔
1335
                             service_name_opt ? (",\"service\":" + nlohmann::json(service_name).dump()) : "");
586✔
1336

1337
    do_authenticated_request(HttpMethod::post, function_call_url_path(), std::move(args), user,
586✔
1338
                             RequestTokenType::AccessToken,
586✔
1339
                             [self = shared_from_this(), name = name, service_name = std::move(service_name),
586✔
1340
                              completion = std::move(completion)](const Response& response) {
586✔
1341
                                 if (auto error = AppUtils::check_for_errors(response)) {
586✔
1342
                                     self->log_error("App: call_function: %1 service_name: %2 -> %3 ERROR: %4", name,
18✔
1343
                                                     service_name, response.http_status_code, error->what());
18✔
1344
                                     return completion(nullptr, error);
18✔
1345
                                 }
18✔
1346
                                 completion(&response.body, util::none);
568✔
1347
                             });
568✔
1348
}
586✔
1349

1350
void App::call_function(const std::shared_ptr<User>& user, const std::string& name, const BsonArray& args_bson,
1351
                        const Optional<std::string>& service_name,
1352
                        UniqueFunction<void(Optional<Bson>&&, Optional<AppError>)>&& completion)
1353
{
586✔
1354
    auto service_name2 = service_name ? *service_name : "<none>";
586✔
1355
    std::stringstream args_ejson;
586✔
1356
    args_ejson << "[";
586✔
1357
    bool not_first = false;
586✔
1358
    for (auto&& arg : args_bson) {
602✔
1359
        if (not_first)
602✔
1360
            args_ejson << ',';
16✔
1361
        args_ejson << arg.toJson();
602✔
1362
        not_first = true;
602✔
1363
    }
602✔
1364
    args_ejson << "]";
586✔
1365

1366
    call_function(user, name, std::move(args_ejson).str(), service_name,
586✔
1367
                  [self = shared_from_this(), name, service_name = std::move(service_name2),
586✔
1368
                   completion = std::move(completion)](const std::string* response, util::Optional<AppError> err) {
586✔
1369
                      if (err) {
586✔
1370
                          return completion({}, err);
18✔
1371
                      }
18✔
1372
                      if (!response) {
568✔
UNCOV
1373
                          return completion({}, AppError{ErrorCodes::AppUnknownError, "Empty response from server"});
×
UNCOV
1374
                      }
×
1375
                      util::Optional<Bson> body_as_bson;
568✔
1376
                      try {
568✔
1377
                          body_as_bson = bson::parse(*response);
568✔
1378
                          if (self->would_log(util::Logger::Level::debug)) {
568✔
1379
                              self->log_debug("App: call_function: %1 service_name: %2 - results: %3", name,
568✔
1380
                                              service_name, body_as_bson ? body_as_bson->to_string() : "<none>");
568✔
1381
                          }
568✔
1382
                      }
568✔
1383
                      catch (const std::exception& e) {
568✔
UNCOV
1384
                          self->log_error("App: call_function: %1 service_name: %2 - error parsing result: %3", name,
×
UNCOV
1385
                                          service_name, e.what());
×
UNCOV
1386
                          return completion(util::none, AppError(ErrorCodes::BadBsonParse, e.what()));
×
1387
                      };
568✔
1388
                      completion(std::move(body_as_bson), util::none);
568✔
1389
                  });
568✔
1390
}
586✔
1391

1392
void App::call_function(const std::shared_ptr<User>& user, const std::string& name, const BsonArray& args_bson,
1393
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
1394
{
4✔
1395
    call_function(user, name, args_bson, util::none, std::move(completion));
4✔
1396
}
4✔
1397

1398
void App::call_function(const std::string& name, const BsonArray& args_bson,
1399
                        const Optional<std::string>& service_name,
1400
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
UNCOV
1401
{
×
UNCOV
1402
    call_function(current_user(), name, args_bson, service_name, std::move(completion));
×
UNCOV
1403
}
×
1404

1405
void App::call_function(const std::string& name, const BsonArray& args_bson,
1406
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
1407
{
2✔
1408
    call_function(current_user(), name, args_bson, std::move(completion));
2✔
1409
}
2✔
1410

1411
Request App::make_streaming_request(const std::shared_ptr<User>& user, const std::string& name,
1412
                                    const BsonArray& args_bson, const Optional<std::string>& service_name) const
1413
{
8✔
1414
    auto args = BsonDocument{
8✔
1415
        {"arguments", args_bson},
8✔
1416
        {"name", name},
8✔
1417
    };
8✔
1418
    if (service_name) {
8✔
1419
        args["service"] = *service_name;
8✔
1420
    }
8✔
1421
    const auto args_json = Bson(args).to_string();
8✔
1422

1423
    auto args_base64 = std::string(util::base64_encoded_size(args_json.size()), '\0');
8✔
1424
    util::base64_encode(args_json, args_base64);
8✔
1425

1426
    auto url = function_call_url_path() + "?baas_request=" + util::uri_percent_encode(args_base64);
8✔
1427
    if (user) {
8✔
1428
        url += "&baas_at=";
2✔
1429
        url += user->access_token(); // doesn't need url encoding
2✔
1430
    }
2✔
1431

1432
    return Request{
8✔
1433
        HttpMethod::get,
8✔
1434
        url,
8✔
1435
        m_request_timeout_ms,
8✔
1436
        {{"Accept", "text/event-stream"}},
8✔
1437
    };
8✔
1438
}
8✔
1439

1440
std::unique_ptr<Request> App::make_request(HttpMethod method, std::string&& url, const std::shared_ptr<User>& user,
1441
                                           RequestTokenType token_type, std::string&& body) const
1442
{
15,473✔
1443
    auto request = std::make_unique<Request>();
15,473✔
1444
    request->method = method;
15,473✔
1445
    request->url = std::move(url);
15,473✔
1446
    request->body = std::move(body);
15,473✔
1447
    request->headers = get_request_headers(user, token_type);
15,473✔
1448
    request->timeout_ms = m_request_timeout_ms;
15,473✔
1449
    return request;
15,473✔
1450
}
15,473✔
1451

1452
PushClient App::push_notification_client(const std::string& service_name)
1453
{
10✔
1454
    return PushClient(service_name, m_config.app_id, std::shared_ptr<AuthRequestClient>(shared_from_this(), this));
10✔
1455
}
10✔
1456

1457
void App::emit_change_to_subscribers()
1458
{
4,954✔
1459
    // This wrapper is needed only to be able to add the `REQUIRES(!m_user_mutex)`
1460
    // annotation. Calling this function with the lock held leads to a deadlock
1461
    // if any of the listeners try to access us.
1462
    Subscribable<App>::emit_change_to_subscribers(*this);
4,954✔
1463
}
4,954✔
1464

1465
// MARK: - UserProvider
1466

1467
void App::register_sync_user(User& user)
1468
{
9,595✔
1469
    auto& tracked_user = m_users[user.user_id()];
9,595✔
1470
    REALM_ASSERT(!tracked_user || !tracked_user->weak_from_this().lock());
9,595!
1471
    tracked_user = &user;
9,595✔
1472
    user.update_backing_data(m_metadata_store->get_user(user.user_id()));
9,595✔
1473
}
9,595✔
1474

1475
void App::unregister_sync_user(User& user)
1476
{
9,595✔
1477
    util::CheckedLockGuard lock(m_user_mutex);
9,595✔
1478
    auto it = m_users.find(user.user_id());
9,595✔
1479
    REALM_ASSERT(it != m_users.end());
9,595✔
1480
    // If the user was requested while we were waiting for the lock, it may
1481
    // have already been replaced with a new instance for the same user id
1482
    if (it != m_users.end() && it->second == &user) {
9,595✔
1483
        m_users.erase(it);
9,595✔
1484
    }
9,595✔
1485
    if (m_current_user == &user) {
9,595✔
1486
        m_current_user = nullptr;
9,371✔
1487
    }
9,371✔
1488
}
9,595✔
1489

1490
bool App::immediately_run_file_actions(std::string_view realm_path)
1491
{
10✔
1492
    return m_metadata_store->immediately_run_file_actions(*m_file_manager, realm_path);
10✔
1493
}
10✔
1494

1495
std::string App::path_for_realm(const SyncConfig& config, std::optional<std::string> custom_file_name) const
1496
{
28✔
1497
    return m_file_manager->path_for_realm(config, std::move(custom_file_name));
28✔
1498
}
28✔
1499

1500
} // namespace realm::app
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc