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

realm / realm-core / 2210

09 Apr 2024 03:41PM UTC coverage: 92.601% (+0.5%) from 92.106%
2210

push

Evergreen

web-flow
Merge pull request #7300 from realm/tg/rework-metadata-storage

Rework sync user handling and metadata storage

102800 of 195548 branches covered (52.57%)

3051 of 3153 new or added lines in 46 files covered. (96.76%)

41 existing lines in 11 files now uncovered.

249129 of 269035 relevant lines covered (92.6%)

46864217.27 hits per line

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

98.91
/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 "external/json/json.hpp"
20
#include <realm/object-store/sync/app.hpp>
21

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

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

35
#include <sstream>
36
#include <string>
37

38
using namespace realm;
39
using namespace realm::app;
40
using namespace bson;
41
using util::Optional;
42
using util::UniqueFunction;
43

44
namespace {
45
// MARK: - Helpers
46

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

1✔
55
template <typename T>
1✔
56
T as(const Bson& bson)
57
{
134,904✔
58
    if (holds_alternative<T>(bson)) {
134,904✔
59
        return static_cast<T>(bson);
167,571✔
60
    }
167,571✔
61
    throw_json_error(ErrorCodes::MalformedJson, "?");
32,667✔
62
}
32,667✔
63

64
template <typename T>
65
T get(const BsonDocument& doc, const std::string& key)
66
{
99,184✔
67
    if (auto val = doc.find(key)) {
99,184✔
68
        return as<T>(*val);
122,498✔
69
    }
122,498✔
70
    throw_json_error(ErrorCodes::MissingJsonKey, key);
23,314✔
71
    return {};
23,314✔
72
}
×
73

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

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

16✔
91
template <typename T>
16✔
92
void read_field(const BsonDocument& data, const std::string& key, Optional<T>& value)
93
{
256✔
94
    if (auto val = data.find(key)) {
256✔
95
        value = as<T>(*val);
2,206✔
96
    }
2,206✔
97
}
264✔
98

8✔
99
template <typename T>
2,142✔
100
T parse(std::string_view str)
101
{
25,176✔
102
    try {
25,176✔
103
        return as<T>(bson::parse(str));
32,093✔
104
    }
32,093✔
105
    catch (const std::exception& e) {
6,933✔
106
        throw_json_error(ErrorCodes::MalformedJson, e.what());
6,933✔
107
    }
17✔
108
}
25,177✔
109

1✔
110
struct UserAPIKeyResponseHandler {
6,917✔
111
    UniqueFunction<void(App::UserAPIKey&&, Optional<AppError>)> completion;
112
    void operator()(const Response& response)
113
    {
352✔
114
        if (auto error = AppUtils::check_for_errors(response)) {
352✔
115
            return completion({}, std::move(error));
198✔
116
        }
198✔
117

99✔
118
        try {
187✔
119
            auto json = parse<BsonDocument>(response.body);
176✔
120
            completion(read_user_api_key(json), {});
187✔
121
        }
187✔
122
        catch (AppError& e) {
99✔
123
            completion({}, std::move(e));
11✔
UNCOV
124
        }
×
125
    }
176✔
126

127
    static App::UserAPIKey read_user_api_key(const BsonDocument& doc)
11✔
128
    {
256✔
129
        App::UserAPIKey user_api_key;
256✔
130
        read_field(doc, "_id", user_api_key.id);
272✔
131
        read_field(doc, "key", user_api_key.key);
272✔
132
        read_field(doc, "name", user_api_key.name);
272✔
133
        read_field(doc, "disabled", user_api_key.disabled);
272✔
134
        return user_api_key;
272✔
135
    }
272✔
136
};
16✔
137

16✔
138
enum class RequestTokenType { NoAuth, AccessToken, RefreshToken };
139

140
// generate the request headers for a HTTP call, by default it will generate headers with a refresh token if a user is
141
// passed
142
HttpHeaders get_request_headers(const std::shared_ptr<SyncUser>& with_user_authorization = nullptr,
143
                                RequestTokenType token_type = RequestTokenType::RefreshToken)
7,601✔
144
{
41,691✔
145
    HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}};
41,691✔
146

18,902✔
147
    if (with_user_authorization) {
34,090✔
148
        switch (token_type) {
15,386✔
149
            case RequestTokenType::NoAuth:
2,754✔
150
                break;
2,754✔
151
            case RequestTokenType::AccessToken:
16,732✔
152
                headers.insert({"Authorization", util::format("Bearer %1", with_user_authorization->access_token())});
14,080✔
153
                break;
14,080✔
154
            case RequestTokenType::RefreshToken:
1,510✔
155
                headers.insert(
9,009✔
156
                    {"Authorization", util::format("Bearer %1", with_user_authorization->refresh_token())});
9,009✔
157
                break;
9,009✔
158
        }
41,691✔
159
    }
34,090✔
160
    return headers;
34,090✔
161
}
34,102✔
162

12✔
163
UniqueFunction<void(const Response&)> handle_default_response(UniqueFunction<void(Optional<AppError>)>&& completion)
12✔
164
{
8,972✔
165
    return [completion = std::move(completion)](const Response& response) {
8,972✔
166
        completion(AppUtils::check_for_errors(response));
8,960✔
167
    };
8,960✔
168
}
8,960✔
169

170
constexpr static std::string_view s_base_path = "/api/client/v2.0";
171
constexpr static std::string_view s_app_path = "/app";
172
constexpr static std::string_view s_auth_path = "/auth";
173
constexpr static std::string_view s_sync_path = "/realm-sync";
174
constexpr static uint64_t s_default_timeout_ms = 60000;
175
constexpr static std::string_view s_username_password_provider_key = "local-userpass";
176
constexpr static std::string_view s_user_api_key_provider_key_path = "api_keys";
177
constexpr static int s_max_http_redirects = 20;
178
static util::FlatMap<std::string, util::FlatMap<std::string, SharedApp>> s_apps_cache; // app_id -> base_url -> app
179
std::mutex s_apps_mutex;
180

181
} // anonymous namespace
182

2,152✔
183
namespace realm {
2,152✔
184
namespace app {
2,152✔
185

186
std::string_view App::default_base_url()
187
{
35,048✔
188
    return "https://services.cloud.mongodb.com";
35,048✔
189
}
35,048✔
190

2,129✔
191
App::Config::DeviceInfo::DeviceInfo()
2,129✔
192
    : platform(util::get_library_platform())
5✔
193
    , cpu_arch(util::get_library_cpu_arch())
5✔
194
    , core_version(REALM_VERSION_STRING)
5✔
195
{
38,691✔
196
}
38,691✔
197

5✔
198
App::Config::DeviceInfo::DeviceInfo(std::string a_platform_version, std::string an_sdk_version, std::string an_sdk,
5✔
199
                                    std::string a_device_name, std::string a_device_version,
2,124✔
200
                                    std::string a_framework_name, std::string a_framework_version,
2,124✔
201
                                    std::string a_bundle_id)
2,124✔
202
    : DeviceInfo()
203
{
4,968✔
204
    platform_version = a_platform_version;
4,973✔
205
    sdk_version = an_sdk_version;
4,973✔
206
    sdk = an_sdk;
4,973✔
207
    device_name = a_device_name;
4,973✔
208
    device_version = a_device_version;
4,968✔
209
    framework_name = a_framework_name;
4,972✔
210
    framework_version = a_framework_version;
4,973✔
211
    bundle_id = a_bundle_id;
4,972✔
212
}
4,972✔
213

1✔
214
// NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive.
215
// "Calling function configure requires negative capability '!app->m_route_mutex'"
1✔
216
// But 'app' is an object just created in this static method so it is not possible to annotate this in the header.
1✔
217
SharedApp App::get_app(CacheMode mode, const Config& config,
218
                       const SyncClientConfig& sync_client_config) NO_THREAD_SAFETY_ANALYSIS
219
{
36,752✔
220
    if (mode == CacheMode::Enabled) {
36,752✔
221
        std::lock_guard<std::mutex> lock(s_apps_mutex);
2,328✔
222
        auto& app = s_apps_cache[config.app_id][config.base_url.value_or(std::string(App::default_base_url()))];
2,328✔
223
        if (!app) {
80✔
224
            app = std::make_shared<App>(Private(), config);
48✔
225
            app->configure(sync_client_config);
48✔
226
        }
48✔
227
        return app;
80!
228
    }
80!
229
    REALM_ASSERT(mode == CacheMode::Disabled);
34,424✔
230
    auto app = std::make_shared<App>(Private(), config);
34,424✔
231
    app->configure(sync_client_config);
34,424✔
232
    return app;
34,424✔
233
}
34,424✔
234

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

40✔
241
        auto app_it = base_url ? apps_by_url.find(*base_url) : apps_by_url.begin();
2,199✔
242
        if (app_it != apps_by_url.end()) {
80✔
243
            return app_it->second;
64✔
244
        }
64✔
245
    }
16✔
246

8✔
247
    return nullptr;
2,143✔
248
}
16✔
249

250
void App::clear_cached_apps()
2,127✔
251
{
36,284✔
252
    std::lock_guard<std::mutex> lock(s_apps_mutex);
36,284✔
253
    s_apps_cache.clear();
35,992✔
254
}
35,992✔
255

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

266
App::App(Private, const Config& config)
267
    : m_config(config)
2,127✔
268
    , m_base_url(m_config.base_url.value_or(std::string(App::default_base_url())))
269
    , m_location_updated(false)
270
    , m_request_timeout_ms(m_config.default_request_timeout_ms.value_or(s_default_timeout_ms))
2,127✔
271
{
34,472✔
272
#ifdef __EMSCRIPTEN__
2,127✔
273
    if (!m_config.transport) {
274
        m_config.transport = std::make_shared<_impl::EmscriptenNetworkTransport>();
275
    }
22,949✔
276
#endif
22,949✔
277
    REALM_ASSERT(m_config.transport);
36,595✔
278
    REALM_ASSERT(!m_config.device_info.platform.empty());
36,595✔
279

17,168✔
280
    // if a base url is provided, then verify the value
17,168✔
281
    if (m_config.base_url) {
36,595✔
282
        if (auto comp = AppUtils::split_url(*m_config.base_url); !comp.is_ok()) {
28,285✔
283
            throw Exception(comp.get_status());
22,949✔
284
        }
285
    }
34,472✔
286
    // Setup a baseline set of routes using the provided or default base url
20,278✔
287
    // These will be updated when the location info is refreshed prior to sending the
20,278✔
288
    // first AppServices HTTP request.
20,278✔
289
    configure_route(m_base_url);
34,472✔
290

17,168✔
291
    if (m_config.device_info.platform_version.empty()) {
34,472✔
292
        throw InvalidArgument("You must specify the Platform Version in App::Config::device_info");
19,809✔
293
    }
19,809✔
294

36,977✔
295
    if (m_config.device_info.sdk.empty()) {
54,281✔
296
        throw InvalidArgument("You must specify the SDK Name in App::Config::device_info");
19,809✔
297
    }
19,809✔
298

17,168✔
299
    if (m_config.device_info.sdk_version.empty()) {
34,472✔
300
        throw InvalidArgument("You must specify the SDK Version in App::Config::device_info");
301
    }
30✔
302
}
34,502!
303

30✔
304
App::~App() {}
34,502✔
305

30✔
306
void App::configure(const SyncClientConfig& sync_client_config)
30✔
307
{
34,472✔
308
    std::string ws_route;
34,472✔
309
    {
39,212✔
310
        util::CheckedLockGuard guard(m_route_mutex);
39,212✔
311
        // Make sure to request the location when the app is configured
21,908✔
312
        m_location_updated = false;
39,212✔
313
        // Create a tentative sync route using the generated ws_host_url
17,168✔
314
        REALM_ASSERT(!m_ws_host_url.empty());
34,472✔
315
        ws_route = make_sync_route();
34,472✔
316
    }
34,472✔
317

17,168✔
318
    // When App starts, the ws_host_url will be populated with the generated value based on
17,168✔
319
    // the provided host_url value and the sync route will be created using this. If this is
17,168✔
320
    // is incorrect, the websocket connection will fail and the SyncSession will request a
17,168✔
321
    // new access token, which will update the location if it has not already.
19,327✔
322
    m_sync_manager = SyncManager::create(shared_from_this(), ws_route, sync_client_config, config().app_id);
36,631✔
323
}
36,631✔
324

2,159✔
325
bool App::init_logger()
326
{
108,714✔
327
    if (!m_logger_ptr && m_sync_manager) {
108,752✔
328
        m_logger_ptr = m_sync_manager->get_logger();
5,550✔
329
    }
5,550✔
330
    return bool(m_logger_ptr);
108,752✔
331
}
108,714✔
332

333
bool App::would_log(util::Logger::Level level)
4,249✔
334
{
22,477✔
335
    return init_logger() && m_logger_ptr->would_log(util::LogCategory::app, level);
22,477✔
336
}
22,477✔
337

338
template <class... Params>
339
void App::log_debug(const char* message, Params&&... params)
4,253✔
340
{
94,259✔
341
    if (init_logger()) {
94,259✔
342
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::debug, message,
94,259✔
343
                          std::forward<Params>(params)...);
92,133✔
344
    }
90,006✔
345
}
90,006✔
346

347
template <class... Params>
4,253✔
348
void App::log_error(const char* message, Params&&... params)
349
{
4,733✔
350
    if (init_logger()) {
480!
351
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::error, message,
4,733✔
352
                          std::forward<Params>(params)...);
4,733✔
353
    }
480✔
354
}
480✔
355

356
std::string App::auth_route()
357
{
18,320✔
358
    util::CheckedLockGuard guard(m_route_mutex);
18,320✔
359
    return m_auth_route;
18,320✔
360
}
18,320✔
361

2,200✔
362
std::string App::base_url()
2,200✔
363
{
2,200✔
364
    util::CheckedLockGuard guard(m_route_mutex);
2,200✔
365
    return m_base_url;
366
}
367

368
std::string App::get_host_url()
2,200✔
369
{
6,137✔
370
    util::CheckedLockGuard guard(m_route_mutex);
6,137✔
371
    return m_host_url;
6,136✔
372
}
8,335✔
373

1,890✔
374
std::string App::get_ws_host_url()
1,890✔
375
{
576✔
376
    util::CheckedLockGuard guard(m_route_mutex);
576✔
377
    return m_ws_host_url;
576✔
378
}
2,775✔
379

6✔
380

6✔
381
std::string App::make_sync_route(Optional<std::string> ws_host_url)
382
{
40,128✔
383
    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,
42,321✔
384
                        s_sync_path);
41,976✔
385
}
41,976✔
386

1,848✔
387
void App::configure_route(const std::string& host_url, const std::optional<std::string>& ws_host_url)
388
{
40,128✔
389
    // We got a new host url, save it
20,257✔
390
    m_host_url = (host_url.length() > 0 ? host_url : m_base_url);
40,473✔
391

19,912✔
392
    // If a valid websocket host url was included, save it
19,912✔
393
    if (ws_host_url && ws_host_url->length() > 0) {
40,128✔
394
        m_ws_host_url = *ws_host_url;
7,782✔
395
    }
7,782✔
396
    // Otherwise, convert the host url to a websocket host url
19,294✔
397
    else {
34,472✔
398
        m_ws_host_url = App::create_ws_host_url(m_host_url);
34,472✔
399
    }
36,598✔
400

22,038✔
401
    // host_url is the url to the server: e.g., https://services.cloud.mongodb.com or https://localhost:9090
22,038✔
402
    // base_route is the baseline client api path: e.g. <host_url>/api/client/v2.0
19,912✔
403
    m_base_route = util::format("%1%2", m_host_url, s_base_path);
40,128✔
404
    // app_route is the cloud app URL: <host_url>/api/client/v2.0/app/<app_id>
19,912✔
405
    m_app_route = util::format("%1%2/%3", m_base_route, s_app_path, m_config.app_id);
40,128✔
406
    // auth_route is cloud app auth URL: <host_url>/api/client/v2.0/app/<app_id>/auth
19,912✔
407
    m_auth_route = util::format("%1%2", m_app_route, s_auth_path);
42,471✔
408
}
42,471✔
409

2,343✔
410
// Create a temporary websocket URL domain using the given host URL
411
// This updates the URL based on the following assumptions:
412
// If the URL doesn't start with 'http' => <host-url>
413
// http[s]://[region-prefix]realm.mongodb.com => ws[s]://ws.[region-prefix]realm.mongodb.com
10✔
414
// http[s]://[region-prefix]services.cloud.mongodb.com => ws[s]://[region-prefix].ws.services.cloud.mongodb.com
10✔
415
// All others => http[s]://<host-url> => ws[s]://<host-url>
10✔
416
std::string App::create_ws_host_url(const std::string_view host_url)
417
{
35,720✔
418
    constexpr static std::string_view old_base_domain = "realm.mongodb.com";
35,720✔
419
    constexpr static std::string_view new_base_domain = "services.cloud.mongodb.com";
35,720✔
420
    const size_t base_len = std::char_traits<char>::length("http://");
35,720✔
421

20,137✔
422
    // Doesn't contain 7 or more characters (length of 'http://') or start with http,
20,137✔
423
    // just return provided string
20,137✔
424
    if (host_url.length() < base_len || host_url.substr(0, 4) != "http") {
38,065✔
425
        return std::string(host_url);
2,361✔
426
    }
16✔
427
    // If it starts with 'https' then ws url will start with 'wss'
17,784✔
428
    bool https = host_url[4] == 's';
35,704✔
429
    size_t prefix_len = base_len + (https ? 1 : 0);
32,817✔
430
    std::string_view prefix = https ? "wss://" : "ws://";
32,817✔
431

17,785✔
432
    // http[s]://[<region-prefix>]realm.mongodb.com[/<path>] =>
17,785✔
433
    //     ws[s]://ws.[<region-prefix>]realm.mongodb.com[/<path>]
17,785✔
434
    if (host_url.find(old_base_domain) != std::string_view::npos) {
35,704✔
435
        return util::format("%1ws.%2", prefix, host_url.substr(prefix_len));
96✔
436
    }
96✔
437
    // http[s]://[<region-prefix>]services.cloud.mongodb.com[/<path>] =>
17,737✔
438
    //     ws[s]://[<region-prefix>].ws.services.cloud.mongodb.com[/<path>]
17,737✔
439
    if (auto start = host_url.find(new_base_domain); start != std::string_view::npos) {
35,609✔
440
        return util::format("%1%2ws.%3", prefix, host_url.substr(prefix_len, start - prefix_len),
29,377✔
441
                            host_url.substr(start));
29,377✔
442
    }
29,377✔
443

3,048✔
444
    // All others => http[s]://<host-url>[/<path>] => ws[s]://<host-url>[/<path>]
3,048✔
445
    return util::format("ws%1", host_url.substr(4));
6,232✔
446
}
6,234✔
447

2✔
448
void App::update_hostname(const std::string& host_url, const std::optional<std::string>& ws_host_url,
2✔
449
                          const std::optional<std::string>& new_base_url)
2✔
450
{
5,658✔
451
    // Update url components based on new hostname (and optional websocket hostname) values
2,746✔
452
    log_debug("App: update_hostname: %1%2%3", host_url, ws_host_url ? util::format(" | %1", *ws_host_url) : "",
5,656✔
453
              new_base_url ? util::format(" | base URL: %1", *new_base_url) : "");
5,656✔
454
    // Save the new base url, if provided
2,744✔
455
    if (new_base_url) {
5,656✔
456
        m_base_url = *new_base_url;
5,656✔
457
    }
5,656✔
458
    // If a new host url was returned from the server, use it to configure the routes
2,744✔
459
    // Otherwise, use the m_base_url value
2,744✔
460
    configure_route(host_url.length() > 0 ? host_url : m_base_url, ws_host_url);
5,656✔
461
}
5,656✔
462

463
// MARK: - Template specializations
464

465
template <>
1✔
466
App::UsernamePasswordProviderClient App::provider_client<App::UsernamePasswordProviderClient>()
1✔
467
{
8,609✔
468
    return App::UsernamePasswordProviderClient(shared_from_this());
8,609✔
469
}
8,609✔
470

471
template <>
472
App::UserAPIKeyProviderClient App::provider_client<App::UserAPIKeyProviderClient>()
473
{
160✔
474
    return App::UserAPIKeyProviderClient(*this);
163✔
475
}
163✔
476

3✔
477
// MARK: - UsernamePasswordProviderClient
3✔
478

3✔
479
void App::UsernamePasswordProviderClient::register_email(const std::string& email, const std::string& password,
3✔
480
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
481
{
8,640✔
482
    m_parent->log_debug("App: register_email: %1", email);
8,640✔
483
    m_parent->post(util::format("%1/providers/%2/register", m_parent->auth_route(), s_username_password_provider_key),
8,640✔
484
                   std::move(completion), {{"email", email}, {"password", password}});
8,681✔
485
}
8,681✔
486

29✔
487
void App::UsernamePasswordProviderClient::confirm_user(const std::string& token, const std::string& token_id,
29✔
488
                                                       UniqueFunction<void(Optional<AppError>)>&& completion)
29✔
489
{
16✔
490
    m_parent->log_debug("App: confirm_user");
28✔
491
    m_parent->post(util::format("%1/providers/%2/confirm", m_parent->auth_route(), s_username_password_provider_key),
28✔
492
                   std::move(completion), {{"token", token}, {"tokenId", token_id}});
16✔
493
}
16✔
494

495
void App::UsernamePasswordProviderClient::resend_confirmation_email(
496
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
5✔
497
{
21✔
498
    m_parent->log_debug("App: resend_confirmation_email: %1", email);
21✔
499
    m_parent->post(
21✔
500
        util::format("%1/providers/%2/confirm/send", m_parent->auth_route(), s_username_password_provider_key),
21✔
501
        std::move(completion), {{"email", email}});
16✔
502
}
16✔
503

504
void App::UsernamePasswordProviderClient::retry_custom_confirmation(
17✔
505
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
17✔
506
{
49✔
507
    m_parent->log_debug("App: retry_custom_confirmation: %1", email);
49✔
508
    m_parent->post(
49✔
509
        util::format("%1/providers/%2/confirm/call", m_parent->auth_route(), s_username_password_provider_key),
32✔
510
        std::move(completion), {{"email", email}});
32✔
511
}
32✔
512

513
void App::UsernamePasswordProviderClient::send_reset_password_email(
7✔
514
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
7✔
515
{
7✔
516
    m_parent->log_debug("App: send_reset_password_email: %1", email);
7✔
517
    m_parent->post(
7✔
518
        util::format("%1/providers/%2/reset/send", m_parent->auth_route(), s_username_password_provider_key),
2✔
519
        std::move(completion), {{"email", email}});
2✔
520
}
521

5✔
522
void App::UsernamePasswordProviderClient::reset_password(const std::string& password, const std::string& token,
5✔
523
                                                         const std::string& token_id,
5✔
524
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
5✔
525
{
21✔
526
    m_parent->log_debug("App: reset_password");
21✔
527
    m_parent->post(util::format("%1/providers/%2/reset", m_parent->auth_route(), s_username_password_provider_key),
21✔
528
                   std::move(completion), {{"password", password}, {"token", token}, {"tokenId", token_id}});
21✔
529
}
21✔
530

531
void App::UsernamePasswordProviderClient::call_reset_password_function(
532
    const std::string& email, const std::string& password, const BsonArray& args,
533
    UniqueFunction<void(Optional<AppError>)>&& completion)
5✔
534
{
55✔
535
    m_parent->log_debug("App: call_reset_password_function: %1", email);
48✔
536
    m_parent->post(
48✔
537
        util::format("%1/providers/%2/reset/call", m_parent->auth_route(), s_username_password_provider_key),
48✔
538
        std::move(completion), {{"email", email}, {"password", password}, {"arguments", args}});
52✔
539
}
52✔
540

4✔
541
// MARK: - UserAPIKeyProviderClient
4✔
542

4✔
543
std::string App::UserAPIKeyProviderClient::url_for_path(const std::string& path = "") const
544
{
656✔
545
    if (!path.empty()) {
656✔
546
        return m_auth_request_client.url_for_path(
468✔
547
            util::format("%1/%2/%3", s_auth_path, s_user_api_key_provider_key_path, path));
468✔
548
    }
468✔
549

100✔
550
    return m_auth_request_client.url_for_path(util::format("%1/%2", s_auth_path, s_user_api_key_provider_key_path));
196✔
551
}
192✔
552

553
void App::UserAPIKeyProviderClient::create_api_key(
554
    const std::string& name, const std::shared_ptr<SyncUser>& user,
4✔
555
    UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
4✔
556
{
84✔
557
    Request req;
84✔
558
    req.method = HttpMethod::post;
84✔
559
    req.url = url_for_path();
80✔
560
    req.body = Bson(BsonDocument{{"name", name}}).to_string();
80✔
561
    req.uses_refresh_token = true;
80✔
562
    m_auth_request_client.do_authenticated_request(std::move(req), user,
80✔
563
                                                   UserAPIKeyResponseHandler{std::move(completion)});
80✔
564
}
80✔
565

566
void App::UserAPIKeyProviderClient::fetch_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
567
                                                  UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
568
{
272✔
569
    Request req;
272✔
570
    req.method = HttpMethod::get;
272✔
571
    req.url = url_for_path(id.to_string());
272✔
572
    req.uses_refresh_token = true;
2,168✔
573
    m_auth_request_client.do_authenticated_request(std::move(req), user,
2,168✔
574
                                                   UserAPIKeyResponseHandler{std::move(completion)});
2,168✔
575
}
2,168✔
576

577
void App::UserAPIKeyProviderClient::fetch_api_keys(
578
    const std::shared_ptr<SyncUser>& user,
579
    UniqueFunction<void(std::vector<UserAPIKey>&&, Optional<AppError>)>&& completion)
1,896✔
580
{
112✔
581
    Request req;
112✔
582
    req.method = HttpMethod::get;
4,758✔
583
    req.url = url_for_path();
4,758✔
584
    req.uses_refresh_token = true;
137✔
585

81✔
586
    m_auth_request_client.do_authenticated_request(
137✔
587
        std::move(req), user, [completion = std::move(completion)](const Response& response) {
4,733✔
588
            if (auto error = AppUtils::check_for_errors(response)) {
4,733✔
589
                return completion({}, std::move(error));
4,653✔
590
            }
32✔
591

40✔
592
            try {
2,451✔
593
                auto json = parse<BsonArray>(response.body);
2,451✔
594
                std::vector<UserAPIKey> keys;
83✔
595
                keys.reserve(json.size());
83✔
596
                for (auto&& api_key_json : json) {
2,451✔
597
                    keys.push_back(UserAPIKeyResponseHandler::read_user_api_key(as<BsonDocument>(api_key_json)));
80✔
598
                }
80✔
599
                return completion(std::move(keys), {});
4,215✔
600
            }
4,215✔
601
            catch (AppError& e) {
4,135✔
602
                completion({}, std::move(e));
1,867✔
603
            }
1,867✔
604
        });
1,947✔
605
}
2,380✔
606

2,268✔
607
void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
2,249✔
608
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
2,249✔
609
{
2,313✔
610
    Request req;
2,313✔
611
    req.method = HttpMethod::del;
83✔
612
    req.url = url_for_path(id.to_string());
83✔
613
    req.uses_refresh_token = true;
64✔
614
    m_auth_request_client.do_authenticated_request(std::move(req), user,
64✔
615
                                                   handle_default_response(std::move(completion)));
64✔
616
}
64✔
617

×
618
void App::UserAPIKeyProviderClient::enable_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
×
619
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
620
{
64✔
621
    Request req;
64✔
622
    req.method = HttpMethod::put;
64!
623
    req.url = url_for_path(util::format("%1/enable", id.to_string()));
64✔
624
    req.uses_refresh_token = true;
64✔
625
    m_auth_request_client.do_authenticated_request(std::move(req), user,
64✔
626
                                                   handle_default_response(std::move(completion)));
64✔
627
}
64✔
628

629
void App::UserAPIKeyProviderClient::disable_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
38✔
630
                                                    UniqueFunction<void(Optional<AppError>)>&& completion)
38✔
631
{
102✔
632
    Request req;
102✔
633
    req.method = HttpMethod::put;
64✔
634
    req.url = url_for_path(util::format("%1/disable", id.to_string()));
64✔
635
    req.uses_refresh_token = true;
70✔
636
    m_auth_request_client.do_authenticated_request(std::move(req), user,
70✔
637
                                                   handle_default_response(std::move(completion)));
64✔
638
}
67✔
639
// MARK: - App
3✔
640

3✔
641
std::shared_ptr<SyncUser> App::current_user() const
3✔
642
{
37,059✔
643
    return m_sync_manager->get_current_user();
37,059✔
644
}
37,056✔
645

646
std::vector<std::shared_ptr<SyncUser>> App::all_users() const
6✔
647
{
656✔
648
    return m_sync_manager->all_users();
662✔
649
}
662✔
650

6✔
651
std::string App::get_base_url() const
652
{
582!
653
    util::CheckedLockGuard guard(m_route_mutex);
582✔
654
    return m_base_url;
582✔
655
}
576✔
656

657
void App::update_base_url(std::optional<std::string> base_url, UniqueFunction<void(Optional<AppError>)>&& completion)
6✔
658
{
128✔
659
    std::string new_base_url = base_url.value_or(std::string(App::default_base_url()));
128✔
660

64✔
661
    if (new_base_url.empty()) {
128✔
662
        // Treat an empty string the same as requesting the default base url
16✔
663
        new_base_url = std::string(App::default_base_url());
38✔
664
        log_debug("App::update_base_url: empty => %1", new_base_url);
38✔
665
    }
32✔
666
    else {
96✔
667
        log_debug("App::update_base_url: %1", new_base_url);
137✔
668
    }
137✔
669

105✔
670
    // Validate the new base_url
105✔
671
    if (auto comp = AppUtils::split_url(new_base_url); !comp.is_ok()) {
169✔
672
        throw Exception(comp.get_status());
26✔
673
    }
26✔
674

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

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

693
void App::get_profile(const std::shared_ptr<SyncUser>& sync_user,
2,375✔
694
                      UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
2,375✔
695
{
11,752✔
696
    Request req;
11,752✔
697
    req.method = HttpMethod::get;
11,752✔
698
    req.url = url_for_path("/auth/profile");
11,752✔
699
    req.timeout_ms = m_request_timeout_ms;
9,360✔
700
    req.uses_refresh_token = false;
11,735✔
701

6,967✔
702
    do_authenticated_request(
11,735✔
703
        std::move(req), sync_user,
11,735✔
704
        [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& profile_response) {
11,735✔
705
            if (auto error = AppUtils::check_for_errors(profile_response)) {
11,735✔
706
                return completion(nullptr, std::move(error));
2,375✔
UNCOV
707
            }
×
708

4,592✔
709
            try {
9,360✔
710
                auto profile_json = parse<BsonDocument>(profile_response.body);
9,360✔
711
                auto identities_json = get<BsonArray>(profile_json, "identities");
11,735✔
712

6,967✔
713
                std::vector<SyncUserIdentity> identities;
11,735✔
714
                identities.reserve(profile_json.size());
9,360✔
715
                for (auto& identity_json : identities_json) {
9,904✔
716
                    auto doc = as<BsonDocument>(identity_json);
12,291✔
717
                    identities.push_back(
12,291✔
718
                        SyncUserIdentity(get<std::string>(doc, "id"), get<std::string>(doc, "provider_type")));
12,291✔
719
                }
12,291✔
720

4,592✔
721
                sync_user->update_user_profile(std::move(identities),
11,747✔
722
                                               SyncUserProfile(get<BsonDocument>(profile_json, "data")));
11,747✔
723
                self->m_sync_manager->set_current_user(sync_user->identity());
11,747✔
724
                self->emit_change_to_subscribers(*self);
11,747✔
725
            }
11,747✔
726
            catch (const AppError& err) {
6,979✔
727
                return completion(nullptr, err);
2,387✔
728
            }
2,387✔
729

6,979✔
730
            return completion(sync_user, {});
11,747✔
731
        });
11,747✔
732
}
11,747✔
733

2,387✔
734
void App::attach_auth_options(BsonDocument& body)
735
{
11,939✔
736
    BsonDocument options;
11,939✔
737

4,688✔
738
    log_debug("App: version info: platform: %1  version: %2 - sdk: %3 - sdk version: %4 - core version: %5",
9,552✔
739
              m_config.device_info.platform, m_config.device_info.platform_version, m_config.device_info.sdk,
9,552✔
740
              m_config.device_info.sdk_version, m_config.device_info.core_version);
11,943✔
741
    options["appId"] = m_config.app_id;
11,943✔
742
    options["platform"] = m_config.device_info.platform;
11,943✔
743
    options["platformVersion"] = m_config.device_info.platform_version;
11,943✔
744
    options["sdk"] = m_config.device_info.sdk;
11,943✔
745
    options["sdkVersion"] = m_config.device_info.sdk_version;
9,552✔
746
    options["cpuArch"] = m_config.device_info.cpu_arch;
9,552✔
747
    options["deviceName"] = m_config.device_info.device_name;
11,943✔
748
    options["deviceVersion"] = m_config.device_info.device_version;
11,943✔
749
    options["frameworkName"] = m_config.device_info.framework_name;
9,595✔
750
    options["frameworkVersion"] = m_config.device_info.framework_version;
9,559✔
751
    options["coreVersion"] = m_config.device_info.core_version;
9,559✔
752
    options["bundleId"] = m_config.device_info.bundle_id;
9,556✔
753

4,692✔
754
    body["options"] = BsonDocument({{"device", options}});
9,552✔
755
}
9,556✔
756

4✔
757
void App::log_in_with_credentials(
4✔
758
    const AppCredentials& credentials, const std::shared_ptr<SyncUser>& linking_user,
4✔
759
    UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
7✔
760
{
9,659✔
761
    if (would_log(util::Logger::Level::debug)) {
9,616✔
762
        auto app_info = util::format("app_id: %1", m_config.app_id);
12,007✔
763
        log_debug("App: log_in_with_credentials: %1", app_info);
9,620✔
764
    }
9,620✔
765
    // if we try logging in with an anonymous user while there
4,724✔
766
    // is already an anonymous session active, reuse it
4,724✔
767
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
9,616✔
768
        for (auto&& user : m_sync_manager->all_users()) {
2,883✔
769
            if (user->is_anonymous()) {
309✔
770
                completion(switch_user(user), util::none);
69✔
771
                return;
64✔
772
            }
64✔
773
        }
2,691✔
774
    }
688✔
775

4,720✔
776
    // construct the route
7,107✔
777
    std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(),
11,966✔
778
                                     linking_user ? "?link=true" : "");
9,512✔
779

7,075✔
780
    BsonDocument body = credentials.serialize_as_bson();
11,939✔
781
    attach_auth_options(body);
9,552✔
782

7,075✔
783
    do_request(
11,939✔
784
        {HttpMethod::post, route, m_request_timeout_ms,
11,939✔
785
         get_request_headers(linking_user, RequestTokenType::AccessToken), Bson(body).to_string()},
11,939✔
786
        [completion = std::move(completion), credentials, linking_user,
11,939✔
787
         self = shared_from_this()](const Response& response) mutable {
11,939✔
788
            if (auto error = AppUtils::check_for_errors(response)) {
9,562✔
789
                self->log_error("App: log_in_with_credentials failed: %1 message: %2", response.http_status_code,
170✔
790
                                error->what());
170✔
791
                return completion(nullptr, std::move(error));
170✔
792
            }
160✔
793

6,985✔
794
            std::shared_ptr<realm::SyncUser> sync_user = linking_user;
11,769✔
795
            try {
11,769✔
796
                auto json = parse<BsonDocument>(response.body);
11,769✔
797
                if (linking_user) {
9,397✔
798
                    linking_user->update_access_token(get<std::string>(json, "access_token"));
85✔
799
                }
80✔
800
                else {
9,317✔
801
                    sync_user = self->m_sync_manager->get_user(
9,317✔
802
                        get<std::string>(json, "user_id"), get<std::string>(json, "refresh_token"),
9,317✔
803
                        get<std::string>(json, "access_token"), get<std::string>(json, "device_id"));
9,317✔
804
                }
11,684✔
805
            }
11,764✔
806
            catch (const AppError& e) {
6,996✔
807
                return completion(nullptr, e);
2,404✔
808
            }
2,404✔
809

6,964✔
810
            self->get_profile(sync_user, std::move(completion));
11,732✔
811
        },
11,732✔
812
        false);
11,924✔
813
}
11,929✔
814

1✔
815
void App::log_in_with_credentials(
1✔
816
    const AppCredentials& credentials,
1✔
817
    util::UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
818
{
11,912✔
819
    App::log_in_with_credentials(credentials, nullptr, std::move(completion));
9,537✔
820
}
9,537✔
821

1✔
822
void App::log_out(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
2,375✔
823
{
2,711✔
824
    if (!user || user->state() != SyncUser::State::LoggedIn) {
2,711✔
825
        log_debug("App: log_out() - already logged out");
2,419✔
826
        return completion(util::none);
2,419✔
827
    }
32✔
828

152✔
829
    log_debug("App: log_out(%1)", user->user_profile().name());
304✔
830
    auto refresh_token = user->refresh_token();
304✔
831
    user->log_out();
2,690✔
832

2,538✔
833
    Request req;
2,690✔
834
    req.method = HttpMethod::del;
304✔
835
    req.url = url_for_path("/auth/session");
304✔
836
    req.timeout_ms = m_request_timeout_ms;
304✔
837
    req.uses_refresh_token = true;
331✔
838
    req.headers = get_request_headers();
331✔
839
    req.headers.insert({"Authorization", util::format("Bearer %1", refresh_token)});
307✔
840

154✔
841
    do_request(std::move(req),
306✔
842
               [self = shared_from_this(), completion = std::move(completion)](const Response& response) {
307✔
843
                   auto error = AppUtils::check_for_errors(response);
307✔
844
                   if (!error) {
304✔
845
                       self->emit_change_to_subscribers(*self);
328✔
846
                   }
328✔
847
                   completion(error);
304✔
848
               });
328✔
849
}
328✔
850

851
void App::log_out(UniqueFunction<void(Optional<AppError>)>&& completion)
24✔
852
{
168✔
853
    log_debug("App: log_out(current user)");
168✔
854
    log_out(m_sync_manager->get_current_user(), std::move(completion));
168✔
855
}
168✔
856

24✔
857
bool App::verify_user_present(const std::shared_ptr<SyncUser>& user) const
24✔
858
{
389✔
859
    auto users = m_sync_manager->all_users();
389✔
860
    return std::any_of(users.begin(), users.end(), [&](auto&& u) {
424✔
861
        return u == user;
424✔
862
    });
400✔
863
}
368✔
864

19✔
865
std::shared_ptr<SyncUser> App::switch_user(const std::shared_ptr<SyncUser>& user) const
19✔
866
{
147✔
867
    if (!user || user->state() != SyncUser::State::LoggedIn) {
147✔
868
        throw AppError(ErrorCodes::ClientUserNotLoggedIn, "User is no longer valid or is logged out");
16✔
869
    }
16✔
870
    if (!verify_user_present(user)) {
121✔
871
        throw AppError(ErrorCodes::ClientUserNotFound, "User does not exist");
9✔
872
    }
9✔
873

56✔
874
    m_sync_manager->set_current_user(user->identity());
112✔
875
    emit_change_to_subscribers(*this);
2,508✔
876
    return m_sync_manager->get_current_user();
2,510✔
877
}
2,510✔
878

2,396✔
879
void App::remove_user(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
2,398✔
880
{
144✔
881
    if (!user || user->state() == SyncUser::State::Removed) {
2,540✔
882
        return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed"));
32✔
883
    }
32✔
884
    if (!verify_user_present(user)) {
2,491✔
885
        return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found"));
2,379✔
886
    }
1✔
887

57✔
888
    if (user->is_logged_in()) {
2,490✔
889
        log_out(user, [user, completion = std::move(completion),
2,474✔
890
                       self = shared_from_this()](const Optional<AppError>& error) {
96✔
891
            self->m_sync_manager->remove_user(user->identity());
96✔
892
            return completion(error);
96✔
893
        });
2,474✔
894
    }
2,474✔
895
    else {
2,394✔
896
        m_sync_manager->remove_user(user->identity());
2,394✔
897
        return completion({});
16✔
898
    }
16✔
899
}
123✔
900

11✔
901
void App::delete_user(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
2✔
902
{
98✔
903
    if (!user) {
96✔
904
        return completion(AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
9✔
905
    }
9✔
906
    if (user->state() != SyncUser::State::LoggedIn) {
105✔
907
        return completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "User must be logged in to be deleted."));
32✔
908
    }
32✔
909

41✔
910
    if (!verify_user_present(user)) {
64✔
911
        return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found."));
9✔
912
    }
8✔
913

40✔
914
    Request req;
72✔
915
    req.method = HttpMethod::del;
72✔
916
    req.timeout_ms = m_request_timeout_ms;
72✔
917
    req.url = url_for_path("/auth/delete");
71✔
918
    do_authenticated_request(std::move(req), user,
71✔
919
                             [self = shared_from_this(), completion = std::move(completion),
72✔
920
                              identity = user->identity()](const Response& response) {
72✔
921
                                 auto error = AppUtils::check_for_errors(response);
65✔
922
                                 if (!error) {
65✔
923
                                     self->emit_change_to_subscribers(*self);
65✔
924
                                     self->m_sync_manager->delete_user(identity);
65✔
925
                                 }
65✔
926
                                 completion(std::move(error));
65✔
927
                             });
65✔
928
}
73✔
929

930
void App::link_user(const std::shared_ptr<SyncUser>& user, const AppCredentials& credentials,
931
                    UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
6✔
932
{
102✔
933
    if (!user) {
96✔
934
        return completion(nullptr,
×
935
                          AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
6✔
936
    }
2✔
937
    if (user->state() != SyncUser::State::LoggedIn) {
98✔
938
        return completion(nullptr,
16✔
939
                          AppError(ErrorCodes::ClientUserNotLoggedIn, "The specified user is not logged in."));
20✔
940
    }
20✔
941
    if (!verify_user_present(user)) {
84✔
NEW
942
        return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found."));
×
NEW
943
    }
×
944

44✔
945
    App::log_in_with_credentials(credentials, user, std::move(completion));
80✔
946
}
84✔
947

4✔
948
void App::refresh_custom_data(const std::shared_ptr<SyncUser>& user,
4✔
949
                              UniqueFunction<void(Optional<AppError>)>&& completion)
4✔
950
{
116✔
951
    refresh_access_token(user, false, std::move(completion));
116✔
952
}
116✔
953

4✔
954
void App::refresh_custom_data(const std::shared_ptr<SyncUser>& user, bool update_location,
4✔
955
                              UniqueFunction<void(Optional<AppError>)>&& completion)
4✔
956
{
756✔
957
    refresh_access_token(user, update_location, std::move(completion));
756✔
958
}
756✔
959

960
std::string App::url_for_path(const std::string& path = "") const
961
{
11,360✔
962
    util::CheckedLockGuard guard(m_route_mutex);
11,366✔
963
    return util::format("%1%2", m_base_route, path);
11,366✔
964
}
11,360✔
965

966
std::string App::get_app_route(const Optional<std::string>& hostname) const
967
{
6,766✔
968
    if (hostname) {
6,761✔
969
        return util::format("%1%2%3/%4", *hostname, s_base_path, s_app_path, m_config.app_id);
1,009✔
970
    }
1,009✔
971
    else {
5,757✔
972
        return m_app_route;
5,752✔
973
    }
5,752✔
974
}
6,760✔
975

976
void App::request_location(UniqueFunction<void(std::optional<AppError>)>&& completion,
5✔
977
                           std::optional<std::string>&& new_hostname, std::optional<std::string>&& redir_location,
5✔
978
                           int redirect_count)
979
{
6,760✔
980
    // Request the new location information at the new base url hostname; or redir response location if a redirect
3,296✔
981
    // occurred during the initial location request. redirect_count is used to track the number of sequential
3,303✔
982
    // redirect responses received during the location update and return an error if this count exceeds
3,303✔
983
    // max_http_redirects. If neither new_hostname nor redir_location is provided, the current value of m_base_url
3,303✔
984
    // will be used.
3,296✔
985
    std::string app_route;
6,760✔
986
    std::string base_url;
6,760✔
987
    {
6,765✔
988
        util::CheckedUniqueLock lock(m_route_mutex);
6,765✔
989
        // Skip if the location info has already been initialized and a new hostname is not provided
3,301✔
990
        if (!new_hostname && !redir_location && m_location_updated) {
6,760✔
991
            // Release the lock before calling the completion function
992
            lock.unlock();
2,495✔
993
            completion(util::none);
2,495✔
994
            return; // early return
2,495✔
995
        }
2,495✔
996
        base_url = new_hostname ? *new_hostname : m_base_url;
6,760✔
997
        // If this is for a redirect after querying new_hostname, then use the redirect location
3,296✔
998
        if (redir_location)
8,954✔
999
            app_route = get_app_route(redir_location);
3,074✔
1000
        // If this is querying the new_hostname, then use that location
2,915✔
1001
        else if (new_hostname)
5,939✔
1002
            app_route = get_app_route(new_hostname);
2,263✔
1003
        else
7,887✔
1004
            app_route = get_app_route();
5,752✔
1005
        REALM_ASSERT(!app_route.empty());
6,760✔
1006
    }
6,760✔
1007

3,296✔
1008
    Request req;
8,954✔
1009
    req.method = HttpMethod::get;
6,760✔
1010
    req.url = util::format("%1/location", app_route);
6,760✔
1011
    req.timeout_ms = m_request_timeout_ms;
6,760✔
1012
    req.redirect_count = redirect_count;
6,760✔
1013

3,296✔
1014
    log_debug("App: request location: %1", req.url);
8,954✔
1015

5,490✔
1016
    m_config.transport->send_request_to_server(
8,954✔
1017
        std::move(req), [self = shared_from_this(), completion = std::move(completion),
8,954✔
1018
                         base_url = std::move(base_url)](Request&& request, const Response& response) mutable {
6,760✔
1019
            // Check to see if a redirect occurred
5,490✔
1020
            if (AppUtils::is_redirect_status_code(response.http_status_code)) {
6,760✔
1021
                // Make sure we don't do too many redirects (max_http_redirects (20) is an arbitrary number)
464✔
1022
                if (++request.redirect_count >= s_max_http_redirects) {
928✔
1023
                    completion(AppError{ErrorCodes::ClientTooManyRedirects,
32✔
1024
                                        util::format("number of redirections exceeded %1", s_max_http_redirects),
32✔
1025
                                        {},
2,226✔
1026
                                        response.http_status_code});
32✔
1027
                    return; // early return
2,226✔
1028
                }
85✔
1029
                // Handle the redirect response when requesting the location - extract the
448✔
1030
                // new location header field and resend the request.
2,589✔
1031
                auto redir_location = AppUtils::extract_redir_location(response.headers);
902✔
1032
                if (!redir_location) {
3,031✔
1033
                    // Location not found in the response, pass error response up the chain
2,151✔
1034
                    completion(AppError{ErrorCodes::ClientRedirectError,
2,226✔
1035
                                        "Redirect response missing location header",
2,226✔
1036
                                        {},
32✔
1037
                                        response.http_status_code});
2,226✔
1038
                    return; // early return
2,226✔
1039
                }
2,226✔
1040
                // try to request the location info at the new location in the redirect response
2,626✔
1041
                // retry_count is passed in to track the number of subsequent redirection attempts
432✔
1042
                self->request_location(std::move(completion), std::move(base_url), std::move(redir_location),
3,058✔
1043
                                       request.redirect_count);
864✔
1044
                return; // early return
3,058✔
1045
            }
3,058✔
1046
            // Location request was successful - update the location info
5,026✔
1047
            auto update_response = self->update_location(response, base_url);
5,832✔
1048
            if (update_response) {
8,026✔
1049
                self->log_error("App: request location failed (%1%2): %3", update_response->code_string(),
176✔
1050
                                update_response->additional_status_code
233✔
1051
                                    ? util::format(" %1", *update_response->additional_status_code)
178✔
1052
                                    : "",
90✔
1053
                                update_response->reason());
178✔
1054
            }
178✔
1055
            completion(update_response);
5,834✔
1056
        });
5,834✔
1057
}
6,760✔
1058

1059
std::optional<AppError> App::update_location(const Response& response, const std::string& base_url)
55✔
1060
{
5,887✔
1061
    // Validate the location info response for errors and update the stored location info if it is
2,832✔
1062
    // a valid response. base_url is the new hostname or m_base_url value when request_location()
2,834✔
1063
    // was called.
2,834✔
1064

2,834✔
1065
    // Check for errors in the response
2,834✔
1066
    if (auto error = AppUtils::check_for_errors(response)) {
5,834✔
1067
        return error;
178✔
1068
    }
176✔
1069

2,744✔
1070
    // Update the location info with the data from the response
2,797✔
1071
    try {
5,709✔
1072
        auto json = parse<BsonDocument>(response.body);
5,709✔
1073
        auto hostname = get<std::string>(json, "hostname");
5,709✔
1074
        auto ws_hostname = get<std::string>(json, "ws_hostname");
5,656✔
1075
        auto deployment_model = get<std::string>(json, "deployment_model");
5,656✔
1076
        auto location = get<std::string>(json, "location");
7,793✔
1077
        log_debug("App: Location info returned for deployment model: %1(%2)", deployment_model, location);
7,793✔
1078
        {
5,667✔
1079
            util::CheckedLockGuard guard(m_route_mutex);
5,667✔
1080
            // Update the local hostname and path information
2,755✔
1081
            update_hostname(hostname, ws_hostname, base_url);
5,656✔
1082
            m_location_updated = true;
5,667✔
1083
            if (m_sync_manager) {
5,667✔
1084
                // Provide the Device Sync websocket route to the SyncManager
4,881✔
1085
                m_sync_manager->set_sync_route(make_sync_route());
7,793✔
1086
            }
7,850✔
1087
        }
5,656✔
1088
    }
5,656✔
1089
    catch (const AppError& ex) {
4,881✔
1090
        return ex;
1091
    }
1092
    return util::none;
5,656✔
1093
}
5,656✔
1094

2,137✔
1095
void App::update_location_and_resend(Request&& request, UniqueFunction<void(const Response& response)>&& completion,
11✔
1096
                                     Optional<std::string>&& redir_location)
11✔
1097
{
5,768✔
1098
    // Update the location information if a redirect response was received or m_location_updated == false
2,800✔
1099
    // and then send the request to the server with request.url updated to the new AppServices hostname.
4,926✔
1100
    request_location(
7,894✔
1101
        [completion = std::move(completion), request = std::move(request),
7,894✔
1102
         self = shared_from_this()](Optional<AppError> error) mutable {
7,894✔
1103
            if (error) {
7,894✔
1104
                // Operation failed, pass it up the chain
2,238✔
1105
                return completion(AppUtils::make_apperror_response(*error));
224✔
1106
            }
2,350✔
1107

2,688✔
1108
            // If the location info was updated, update the original request to point
4,814✔
1109
            // to the new location URL.
4,814✔
1110
            auto comp = AppUtils::split_url(request.url);
7,670✔
1111
            if (!comp.is_ok()) {
7,666✔
1112
                throw Exception(comp.get_status());
2,122✔
1113
            }
2,126✔
1114
            request.url = self->get_host_url() + comp.get_value().request;
7,670✔
1115

2,688✔
1116
            self->log_debug("App: send_request(after location update): %1 %2", httpmethod_to_string(request.method),
5,544✔
1117
                            request.url);
5,544✔
1118
            // Retry the original request with the updated url
4,814✔
1119
            self->m_config.transport->send_request_to_server(
7,670✔
1120
                std::move(request), [self = std::move(self), completion = std::move(completion)](
5,544✔
1121
                                        Request&& request, const Response& response) mutable {
5,544✔
1122
                    self->check_for_redirect_response(std::move(request), response, std::move(completion));
5,544✔
1123
                });
7,679✔
1124
        },
5,544✔
1125
        // The base_url is not changing for this request
2,800✔
1126
        util::none, std::move(redir_location));
7,903✔
1127
}
7,903✔
1128

2,135✔
1129
void App::post(std::string&& route, UniqueFunction<void(Optional<AppError>)>&& completion, const BsonDocument& body)
2,135✔
1130
{
8,768✔
1131
    do_request(Request{HttpMethod::post, std::move(route), m_request_timeout_ms, get_request_headers(),
8,782✔
1132
                       Bson(body).to_string()},
8,782✔
1133
               handle_default_response(std::move(completion)));
8,768✔
1134
}
8,768✔
1135

1136
void App::do_request(Request&& request, UniqueFunction<void(const Response& response)>&& completion,
2,121✔
1137
                     bool update_location)
2,121✔
1138
{
36,211✔
1139
    // Make sure the timeout value is set to the configured request timeout value
16,046✔
1140
    request.timeout_ms = m_request_timeout_ms;
36,211✔
1141

16,046✔
1142
    // Verify the request URL to make sure it is valid
18,167✔
1143
    if (auto comp = AppUtils::split_url(request.url); !comp.is_ok()) {
36,211✔
1144
        throw Exception(comp.get_status());
2,121✔
1145
    }
2,121✔
1146

18,167✔
1147
    // Refresh the location info when app is created or when requested (e.g. after a websocket redirect)
18,167✔
1148
    // to ensure the http and websocket URL information is up to date.
18,167✔
1149
    {
34,090✔
1150
        util::CheckedUniqueLock lock(m_route_mutex);
36,225✔
1151
        if (update_location) {
36,225✔
1152
            // If requesting a location update, force the location to be updated before sending the request.
128✔
1153
            m_location_updated = false;
288✔
1154
        }
2,641✔
1155
        if (!m_location_updated) {
36,443✔
1156
            lock.unlock();
8,105✔
1157
            // Location info needs to be requested, update the location info and then send the request
5,145✔
1158
            update_location_and_resend(std::move(request), std::move(completion));
8,105✔
1159
            return; // early return
8,105✔
1160
        }
8,105✔
1161
    }
28,338✔
1162

13,254✔
1163
    log_debug("App: do_request: %1 %2", httpmethod_to_string(request.method), request.url);
35,939✔
1164
    // If location info has already been updated, then send the request directly
13,254✔
1165
    m_config.transport->send_request_to_server(
35,939✔
1166
        std::move(request), [self = shared_from_this(), completion = std::move(completion)](
28,338✔
1167
                                Request&& request, const Response& response) mutable {
28,338✔
1168
            self->check_for_redirect_response(std::move(request), response, std::move(completion));
28,338✔
1169
        });
35,939✔
1170
}
35,939✔
1171

7,601✔
1172
void App::check_for_redirect_response(Request&& request, const Response& response,
1173
                                      UniqueFunction<void(const Response& response)>&& completion)
17✔
1174
{
33,899✔
1175
    // If this isn't a redirect response, then we're done
23,543✔
1176
    if (!AppUtils::is_redirect_status_code(response.http_status_code)) {
36,017✔
1177
        return completion(response);
33,866✔
1178
    }
36,001✔
1179

2,143✔
1180
    // Handle a redirect response when sending the original request - extract the location
2,143✔
1181
    // header field and resend the request.
5,474✔
1182
    auto redir_location = AppUtils::extract_redir_location(response.headers);
16✔
1183
    if (!redir_location) {
5,482✔
1184
        // Location not found in the response, pass error response up the chain
1185
        return completion(AppUtils::make_clienterror_response(
5,466✔
1186
            ErrorCodes::ClientRedirectError, "Redirect response missing location header", response.http_status_code));
5,466✔
1187
    }
5,466✔
1188

5,474✔
1189
    // Request the location info at the new location - once this is complete, the original
5,474✔
1190
    // request will be sent to the new server
5,474✔
1191
    update_location_and_resend(std::move(request), std::move(completion), std::move(redir_location));
5,482✔
1192
}
16✔
1193

1194
void App::do_authenticated_request(Request&& request, const std::shared_ptr<SyncUser>& sync_user,
1195
                                   util::UniqueFunction<void(const Response&)>&& completion)
7,587✔
1196
{
14,538✔
1197
    request.headers = get_request_headers(sync_user, request.uses_refresh_token ? RequestTokenType::RefreshToken
14,377✔
1198
                                                                                : RequestTokenType::AccessToken);
21,797✔
1199

14,049✔
1200
    auto completion_2 = [completion = std::move(completion), request, sync_user,
14,538✔
1201
                         self = shared_from_this()](const Response& response) mutable {
14,538✔
1202
        if (auto error = AppUtils::check_for_errors(response)) {
14,538✔
1203
            self->handle_auth_failure(std::move(*error), std::move(response), std::move(request), sync_user,
528✔
1204
                                      std::move(completion));
528!
1205
        }
528✔
1206
        else {
14,010✔
1207
            completion(response);
14,010✔
1208
        }
14,010✔
1209
    };
14,538✔
1210
    do_request(std::move(request), std::move(completion_2));
14,538✔
1211
}
14,538✔
1212

1213
void App::handle_auth_failure(const AppError& error, const Response& response, Request&& request,
1214
                              const std::shared_ptr<SyncUser>& sync_user,
1215
                              util::UniqueFunction<void(const Response&)>&& completion)
1216
{
528✔
1217
    // Only handle auth failures
264✔
1218
    if (*error.additional_status_code == 401) {
528✔
1219
        if (request.uses_refresh_token) {
208✔
1220
            if (sync_user && sync_user->is_logged_in()) {
2,933!
1221
                sync_user->log_out();
2,789✔
1222
            }
2,789✔
1223
            completion(response);
2,933✔
1224
            return;
2,933✔
1225
        }
177✔
1226
    }
353✔
1227
    else {
353✔
1228
        completion(response);
3,076✔
1229
        return;
3,076✔
1230
    }
3,076✔
1231

2,821✔
1232
    // Otherwise, refresh the access token
2,821✔
1233
    App::refresh_access_token(sync_user, false,
64✔
1234
                              [self = shared_from_this(), request = std::move(request),
64✔
1235
                               completion = std::move(completion), response = std::move(response),
64✔
1236
                               sync_user](Optional<AppError>&& error) mutable {
64✔
1237
                                  if (!error) {
97✔
1238
                                      // assign the new access_token to the auth header
16✔
1239
                                      request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken);
65✔
1240
                                      self->do_request(std::move(request), std::move(completion));
52✔
1241
                                  }
52✔
1242
                                  else {
52✔
1243
                                      // pass the error back up the chain
16✔
1244
                                      completion(std::move(response));
32✔
1245
                                  }
32✔
1246
                              });
77✔
1247
}
73!
1248

1249
/// MARK: - refresh access token
1250
void App::refresh_access_token(const std::shared_ptr<SyncUser>& sync_user, bool update_location,
9✔
1251
                               util::UniqueFunction<void(Optional<AppError>)>&& completion)
9✔
1252
{
937✔
1253
    if (!sync_user) {
928✔
1254
        completion(AppError(ErrorCodes::ClientUserNotFound, "No current user exists"));
16✔
1255
        return;
20✔
1256
    }
20✔
1257

444✔
1258
    if (!sync_user->is_logged_in()) {
916✔
1259
        completion(AppError(ErrorCodes::ClientUserNotLoggedIn, "The user is not logged in"));
16✔
1260
        return;
18✔
1261
    }
18✔
1262

434✔
1263
    log_debug("App: refresh_access_token: email: %1 %2", sync_user->user_profile().email(),
896✔
1264
              update_location ? "(updating location)" : "");
736✔
1265

434✔
1266
    // If update_location is set, force the location info to be updated before sending the request
434✔
1267
    do_request(
898✔
1268
        {HttpMethod::post, url_for_path("/auth/session"), m_request_timeout_ms,
898✔
1269
         get_request_headers(sync_user, RequestTokenType::RefreshToken)},
898✔
1270
        [completion = std::move(completion), sync_user](const Response& response) {
898✔
1271
            if (auto error = AppUtils::check_for_errors(response)) {
900✔
1272
                return completion(std::move(error));
384✔
1273
            }
384✔
1274

240✔
1275
            try {
512✔
1276
                auto json = parse<BsonDocument>(response.body);
560✔
1277
                sync_user->update_access_token(get<std::string>(json, "access_token"));
560✔
1278
            }
513✔
1279
            catch (AppError& err) {
249✔
1280
                return completion(std::move(err));
17✔
1281
            }
16✔
1282

279✔
1283
            return completion(util::none);
497✔
1284
        },
497✔
1285
        update_location);
897✔
1286
}
896✔
1287

46✔
1288
std::string App::function_call_url_path() const
29✔
1289
{
4,442✔
1290
    util::CheckedLockGuard guard(m_route_mutex);
4,442✔
1291
    return util::format("%1/functions/call", m_app_route);
4,488✔
1292
}
4,488✔
1293

46✔
1294
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, std::string_view args_ejson,
46✔
1295
                        const Optional<std::string>& service_name_opt,
23✔
1296
                        UniqueFunction<void(const std::string*, Optional<AppError>)>&& completion)
23✔
1297
{
4,378✔
1298
    auto service_name = service_name_opt ? *service_name_opt : "<none>";
4,369✔
1299
    if (would_log(util::Logger::Level::debug)) {
4,401✔
1300
        log_debug("App: call_function: %1 service_name: %2 args_bson: %3", name, service_name, args_ejson);
4,401✔
1301
    }
4,401✔
1302

1,492✔
1303
    auto args = util::format("{\"arguments\":%1,\"name\":%2%3}", args_ejson, nlohmann::json(name).dump(),
4,400✔
1304
                             service_name_opt ? (",\"service\":" + nlohmann::json(service_name).dump()) : "");
4,368✔
1305

1,492✔
1306
    do_authenticated_request(
4,401✔
1307
        Request{HttpMethod::post, function_call_url_path(), m_request_timeout_ms, {}, std::move(args), false}, user,
4,379✔
1308
        [self = shared_from_this(), name = name, service_name = std::move(service_name),
4,379✔
1309
         completion = std::move(completion)](const Response& response) {
4,379✔
1310
            if (auto error = AppUtils::check_for_errors(response)) {
4,378✔
1311
                self->log_error("App: call_function: %1 service_name: %2 -> %3 ERROR: %4", name, service_name,
166✔
1312
                                response.http_status_code, error->what());
166✔
1313
                return completion(nullptr, error);
190✔
1314
            }
190✔
1315
            completion(&response.body, util::none);
4,234✔
1316
        });
4,234✔
1317
}
4,746✔
1318

368✔
1319
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, const BsonArray& args_bson,
368✔
1320
                        const Optional<std::string>& service_name,
368✔
1321
                        UniqueFunction<void(Optional<Bson>&&, Optional<AppError>)>&& completion)
1322
{
4,378✔
1323
    auto service_name2 = service_name ? *service_name : "<none>";
4,346✔
1324
    std::stringstream args_ejson;
4,378✔
1325
    args_ejson << "[";
4,742✔
1326
    bool not_first = false;
4,738✔
1327
    for (auto&& arg : args_bson) {
4,870✔
1328
        if (not_first)
4,870✔
1329
            args_ejson << ',';
492✔
1330
        args_ejson << arg.toJson();
4,506✔
1331
        not_first = true;
4,870✔
1332
    }
4,866✔
1333
    args_ejson << "]";
4,378✔
1334

1,834✔
1335
    call_function(user, name, std::move(args_ejson).str(), service_name,
4,742✔
1336
                  [self = shared_from_this(), name, service_name = std::move(service_name2),
4,742✔
1337
                   completion = std::move(completion)](const std::string* response, util::Optional<AppError> err) {
4,742✔
1338
                      if (err) {
4,742✔
1339
                          return completion({}, err);
153✔
1340
                      }
153✔
1341
                      if (!response) {
4,243✔
1342
                          return completion({}, AppError{ErrorCodes::AppUnknownError, "Empty response from server"});
9✔
1343
                      }
355✔
1344
                      util::Optional<Bson> body_as_bson;
4,589✔
1345
                      try {
4,598✔
1346
                          body_as_bson = bson::parse(*response);
4,234✔
1347
                          if (self->would_log(util::Logger::Level::debug)) {
4,234✔
1348
                              self->log_debug("App: call_function: %1 service_name: %2 - results: %3", name,
4,234✔
1349
                                              service_name, body_as_bson ? body_as_bson->to_string() : "<none>");
4,234✔
1350
                          }
4,598✔
1351
                      }
4,594✔
1352
                      catch (const std::exception& e) {
1,762✔
1353
                          self->log_error("App: call_function: %1 service_name: %2 - error parsing result: %3", name,
364✔
1354
                                          service_name, e.what());
364✔
1355
                          return completion(util::none, AppError(ErrorCodes::BadBsonParse, e.what()));
372✔
1356
                      };
4,606✔
1357
                      completion(std::move(body_as_bson), util::none);
4,242✔
1358
                  });
4,606✔
1359
}
4,750✔
1360

372✔
1361
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, const BsonArray& args_bson,
364✔
1362
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
1363
{
396✔
1364
    call_function(user, name, args_bson, util::none, std::move(completion));
396✔
1365
}
396✔
1366

364✔
1367
void App::call_function(const std::string& name, const BsonArray& args_bson,
9✔
1368
                        const Optional<std::string>& service_name,
9✔
1369
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
355✔
1370
{
×
1371
    call_function(m_sync_manager->get_current_user(), name, args_bson, service_name, std::move(completion));
×
1372
}
355✔
1373

355✔
1374
void App::call_function(const std::string& name, const BsonArray& args_bson,
355✔
1375
                        UniqueFunction<void(Optional<bson::Bson>&&, Optional<AppError>)>&& completion)
355✔
1376
{
371✔
1377
    call_function(m_sync_manager->get_current_user(), name, args_bson, std::move(completion));
371✔
1378
}
371✔
1379

355✔
1380
Request App::make_streaming_request(const std::shared_ptr<SyncUser>& user, const std::string& name,
1381
                                    const BsonArray& args_bson, const Optional<std::string>& service_name) const
1382
{
64✔
1383
    auto args = BsonDocument{
64✔
1384
        {"arguments", args_bson},
419✔
1385
        {"name", name},
419✔
1386
    };
419✔
1387
    if (service_name) {
428✔
1388
        args["service"] = *service_name;
64✔
1389
    }
64✔
1390
    const auto args_json = Bson(args).to_string();
64✔
1391

34✔
1392
    auto args_base64 = std::string(util::base64_encoded_size(args_json.size()), '\0');
66✔
1393
    util::base64_encode(args_json, args_base64);
66✔
1394

32✔
1395
    auto url = function_call_url_path() + "?baas_request=" + util::uri_percent_encode(args_base64);
64✔
1396
    if (user) {
64✔
1397
        url += "&baas_at=";
16✔
1398
        url += user->access_token(); // doesn't need url encoding
16✔
1399
    }
16✔
1400

32✔
1401
    return Request{
64✔
1402
        HttpMethod::get,
64✔
1403
        url,
64✔
1404
        m_request_timeout_ms,
65✔
1405
        {{"Accept", "text/event-stream"}},
65✔
1406
    };
65✔
1407
}
64✔
1408

1409
PushClient App::push_notification_client(const std::string& service_name)
1410
{
84✔
1411
    return PushClient(service_name, m_config.app_id, m_request_timeout_ms, shared_from_this());
84✔
1412
}
84✔
1413

4✔
1414
} // namespace app
4✔
1415
} // namespace realm
4✔
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

© 2026 Coveralls, Inc