Skip to content

Commit 90f9053

Browse files
rassilonSreeshail1
authored andcommitted
Start paying attention to some of the Options.RedirectionOptions properties.
1 parent f27574e commit 90f9053

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;
@@ -141,8 +142,7 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
141142

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

144-
if (!IsRedirect(responseMessage)) {
145-
// || !Options.FollowRedirects) {
145+
if (!IsRedirect(Options.RedirectOptions, responseMessage)) {
146146
break;
147147
}
148148

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

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

179181
if (responseMessage.StatusCode == HttpStatusCode.RedirectMethod) {
182+
// TODO: Add RedirectionOptions property for this decision:
180183
httpMethod = HttpMethod.Get;
181184
}
185+
182186
// Based on Wikipedia https://en.wikipedia.org/wiki/HTTP_302:
183187
// Many web browsers implemented this code in a manner that violated this standard, changing
184188
// the request type of the new request to GET, regardless of the type employed in the original request
@@ -192,13 +196,20 @@ async Task<HttpResponse> ExecuteRequestAsync(RestRequest request, CancellationTo
192196
// solves this problem by a helper method:
193197
if (RedirectRequestRequiresForceGet(responseMessage.StatusCode, httpMethod)) {
194198
httpMethod = HttpMethod.Get;
195-
// HttpClient sets request.Content to null here:
196-
// TODO: However... should we be allowed to modify Request like that here?
197-
message.Content = null;
198-
// HttpClient Redirect handler also does this:
199-
//if (message.Headers.TansferEncodingChunked == true) {
200-
// request.Headers.TransferEncodingChunked = false;
201-
//}
199+
if (!Options.RedirectOptions.ForceForwardBody) {
200+
// HttpClient RedirectHandler sets request.Content to null here:
201+
message.Content = null;
202+
// HttpClient Redirect handler also does this:
203+
//if (message.Headers.TansferEncodingChunked == true) {
204+
// request.Headers.TransferEncodingChunked = false;
205+
//}
206+
Parameter? transferEncoding = request.Parameters.TryFind(KnownHeaders.TransferEncoding);
207+
if (transferEncoding != null
208+
&& transferEncoding.Type == ParameterType.HttpHeader
209+
&& string.Equals((string)transferEncoding.Value!, "chunked", StringComparison.OrdinalIgnoreCase)) {
210+
message.Headers.Remove(KnownHeaders.TransferEncoding);
211+
}
212+
}
202213
}
203214

204215
url = location;
@@ -257,6 +268,35 @@ async Task OnAfterRequest(HttpResponseMessage responseMessage) {
257268
}
258269
}
259270

271+
/// <summary>
272+
/// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.cs
273+
/// </summary>
274+
private static class HttpUtilities {
275+
internal static bool IsSupportedScheme(string scheme) =>
276+
IsSupportedNonSecureScheme(scheme) ||
277+
IsSupportedSecureScheme(scheme);
278+
279+
internal static bool IsSupportedNonSecureScheme(string scheme) =>
280+
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsNonSecureWebSocketScheme(scheme);
281+
282+
internal static bool IsSupportedSecureScheme(string scheme) =>
283+
string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSecureWebSocketScheme(scheme);
284+
285+
internal static bool IsNonSecureWebSocketScheme(string scheme) =>
286+
string.Equals(scheme, "ws", StringComparison.OrdinalIgnoreCase);
287+
288+
internal static bool IsSecureWebSocketScheme(string scheme) =>
289+
string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase);
290+
291+
internal static bool IsSupportedProxyScheme(string scheme) =>
292+
string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme);
293+
294+
internal static bool IsSocksScheme(string scheme) =>
295+
string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) ||
296+
string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) ||
297+
string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase);
298+
}
299+
260300
/// <summary>
261301
/// Based on .net core RedirectHandler class:
262302
/// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
@@ -283,17 +323,10 @@ HttpRequestMessage PrepareRequestMessage(HttpMethod httpMethod, Uri url, HttpCon
283323
return message;
284324
}
285325

286-
static bool IsRedirect(HttpResponseMessage responseMessage)
287-
=> responseMessage.StatusCode switch {
288-
HttpStatusCode.MovedPermanently => true,
289-
HttpStatusCode.SeeOther => true,
290-
HttpStatusCode.TemporaryRedirect => true,
291-
HttpStatusCode.Redirect => true,
292-
#if NET
293-
HttpStatusCode.PermanentRedirect => true,
294-
#endif
295-
_ => false
296-
};
326+
static bool IsRedirect(RestClientRedirectionOptions options, HttpResponseMessage responseMessage)
327+
{
328+
return options.RedirectStatusCodes.Contains(responseMessage.StatusCode);
329+
}
297330

298331
record HttpResponse(
299332
HttpResponseMessage? ResponseMessage,

0 commit comments

Comments
 (0)