8000 Automatically strip Authorization header on redirects by dantraMSFT · Pull Request #3885 · PowerShell/PowerShell · GitHub
[go: up one dir, main page]

Skip to content

Automatically strip Authorization header on redirects #3885

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

Merged
merged 23 commits into from
Jun 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1282184
WebRequestPSCmdlet: Automatically strip Authorization header on redir…
dantraMSFT May 30, 2017
93578d6
Handle all redirection codes
dantraMSFT Jun 3, 2017
9f54b6b
WebRequestPSCmdlet: Automatically strip Authorization header on 302 r…
dantraMSFT Jun 6, 2017
9ebdee0
Handle all redirection codes
dantraMSFT Jun 6, 2017
90f7fd0
Refactor tests to use -TestCases to reduce redundant code.
dantraMSFT Jun 6, 2017
f379e4a
Fix redirect tets - move supportedRedirects into script block.
dantraMSFT Jun 7, 2017
1769d0e
Refactor redirect tests to cover all redirect codes for each test.
dantraMSFT Jun 8, 2017
370438a
Remove unused $redirectedMethod parameter
dantraMSFT Jun 9, 2017
10ed934
Add Content-Type header in HTTPListener and remove Convert-ResponseCo…
dantraMSFT Jun 9, 2017
706b584
Add Invoke-RestMethod tests and move tests into existing describe block.
dantraMSFT Jun 12, 2017
7eeebbf
WebRequestPSCmdlet: Automatically strip Authorization header on redir…
dantraMSFT May 30, 2017
1cf6853
WebRequestPSCmdlet: Automatically strip Authorization header on 302 r…
dantraMSFT May 30, 2017
12d9014
Handle all redirection codes
dantraMSFT Jun 3, 2017
3ecd5a7
WebRequestPSCmdlet: Automatically strip Authorization header on 302 r…
dantraMSFT Jun 6, 2017
8f837fa
Handle all redirection codes
dantraMSFT Jun 6, 2017
e814056
Refactor tests to use -TestCases to reduce redundant code.
dantraMSFT Jun 6, 2017
18850e4
Fix redirect tets - move supportedRedirects into script block.
dantraMSFT Jun 7, 2017
94d5c41
Refactor redirect tests to cover all redirect codes for each test.
dantraMSFT Jun 8, 2017
de27f76
Remove unused $redirectedMethod parameter
dantraMSFT Jun 9, 2017
9d2b095
Add Content-Type header in HTTPListener and remove Convert-ResponseCo…
dantraMSFT Jun 9, 2017
7569140
Add Invoke-RestMethod tests and move tests into existing describe block.
dantraMSFT Jun 12, 2017
1b9eb0f
Address review feedback.
dantraMSFT Jun 14, 2017
b969b20
Change CRLF to LF
dantraMSFT Jun 14, 2017
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// Exception class for webcmdlets to enable returning HTTP error response
/// </summary>
/// </summary>
public sealed class HttpResponseException : HttpRequestException
{
/// <summary>
Expand All @@ -48,6 +48,22 @@ public HttpResponseException (string message, HttpResponseMessage response) : ba
/// </summary>
public abstract partial class WebRequestPSCmdlet : PSCmdlet
{

/// <summary>
/// gets or sets the PreserveAuthorizationOnRedirect property
/// </summary>
/// <remarks>
/// This property overrides compatibility with web requests on Windows.
/// On FullCLR (WebRequest), authorization headers are stripped during redirect.
/// CoreCLR (HTTPClient) does not have this behavior so web requests that work on
/// PowerShell/FullCLR can fail with PowerShell/CoreCLR. To provide compatibility,
/// we'll detect requests with an Authorization header and automatically strip
/// the header when the first redirect occurs. This switch turns off this logic for
/// edge cases where the authorization header needs to be preserved across redirects.
/// </remarks>
[Parameter]
public virtual SwitchParameter PreserveAuthorizationOnRedirect { get; set; }

#region Abstract Methods

/// <summary>
Expand Down 10000 Expand Up @@ -111,7 +127,9 @@ private HttpMethod GetHttpMethod(WebRequestMethod method)

#region Virtual Methods

internal virtual HttpClient GetHttpClient()
// NOTE: Only pass true for handleRedirect if the original request has an authorization header
// and PreserveAuthorizationOnRedirect is NOT set.
internal virtual HttpClient GetHttpClient(bool handleRedirect)
{
// By default the HttpClientHandler will automatically decompress GZip and Deflate content
HttpClientHandler handler = new HttpClientHandler();
Expand Down Expand Up @@ -150,7 +168,12 @@ internal virtual HttpClient GetHttpClient()
handler.ServerCertificateCustomValidationCallback = delegate { return true; };
}

if (WebSession.MaximumRedirection > -1)
// This indicates GetResponse will handle redirects.
if (handleRedirect)
{
handler.AllowAutoRedirect = false;
}
else if (WebSession.MaximumRedirection > -1)
{
if (WebSession.MaximumRedirection == 0)
{
Expand Down Expand Up @@ -178,7 +201,7 @@ internal virtual HttpClient GetHttpClient()
return httpClient;
}

internal virtual HttpRequestMessage GetRequest(Uri uri)
internal virtual HttpRequestMessage GetRequest(Uri uri, bool stripAuthorization)
{
Uri requestUri = PrepareUri(uri);
HttpMethod httpMethod = null;
Expand Down Expand Up @@ -217,6 +240,13 @@ internal virtual HttpRequestMessage GetRequest(Uri uri)
}
else
{
if (stripAuthorization
&&
String.Equals(entry.Key, HttpKnownHeaderNames.Authorization.ToString(), StringComparison.OrdinalIgnoreCase)
)
{
continue;
}
request.Headers.Add(entry.Key, entry.Value);
}
}
Expand Down Expand Up @@ -367,13 +397,77 @@ internal virtual void FillRequestStream(HttpRequestMessage request)
}
}

internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request)
// Returns true if the status code is one of the supported redirection codes.
static bool IsRedirectCode(HttpStatusCode code)
{
int intCode = (int) code;
return
(
(intCode >= 300 && intCode < 304)
||
intCode == 307
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't 308 also valid?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you have this specific checks it seems you should have TestCases that go through the different status codes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

308 isn't currently supported in CoreCLR - HttpStatusCode doesn't have a define for it so there's no way for the code to see it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored all tests to cover each redirect status code.

);
}

// Returns true if the status code is a redirection code and the action requires switching from POST to GET on redirection.
// NOTE: Some of these status codes map to the same underlying value but spelling them out for completeness.
static bool IsRedirectToGet(HttpStatusCode code)
{
return
(
code == HttpStatusCode.Found
||
code == HttpStatusCode.Moved
||
code == HttpStatusCode.Redirect
||
code == HttpStatusCode.RedirectMethod
||
code == HttpStatusCode.TemporaryRedirect
||
code == HttpStatusCode.RedirectKeepVerb
||
code == HttpStatusCode.SeeOther
);
}

internal virtual HttpResponseMessage GetResponse(HttpClient client, HttpRequestMessage request, bool stripAuthorization)
{
if (client == null) { throw new ArgumentNullException("client"); }
if (request == null) { throw new ArgumentNullException("request"); }

_cancelToken = new CancellationTokenSource();
return client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
HttpResponseMessage response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();

if (stripAuthorization && IsRedirectCode(response.StatusCode))
{
_cancelToken.Cancel();
_cancelToken = null;

// if explicit count was provided, reduce it for this redirection.
if (WebSession.MaximumRedirection > 0)
{
WebSession.MaximumRedirection--;
}
// For selected redirects that used POST, GET must be used with the
// redirected Location.
// Since GET is the default; POST only occurs when -Method POST is used.
if (Method == WebRequestMethod.Post && IsRedirectToGet(response.StatusCode))
{
// See https://msdn.microsoft.com/en-us/library/system.net.httpstatuscode(v=vs.110).aspx
Method = WebRequestMethod.Get;
}

// recreate the HttpClient with redirection enabled since the first call suppressed redirection
using (client = GetHttpClient(false))
using (HttpRequestMessage redirectRequest = GetRequest(response.Headers.Location, stripAuthorization:true))
{
FillRequestStream(redirectRequest);
_cancelToken = new CancellationTokenSource();
response = client.SendAsync(redirectRequest, HttpCompletionOption.ResponseHeadersRead, _cancelToken.Token).GetAwaiter().GetResult();
}
}
return response;
}

internal virtual void UpdateSession(HttpResponseMessage response)
Expand All @@ -396,7 +490,17 @@ protected override void ProcessRecord()
ValidateParameters();
PrepareSession();

using (HttpClient client = GetHttpClient())
// if the request contains an authorization header and PreserveAuthorizationOnRedirect is not set,
// it needs to be stripped on the first redirect.
bool stripAuthorization = null != WebSession
&&
null != WebSession.Headers
&&
!PreserveAuthorizationOnRedirect.IsPresent
&&
WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization.ToString());

using (HttpClient client = GetHttpClient(stripAuthorization))
{
int followedRelLink = 0;
Uri uri = Uri;
Expand All @@ -410,7 +514,7 @@ protected override void ProcessRecord()
WriteVerbose(linkVerboseMsg);
}

using (HttpRequestMessage request = GetRequest(uri))
using (HttpRequestMessage request = GetRequest(uri, stripAuthorization:false))
{
FillRequestStream(request);
try
Expand All @@ -426,7 +530,7 @@ protected override void ProcessRecord()
requestContentLength);
WriteVerbose(reqVerboseMsg);

HttpResponseMessage response = GetResponse(client, request);
HttpResponseMessage response = GetResponse(client, request, stripAuthorization);

string contentType = ContentHelper.GetContentType(response);
string respVerboseMsg = string.Format(CultureInfo.CurrentCulture,
Expand Down
Loading
0