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

realm / realm-core / 2211

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

push

Evergreen

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

Rework sync user handling and metadata storage

102820 of 195548 branches covered (52.58%)

3165 of 3247 new or added lines in 46 files covered. (97.47%)

31 existing lines in 8 files now uncovered.

249584 of 269432 relevant lines covered (92.63%)

49986309.51 hits per line

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

99.09
/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
}
18✔
54

2✔
55
template <typename T>
2✔
56
T as(const Bson& bson)
57
{
134,904✔
58
    if (holds_alternative<T>(bson)) {
134,904✔
59
        return static_cast<T>(bson);
199,945✔
60
    }
199,945✔
61
    throw_json_error(ErrorCodes::MalformedJson, "?");
65,041✔
62
}
65,041✔
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);
145,602✔
69
    }
145,602✔
70
    throw_json_error(ErrorCodes::MissingJsonKey, key);
46,418✔
71
    return {};
46,418✔
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);
576✔
79
    }
576✔
80
    else {
64✔
81
        throw_json_error(ErrorCodes::MissingJsonKey, key);
64✔
82
    }
×
83
}
512✔
84

85
template <>
64✔
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
}
288✔
90

32✔
91
template <typename T>
32✔
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);
4,331✔
96
    }
4,331✔
97
}
272✔
98

16✔
99
template <typename T>
4,267✔
100
T parse(std::string_view str)
101
{
25,176✔
102
    try {
25,176✔
103
        return as<T>(bson::parse(str));
38,949✔
104
    }
38,949✔
105
    catch (const std::exception& e) {
13,789✔
106
        throw_json_error(ErrorCodes::MalformedJson, e.what());
13,789✔
107
    }
18✔
108
}
25,178✔
109

2✔
110
struct UserAPIKeyResponseHandler {
13,773✔
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));
220✔
116
        }
220✔
117

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

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

32✔
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)
14,957✔
144
{
49,047✔
145
    HttpHeaders headers{{"Content-Type", "application/json;charset=utf-8"}, {"Accept", "application/json"}};
49,047✔
146

21,557✔
147
    if (with_user_authorization) {
34,090✔
148
        switch (token_type) {
15,386✔
149
            case RequestTokenType::NoAuth:
5,309✔
150
                break;
5,309✔
151
            case RequestTokenType::AccessToken:
19,287✔
152
                headers.insert({"Authorization", util::format("Bearer %1", with_user_authorization->access_token())});
14,180✔
153
                break;
14,180✔
154
            case RequestTokenType::RefreshToken:
1,610✔
155
                headers.insert(
16,365✔
156
                    {"Authorization", util::format("Bearer %1", with_user_authorization->refresh_token())});
16,365✔
157
                break;
16,365✔
158
        }
49,047✔
159
    }
34,090✔
160
    return headers;
34,090✔
161
}
34,114✔
162

24✔
163
UniqueFunction<void(const Response&)> handle_default_response(UniqueFunction<void(Optional<AppError>)>&& completion)
24✔
164
{
8,984✔
165
    return [completion = std::move(completion)](const Response& response) {
8,984✔
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

4,287✔
183
namespace realm {
4,287✔
184
namespace app {
4,287✔
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

4,241✔
191
App::Config::DeviceInfo::DeviceInfo()
4,241✔
192
    : platform(util::get_library_platform())
10✔
193
    , cpu_arch(util::get_library_cpu_arch())
10✔
194
    , core_version(REALM_VERSION_STRING)
10✔
195
{
38,694✔
196
}
38,694✔
197

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

2✔
214
// NO_THREAD_SAFETY_ANALYSIS because clang generates a false positive.
1✔
215
// "Calling function configure requires negative capability '!app->m_route_mutex'"
2✔
216
// But 'app' is an object just created in this static method so it is not possible to annotate this in the header.
2✔
217
SharedApp App::get_app(CacheMode mode, const Config& config,
218
                       const SyncClientConfig& sync_client_config) NO_THREAD_SAFETY_ANALYSIS
219
{
38,929✔
220
    if (mode == CacheMode::Enabled) {
38,929✔
221
        std::lock_guard<std::mutex> lock(s_apps_mutex);
4,505✔
222
        auto& app = s_apps_cache[config.app_id][config.base_url.value_or(std::string(App::default_base_url()))];
4,505✔
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();
4,309✔
242
        if (app_it != apps_by_url.end()) {
80✔
243
            return app_it->second;
64✔
244
        }
64✔
245
    }
16✔
246

8✔
247
    return nullptr;
4,253✔
248
}
2,126✔
249

2,110✔
250
void App::clear_cached_apps()
4,237✔
251
{
36,559✔
252
    std::lock_guard<std::mutex> lock(s_apps_mutex);
36,559✔
253
    s_apps_cache.clear();
38,102✔
254
}
38,102✔
255

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

266
App::App(Private, const Config& config)
2,110✔
267
    : m_config(config)
4,237✔
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))
4,237✔
271
{
34,472✔
272
#ifdef __EMSCRIPTEN__
4,237✔
273
    if (!m_config.transport) {
274
        m_config.transport = std::make_shared<_impl::EmscriptenNetworkTransport>();
275
    }
44,823✔
276
#endif
44,823✔
277
    REALM_ASSERT(m_config.transport);
38,701✔
278
    REALM_ASSERT(!m_config.device_info.platform.empty());
38,701✔
279

17,168✔
280
    // if a base url is provided, then verify the value
17,168✔
281
    if (m_config.base_url) {
38,701✔
282
        if (auto comp = AppUtils::split_url(*m_config.base_url); !comp.is_ok()) {
50,159✔
283
            throw Exception(comp.get_status());
44,823✔
284
        }
285
    }
34,472✔
286
    // Setup a baseline set of routes using the provided or default base url
23,012✔
287
    // These will be updated when the location info is refreshed prior to sending the
23,012✔
288
    // first AppServices HTTP request.
23,012✔
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");
38,919✔
293
    }
38,919✔
294

56,087✔
295
    if (m_config.device_info.sdk.empty()) {
73,391✔
296
        throw InvalidArgument("You must specify the SDK Name in App::Config::device_info");
38,919✔
297
    }
38,919✔
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
    }
60✔
302
}
34,532!
303

60✔
304
App::~App() {}
34,532✔
305

60✔
306
void App::configure(const SyncClientConfig& sync_client_config)
60✔
307
{
34,472✔
308
    std::string ws_route;
34,472✔
309
    {
43,908✔
310
        util::CheckedLockGuard guard(m_route_mutex);
43,908✔
311
        // Make sure to request the location when the app is configured
26,604✔
312
        m_location_updated = false;
43,908✔
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.
21,469✔
322
    m_sync_manager = SyncManager::create(shared_from_this(), ws_route, sync_client_config, config().app_id);
38,773✔
323
}
38,773✔
324

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

333
bool App::would_log(util::Logger::Level level)
8,464✔
334
{
26,692✔
335
    return init_logger() && m_logger_ptr->would_log(util::LogCategory::app, level);
26,692✔
336
}
26,692✔
337

338
template <class... Params>
339
void App::log_debug(const char* message, Params&&... params)
8,472✔
340
{
98,478✔
341
    if (init_logger()) {
98,478✔
342
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::debug, message,
98,478✔
343
                          std::forward<Params>(params)...);
94,243✔
344
    }
94,225✔
345
}
94,225✔
346

4,219✔
347
template <class... Params>
8,472✔
348
void App::log_error(const char* message, Params&&... params)
4,219✔
349
{
8,952✔
350
    if (init_logger()) {
4,699!
351
        m_logger_ptr->log(util::LogCategory::app, util::Logger::Level::error, message,
8,952✔
352
                          std::forward<Params>(params)...);
8,952✔
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

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

2,183✔
368
std::string App::get_host_url()
4,383✔
369
{
6,138✔
370
    util::CheckedLockGuard guard(m_route_mutex);
6,138✔
371
    return m_host_url;
8,318✔
372
}
10,517✔
373

4,072✔
374
std::string App::get_ws_host_url()
4,072✔
375
{
2,758✔
376
    util::CheckedLockGuard guard(m_route_mutex);
2,758✔
377
    return m_ws_host_url;
2,758✔
378
}
4,957✔
379

12✔
380

12✔
381
std::string App::make_sync_route(Optional<std::string> ws_host_url)
2,176✔
382
{
42,304✔
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,
44,497✔
384
                        s_sync_path);
43,824✔
385
}
43,824✔
386

3,696✔
387
void App::configure_route(const std::string& host_url, const std::optional<std::string>& ws_host_url)
328✔
388
{
40,456✔
389
    // We got a new host url, save it
20,585✔
390
    m_host_url = (host_url.length() > 0 ? host_url : m_base_url);
40,801✔
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;
9,891✔
395
    }
9,891✔
396
    // Otherwise, convert the host url to a websocket host url
21,403✔
397
    else {
36,581✔
398
        m_ws_host_url = App::create_ws_host_url(m_host_url);
36,581✔
399
    }
38,707✔
400

24,147✔
401
    // host_url is the url to the server: e.g., https://services.cloud.mongodb.com or https://localhost:9090
24,147✔
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);
44,792✔
408
}
44,792✔
409

4,664✔
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
20✔
414
// http[s]://[region-prefix]services.cloud.mongodb.com => ws[s]://[region-prefix].ws.services.cloud.mongodb.com
20✔
415
// All others => http[s]://<host-url> => ws[s]://<host-url>
20✔
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

22,460✔
422
    // Doesn't contain 7 or more characters (length of 'http://') or start with http,
22,460✔
423
    // just return provided string
22,460✔
424
    if (host_url.length() < base_len || host_url.substr(0, 4) != "http") {
40,388✔
425
        return std::string(host_url);
4,684✔
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,818✔
430
    std::string_view prefix = https ? "wss://" : "ws://";
32,818✔
431

17,786✔
432
    // http[s]://[<region-prefix>]realm.mongodb.com[/<path>] =>
17,786✔
433
    //     ws[s]://ws.[<region-prefix>]realm.mongodb.com[/<path>]
17,786✔
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,738✔
438
    //     ws[s]://[<region-prefix>].ws.services.cloud.mongodb.com[/<path>]
17,738✔
439
    if (auto start = host_url.find(new_base_domain); start != std::string_view::npos) {
35,610✔
440
        return util::format("%1%2ws.%3", prefix, host_url.substr(prefix_len, start - prefix_len),
29,378✔
441
                            host_url.substr(start));
29,378✔
442
    }
29,378✔
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,236✔
447

4✔
448
void App::update_hostname(const std::string& host_url, const std::optional<std::string>& ws_host_url,
4✔
449
                          const std::optional<std::string>& new_base_url)
4✔
450
{
5,660✔
451
    // Update url components based on new hostname (and optional websocket hostname) values
2,748✔
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 <>
2✔
466
App::UsernamePasswordProviderClient App::provider_client<App::UsernamePasswordProviderClient>()
2✔
467
{
8,610✔
468
    return App::UsernamePasswordProviderClient(shared_from_this());
8,610✔
469
}
8,610✔
470

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

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

6✔
479
void App::UsernamePasswordProviderClient::register_email(const std::string& email, const std::string& password,
6✔
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,722✔
485
}
8,722✔
486

58✔
487
void App::UsernamePasswordProviderClient::confirm_user(const std::string& token, const std::string& token_id,
58✔
488
                                                       UniqueFunction<void(Optional<AppError>)>&& completion)
58✔
489
{
28✔
490
    m_parent->log_debug("App: confirm_user");
40✔
491
    m_parent->post(util::format("%1/providers/%2/confirm", m_parent->auth_route(), s_username_password_provider_key),
40✔
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)
10✔
497
{
26✔
498
    m_parent->log_debug("App: resend_confirmation_email: %1", email);
26✔
499
    m_parent->post(
26✔
500
        util::format("%1/providers/%2/confirm/send", m_parent->auth_route(), s_username_password_provider_key),
26✔
501
        std::move(completion), {{"email", email}});
16✔
502
}
16✔
503

504
void App::UsernamePasswordProviderClient::retry_custom_confirmation(
34✔
505
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
34✔
506
{
66✔
507
    m_parent->log_debug("App: retry_custom_confirmation: %1", email);
66✔
508
    m_parent->post(
66✔
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(
14✔
514
    const std::string& email, UniqueFunction<void(Optional<AppError>)>&& completion)
14✔
515
{
14✔
516
    m_parent->log_debug("App: send_reset_password_email: %1", email);
14✔
517
    m_parent->post(
14✔
518
        util::format("%1/providers/%2/reset/send", m_parent->auth_route(), s_username_password_provider_key),
4✔
519
        std::move(completion), {{"email", email}});
4✔
520
}
5✔
521

10✔
522
void App::UsernamePasswordProviderClient::reset_password(const std::string& password, const std::string& token,
10✔
523
                                                         const std::string& token_id,
10✔
524
                                                         UniqueFunction<void(Optional<AppError>)>&& completion)
10✔
525
{
26✔
526
    m_parent->log_debug("App: reset_password");
26✔
527
    m_parent->post(util::format("%1/providers/%2/reset", m_parent->auth_route(), s_username_password_provider_key),
26✔
528
                   std::move(completion), {{"password", password}, {"token", token}, {"tokenId", token_id}});
26✔
529
}
26✔
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)
10✔
534
{
62✔
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}});
56✔
539
}
56✔
540

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

8✔
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(
472✔
547
            util::format("%1/%2/%3", s_auth_path, s_user_api_key_provider_key_path, path));
472✔
548
    }
472✔
549

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

553
void App::UserAPIKeyProviderClient::create_api_key(
554
    const std::string& name, const std::shared_ptr<SyncUser>& user,
8✔
555
    UniqueFunction<void(UserAPIKey&&, Optional<AppError>)>&& completion)
8✔
556
{
88✔
557
    Request req;
88✔
558
    req.method = HttpMethod::post;
88✔
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;
4,057✔
573
    m_auth_request_client.do_authenticated_request(std::move(req), user,
4,057✔
574
                                                   UserAPIKeyResponseHandler{std::move(completion)});
4,057✔
575
}
4,057✔
576

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

106✔
586
    m_auth_request_client.do_authenticated_request(
162✔
587
        std::move(req), user, [completion = std::move(completion)](const Response& response) {
9,313✔
588
            if (auto error = AppUtils::check_for_errors(response)) {
9,313✔
589
                return completion({}, std::move(error));
9,233✔
590
            }
32✔
591

40✔
592
            try {
4,800✔
593
                auto json = parse<BsonArray>(response.body);
4,800✔
594
                std::vector<UserAPIKey> keys;
86✔
595
                keys.reserve(json.size());
86✔
596
                for (auto&& api_key_json : json) {
4,800✔
597
                    keys.push_back(UserAPIKeyResponseHandler::read_user_api_key(as<BsonDocument>(api_key_json)));
80✔
598
                }
80✔
599
                return completion(std::move(keys), {});
8,324✔
600
            }
8,324✔
601
            catch (AppError& e) {
8,244✔
602
                completion({}, std::move(e));
3,727✔
603
            }
3,727✔
604
        });
3,807✔
605
}
4,629✔
606

4,517✔
607
void App::UserAPIKeyProviderClient::delete_api_key(const realm::ObjectId& id, const std::shared_ptr<SyncUser>& user,
4,479✔
608
                                                   UniqueFunction<void(Optional<AppError>)>&& completion)
4,479✔
609
{
4,543✔
610
    Request req;
4,543✔
611
    req.method = HttpMethod::del;
102✔
612
    req.url = url_for_path(id.to_string());
102✔
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,
76✔
630
                                                    UniqueFunction<void(Optional<AppError>)>&& completion)
76✔
631
{
140✔
632
    Request req;
140✔
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;
76✔
636
    m_auth_request_client.do_authenticated_request(std::move(req), user,
76✔
637
                                                   handle_default_response(std::move(completion)));
67✔
638
}
70✔
639
// MARK: - App
6✔
640

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

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

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

6✔
657
void App::update_base_url(std::optional<std::string> base_url, UniqueFunction<void(Optional<AppError>)>&& completion)
12✔
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()) {
134✔
662
        // Treat an empty string the same as requesting the default base url
22✔
663
        new_base_url = std::string(App::default_base_url());
44✔
664
        log_debug("App::update_base_url: empty => %1", new_base_url);
44✔
665
    }
32✔
666
    else {
96✔
667
        log_debug("App::update_base_url: %1", new_base_url);
178✔
668
    }
178✔
669

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

116✔
675
    bool update_not_needed;
210✔
676
    {
210✔
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;
4,856✔
681
    }
4,856✔
682
    // If the new base_url is the same as the current base_url and the location has already been updated,
4,792✔
683
    // then we're done
4,792✔
684
    if (update_not_needed) {
4,856✔
685
        completion(util::none);
4,728✔
686
        return;
×
687
    }
×
688

2,417✔
689
    // Otherwise, request the location information at the new base URL
4,792✔
690
    request_location(std::move(completion), new_base_url);
4,856✔
691
}
4,856✔
692

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

9,320✔
702
    do_authenticated_request(
14,088✔
703
        std::move(req), sync_user,
14,088✔
704
        [completion = std::move(completion), self = shared_from_this(), sync_user](const Response& profile_response) {
14,088✔
705
            if (auto error = AppUtils::check_for_errors(profile_response)) {
14,088✔
706
                return completion(nullptr, std::move(error));
4,728✔
707
            }
2,353✔
708

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

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

6,957✔
721
                sync_user->update_user_profile(std::move(identities),
14,112✔
722
                                               SyncUserProfile(get<BsonDocument>(profile_json, "data")));
14,112✔
723
                self->m_sync_manager->set_current_user(sync_user->identity());
14,112✔
724
                self->emit_change_to_subscribers(*self);
14,112✔
725
            }
14,112✔
726
            catch (const AppError& err) {
9,344✔
727
                return completion(nullptr, err);
4,752✔
728
            }
4,752✔
729

9,344✔
730
            return completion(sync_user, {});
14,112✔
731
        });
14,112✔
732
}
14,112✔
733

4,752✔
734
void App::attach_auth_options(BsonDocument& body)
2,365✔
735
{
14,304✔
736
    BsonDocument options;
14,304✔
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);
14,312✔
741
    options["appId"] = m_config.app_id;
14,312✔
742
    options["platform"] = m_config.device_info.platform;
14,312✔
743
    options["platformVersion"] = m_config.device_info.platform_version;
14,312✔
744
    options["sdk"] = m_config.device_info.sdk;
14,312✔
745
    options["sdkVersion"] = m_config.device_info.sdk_version;
11,921✔
746
    options["cpuArch"] = m_config.device_info.cpu_arch;
11,921✔
747
    options["deviceName"] = m_config.device_info.device_name;
14,312✔
748
    options["deviceVersion"] = m_config.device_info.device_version;
14,312✔
749
    options["frameworkName"] = m_config.device_info.framework_name;
9,638✔
750
    options["frameworkVersion"] = m_config.device_info.framework_version;
9,602✔
751
    options["coreVersion"] = m_config.device_info.core_version;
9,566✔
752
    options["bundleId"] = m_config.device_info.bundle_id;
9,560✔
753

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

8✔
757
void App::log_in_with_credentials(
8✔
758
    const AppCredentials& credentials, const std::shared_ptr<SyncUser>& linking_user,
8✔
759
    UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
14✔
760
{
9,702✔
761
    if (would_log(util::Logger::Level::debug)) {
11,985✔
762
        auto app_info = util::format("app_id: %1", m_config.app_id);
14,376✔
763
        log_debug("App: log_in_with_credentials: %1", app_info);
9,624✔
764
    }
9,624✔
765
    // if we try logging in with an anonymous user while there
4,728✔
766
    // is already an anonymous session active, reuse it
4,728✔
767
    if (credentials.provider() == AuthProvider::ANONYMOUS) {
11,981✔
768
        for (auto&& user : m_sync_manager->all_users()) {
5,248✔
769
            if (user->is_anonymous()) {
314✔
770
                completion(switch_user(user), util::none);
74✔
771
                return;
64✔
772
            }
64✔
773
        }
5,056✔
774
    }
3,053✔
775

7,085✔
776
    // construct the route
9,472✔
777
    std::string route = util::format("%1/providers/%2/login%3", auth_route(), credentials.provider_as_string(),
14,331✔
778
                                     linking_user ? "?link=true" : "");
11,877✔
779

9,440✔
780
    BsonDocument body = credentials.serialize_as_bson();
14,304✔
781
    attach_auth_options(body);
11,917✔
782

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

9,340✔
794
            std::shared_ptr<realm::SyncUser> sync_user = linking_user;
14,124✔
795
            try {
14,124✔
796
                auto json = parse<BsonDocument>(response.body);
14,124✔
797
                if (linking_user) {
9,402✔
798
                    linking_user->update_access_token(get<std::string>(json, "access_token"));
90✔
799
                }
85✔
800
                else {
9,322✔
801
                    sync_user = self->m_sync_manager->get_user(
9,322✔
802
                        get<std::string>(json, "user_id"), get<std::string>(json, "refresh_token"),
9,322✔
803
                        get<std::string>(json, "access_token"), get<std::string>(json, "device_id"));
9,322✔
804
                }
14,034✔
805
            }
14,114✔
806
            catch (const AppError& e) {
9,346✔
807
                return completion(nullptr, e);
4,754✔
808
            }
4,754✔
809

9,314✔
810
            self->get_profile(sync_user, std::move(completion));
14,082✔
811
        },
14,082✔
812
        false);
14,274✔
813
}
14,284✔
814

2,356✔
815
void App::log_in_with_credentials(
2✔
816
    const AppCredentials& credentials,
2✔
817
    util::UniqueFunction<void(const std::shared_ptr<SyncUser>&, Optional<AppError>)>&& completion)
2,354✔
818
{
14,266✔
819
    App::log_in_with_credentials(credentials, nullptr, std::move(completion));
9,538✔
820
}
9,538✔
821

2✔
822
void App::log_out(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
4,728✔
823
{
5,064✔
824
    if (!user || user->state() != SyncUser::State::LoggedIn) {
5,064✔
825
        log_debug("App: log_out() - already logged out");
4,784✔
826
        return completion(util::none);
4,784✔
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();
5,054✔
832

4,902✔
833
    Request req;
5,054✔
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;
356✔
838
    req.headers = get_request_headers();
356✔
839
    req.headers.insert({"Authorization", util::format("Bearer %1", refresh_token)});
310✔
840

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

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

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

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

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

4,769✔
879
void App::remove_user(const std::shared_ptr<SyncUser>& user, UniqueFunction<void(Optional<AppError>)>&& completion)
4,771✔
880
{
2,517✔
881
    if (!user || user->state() == SyncUser::State::Removed) {
4,913✔
882
        return completion(AppError(ErrorCodes::ClientUserNotFound, "User has already been removed"));
32✔
883
    }
32✔
884
    if (!verify_user_present(user)) {
4,848✔
885
        return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found"));
4,736✔
886
    }
2✔
887

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

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

49✔
910
    if (!verify_user_present(user)) {
72✔
911
        return completion(AppError(ErrorCodes::ClientUserNotFound, "No user has been found."));
17✔
912
    }
15✔
913

47✔
914
    Request req;
79✔
915
    req.method = HttpMethod::del;
79✔
916
    req.timeout_ms = m_request_timeout_ms;
79✔
917
    req.url = url_for_path("/auth/delete");
78✔
918
    do_authenticated_request(std::move(req), user,
78✔
919
                             [self = shared_from_this(), completion = std::move(completion),
79✔
920
                              identity = user->identity()](const Response& response) {
79✔
921
                                 auto error = AppUtils::check_for_errors(response);
66✔
922
                                 if (!error) {
66✔
923
                                     self->emit_change_to_subscribers(*self);
66✔
924
                                     self->m_sync_manager->delete_user(identity);
66✔
925
                                 }
66✔
926
                                 completion(std::move(error));
66✔
927
                             });
66✔
928
}
81✔
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)
12✔
932
{
108✔
933
    if (!user) {
96✔
934
        return completion(nullptr,
×
935
                          AppError(ErrorCodes::ClientUserNotFound, "The specified user could not be found."));
12✔
936
    }
4✔
937
    if (user->state() != SyncUser::State::LoggedIn) {
100✔
938
        return completion(nullptr,
20✔
939
                          AppError(ErrorCodes::ClientUserNotLoggedIn, "The specified user is not logged in."));
24✔
940
    }
24✔
941
    if (!verify_user_present(user)) {
88✔
NEW
942
        return completion(nullptr, AppError(ErrorCodes::ClientUserNotFound, "The specified user was not found."));
×
NEW
943
    }
×
944

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

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

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

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

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

5✔
976
void App::request_location(UniqueFunction<void(std::optional<AppError>)>&& completion,
10✔
977
                           std::optional<std::string>&& new_hostname, std::optional<std::string>&& redir_location,
10✔
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,310✔
982
    // redirect responses received during the location update and return an error if this count exceeds
3,310✔
983
    // max_http_redirects. If neither new_hostname nor redir_location is provided, the current value of m_base_url
3,310✔
984
    // will be used.
3,296✔
985
    std::string app_route;
6,760✔
986
    std::string base_url;
6,760✔
987
    {
6,770✔
988
        util::CheckedUniqueLock lock(m_route_mutex);
6,770✔
989
        // Skip if the location info has already been initialized and a new hostname is not provided
3,306✔
990
        if (!new_hostname && !redir_location && m_location_updated) {
6,760✔
991
            // Release the lock before calling the completion function
992
            lock.unlock();
4,966✔
993
            completion(util::none);
4,966✔
994
            return; // early return
4,966✔
995
        }
4,966✔
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)
11,131✔
999
            app_route = get_app_route(redir_location);
5,251✔
1000
        // If this is querying the new_hostname, then use that location
2,974✔
1001
        else if (new_hostname)
5,998✔
1002
            app_route = get_app_route(new_hostname);
4,381✔
1003
        else
10,005✔
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;
11,131✔
1009
    req.method = HttpMethod::get;
8,937✔
1010
    req.url = util::format("%1/location", app_route);
8,937✔
1011
    req.timeout_ms = m_request_timeout_ms;
8,937✔
1012
    req.redirect_count = redirect_count;
8,937✔
1013

5,473✔
1014
    log_debug("App: request location: %1", req.url);
11,131✔
1015

7,667✔
1016
    m_config.transport->send_request_to_server(
11,131✔
1017
        std::move(req), [self = shared_from_this(), completion = std::move(completion),
11,131✔
1018
                         base_url = std::move(base_url)](Request&& request, const Response& response) mutable {
8,937✔
1019
            // Check to see if a redirect occurred
7,667✔
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
                                        {},
4,403✔
1026
                                        response.http_status_code});
2,209✔
1027
                    return; // early return
4,403✔
1028
                }
138✔
1029
                // Handle the redirect response when requesting the location - extract the
2,572✔
1030
                // new location header field and resend the request.
4,713✔
1031
                auto redir_location = AppUtils::extract_redir_location(response.headers);
908✔
1032
                if (!redir_location) {
5,149✔
1033
                    // Location not found in the response, pass error response up the chain
4,269✔
1034
                    completion(AppError{ErrorCodes::ClientRedirectError,
4,403✔
1035
                                        "Redirect response missing location header",
4,403✔
1036
                                        {},
2,209✔
1037
                                        response.http_status_code});
4,403✔
1038
                    return; // early return
4,403✔
1039
                }
4,403✔
1040
                // try to request the location info at the new location in the redirect response
4,803✔
1041
                // retry_count is passed in to track the number of subsequent redirection attempts
2,609✔
1042
                self->request_location(std::move(completion), std::move(base_url), std::move(redir_location),
5,235✔
1043
                                       request.redirect_count);
3,041✔
1044
                return; // early return
5,235✔
1045
            }
5,235✔
1046
            // Location request was successful - update the location info
7,203✔
1047
            auto update_response = self->update_location(response, base_url);
8,009✔
1048
            if (update_response) {
10,203✔
1049
                self->log_error("App: request location failed (%1%2): %3", update_response->code_string(),
233✔
1050
                                update_response->additional_status_code
290✔
1051
                                    ? util::format(" %1", *update_response->additional_status_code)
180✔
1052
                                    : "",
92✔
1053
                                update_response->reason());
180✔
1054
            }
180✔
1055
            completion(update_response);
5,836✔
1056
        });
5,836✔
1057
}
6,815✔
1058

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

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

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

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

4,797✔
1108
            // If the location info was updated, update the original request to point
6,923✔
1109
            // to the new location URL.
6,923✔
1110
            auto comp = AppUtils::split_url(request.url);
9,779✔
1111
            if (!comp.is_ok()) {
9,771✔
1112
                throw Exception(comp.get_status());
4,227✔
1113
            }
4,235✔
1114
            request.url = self->get_host_url() + comp.get_value().request;
9,779✔
1115

4,797✔
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
6,923✔
1119
            self->m_config.transport->send_request_to_server(
9,779✔
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
                });
9,797✔
1124
        },
7,662✔
1125
        // The base_url is not changing for this request
4,918✔
1126
        util::none, std::move(redir_location));
10,021✔
1127
}
10,021✔
1128

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

2,104✔
1136
void App::do_request(Request&& request, UniqueFunction<void(const Response& response)>&& completion,
4,225✔
1137
                     bool update_location)
4,225✔
1138
{
38,315✔
1139
    // Make sure the timeout value is set to the configured request timeout value
18,150✔
1140
    request.timeout_ms = m_request_timeout_ms;
38,315✔
1141

18,150✔
1142
    // Verify the request URL to make sure it is valid
20,271✔
1143
    if (auto comp = AppUtils::split_url(request.url); !comp.is_ok()) {
38,315✔
1144
        throw Exception(comp.get_status());
4,225✔
1145
    }
4,225✔
1146

20,271✔
1147
    // Refresh the location info when app is created or when requested (e.g. after a websocket redirect)
20,271✔
1148
    // to ensure the http and websocket URL information is up to date.
20,271✔
1149
    {
36,208✔
1150
        util::CheckedUniqueLock lock(m_route_mutex);
38,343✔
1151
        if (update_location) {
38,343✔
1152
            // If requesting a location update, force the location to be updated before sending the request.
128✔
1153
            m_location_updated = false;
288✔
1154
        }
4,972✔
1155
        if (!m_location_updated) {
38,774✔
1156
            lock.unlock();
10,436✔
1157
            // Location info needs to be requested, update the location info and then send the request
7,476✔
1158
            update_location_and_resend(std::move(request), std::move(completion));
10,436✔
1159
            return; // early return
10,436✔
1160
        }
10,436✔
1161
    }
28,338✔
1162

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

14,957✔
1172
void App::check_for_redirect_response(Request&& request, const Response& response,
17✔
1173
                                      UniqueFunction<void(const Response& response)>&& completion)
34✔
1174
{
33,916✔
1175
    // If this isn't a redirect response, then we're done
30,899✔
1176
    if (!AppUtils::is_redirect_status_code(response.http_status_code)) {
38,135✔
1177
        return completion(response);
35,984✔
1178
    }
38,119✔
1179

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

10,712✔
1189
    // Request the location info at the new location - once this is complete, the original
10,712✔
1190
    // request will be sent to the new server
10,712✔
1191
    update_location_and_resend(std::move(request), std::move(completion), std::move(redir_location));
10,720✔
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)
14,929✔
1196
{
21,877✔
1197
    request.headers = get_request_headers(sync_user, request.uses_refresh_token ? RequestTokenType::RefreshToken
21,719✔
1198
                                                                                : RequestTokenType::AccessToken);
29,139✔
1199

21,391✔
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()) {
5,523!
1221
                sync_user->log_out();
5,379✔
1222
            }
5,379✔
1223
            completion(response);
5,523✔
1224
            return;
5,523✔
1225
        }
210✔
1226
    }
386✔
1227
    else {
386✔
1228
        completion(response);
5,633✔
1229
        return;
5,633✔
1230
    }
5,633✔
1231

5,411✔
1232
    // Otherwise, refresh the access token
5,411✔
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) {
130✔
1238
                                      // assign the new access_token to the auth header
49✔
1239
                                      request.headers = get_request_headers(sync_user, RequestTokenType::AccessToken);
98✔
1240
                                      self->do_request(std::move(request), std::move(completion));
72✔
1241
                                  }
72✔
1242
                                  else {
72✔
1243
                                      // pass the error back up the chain
29✔
1244
                                      completion(std::move(response));
45✔
1245
                                  }
45✔
1246
                              });
90✔
1247
}
82!
1248

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

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

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

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

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

326✔
1283
            return completion(util::none);
498✔
1284
        },
498✔
1285
        update_location);
898✔
1286
}
942✔
1287

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

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

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

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

559✔
1319
void App::call_function(const std::shared_ptr<SyncUser>& user, const std::string& name, const BsonArray& args_bson,
559✔
1320
                        const Optional<std::string>& service_name,
559✔
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,929✔
1326
    bool not_first = false;
4,925✔
1327
    for (auto&& arg : args_bson) {
5,057✔
1328
        if (not_first)
5,057✔
1329
            args_ejson << ',';
679✔
1330
        args_ejson << arg.toJson();
4,690✔
1331
        not_first = true;
5,057✔
1332
    }
5,053✔
1333
    args_ejson << "]";
4,562✔
1334

2,021✔
1335
    call_function(user, name, std::move(args_ejson).str(), service_name,
4,929✔
1336
                  [self = shared_from_this(), name, service_name = std::move(service_name2),
4,929✔
1337
                   completion = std::move(completion)](const std::string* response, util::Optional<AppError> err) {
4,929✔
1338
                      if (err) {
4,929✔
1339
                          return completion({}, err);
162✔
1340
                      }
162✔
1341
                      if (!response) {
4,252✔
1342
                          return completion({}, AppError{ErrorCodes::AppUnknownError, "Empty response from server"});
18✔
1343
                      }
533✔
1344
                      util::Optional<Bson> body_as_bson;
4,767✔
1345
                      try {
4,785✔
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,785✔
1351
                      }
4,781✔
1352
                      catch (const std::exception& e) {
1,949✔
1353
                          self->log_error("App: call_function: %1 service_name: %2 - error parsing result: %3", name,
551✔
1354
                                          service_name, e.what());
551✔
1355
                          return completion(util::none, AppError(ErrorCodes::BadBsonParse, e.what()));
567✔
1356
                      };
4,801✔
1357
                      completion(std::move(body_as_bson), util::none);
4,250✔
1358
                  });
4,801✔
1359
}
4,945✔
1360

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

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

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

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

36✔
1392
    auto args_base64 = std::string(util::base64_encoded_size(args_json.size()), '\0');
68✔
1393
    util::base64_encode(args_json, args_base64);
68✔
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,
66✔
1405
        {{"Accept", "text/event-stream"}},
66✔
1406
    };
66✔
1407
}
64✔
1408

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

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