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

realm / realm-core / michael.wilkersonbarker_1387

07 Sep 2024 05:17AM UTC coverage: 91.103% (-0.005%) from 91.108%
michael.wilkersonbarker_1387

Pull #8011

Evergreen

michael-wb
Fixed refresh access token test
Pull Request #8011: RCORE-2253 Redirected user authenticated app requests cause user to be logged out and location is not updated

102888 of 181598 branches covered (56.66%)

222 of 258 new or added lines in 5 files covered. (86.05%)

65 existing lines in 19 files now uncovered.

217392 of 238621 relevant lines covered (91.1%)

5668504.84 hits per line

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

72.68
/test/object-store/util/sync/redirect_server.hpp
1
////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2024 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 implied.
14
// See the License for the specific language governing permissions and
15
// limitations under the License.
16
//
17
////////////////////////////////////////////////////////////////////////////
18

19
#pragma once
20

21
#if defined(REALM_ENABLE_SYNC) && defined(REALM_ENABLE_AUTH_TESTS)
22

23
#include <realm/sync/network/http.hpp>
24
#include <realm/sync/network/network.hpp>
25
#include <realm/sync/network/websocket.hpp>
26
#include <realm/util/future.hpp>
27
#include <realm/util/random.hpp>
28
#include <realm/util/uri.hpp>
29
#include <external/json/json.hpp>
30

31
#include <catch2/catch_all.hpp>
32

33
#include <thread>
34

35
namespace realm::sync {
36

37
class RedirectingHttpServer {
38
public:
39
    enum Event {
40
        error,
41
        location,
42
        redirect,
43
        ws_redirect,
44
    };
45

46
    // Allow the redirecting server to choose the listen port
47
    RedirectingHttpServer(std::string redirect_to_base_url, std::shared_ptr<util::Logger> logger)
48
        : m_redirect_to_base_url{redirect_to_base_url}
3✔
49
        , m_redirect_to_base_wsurl(make_wsurl(m_redirect_to_base_url))
3✔
50
        , m_logger(std::make_shared<util::PrefixLogger>("HTTP Redirector ", std::move(logger)))
3✔
51
        , m_acceptor(m_service)
3✔
52
        , m_server_thread([this] {
5✔
53
            m_service.run_until_stopped();
5✔
54
        })
5✔
55
    {
5✔
56
        network::Endpoint ep;
5✔
57
        m_acceptor.open(ep.protocol());
5✔
58
        m_acceptor.bind(ep);
5✔
59
        ep = m_acceptor.local_endpoint();
5✔
60
        m_base_url = util::format("http://localhost:%1", ep.port());
5✔
61
        m_base_wsurl = make_wsurl(m_base_url);
5✔
62
        m_acceptor.listen();
5✔
63
        m_service.post([this](Status status) {
5✔
64
            REALM_ASSERT(status.is_ok());
5✔
65
            do_accept();
5✔
66
        });
5✔
67
    }
5✔
68

69
    ~RedirectingHttpServer()
70
    {
5✔
71
        m_service.post([this](Status status) {
5✔
72
            if (status == ErrorCodes::OperationAborted)
5✔
NEW
73
                return;
×
74
            m_acceptor.cancel();
5✔
75
            m_service.stop();
5✔
76
        });
5✔
77
        m_server_thread.join();
5✔
78
    }
5✔
79

80
    void set_event_hook(std::function<void(Event, std::optional<std::string>)> hook)
81
    {
6✔
82
        m_hook = hook;
6✔
83
    }
6✔
84

85
    // If true, http (app services) requests will first hit the redirect server and
86
    // receive a redirect response which will contain the location to the actual
87
    // server.
88
    // NOTE: some http transport redirect implementations may strip the authorization
89
    // header from the request after it is redirected and the user will be logged out
90
    // from the client app as a result.
91
    void force_http_redirect(bool remote)
92
    {
12✔
93
        m_http_redirect = remote;
12✔
94
    }
12✔
95

96
    // If true, websockets will be first directed to the redirect server which will
97
    // return a redirect close code. The client will then update the location by
98
    // querying the actual server location endpoint (from the 'hostname' location
99
    // value) and open a websocket conneciton to the actual server.
100
    // NOTE: the websocket will never connect if both http and websockets are
101
    // redirecting and will just keep getting the redirect close code.
102
    void force_websocket_redirect(bool force)
103
    {
10✔
104
        m_websocket_redirect = force;
10✔
105
    }
10✔
106

107
    std::string base_url() const
108
    {
201✔
109
        return m_base_url;
201✔
110
    }
201✔
111

112
    std::string server_url() const
113
    {
6✔
114
        return m_redirect_to_base_url;
6✔
115
    }
6✔
116

117
private:
118
    struct BufferedSocket : network::Socket {
119
        BufferedSocket(network::Service& service)
120
            : network::Socket(service)
287✔
121
        {
306✔
122
        }
306✔
123

124
        BufferedSocket(network::Service& service, const network::StreamProtocol& protocol,
125
                       native_handle_type native_handle)
126
            : network::Socket(service, protocol, native_handle)
127
        {
×
128
        }
×
129

130

131
        template <class H>
132
        void async_read_until(char* buffer, std::size_t size, char delim, H handler)
133
        {
1,257✔
134
            network::Socket::async_read_until(buffer, size, delim, m_read_buffer, std::move(handler));
1,257✔
135
        }
1,257✔
136

137
        template <class H>
138
        void async_read(char* buffer, std::size_t size, H handler)
139
        {
15✔
140
            network::Socket::async_read(buffer, size, m_read_buffer, std::move(handler));
15✔
141
        }
15✔
142

143
    private:
144
        network::ReadAheadBuffer m_read_buffer;
145
    };
146

147
    struct Conn : public util::RefCountBase, websocket::Config {
148
        Conn(network::Service& service, const std::shared_ptr<util::Logger>& logger)
149
            : random(Catch::getSeed())
287✔
150
            , logger(logger)
287✔
151
            , socket(service)
287✔
152
            , http_server(socket, logger)
287✔
153
        {
306✔
154
        }
306✔
155

156
        // Implement the websocket::Config interface
157
        const std::shared_ptr<util::Logger>& websocket_get_logger() noexcept override
158
        {
3✔
159
            return logger;
3✔
160
        }
3✔
161

162
        std::mt19937_64& websocket_get_random() noexcept override
163
        {
3✔
164
            return random;
3✔
165
        }
3✔
166

167
        void async_write(const char* data, size_t size, websocket::WriteCompletionHandler handler) override
168
        {
3✔
169
            socket.async_write(data, size, std::move(handler));
3✔
170
        }
3✔
171

172
        void async_read(char* buffer, size_t size, websocket::ReadCompletionHandler handler) override
173
        {
3✔
174
            socket.async_read(buffer, size, std::move(handler));
3✔
175
        }
3✔
176

177
        void async_read_until(char* buffer, size_t size, char delim,
178
                              websocket::ReadCompletionHandler handler) override
179
        {
×
180
            socket.async_read_until(buffer, size, delim, std::move(handler));
×
181
        }
×
182

183
        void websocket_handshake_completion_handler(const HTTPHeaders&) override {}
×
184

185
        void websocket_read_error_handler(std::error_code) override {}
×
186

187
        void websocket_write_error_handler(std::error_code) override {}
×
188

189
        void websocket_handshake_error_handler(std::error_code, const HTTPHeaders*, std::string_view) override {}
×
190

191
        void websocket_protocol_error_handler(std::error_code) override {}
×
192

193
        bool websocket_text_message_received(const char*, size_t) override
194
        {
×
195
            return false;
×
196
        }
×
197

198
        bool websocket_binary_message_received(const char*, size_t) override
199
        {
×
200
            return false;
×
201
        }
×
202

203
        bool websocket_close_message_received(websocket::WebSocketError, std::string_view) override
204
        {
×
205
            return false;
×
206
        }
×
207

208
        bool websocket_ping_message_received(const char*, size_t) override
209
        {
×
210
            return false;
×
211
        }
×
212

213
        bool websocket_pong_message_received(const char*, size_t) override
214
        {
×
215
            return false;
×
216
        }
×
217

218
        std::mt19937_64 random;
219
        const std::shared_ptr<util::Logger> logger;
220
        BufferedSocket socket;
221
        HTTPServer<BufferedSocket> http_server;
222
        std::optional<websocket::Socket> websocket;
223
    };
224

225
    void send_simple_response(util::bind_ptr<Conn> conn, HTTPStatus status, std::string reason,
226
                              std::optional<std::string> body)
NEW
227
    {
×
NEW
228
        send_http_response(conn, status, std::move(reason), {}, std::move(body));
×
NEW
229
    }
×
230

231
    void send_http_response(util::bind_ptr<Conn> conn, HTTPStatus status, std::string reason, HTTPHeaders headers,
232
                            std::optional<std::string> body)
233
    {
298✔
234
        m_logger->debug("sending http response %1: %2 '%3'", status, reason, body.value_or(""));
298✔
235
        HTTPResponse resp{status, std::move(reason), std::move(headers), std::move(body)};
298✔
236
        conn->http_server.async_send_response(resp, [this, conn](std::error_code ec) {
298✔
237
            if (ec && ec != util::error::operation_aborted) {
298!
NEW
238
                m_logger->warn("Error sending response: [%1]: %2", ec, ec.message());
×
NEW
239
                if (m_hook)
×
NEW
240
                    m_hook(Event::error, ec.message());
×
UNCOV
241
            }
×
242
        });
298✔
243
    }
298✔
244

245
    void do_websocket_redirect(util::bind_ptr<Conn> conn, const HTTPRequest& req)
246
    {
3✔
247
        auto protocol_it = req.headers.find("Sec-WebSocket-Protocol");
3✔
248
        REALM_ASSERT(protocol_it != req.headers.end());
3✔
249
        auto protocols = protocol_it->second;
3✔
250

251
        auto first_comma = protocols.find(',');
3✔
252
        std::string protocol;
3✔
253
        if (first_comma == std::string::npos) {
3✔
254
            protocol = protocols;
×
255
        }
×
256
        else {
3✔
257
            protocol = protocols.substr(0, first_comma);
3✔
258
        }
3✔
259
        std::error_code ec;
3✔
260
        auto maybe_resp = websocket::make_http_response(req, protocol, ec);
3✔
261
        REALM_ASSERT(maybe_resp);
3✔
262
        REALM_ASSERT(!ec);
3✔
263
        conn->http_server.async_send_response(*maybe_resp, [this, conn](std::error_code ec) {
3✔
264
            if (ec) {
3✔
265
                if (ec != util::error::operation_aborted) {
×
NEW
266
                    m_logger->warn("Error sending websocket HTTP upgrade response: [%1]: %2", ec, ec.message());
×
NEW
267
                    if (m_hook)
×
NEW
268
                        m_hook(Event::error, ec.message());
×
269
                }
×
270
                return;
×
271
            }
×
272

273
            conn->websocket.emplace(*conn);
3✔
274
            conn->websocket->initiate_server_websocket_after_handshake();
3✔
275

276
            static const std::string_view msg("\x0f\xa3Permanently moved");
3✔
277
            conn->websocket->async_write_close(msg.data(), msg.size(), [this, conn](std::error_code, size_t) {
3✔
278
                conn->logger->debug("Sent close frame with move code");
3✔
279
                conn->websocket.reset();
3✔
280
                if (m_hook)
3✔
281
                    m_hook(Event::ws_redirect, std::nullopt);
2✔
282
            });
3✔
283
        });
3✔
284
    }
3✔
285

286
    void do_accept()
287
    {
306✔
288
        auto conn = util::make_bind<Conn>(m_service, m_logger);
306✔
289
        m_acceptor.async_accept(conn->socket, [this, conn](std::error_code ec) {
306✔
290
            if (ec == util::error::operation_aborted) {
306✔
291
                return;
5✔
292
            }
5✔
293
            // Allow additonal connections to be accepted
294
            do_accept();
301✔
295
            if (ec) {
301✔
NEW
296
                m_logger->error("Error accepting new connection to %1 [%2]: %3", base_url(), ec, ec.message());
×
297
                return;
×
298
            }
×
299

300
            conn->http_server.async_receive_request([this, conn](HTTPRequest req, std::error_code ec) {
301✔
301
                if (ec) {
301✔
302
                    if (ec != util::error::operation_aborted) {
×
NEW
303
                        m_logger->error("Error receiving HTTP request to redirect [%1]: %2", ec, ec.message());
×
304
                    }
×
305
                    return;
×
306
                }
×
307

308
                m_logger->debug("Received request: %1", req.path);
301✔
309

310
                if (req.path.find("/location") != std::string::npos) {
301✔
311
                    nlohmann::json body{
280✔
312
                        {"deployment_model", "GLOBAL"},
280✔
313
                        {"location", "US-VA"},
280✔
314
                        {"hostname", m_http_redirect ? m_base_url : m_redirect_to_base_url},
280✔
315
                        {"ws_hostname", m_websocket_redirect ? m_base_wsurl : m_redirect_to_base_wsurl}};
280✔
316
                    auto body_str = body.dump();
280✔
317

318
                    send_http_response(conn, HTTPStatus::Ok, "Okay", {{"Content-Type", "application/json"}},
280✔
319
                                       std::move(body_str));
280✔
320
                    if (m_hook)
280✔
321
                        m_hook(Event::location, std::nullopt);
14✔
322
                    return;
280✔
323
                }
280✔
324

325
                if (req.path.find("/realm-sync") != std::string::npos) {
21✔
326
                    do_websocket_redirect(conn, req);
3✔
327
                    return;
3✔
328
                }
3✔
329

330
                // Send redirect response for appservices calls
331
                // Starts with 'http' and contains api path
332
                if (req.path.find("/api/client/v2.0/") == 0) {
18✔
333
                    // Alternate sending 301 and 308 redirect status codes
334
                    auto status = m_use_301 ? HTTPStatus::MovedPermanently : HTTPStatus::PermanentRedirect;
18✔
335
                    auto reason = m_use_301 ? "Moved Permanently" : "Permanent Redirect";
18✔
336
                    m_use_301 = !m_use_301;
18✔
337
                    auto location = m_redirect_to_base_url + req.path;
18✔
338
                    send_http_response(conn, status, reason, {{"location", location}}, std::nullopt);
18✔
339
                    if (m_hook)
18✔
340
                        m_hook(Event::redirect, std::nullopt);
18✔
341
                    return;
18✔
342
                }
18✔
343

NEW
344
                send_simple_response(conn, HTTPStatus::NotFound, "Not found",
×
NEW
345
                                     util::format("Not found: %1", req.path));
×
UNCOV
346
            });
×
347
        });
301✔
348
    }
306✔
349

350
    std::string make_wsurl(std::string base_url)
351
    {
10✔
352
        if (base_url.find("http") == 0) {
10✔
353
            // Replace the first 4 ('http') characters with 'ws', so we get 'ws://' or 'wss://'
354
            return base_url.replace(0, 4, "ws");
10✔
355
        }
10✔
NEW
356
        else {
×
357
            // If no scheme, return the original base_url
NEW
358
            return base_url;
×
NEW
359
        }
×
360
    }
10✔
361

362
    const std::string m_redirect_to_base_url;
363
    const std::string m_redirect_to_base_wsurl;
364
    const std::shared_ptr<util::Logger> m_logger;
365

366
    bool m_http_redirect = false;
367
    bool m_websocket_redirect = false;
368
    std::string m_base_url;
369
    std::string m_base_wsurl;
370
    std::function<void(Event, std::optional<std::string>)> m_hook;
371
    bool m_use_301 = true;
372

373
    network::Service m_service;
374
    network::Acceptor m_acceptor;
375
    std::thread m_server_thread;
376
};
377

378
} // namespace realm::sync
379

380
#endif
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