-
Notifications
You must be signed in to change notification settings - Fork 22
RCPP-89 Add 301/308 redirection support for HTTP transport requests #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,8 +105,8 @@ namespace realm::networking { | |
| // Interface for providing http transport | ||
| struct http_transport_client { | ||
| virtual ~http_transport_client() = default; | ||
| virtual void send_request_to_server(const request& request, | ||
| std::function<void(const response&)>&& completion) = 0; | ||
| virtual void send_request_to_server(::realm::networking::request &&request, | ||
| std::function<void(const response &)> &&completion) = 0; | ||
| }; | ||
|
|
||
| /// Produces a http transport client from the factory. | ||
|
|
@@ -115,7 +115,7 @@ namespace realm::networking { | |
| [[maybe_unused]] void set_http_client_factory(std::function<std::shared_ptr<http_transport_client>()>&&); | ||
|
|
||
| /// Built in HTTP transport client. | ||
| struct default_http_transport : public http_transport_client { | ||
| struct default_http_transport : public http_transport_client, public std::enable_shared_from_this<default_http_transport> { | ||
|
||
| struct configuration { | ||
| /** | ||
| * Extra HTTP headers to be set on each request to Atlas Device Sync when using the internal HTTP client. | ||
|
|
@@ -143,17 +143,30 @@ namespace realm::networking { | |
| * is not set. | ||
| */ | ||
| std::function<SSLVerifyCallback> ssl_verify_callback; | ||
|
|
||
| /** | ||
| * Maximum number of subsequent redirect responses from the server to prevent getting stuck | ||
| * in a redirect loop indefinitely. Set to 0 to disable redirect support or -1 to allow | ||
| * redirecting indefinitely. | ||
| */ | ||
| int max_redirect_count = 30; | ||
| }; | ||
|
|
||
| default_http_transport() = default; | ||
| default_http_transport(const configuration& c) : m_configuration(c) {} | ||
|
|
||
| ~default_http_transport() = default; | ||
|
|
||
| void send_request_to_server(const ::realm::networking::request& request, | ||
| std::function<void(const ::realm::networking::response&)>&& completion); | ||
| void send_request_to_server(::realm::networking::request &&request, | ||
| std::function<void(const ::realm::networking::response &)> &&completion) { | ||
| send_request_to_server(std::move(request), std::move(completion), 0); | ||
| } | ||
|
|
||
| protected: | ||
| void send_request_to_server(::realm::networking::request &&request, | ||
| std::function<void(const ::realm::networking::response &)> &&completion, | ||
| int redirect_count); | ||
|
|
||
| configuration m_configuration; | ||
| }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,8 @@ | |
| #include "realm/util/base64.hpp" | ||
| #include "realm/util/uri.hpp" | ||
|
|
||
| #include <algorithm> | ||
| #include <cctype> | ||
| #include <regex> | ||
|
|
||
| namespace realm::networking { | ||
|
|
@@ -124,8 +126,9 @@ namespace realm::networking { | |
| } | ||
| } | ||
|
|
||
| void default_http_transport::send_request_to_server(const ::realm::networking::request& request, | ||
| std::function<void(const ::realm::networking::response&)>&& completion_block) { | ||
| void default_http_transport::send_request_to_server(::realm::networking::request &&request, | ||
| std::function<void(const ::realm::networking::response &)> &&completion_block, | ||
| int redirect_count) { | ||
| const auto uri = realm::util::Uri(request.url); | ||
| std::string userinfo, host, port; | ||
| uri.get_auth(userinfo, host, port); | ||
|
|
@@ -170,7 +173,7 @@ namespace realm::networking { | |
| auto address = realm::sync::network::make_address(host, e); | ||
| ep = realm::sync::network::Endpoint(address, stoi(port)); | ||
| } else { | ||
| auto resolved = resolver.resolve(sync::network::Resolver::Query(host, is_localhost ? "9090" : "443")); | ||
| auto resolved = resolver.resolve(sync::network::Resolver::Query(host, port)); | ||
| ep = *resolved.begin(); | ||
| } | ||
| } | ||
|
|
@@ -243,45 +246,45 @@ namespace realm::networking { | |
| socket.ssl_stream->set_logger(logger.get()); | ||
| } | ||
|
|
||
| realm::sync::HTTPHeaders headers; | ||
| realm::sync::HTTPClient<DefaultSocket> m_http_client = realm::sync::HTTPClient<DefaultSocket>(socket, logger); | ||
|
||
| auto convert_method = [](::realm::networking::http_method method) { | ||
| switch (method) { | ||
| case ::realm::networking::http_method::get: | ||
| return realm::sync::HTTPMethod::Get; | ||
| case ::realm::networking::http_method::put: | ||
| return realm::sync::HTTPMethod::Put; | ||
| case ::realm::networking::http_method::post: | ||
| return realm::sync::HTTPMethod::Post; | ||
| case ::realm::networking::http_method::patch: | ||
| return realm::sync::HTTPMethod::Patch; | ||
| case ::realm::networking::http_method::del: | ||
| return realm::sync::HTTPMethod::Delete; | ||
| default: | ||
| REALM_UNREACHABLE(); | ||
| } | ||
| }; | ||
|
|
||
| realm::sync::HTTPRequest http_req{ | ||
| convert_method(request.method), | ||
| {}, | ||
| request.url, | ||
| request.body.empty() ? std::nullopt : std::make_optional<std::string>(request.body)}; | ||
| for (auto& [k, v] : request.headers) { | ||
| headers[k] = v; | ||
| http_req.headers[k] = v; | ||
| } | ||
| headers["Host"] = host; | ||
| headers["User-Agent"] = "Realm C++ SDK"; | ||
| http_req.headers["Host"] = host; | ||
| http_req.headers["User-Agent"] = "Realm C++ SDK"; | ||
|
|
||
| if (!request.body.empty()) { | ||
| headers["Content-Length"] = util::to_string(request.body.size()); | ||
| http_req.headers["Content-Length"] = util::to_string(request.body.size()); | ||
| } | ||
|
|
||
| if (m_configuration.custom_http_headers) { | ||
| for (auto& header : *m_configuration.custom_http_headers) { | ||
| headers.emplace(header); | ||
| http_req.headers.emplace(header); | ||
| } | ||
| } | ||
|
|
||
| realm::sync::HTTPClient<DefaultSocket> m_http_client = realm::sync::HTTPClient<DefaultSocket>(socket, logger); | ||
| realm::sync::HTTPMethod method; | ||
| switch (request.method) { | ||
| case ::realm::networking::http_method::get: | ||
| method = realm::sync::HTTPMethod::Get; | ||
| break; | ||
| case ::realm::networking::http_method::put: | ||
| method = realm::sync::HTTPMethod::Put; | ||
| break; | ||
| case ::realm::networking::http_method::post: | ||
| method = realm::sync::HTTPMethod::Post; | ||
| break; | ||
| case ::realm::networking::http_method::patch: | ||
| method = realm::sync::HTTPMethod::Patch; | ||
| break; | ||
| case ::realm::networking::http_method::del: | ||
| method = realm::sync::HTTPMethod::Delete; | ||
| break; | ||
| default: | ||
| REALM_UNREACHABLE(); | ||
| } | ||
|
|
||
| /* | ||
| * Flow of events: | ||
| * 1. hostname is resolved from DNS | ||
|
|
@@ -295,26 +298,59 @@ namespace realm::networking { | |
| service.post([&](realm::Status&&){ | ||
| auto handler = [&](std::error_code ec) { | ||
| if (ec.value() == 0) { | ||
| realm::sync::HTTPRequest req; | ||
| req.method = method; | ||
| req.headers = headers; | ||
| req.path = request.url; | ||
| req.body = request.body.empty() ? std::nullopt : std::optional<std::string>(request.body); | ||
|
|
||
| m_http_client.async_request(std::move(req), [cb = std::move(completion_block)](const realm::sync::HTTPResponse& r, const std::error_code&) { | ||
| ::realm::networking::response res; | ||
| res.body = r.body ? *r.body : ""; | ||
| for (auto& [k, v] : r.headers) { | ||
| res.headers[k] = v; | ||
| } | ||
| res.http_status_code = static_cast<int>(r.status); | ||
| res.custom_status_code = 0; | ||
| cb(res); | ||
| }); | ||
| // Pass along the original request so it can be resent to the redirected location URL if needed | ||
| m_http_client.async_request(std::move(http_req), | ||
| [self = weak_from_this(), orig_request = std::move(request), cb = std::move(completion_block), redirect_count](const realm::sync::HTTPResponse &resp, const std::error_code &ec) mutable { | ||
|
||
| constexpr std::string_view location_header = "location"; | ||
| auto transport = self.lock(); | ||
|
||
| // If an error occurred or the transport has gone away, then send "operation aborted" to callback | ||
| if (ec || !transport) { | ||
| cb({0, util::error::operation_aborted, {}, {}, std::nullopt}); | ||
| return; | ||
| } | ||
| // Was a redirect response (301 or 308) received? | ||
| if (resp.status == realm::sync::HTTPStatus::PermanentRedirect || resp.status == realm::sync::HTTPStatus::MovedPermanently) { | ||
| auto max_redirects = transport->m_configuration.max_redirect_count; | ||
| // Are redirects still allowed to continue? | ||
| if (max_redirects < 0 || ++redirect_count < max_redirects) { | ||
| // A possible future enhancement could be to cache the redirect URLs to prevent having | ||
| // to perform redirections every time. | ||
| std::string redirect_url; | ||
| // Grab the new location from the 'Location' header and retry the request | ||
| for (auto &[key, value]: resp.headers) { | ||
|
||
| if (key.size() == location_header.size() && | ||
| std::equal(key.begin(), key.end(), location_header.begin(), location_header.end(), [](char a, char b) { | ||
| return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b)); | ||
| })) { | ||
| // If the redirect URL path returned from the server was not empty, save it and exit the loop | ||
| if (!value.empty()) { | ||
| redirect_url = value; | ||
| break; | ||
| } | ||
| // Otherwise, keep looking, in case there's another 'location' entry | ||
| } | ||
| } | ||
| // If the redirect URL path wasn't found in headers, then send the response to the client... | ||
| if (!redirect_url.empty()) { | ||
| // Otherwise, resend the original request to the new redirect URL path | ||
| // Perform the entire operation again, since the remote host is likely changing and | ||
| // a newe socket will need to be opened, | ||
| orig_request.url = redirect_url; | ||
|
||
| return transport->send_request_to_server(std::move(orig_request), std::move(cb), redirect_count); | ||
| } | ||
| } | ||
| // If redirects disabled, max redirect reached or location was missing from response, then pass the | ||
| // redirect response to the callback function | ||
| } | ||
| ::realm::networking::response res{static_cast<int>(resp.status), 0, {}, resp.body ? std::move(*resp.body) : "", std::nullopt}; | ||
| // Copy over all the headers | ||
| for (auto &[k, v]: resp.headers) { | ||
|
||
| res.headers[k] = v; | ||
| } | ||
| cb(res); | ||
| }); | ||
| } else { | ||
| ::realm::networking::response response; | ||
| response.custom_status_code = util::error::operation_aborted; | ||
| completion_block(std::move(response)); | ||
| completion_block({0, util::error::operation_aborted, {}, {}, std::nullopt}); | ||
| return; | ||
| } | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
git clang-formatformatted these files differently than what is currently configured for realm-core. The.clang-formatsays it was based off the CLion configuration.