Skip to content
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

Add support for HTTP redirecti in ASIO-based http_client #1328

Merged
merged 15 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Release/include/cpprest/http_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class http_client_config
#if (defined(_WIN32) && !defined(__cplusplus_winrt)) || defined(CPPREST_FORCE_HTTP_CLIENT_WINHTTPPAL)
, m_buffer_request(false)
#endif
, m_max_redirects(10)
, m_https_to_http_redirects(false)
{
}

Expand Down Expand Up @@ -279,6 +281,38 @@ class http_client_config
void set_buffer_request(bool buffer_request) { m_buffer_request = buffer_request; }
#endif

/// <summary>
/// Get the maximum number of redirects to follow automatically.
/// A value of 0 indicates that no automatic redirection is performed.
/// </summary>
/// <returns>The maximum number of redirects to follow automatically.</returns>
/// <remarks>This is a hint -- an implementation may enforce a lower value.</remarks>
size_t max_redirects() const { return m_max_redirects; }
garethsb marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Set the maximum number of redirects to follow automatically.
/// A value of 0 indicates that no automatic redirection is performed.
/// </summary>
/// <param name="max_redirects">The maximum number of redirects to follow automatically.</param>
/// <remarks>This is a hint -- an implementation may enforce a lower value.</remarks>
void set_max_redirects(size_t max_redirects) { m_max_redirects = max_redirects; }

/// <summary>
/// Checks if HTTPS to HTTP redirects are automatically followed.
/// </summary>
/// <returns>True if HTTPS to HTTP redirects are automatically followed, false otherwise.</returns>
bool https_to_http_redirects() const { return m_https_to_http_redirects; }

/// <summary>
/// Sets if HTTPS to HTTP redirects are automatically followed.
/// </summary>
/// <param name="https_to_http_redirects">True if HTTPS to HTTP redirects are to be automatically
/// followed, false otherwise.</param>
void set_https_to_http_redirects(bool https_to_http_redirects)
{
m_https_to_http_redirects = https_to_http_redirects;
}

/// <summary>
/// Sets a callback to enable custom setting of platform specific options.
/// </summary>
Expand Down Expand Up @@ -392,6 +426,9 @@ class http_client_config
#if (defined(_WIN32) && !defined(__cplusplus_winrt)) || defined(CPPREST_FORCE_HTTP_CLIENT_WINHTTPPAL)
bool m_buffer_request;
#endif

size_t m_max_redirects;
bool m_https_to_http_redirects;
};

class http_pipeline;
Expand Down
160 changes: 159 additions & 1 deletion Release/src/http/client/http_client_asio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1976,6 +1976,162 @@ void asio_client::send_request(const std::shared_ptr<request_context>& request_c
ctx->start_request();
}

static bool is_retrieval_redirection(status_code code)
{
// See https://tools.ietf.org/html/rfc7231#section-6.4

switch (code)
{
case status_codes::MovedPermanently:
// "For historical reasons, a user agent MAY change the request method
// from POST to GET for the subsequent request."
return true;
case status_codes::Found:
// "For historical reasons, a user agent MAY change the request method
// from POST to GET for the subsequent request."
return true;
case status_codes::SeeOther:
// "A user agent can perform a [GET or HEAD] request. It is primarily
// used to allow the output of a POST action to redirect the user agent
// to a selected resource."
return true;
default:
return false;
}
}

static bool is_unchanged_redirection(status_code code)
{
// See https://tools.ietf.org/html/rfc7231#section-6.4
// and https://tools.ietf.org/html/rfc7538#section-3

switch (code)
{
case status_codes::TemporaryRedirect:
// "The user agent MUST NOT change the request method if it performs an
// automatic redirection to that URI."
return true;
case status_codes::PermanentRedirect:
// This status code "does not allow changing the request method from POST
// to GET."
return true;
default:
return false;
}
}

static bool is_recognized_redirection(status_code code)
{
// other 3xx status codes, e.g. 300 Multiple Choices, are not handled
// and should be handled externally
return is_retrieval_redirection(code) || is_unchanged_redirection(code);
}

static bool is_retrieval_request(method method)
{
return methods::GET == method || methods::HEAD == method;
}

static const std::vector<utility::string_t> request_body_header_names =
{
header_names::content_encoding,
header_names::content_language,
header_names::content_length,
header_names::content_location,
header_names::content_type
};

// A request continuation that follows redirects according to the specified configuration.
// This implementation only supports retrieval redirects, as it cannot redirect e.g. a POST request
// using the same method since the request body may have been consumed.
struct http_redirect_follower
{
http_client_config config;
std::vector<uri> followed_urls;
http_request redirect;

http_redirect_follower(http_client_config config, const http_request& request);

uri url_to_follow(const http_response& response) const;

pplx::task<http_response> operator()(http_response response);
};

http_redirect_follower::http_redirect_follower(http_client_config config, const http_request& request)
: config(std::move(config))
, followed_urls(1, request.absolute_uri())
, redirect(request.method())
{
// Stash the original request URL, etc. to be prepared for an automatic redirect

// Basically, it makes sense to send the redirects with the same headers as the original request
redirect.headers() = request.headers();
// However, this implementation only supports retrieval redirects, with no body, so Content-* headers
// should be removed
for (const auto& content_header : request_body_header_names)
{
redirect.headers().remove(content_header);
}

redirect._set_cancellation_token(request._cancellation_token());
}

uri http_redirect_follower::url_to_follow(const http_response& response) const
{
// Return immediately if the response is not a supported redirection
garethsb marked this conversation as resolved.
Show resolved Hide resolved
if (!is_recognized_redirection(response.status_code()))
return{};

// Although not required by RFC 7231, config may limit the number of automatic redirects
// (followed_urls includes the initial request URL, hence '<' here)
if (config.max_redirects() < followed_urls.size())
return{};

// Can't very well automatically redirect if the server hasn't provided a Location
const auto location = response.headers().find(header_names::location);
if (response.headers().end() == location)
return{};

uri to_follow(followed_urls.back().resolve_uri(location->second));

// Config may prohibit automatic redirects from HTTPS to HTTP
if (!config.https_to_http_redirects() && followed_urls.back().scheme() == _XPLATSTR("https")
&& to_follow.scheme() != _XPLATSTR("https"))
return{};

// "A client SHOULD detect and intervene in cyclical redirections."
if (followed_urls.end() != std::find(followed_urls.begin(), followed_urls.end(), to_follow))
return{};

return to_follow;
}

pplx::task<http_response> http_redirect_follower::operator()(http_response response)
{
// Return immediately if the response doesn't indicate a valid automatic redirect
uri to_follow = url_to_follow(response);
if (to_follow.is_empty())
return pplx::task_from_result(response);

// This implementation only supports retrieval redirects, as it cannot redirect e.g. a POST request
// using the same method since the request body may have been consumed.
if (!is_retrieval_request(redirect.method()) && !is_retrieval_redirection(response.status_code()))
return pplx::task_from_result(response);

if (!is_retrieval_request(redirect.method()))
redirect.set_method(methods::GET);

// If the reply to this request is also a redirect, we want visibility of that
auto config_no_redirects = config;
config_no_redirects.set_max_redirects(0);
http_client client(to_follow, config_no_redirects);

// Stash the redirect request URL and make the request with the same continuation
auto request_task = client.request(redirect, redirect._cancellation_token());
followed_urls.push_back(std::move(to_follow));
return request_task.then(std::move(*this));
}

pplx::task<http_response> asio_client::propagate(http_request request)
{
auto self = std::static_pointer_cast<_http_client_communicator>(shared_from_this());
Expand All @@ -1995,7 +2151,9 @@ pplx::task<http_response> asio_client::propagate(http_request request)
// Asynchronously send the response with the HTTP client implementation.
this->async_send_request(context);

return result_task;
return client_config().max_redirects() > 0
? result_task.then(http_redirect_follower(client_config(), request))
: result_task;
}
} // namespace details
} // namespace client
Expand Down
Loading