Skip to content

Commit f4124a9

Browse files
authored
Add support for HTTP redirection in ASIO and WinHTTP-based http_clients (microsoft#1328)
1 parent 1d7550f commit f4124a9

File tree

8 files changed

+691
-28
lines changed

8 files changed

+691
-28
lines changed

Release/include/cpprest/http_client.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ class http_client_config
107107
#if (defined(_WIN32) && !defined(__cplusplus_winrt)) || defined(CPPREST_FORCE_HTTP_CLIENT_WINHTTPPAL)
108108
, m_buffer_request(false)
109109
#endif
110+
, m_max_redirects(10)
111+
, m_https_to_http_redirects(false)
110112
{
111113
}
112114

@@ -279,6 +281,38 @@ class http_client_config
279281
void set_buffer_request(bool buffer_request) { m_buffer_request = buffer_request; }
280282
#endif
281283

284+
/// <summary>
285+
/// Get the maximum number of redirects to follow automatically.
286+
/// A value of 0 indicates that no automatic redirection is performed.
287+
/// </summary>
288+
/// <returns>The maximum number of redirects to follow automatically.</returns>
289+
/// <remarks>This is a hint -- an implementation may enforce a lower value.</remarks>
290+
size_t max_redirects() const { return m_max_redirects; }
291+
292+
/// <summary>
293+
/// Set the maximum number of redirects to follow automatically.
294+
/// A value of 0 indicates that no automatic redirection is performed.
295+
/// </summary>
296+
/// <param name="max_redirects">The maximum number of redirects to follow automatically.</param>
297+
/// <remarks>This is a hint -- an implementation may enforce a lower value.</remarks>
298+
void set_max_redirects(size_t max_redirects) { m_max_redirects = max_redirects; }
299+
300+
/// <summary>
301+
/// Checks if HTTPS to HTTP redirects are automatically followed.
302+
/// </summary>
303+
/// <returns>True if HTTPS to HTTP redirects are automatically followed, false otherwise.</returns>
304+
bool https_to_http_redirects() const { return m_https_to_http_redirects; }
305+
306+
/// <summary>
307+
/// Sets if HTTPS to HTTP redirects are automatically followed.
308+
/// </summary>
309+
/// <param name="https_to_http_redirects">True if HTTPS to HTTP redirects are to be automatically
310+
/// followed, false otherwise.</param>
311+
void set_https_to_http_redirects(bool https_to_http_redirects)
312+
{
313+
m_https_to_http_redirects = https_to_http_redirects;
314+
}
315+
282316
/// <summary>
283317
/// Sets a callback to enable custom setting of platform specific options.
284318
/// </summary>
@@ -392,6 +426,9 @@ class http_client_config
392426
#if (defined(_WIN32) && !defined(__cplusplus_winrt)) || defined(CPPREST_FORCE_HTTP_CLIENT_WINHTTPPAL)
393427
bool m_buffer_request;
394428
#endif
429+
430+
size_t m_max_redirects;
431+
bool m_https_to_http_redirects;
395432
};
396433

397434
class http_pipeline;

Release/src/http/client/http_client_asio.cpp

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1976,6 +1976,162 @@ void asio_client::send_request(const std::shared_ptr<request_context>& request_c
19761976
ctx->start_request();
19771977
}
19781978

1979+
static bool is_retrieval_redirection(status_code code)
1980+
{
1981+
// See https://tools.ietf.org/html/rfc7231#section-6.4
1982+
1983+
switch (code)
1984+
{
1985+
case status_codes::MovedPermanently:
1986+
// "For historical reasons, a user agent MAY change the request method
1987+
// from POST to GET for the subsequent request."
1988+
return true;
1989+
case status_codes::Found:
1990+
// "For historical reasons, a user agent MAY change the request method
1991+
// from POST to GET for the subsequent request."
1992+
return true;
1993+
case status_codes::SeeOther:
1994+
// "A user agent can perform a [GET or HEAD] request. It is primarily
1995+
// used to allow the output of a POST action to redirect the user agent
1996+
// to a selected resource."
1997+
return true;
1998+
default:
1999+
return false;
2000+
}
2001+
}
2002+
2003+
static bool is_unchanged_redirection(status_code code)
2004+
{
2005+
// See https://tools.ietf.org/html/rfc7231#section-6.4
2006+
// and https://tools.ietf.org/html/rfc7538#section-3
2007+
2008+
switch (code)
2009+
{
2010+
case status_codes::TemporaryRedirect:
2011+
// "The user agent MUST NOT change the request method if it performs an
2012+
// automatic redirection to that URI."
2013+
return true;
2014+
case status_codes::PermanentRedirect:
2015+
// This status code "does not allow changing the request method from POST
2016+
// to GET."
2017+
return true;
2018+
default:
2019+
return false;
2020+
}
2021+
}
2022+
2023+
static bool is_recognized_redirection(status_code code)
2024+
{
2025+
// other 3xx status codes, e.g. 300 Multiple Choices, are not handled
2026+
// and should be handled externally
2027+
return is_retrieval_redirection(code) || is_unchanged_redirection(code);
2028+
}
2029+
2030+
static bool is_retrieval_request(method method)
2031+
{
2032+
return methods::GET == method || methods::HEAD == method;
2033+
}
2034+
2035+
static const std::vector<utility::string_t> request_body_header_names =
2036+
{
2037+
header_names::content_encoding,
2038+
header_names::content_language,
2039+
header_names::content_length,
2040+
header_names::content_location,
2041+
header_names::content_type
2042+
};
2043+
2044+
// A request continuation that follows redirects according to the specified configuration.
2045+
// This implementation only supports retrieval redirects, as it cannot redirect e.g. a POST request
2046+
// using the same method since the request body may have been consumed.
2047+
struct http_redirect_follower
2048+
{
2049+
http_client_config config;
2050+
std::vector<uri> followed_urls;
2051+
http_request redirect;
2052+
2053+
http_redirect_follower(http_client_config config, const http_request& request);
2054+
2055+
uri url_to_follow(const http_response& response) const;
2056+
2057+
pplx::task<http_response> operator()(http_response response);
2058+
};
2059+
2060+
http_redirect_follower::http_redirect_follower(http_client_config config, const http_request& request)
2061+
: config(std::move(config))
2062+
, followed_urls(1, request.absolute_uri())
2063+
, redirect(request.method())
2064+
{
2065+
// Stash the original request URL, etc. to be prepared for an automatic redirect
2066+
2067+
// Basically, it makes sense to send the redirects with the same headers as the original request
2068+
redirect.headers() = request.headers();
2069+
// However, this implementation only supports retrieval redirects, with no body, so Content-* headers
2070+
// should be removed
2071+
for (const auto& content_header : request_body_header_names)
2072+
{
2073+
redirect.headers().remove(content_header);
2074+
}
2075+
2076+
redirect._set_cancellation_token(request._cancellation_token());
2077+
}
2078+
2079+
uri http_redirect_follower::url_to_follow(const http_response& response) const
2080+
{
2081+
// Return immediately if the response is not a supported redirection
2082+
if (!is_recognized_redirection(response.status_code()))
2083+
return{};
2084+
2085+
// Although not required by RFC 7231, config may limit the number of automatic redirects
2086+
// (followed_urls includes the initial request URL, hence '<' here)
2087+
if (config.max_redirects() < followed_urls.size())
2088+
return{};
2089+
2090+
// Can't very well automatically redirect if the server hasn't provided a Location
2091+
const auto location = response.headers().find(header_names::location);
2092+
if (response.headers().end() == location)
2093+
return{};
2094+
2095+
uri to_follow(followed_urls.back().resolve_uri(location->second));
2096+
2097+
// Config may prohibit automatic redirects from HTTPS to HTTP
2098+
if (!config.https_to_http_redirects() && followed_urls.back().scheme() == _XPLATSTR("https")
2099+
&& to_follow.scheme() != _XPLATSTR("https"))
2100+
return{};
2101+
2102+
// "A client SHOULD detect and intervene in cyclical redirections."
2103+
if (followed_urls.end() != std::find(followed_urls.begin(), followed_urls.end(), to_follow))
2104+
return{};
2105+
2106+
return to_follow;
2107+
}
2108+
2109+
pplx::task<http_response> http_redirect_follower::operator()(http_response response)
2110+
{
2111+
// Return immediately if the response doesn't indicate a valid automatic redirect
2112+
uri to_follow = url_to_follow(response);
2113+
if (to_follow.is_empty())
2114+
return pplx::task_from_result(response);
2115+
2116+
// This implementation only supports retrieval redirects, as it cannot redirect e.g. a POST request
2117+
// using the same method since the request body may have been consumed.
2118+
if (!is_retrieval_request(redirect.method()) && !is_retrieval_redirection(response.status_code()))
2119+
return pplx::task_from_result(response);
2120+
2121+
if (!is_retrieval_request(redirect.method()))
2122+
redirect.set_method(methods::GET);
2123+
2124+
// If the reply to this request is also a redirect, we want visibility of that
2125+
auto config_no_redirects = config;
2126+
config_no_redirects.set_max_redirects(0);
2127+
http_client client(to_follow, config_no_redirects);
2128+
2129+
// Stash the redirect request URL and make the request with the same continuation
2130+
auto request_task = client.request(redirect, redirect._cancellation_token());
2131+
followed_urls.push_back(std::move(to_follow));
2132+
return request_task.then(std::move(*this));
2133+
}
2134+
19792135
pplx::task<http_response> asio_client::propagate(http_request request)
19802136
{
19812137
auto self = std::static_pointer_cast<_http_client_communicator>(shared_from_this());
@@ -1995,7 +2151,9 @@ pplx::task<http_response> asio_client::propagate(http_request request)
19952151
// Asynchronously send the response with the HTTP client implementation.
19962152
this->async_send_request(context);
19972153

1998-
return result_task;
2154+
return client_config().max_redirects() > 0
2155+
? result_task.then(http_redirect_follower(client_config(), request))
2156+
: result_task;
19992157
}
20002158
} // namespace details
20012159
} // namespace client

0 commit comments

Comments
 (0)