Skip to content

Commit cff380d

Browse files
committed
Start paying attention to some of the Options.RedirectionOptions properties.
1 parent 72ff2eb commit cff380d

File tree

3 files changed

+146
-33
lines changed

3 files changed

+146
-33
lines changed

src/RestSharp/KnownHeaders.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static class KnownHeaders {
3636
public const string Cookie = "Cookie";
3737
public const string SetCookie = "Set-Cookie";
3838
public const string UserAgent = "User-Agent";
39+
public const string TransferEncoding = "Transfer-Encoding";
3940

4041
internal static readonly string[] ContentHeaders = {
4142
Allow, Expires, ContentDisposition, ContentEncoding, ContentLanguage, ContentLength, ContentLocation, ContentRange, ContentType, ContentMD5,

src/RestSharp/Options/RestClientRedirectionOptions.cs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,98 @@
44

55
namespace RestSharp;
66

7+
/// <summary>
8+
/// Options related to redirect processing.
9+
/// </summary>
710
[GenerateImmutable]
811
public class RestClientRedirectionOptions {
912
static readonly Version Version = new AssemblyName(typeof(RestClientOptions).Assembly.FullName!).Version!;
1013

14+
/// <summary>
15+
/// Set to true (default), when you want to follow redirects
16+
/// </summary>
1117
public bool FollowRedirects { get; set; } = true;
18+
19+
/// <summary>
20+
/// Set to true (default is false), when you want to follow a
21+
/// redirect from HTTPS to HTTP.
22+
/// </summary>
1223
public bool FollowRedirectsToInsecure { get; set; } = false;
24+
/// <summary>
25+
/// Set to true (default), when you want to include the originally
26+
/// requested headers in redirected requests.
27+
/// </summary>
1328
public bool ForwardHeaders { get; set; } = true;
29+
30+
/// <summary>
31+
/// Set to true (default is false), when you want to send the original
32+
/// Authorization header to the redirected destination.
33+
/// </summary>
1434
public bool ForwardAuthorization { get; set; } = false;
35+
/// <summary>
36+
/// Set to true (default), when you want to include cookie3s from the
37+
/// CookieContainer on the redirected URL.
38+
/// </summary>
39+
/// <remarks>
40+
/// NOTE: The exact cookies sent to the redirected url DEPENDS directly
41+
/// on the redirected url. A redirection to a completly differnet FQDN
42+
/// for example is unlikely to actually propagate any cookies from the
43+
/// CookieContqainer.
44+
/// </remarks>
1545
public bool ForwardCookies { get; set; } = true;
46+
47+
/// <summary>
48+
/// Set to true (default) in order to send the body to the
49+
/// redirected URL, unless the force verb to GET behavior is triggered.
50+
/// <see cref="ForceForwardBody"/>
51+
/// </summary>
1652
public bool ForwardBody { get; set; } = true;
53+
54+
/// <summary>
55+
/// Set to true (default is false) to force forwarding the body of the
56+
/// request even when normally, the verb might be altered to GET based
57+
/// on backward compatiblity with browser processing of HTTP status codes.
58+
/// </summary>
59+
/// <remarks>
60+
/// Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
61+
/// <pre>
62+
/// Many web browsers implemented this code in a manner that violated this standard, changing
63+
/// the request type of the new request to GET, regardless of the type employed in the original request
64+
/// (e.g. POST). For this reason, HTTP/1.1 (RFC 2616) added the new status codes 303 and 307 to disambiguate
65+
/// between the two behaviours, with 303 mandating the change of request type to GET, and 307 preserving the
66+
/// request type as originally sent. Despite the greater clarity provided by this disambiguation, the 302 code
67+
/// is still employed in web frameworks to preserve compatibility with browsers that do not implement the HTTP/1.1
68+
/// specification.
69+
/// </pre>
70+
/// </remarks>
71+
public bool ForceForwardBody { get; set; } = false;
72+
73+
/// <summary>
74+
/// Set to true (default) to forward the query string to the redirected URL.
75+
/// </summary>
1776
public bool ForwardQuery { get; set; } = true;
18-
public int MaxRedirects { get; set; }
77+
78+
/// <summary>
79+
/// The maximum number of redirects to follow.
80+
/// </summary>
81+
public int MaxRedirects { get; set; } = 10;
82+
83+
/// <summary>
84+
/// Set to true (default), to supply any requested fragment portion of the original URL to the destination URL.
85+
/// </summary>
86+
/// <remarks>
87+
/// Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
88+
/// fragment should inherit the fragment from the original URI.
89+
/// </remarks>
1990
public bool ForwardFragment { get; set; } = true;
91+
92+
/// <summary>
93+
/// HttpStatusCodes that trigger redirect processing. Defaults to MovedPermanently (301),
94+
/// SeeOther (303),
95+
/// TemporaryRedirect (307),
96+
/// Redirect (302),
97+
/// PermanentRedirect (308)
98+
/// </summary>
2099
public IReadOnlyList<HttpStatusCode> RedirectStatusCodes { get; set; }
21100

22101
public RestClientRedirectionOptions() {

src/RestSharp/RestClient.Async.cs

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System.Net;
16+
using System.Web;
1617
using RestSharp.Extensions;
1718

1819
namespace RestSharp;
@@ -124,8 +125,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
124125

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

127-
if (!IsRedirect(responseMessage)) {
128-
// || !Options.FollowRedirects) {
128+
if (!IsRedirect(Options.RedirectOptions, responseMessage)) {
129129
break;
130130
}
131131

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

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

162164
if (responseMessage.StatusCode == HttpStatusCode.RedirectMethod) {
165+
// TODO: Add RedirectionOptions property for this decision:
163166
httpMethod = HttpMethod.Get;
164167
}
168+
165169
// Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
166170
// Many web browsers implemented this code in a manner that violated this standard, changing
167171
// the request type of the new request to GET, regardless of the type employed in the original request
@@ -175,13 +179,20 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
175179
// solves this problem by a helper method:
176180
if (RedirectRequestRequiresForceGet(responseMessage.StatusCode, httpMethod)) {
177181
httpMethod = HttpMethod.Get;
178-
// HttpClient sets request.Content to null here:
179-
// TODO: However... should we be allowed to modify Request like that here?
180-
message.Content = null;
181-
// HttpClient Redirect handler also does this:
182-
//if (message.Headers.TansferEncodingChunked == true) {
183-
// request.Headers.TransferEncodingChunked = false;
184-
//}
182+
if (!Options.RedirectOptions.ForceForwardBody) {
183+
// HttpClient RedirectHandler sets request.Content to null here:
184+
message.Content = null;
185+
// HttpClient Redirect handler also does this:
186+
//if (message.Headers.TansferEncodingChunked == true) {
187+
// request.Headers.TransferEncodingChunked = false;
188+
//}
189+
Parameter? transferEncoding = request.Parameters.TryFind(KnownHeaders.TransferEncoding);
190+
if (transferEncoding != null
191+
&& transferEncoding.Type == ParameterType.HttpHeader
192+
&& string.Equals((string)transferEncoding.Value!, "chunked", StringComparison.OrdinalIgnoreCase)) {
193+
message.Headers.Remove(KnownHeaders.TransferEncoding);
194+
}
195+
}
185196
}
186197

187198
url = location;
@@ -212,6 +223,35 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
212223
}
213224
}
214225

226+
/// <summary>
227+
/// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs
228+
/// </summary>
229+
private static class HttpUtilities {
230+
internal static bool IsSupportedScheme(string scheme) =>
231+
IsSupportedNonSecureScheme(scheme) ||
232+
IsSupportedSecureScheme(scheme);
233+
234+
internal static bool IsSupportedNonSecureScheme(string scheme) =>
235+
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsNonSecureWebSocketScheme(scheme);
236+
237+
internal static bool IsSupportedSecureScheme(string scheme) =>
238+
string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSecureWebSocketScheme(scheme);
239+
240+
internal static bool IsNonSecureWebSocketScheme(string scheme) =>
241+
string.Equals(scheme, "ws", StringComparison.OrdinalIgnoreCase);
242+
243+
internal static bool IsSecureWebSocketScheme(string scheme) =>
244+
string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase);
245+
246+
internal static bool IsSupportedProxyScheme(string scheme) =>
247+
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme);
248+
249+
internal static bool IsSocksScheme(string scheme) =>
250+
string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) ||
251+
string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) ||
252+
string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase);
253+
}
254+
215255
/// <summary>
216256
/// Based on .net core RedirectHandler class:
217257
/// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
@@ -238,17 +278,10 @@ HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, HttpCon
238278
return message;
239279
}
240280

241-
static bool IsRedirect(HttpResponseMessage responseMessage)
242-
=> responseMessage.StatusCode switch {
243-
HttpStatusCode.MovedPermanently => true,
244-
HttpStatusCode.SeeOther => true,
245-
HttpStatusCode.TemporaryRedirect => true,
246-
HttpStatusCode.Redirect => true,
247-
#if NET
248-
HttpStatusCode.PermanentRedirect => true,
249-
#endif
250-
_ => false
251-
};
281+
static bool IsRedirect(RestClientRedirectionOptions options, HttpResponseMessage responseMessage)
282+
{
283+
return options.RedirectStatusCodes.Contains(responseMessage.StatusCode);
284+
}
252285

253286
record HttpResponse(
254287
HttpResponseMessage? ResponseMessage,

0 commit comments

Comments
 (0)