[Fixes #11454] Fix encoding bug on prerendering (#11504)

We replace double dash sequences with double dots sequences as double dash sequences are not allowed into HTML.
This commit is contained in:
Javier Calvarro Nelson 2019-07-12 18:53:21 +02:00 committed by GitHub
parent 72f91b8507
commit 8fa4df9bda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 98 additions and 15 deletions

View File

@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Http;
@ -15,11 +16,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
private static object CircuitHostKey = new object();
private static object CancellationStatusKey = new object();
private static readonly JsonSerializerOptions _jsonSerializationOptions =
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private readonly CircuitFactory _circuitFactory;
private readonly CircuitRegistry _registry;
public CircuitPrerenderer(CircuitFactory circuitFactory, CircuitRegistry registry)
public CircuitPrerenderer(
CircuitFactory circuitFactory,
CircuitRegistry registry)
{
_circuitFactory = circuitFactory;
_registry = registry;
@ -76,8 +81,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Prerendered = true
});
var record = JsonSerializer.Serialize(new PrerenderedComponentRecord(
// We need to do this due to the fact that -- is not allowed within HTML comments and HTML doesn't encode '-'.
// We will never have '..' sequences because we Base64UrlEncode the circuit id
circuitHost.CircuitId.Replace("--", ".."),
circuitHost.Renderer.Id,
renderResult.ComponentId),
_jsonSerializationOptions);
var result = (new[] {
$"<!-- M.A.C.Component:{{\"circuitId\":\"{circuitHost.CircuitId}\",\"rendererId\":\"{circuitHost.Renderer.Id}\",\"componentId\":\"{renderResult.ComponentId}\"}} -->",
$"<!-- M.A.C.Component: {record} -->",
}).Concat(renderResult.Tokens).Concat(
new[] {
$"<!-- M.A.C.Component: {renderResult.ComponentId} -->"
@ -169,6 +182,22 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return result;
}
private readonly struct PrerenderedComponentRecord
{
public PrerenderedComponentRecord(string circuitId, int rendererId, int componentId)
{
CircuitId = circuitId;
RendererId = rendererId;
ComponentId = componentId;
}
public string CircuitId { get; }
public int RendererId { get; }
public int ComponentId { get; }
}
private class PrerenderingCancellationStatus
{
public bool Canceled { get; set; }

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
@ -19,7 +20,11 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
public class CircuitPrerendererTest
{
private static readonly Regex ContentWrapperRegex = new Regex(
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"0\",\"componentId\":\"0\"}} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
"<!-- M.A.C.Component: {\"circuitId\":\"[^\"]+\",\"rendererId\":0,\"componentId\":0} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
private static readonly Regex CircuitInfoRegex = new Regex(
"<!-- M.A.C.Component: (?<info>.*?) -->.*",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
// Because CircuitPrerenderer is a point of integration with HttpContext,
@ -74,6 +79,15 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
.Replace("\r\n","\n");
}
private JsonDocument GetUnwrappedCircuitInfo(ComponentPrerenderResult rawResult)
{
var writer = new StringWriter();
rawResult.WriteTo(writer);
var circuitInfo = CircuitInfoRegex.Match(writer.ToString()).Groups["info"].Value;
return JsonDocument.Parse(circuitInfo);
}
[Fact]
public async Task ExtractsUriFromHttpContext_NonemptyPathBase()
{
@ -111,6 +125,36 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
}), GetUnwrappedContent(result));
}
[Fact]
public async Task ReplacesDashesWithDots_WhenTheyAppearInPairs()
{
// Arrange
var circuitFactory = new TestCircuitFactory(() => "--1234--");
var circuitRegistry = new CircuitRegistry(
Options.Create(new CircuitOptions()),
Mock.Of<ILogger<CircuitRegistry>>(),
TestCircuitIdFactory.CreateTestFactory());
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var httpContext = new DefaultHttpContext();
var httpRequest = httpContext.Request;
httpRequest.Scheme = "https";
httpRequest.Host = new HostString("example.com", 1234);
httpRequest.Path = "/some/path";
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(UriDisplayComponent),
Parameters = ParameterCollection.Empty,
Context = httpContext
};
// Act
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
// Assert
Assert.Equal("..1234..", GetUnwrappedCircuitInfo(result).RootElement.GetProperty("circuitId").GetString());
}
[Fact]
public async Task DisposesCircuitScopeEvenIfPrerenderingThrows()
{
@ -139,6 +183,13 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
class TestCircuitFactory : CircuitFactory
{
private readonly Func<string> _circuitIdFactory;
public TestCircuitFactory(Func<string> circuitIdFactory = null)
{
_circuitIdFactory = circuitIdFactory ?? (() => Guid.NewGuid().ToString());
}
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute)
{
var serviceCollection = new ServiceCollection();
@ -149,7 +200,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
return uriHelper;
});
var serviceScope = serviceCollection.BuildServiceProvider().CreateScope();
return TestCircuitHost.Create(Guid.NewGuid().ToString(), serviceScope);
return TestCircuitHost.Create(_circuitIdFactory(), serviceScope);
}
}

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@ import '@dotnet/jsinterop';
import './GlobalExports';
import * as signalR from '@aspnet/signalr';
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync, shouldAutoStart } from './BootCommon';
import { shouldAutoStart } from './BootCommon';
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
import RenderQueue from './Platform/Circuits/RenderQueue';

View File

@ -22,12 +22,15 @@ export function discoverPrerenderedCircuits(document: Document): CircuitDescript
const discoveredCircuits = new Map<string, ComponentDescriptor[]>();
for (let i = 0; i < commentPairs.length; i++) {
const pair = commentPairs[i];
let circuit = discoveredCircuits.get(pair.start.circuitId);
// We replace '--' on the server with '..' when we prerender due to the fact that this
// is not allowed in HTML comments and doesn't get encoded by default.
const circuitId = pair.start.circuitId.replace('..', '--');
let circuit = discoveredCircuits.get(circuitId);
if (!circuit) {
circuit = [];
discoveredCircuits.set(pair.start.circuitId, circuit);
discoveredCircuits.set(circuitId, circuit);
}
const entry = new ComponentDescriptor(pair.start.componentId, pair.start.circuitId, pair.start.rendererId, pair);
const entry = new ComponentDescriptor(pair.start.componentId, circuitId, pair.start.rendererId, pair);
circuit.push(entry);
}
const circuits: CircuitDescriptor[] = [];
@ -82,14 +85,14 @@ function getComponentStartComment(node: Node): StartComponentComment | undefined
const json = definition && definition[1];
if (json) {
try {
const { componentId, circuitId, rendererId } = JSON.parse(json);
const allComponents = !!componentId && !!circuitId && !!rendererId;
const { componentId, rendererId, circuitId } = JSON.parse(json);
const allComponents = componentId !== undefined && rendererId !== undefined && !!circuitId;
if (allComponents) {
return {
node: node as Comment,
circuitId,
rendererId: Number.parseInt(rendererId),
componentId: Number.parseInt(componentId),
rendererId: rendererId,
componentId: componentId,
};
} else {
throw new Error(`Found malformed start component comment at ${node.textContent}`);

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
public class HtmlHelperComponentExtensionsTests
{
private static readonly Regex ContentWrapperRegex = new Regex(
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"0\",\"componentId\":\"0\"}} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
"<!-- M.A.C.Component: {\"circuitId\":\"[^\"]+\",\"rendererId\":0,\"componentId\":0} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
[Fact]

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public class ComponentRenderingFunctionalTests : IClassFixture<MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting>>
{
private static readonly Regex ContentWrapperRegex = new Regex(
$"<!-- M.A.C.Component:{{\"circuitId\":\"[^\"]+\",\"rendererId\":\"\\d+\",\"componentId\":\"\\d+\"}} -->(?<content>.*)<!-- M.A.C.Component: \\d+ -->",
"<!-- M.A.C.Component: {\"circuitId\":\"[^\"]+\",\"rendererId\":\\d+,\"componentId\":\\d+} -->(?<content>.*)<!-- M.A.C.Component: \\d+ -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)