Skip to content

Commit

Permalink
Start paying attention to some of the Options.RedirectionOptions prop…
Browse files Browse the repository at this point in the history
…erties.
  • Loading branch information
rassilon authored and Sreeshail1 committed Nov 2, 2023
1 parent f27574e commit 90f9053
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 33 deletions.
1 change: 1 addition & 0 deletions src/RestSharp/KnownHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static class KnownHeaders {
public const string Cookie = "Cookie";
public const string SetCookie = "Set-Cookie";
public const string UserAgent = "User-Agent";
public const string TransferEncoding = "Transfer-Encoding";

internal static readonly string[] ContentHeaders = {
Allow, Expires, ContentDisposition, ContentEncoding, ContentLanguage, ContentLength, ContentLocation, ContentRange, ContentType, ContentMD5,
Expand Down
81 changes: 80 additions & 1 deletion src/RestSharp/Options/RestClientRedirectionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,98 @@

namespace RestSharp;

/// <summary>
/// Options related to redirect processing.
/// </summary>
[GenerateImmutable]
public class RestClientRedirectionOptions {
static readonly Version Version = new AssemblyName(typeof(RestClientOptions).Assembly.FullName!).Version!;

/// <summary>
/// Set to true (default), when you want to follow redirects
/// </summary>
public bool FollowRedirects { get; set; } = true;

/// <summary>
/// Set to true (default is false), when you want to follow a
/// redirect from HTTPS to HTTP.
/// </summary>
public bool FollowRedirectsToInsecure { get; set; } = false;
/// <summary>
/// Set to true (default), when you want to include the originally
/// requested headers in redirected requests.
/// </summary>
public bool ForwardHeaders { get; set; } = true;

/// <summary>
/// Set to true (default is false), when you want to send the original
/// Authorization header to the redirected destination.
/// </summary>
public bool ForwardAuthorization { get; set; } = false;
/// <summary>
/// Set to true (default), when you want to include cookie3s from the
/// CookieContainer on the redirected URL.
/// </summary>
/// <remarks>
/// NOTE: The exact cookies sent to the redirected url DEPENDS directly
/// on the redirected url. A redirection to a completly differnet FQDN
/// for example is unlikely to actually propagate any cookies from the
/// CookieContqainer.
/// </remarks>
public bool ForwardCookies { get; set; } = true;

/// <summary>
/// Set to true (default) in order to send the body to the
/// redirected URL, unless the force verb to GET behavior is triggered.
/// <see cref="ForceForwardBody"/>
/// </summary>
public bool ForwardBody { get; set; } = true;

/// <summary>
/// Set to true (default is false) to force forwarding the body of the
/// request even when normally, the verb might be altered to GET based
/// on backward compatiblity with browser processing of HTTP status codes.
/// </summary>
/// <remarks>
/// Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
/// <pre>
/// Many web browsers implemented this code in a manner that violated this standard, changing
/// the request type of the new request to GET, regardless of the type employed in the original request
/// (e.g. POST). For this reason, HTTP/1.1 (RFC 2616) added the new status codes 303 and 307 to disambiguate
/// between the two behaviours, with 303 mandating the change of request type to GET, and 307 preserving the
/// request type as originally sent. Despite the greater clarity provided by this disambiguation, the 302 code
/// is still employed in web frameworks to preserve compatibility with browsers that do not implement the HTTP/1.1
/// specification.
/// </pre>
/// </remarks>
public bool ForceForwardBody { get; set; } = false;

/// <summary>
/// Set to true (default) to forward the query string to the redirected URL.
/// </summary>
public bool ForwardQuery { get; set; } = true;
public int MaxRedirects { get; set; }

/// <summary>
/// The maximum number of redirects to follow.
/// </summary>
public int MaxRedirects { get; set; } = 10;

/// <summary>
/// Set to true (default), to supply any requested fragment portion of the original URL to the destination URL.
/// </summary>
/// <remarks>
/// Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
/// fragment should inherit the fragment from the original URI.
/// </remarks>
public bool ForwardFragment { get; set; } = true;

/// <summary>
/// HttpStatusCodes that trigger redirect processing. Defaults to MovedPermanently (301),
/// SeeOther (303),
/// TemporaryRedirect (307),
/// Redirect (302),
/// PermanentRedirect (308)
/// </summary>
public IReadOnlyList<HttpStatusCode> RedirectStatusCodes { get; set; }

public RestClientRedirectionOptions() {
Expand Down
97 changes: 65 additions & 32 deletions src/RestSharp/RestClient.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

using System.Net;
using System.Web;
using RestSharp.Extensions;

namespace RestSharp;
Expand Down Expand Up @@ -141,8 +142,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo

if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);

if (!IsRedirect(responseMessage)) {
// || !Options.FollowRedirects) {
if (!IsRedirect(Options.RedirectOptions, responseMessage)) {
break;
}

Expand All @@ -159,26 +159,30 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
// Mirror HttpClient redirection behavior as of 07/25/2023:
// Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
// fragment should inherit the fragment from the original URI.
string requestFragment = originalUrl.Fragment;
if (!string.IsNullOrEmpty(requestFragment)) {
string redirectFragment = location.Fragment;
if (string.IsNullOrEmpty(redirectFragment)) {
location = new UriBuilder(location) { Fragment = requestFragment }.Uri;
if (Options.RedirectOptions.ForwardFragment) {
string requestFragment = originalUrl.Fragment;
if (!string.IsNullOrEmpty(requestFragment)) {
string redirectFragment = location.Fragment;
if (string.IsNullOrEmpty(redirectFragment)) {
location = new UriBuilder(location) { Fragment = requestFragment }.Uri;
}
}
}

// Disallow automatic redirection from secure to non-secure schemes
// From HttpClient's RedirectHandler:
//if (HttpUtilities.IsSupportedSecureScheme(requestUri.Scheme) && !HttpUtilities.IsSupportedSecureScheme(location.Scheme)) {
// if (NetEventSource.Log.IsEnabled()) {
// TraceError($"Insecure https to http redirect from '{requestUri}' to '{location}' blocked.", response.RequestMessage!.GetHashCode());
// }
// break;
//}
// based on the option setting:
if (HttpUtilities.IsSupportedSecureScheme(requestUri.Scheme)
&& !HttpUtilities.IsSupportedSecureScheme(location.Scheme)
&& !Options.RedirectOptions.FollowRedirectsToInsecure) {
// TODO: Log here...
break;
}

if (responseMessage.StatusCode == HttpStatusCode.RedirectMethod) {
// TODO: Add RedirectionOptions property for this decision:
httpMethod = HttpMethod.Get;
}

// Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
// Many web browsers implemented this code in a manner that violated this standard, changing
// the request type of the new request to GET, regardless of the type employed in the original request
Expand All @@ -192,13 +196,20 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
// solves this problem by a helper method:
if (RedirectRequestRequiresForceGet(responseMessage.StatusCode, httpMethod)) {
httpMethod = HttpMethod.Get;
// HttpClient sets request.Content to null here:
// TODO: However... should we be allowed to modify Request like that here?
message.Content = null;
// HttpClient Redirect handler also does this:
//if (message.Headers.TansferEncodingChunked == true) {
// request.Headers.TransferEncodingChunked = false;
//}
if (!Options.RedirectOptions.ForceForwardBody) {
// HttpClient RedirectHandler sets request.Content to null here:
message.Content = null;
// HttpClient Redirect handler also does this:
//if (message.Headers.TansferEncodingChunked == true) {
// request.Headers.TransferEncodingChunked = false;
//}
Parameter? transferEncoding = request.Parameters.TryFind(KnownHeaders.TransferEncoding);
if (transferEncoding != null
&& transferEncoding.Type == ParameterType.HttpHeader
&& string.Equals((string)transferEncoding.Value!, "chunked", StringComparison.OrdinalIgnoreCase)) {
message.Headers.Remove(KnownHeaders.TransferEncoding);
}
}
}

url = location;
Expand Down Expand Up @@ -257,6 +268,35 @@ async Task OnAfterRequest(HttpResponseMessage responseMessage) {
}
}

/// <summary>
/// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs
/// </summary>
private static class HttpUtilities {
internal static bool IsSupportedScheme(string scheme) =>
IsSupportedNonSecureScheme(scheme) ||
IsSupportedSecureScheme(scheme);

internal static bool IsSupportedNonSecureScheme(string scheme) =>
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsNonSecureWebSocketScheme(scheme);

internal static bool IsSupportedSecureScheme(string scheme) =>
string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSecureWebSocketScheme(scheme);

internal static bool IsNonSecureWebSocketScheme(string scheme) =>
string.Equals(scheme, "ws", StringComparison.OrdinalIgnoreCase);

internal static bool IsSecureWebSocketScheme(string scheme) =>
string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase);

internal static bool IsSupportedProxyScheme(string scheme) =>
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme);

internal static bool IsSocksScheme(string scheme) =>
string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) ||
string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) ||
string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Based on .net core RedirectHandler class:
/// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
Expand All @@ -283,17 +323,10 @@ HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, HttpCon
return message;
}

static bool IsRedirect(HttpResponseMessage responseMessage)
=> responseMessage.StatusCode switch {
HttpStatusCode.MovedPermanently => true,
HttpStatusCode.SeeOther => true,
HttpStatusCode.TemporaryRedirect => true,
HttpStatusCode.Redirect => true,
#if NET
HttpStatusCode.PermanentRedirect => true,
#endif
_ => false
};
static bool IsRedirect(RestClientRedirectionOptions options, HttpResponseMessage responseMessage)
{
return options.RedirectStatusCodes.Contains(responseMessage.StatusCode);
}

record HttpResponse(
HttpResponseMessage? ResponseMessage,
Expand Down

0 comments on commit 90f9053

Please sign in to comment.