-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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
Make enhanced nav much more conservative, and better handle redirections #50551
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
037d2ff
Support data-enhance to change the enhancement of links and forms
SteveSandersonMS 7b112d0
Update E2E tests
SteveSandersonMS 3508fc5
Add E2E tests for controlling form enhancement
SteveSandersonMS 907f688
Change attribute for links to data-enhance-nav
SteveSandersonMS 393e4e3
E2E tests
SteveSandersonMS 4680e27
Implement non-SSR redirections more properly
SteveSandersonMS bc9f488
Support redirection during streaming SSR via data protection
SteveSandersonMS 754ec52
Go back to "blazor-enhanced-nav-redirect-location" header for enhance…
SteveSandersonMS 7638569
E2E tests pages for streaming non-enhanced cases
SteveSandersonMS 108ce84
Same for enhanced non-streaming cases
SteveSandersonMS 9df1703
Same for enhanced+streaming cases
SteveSandersonMS 6f9afe3
Same for non-Blazor endpoint cases
SteveSandersonMS 42c24b5
Change E2E destination so we can check if hash is preserved
SteveSandersonMS eaa95ce
Start implementing and tidying up actual E2E test code
SteveSandersonMS b0f50aa
E2E cases for streaming POST
SteveSandersonMS 754ef7d
E2E cases for enhanced nav, non-streaming
SteveSandersonMS 6ae0d22
Cases for enhanced+streaming, including fixes
SteveSandersonMS 027dde7
Clarify about hashes in URLs
SteveSandersonMS 3291461
Non-Blazor endpoint cases
SteveSandersonMS 3902de4
Remove unused method
SteveSandersonMS e64180b
Test fixes
SteveSandersonMS 6baca02
Make RazorComponentResultExecutor consistent with RazorComponentEndpo…
SteveSandersonMS b732559
Experiment: only allow enhanced nav to Blazor endpoints
SteveSandersonMS f3c3e6a
Test fixes
SteveSandersonMS 04aa153
E2E test for enhanced nav auto-disabling itself for non-Blazor endpoints
SteveSandersonMS 7edfbb7
Comment cleanups
SteveSandersonMS ca0dd33
Fix test after rebase
SteveSandersonMS ec54eb0
Another test fix
SteveSandersonMS e81989a
CR feedback plus a test fix
SteveSandersonMS d72681b
Update .js
SteveSandersonMS File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Security.Cryptography; | ||
using System.Text.Encodings.Web; | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.DataProtection; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Routing; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
||
internal partial class OpaqueRedirection | ||
{ | ||
// During streaming SSR, a component may try to perform a redirection. Since the response has already started | ||
// this can only work if we communicate the redirection back via some command that can get handled by JS, | ||
// rather than a true 301/302/etc. But we don't want to disclose the redirection target URL to JS because that | ||
// info would not normally be available, e.g., when using 'fetch'. So we data-protect the URL and round trip | ||
// through a special endpoint that can issue a true redirection. | ||
// | ||
// The same is used during enhanced navigation if it happens to go to a Blazor endpoint that calls | ||
// NavigationManager.NavigateTo, for the same reasons. | ||
// | ||
// However, if enhanced navigation goes to a non-Blazor endpoint, the server won't do anything special and just | ||
// returns a regular 301/302/etc. To handle this, | ||
// | ||
// - If it's redirected to an internal URL, the browser will just follow the redirection automatically | ||
// and client-side code will then: | ||
// - Check if it went to a Blazor endpoint, and if so, simply update the client-side URL to match | ||
// - Or if it's a non-Blazor endpoint, behaves like "external URL" below | ||
// - If it's to an external URL: | ||
// - If it's a GET request, the client-side code will retry as a non-enhanced request | ||
// - For other request types, we have to let it fail as it would be unsafe to retry | ||
|
||
private const string RedirectionDataProtectionProviderPurpose = "Microsoft.AspNetCore.Components.Endpoints.OpaqueRedirection,V1"; | ||
private const string RedirectionEndpointBaseRelativeUrl = "_framework/opaque-redirect"; | ||
|
||
public static string CreateProtectedRedirectionUrl(HttpContext httpContext, string destinationUrl) | ||
{ | ||
var protector = CreateProtector(httpContext); | ||
var options = httpContext.RequestServices.GetRequiredService<IOptions<RazorComponentsServiceOptions>>(); | ||
var lifetime = options.Value.TemporaryRedirectionUrlValidityDuration; | ||
var protectedUrl = protector.Protect(destinationUrl, lifetime); | ||
return $"{RedirectionEndpointBaseRelativeUrl}?url={UrlEncoder.Default.Encode(protectedUrl)}"; | ||
} | ||
|
||
public static void AddBlazorOpaqueRedirectionEndpoint(IEndpointRouteBuilder endpoints) | ||
{ | ||
endpoints.MapGet($"/{RedirectionEndpointBaseRelativeUrl}", httpContext => | ||
{ | ||
if (!httpContext.Request.Query.TryGetValue("url", out var protectedUrl)) | ||
{ | ||
httpContext.Response.StatusCode = 400; | ||
return Task.CompletedTask; | ||
} | ||
|
||
var protector = CreateProtector(httpContext); | ||
string url; | ||
|
||
try | ||
{ | ||
url = protector.Unprotect(protectedUrl[0]!); | ||
} | ||
catch (CryptographicException ex) | ||
{ | ||
if (httpContext.RequestServices.GetService<ILogger<OpaqueRedirection>>() is { } logger) | ||
{ | ||
Log.OpaqueUrlUnprotectionFailed(logger, ex); | ||
} | ||
|
||
httpContext.Response.StatusCode = 400; | ||
return Task.CompletedTask; | ||
} | ||
|
||
httpContext.Response.Redirect(url); | ||
return Task.CompletedTask; | ||
}); | ||
} | ||
|
||
private static ITimeLimitedDataProtector CreateProtector(HttpContext httpContext) | ||
{ | ||
var dataProtectionProvider = httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>(); | ||
return dataProtectionProvider.CreateProtector(RedirectionDataProtectionProviderPurpose).ToTimeLimitedDataProtector(); | ||
} | ||
|
||
public static partial class Log | ||
{ | ||
[LoggerMessage(1, LogLevel.Information, "Opaque URL unprotection failed.", EventName = "OpaqueUrlUnprotectionFailed")] | ||
public static partial void OpaqueUrlUnprotectionFailed(ILogger<OpaqueRedirection> logger, Exception exception); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is separate from the rest of the PR, but I realised that
RazorComponentResultExecutor
is inconsistent withRazorComponentEndpointInvoker
.It's dangerous that we have two different implementations as they could be out of sync and not support the same features. And here in fact one of them was not using the new buffering solution. However I don't want to unpick all the complexities of unifying these code paths within this PR (and I think we likely should leave that for .NET 9), so I'm just changing
RazorComponentResultExecutor
to use the same buffering technique asRazorComponentEndpointInvoker
. Longer term, we should unify the code.Note that fixing this forced me to make some other unit test updates (see
WaitForContentWrittenAsync
in this PR), as the unit tests were relying on content writes being completely synchronous.