aspnetcore/src/Components/Server/src/Circuits/ServerComponentDeserializer.cs

264 lines
13 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Server
{
// **Component descriptor protocol**
// MVC serializes one or more components as comments in HTML.
// Each comment is in the form <!-- Blazor:<<Json>>--> for example { "type": "server", "sequence": 0, descriptor: "base64(dataprotected(<<ServerComponent>>))" }
// Where <<Json>> has the following properties:
// 'type' indicates the marker type. For now it's limited to server.
// 'sequence' indicates the order in which this component got rendered on the server.
// 'descriptor' a data-protected payload that allows the server to validate the legitimacy of the rendered component.
// 'prerenderId' a unique identifier that uniquely identifies the marker to match start and end markers.
//
// descriptor holds the information to validate a component render request. It prevents an infinite number of components
// from being rendered by a given client.
//
// descriptor is a data protected json payload that holds the following information
// 'sequence' indicates the order in which this component got rendered on the server.
// 'assemblyName' the assembly name for the rendered component.
// 'type' the full type name for the rendered component.
// 'invocationId' a random string that matches all components rendered by as part of a single HTTP response.
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>"}))
// Serialization:
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
// component containing the information described above.
// Deserialization:
// To prevent a client from rendering an infinite amount of components, we require clients to send all component
// markers in order. They can do so thanks to the sequence included in the marker.
// When we process a marker we do the following.
// * We unprotect the data-protected information.
// * We validate that the sequence number for the descriptor goes after the previous descriptor.
// * We compare the invocationId for the previous descriptor against the invocationId for the current descriptor to make sure they match.
// By doing this we achieve three things:
// * We ensure that the descriptor came from the server.
// * We ensure that a client can't just send an infinite amount of components to render.
// * We ensure that we do the minimal amount of work in the case of an invalid sequence of descriptors.
//
// For example:
// A client can't just send 100 component markers and force us to process them if the server didn't generate those 100 markers.
// * If a marker is out of sequence we will fail early, so we process at most n-1 markers.
// * If a marker has the right sequence but the invocation ID is different we will fail at that point. We know for sure that the
// component wasn't render as part of the same response.
// * If a marker can't be unprotected we will fail early. We know that the marker was tampered with and can't be trusted.
internal class ServerComponentDeserializer
{
private readonly IDataProtector _dataProtector;
private readonly ILogger<ServerComponentDeserializer> _logger;
private readonly ServerComponentTypeCache _rootComponentTypeCache;
public ServerComponentDeserializer(
IDataProtectionProvider dataProtectionProvider,
ILogger<ServerComponentDeserializer> logger,
ServerComponentTypeCache rootComponentTypeCache)
{
// When we protect the data we use a time-limited data protector with the
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
// We don't use any of the additional methods provided by ITimeLimitedDataProtector
// in this class, but we need to create one for the unprotect operations to work
// even though we simply call '_dataProtector.Unprotect'.
// See the comment in ServerComponentSerializationSettings.DataExpiration to understand
// why we limit the validity of the protected payloads.
_dataProtector = dataProtectionProvider
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
_logger = logger;
_rootComponentTypeCache = rootComponentTypeCache;
}
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
{
var markers = JsonSerializer.Deserialize<IEnumerable<ServerComponentMarker>>(serializedComponentRecords, ServerComponentSerializationSettings.JsonSerializationOptions);
descriptors = new List<ComponentDescriptor>();
int lastSequence = -1;
var previousInstance = new ServerComponent();
foreach (var marker in markers)
{
if (marker.Type != ServerComponentMarker.ServerMarkerType)
{
Log.InvalidMarkerType(_logger, marker.Type);
descriptors.Clear();
return false;
}
if (marker.Descriptor == null)
{
Log.MissingMarkerDescriptor(_logger);
descriptors.Clear();
return false;
}
var (descriptor, serverComponent) = DeserializeServerComponent(marker);
if (descriptor == null)
{
// We failed to deserialize the component descriptor for some reason.
descriptors.Clear();
return false;
}
// We force our client to send the descriptors in order so that we do minimal work.
// The list of descriptors starts with 0 and lastSequence is initialized to -1 so this
// check covers that the sequence starts by 0.
if (lastSequence != serverComponent.Sequence - 1)
{
if (lastSequence == -1)
{
Log.DescriptorSequenceMustStartAtZero(_logger, serverComponent.Sequence);
}
else
{
Log.OutOfSequenceDescriptor(_logger, lastSequence, serverComponent.Sequence);
}
descriptors.Clear();
return false;
}
if (lastSequence != -1 && !previousInstance.InvocationId.Equals(serverComponent.InvocationId))
{
Log.MismatchedInvocationId(_logger, previousInstance.InvocationId.ToString("N"), serverComponent.InvocationId.ToString("N"));
descriptors.Clear();
return false;
}
// As described below, we build a chain of descriptors to prevent being flooded by
// descriptors from a client not behaving properly.
lastSequence = serverComponent.Sequence;
previousInstance = serverComponent;
descriptors.Add(descriptor);
}
return true;
}
private (ComponentDescriptor, ServerComponent) DeserializeServerComponent(ServerComponentMarker record)
{
string unprotected;
try
{
unprotected = _dataProtector.Unprotect(record.Descriptor);
}
catch (Exception e)
{
Log.FailedToUnprotectDescriptor(_logger, e);
return default;
}
ServerComponent serverComponent;
try
{
serverComponent = JsonSerializer.Deserialize<ServerComponent>(
unprotected,
ServerComponentSerializationSettings.JsonSerializationOptions);
}
catch (Exception e)
{
Log.FailedToDeserializeDescriptor(_logger, e);
return default;
}
var componentType = _rootComponentTypeCache
.GetRootComponent(serverComponent.AssemblyName, serverComponent.TypeName);
if (componentType == null)
{
Log.FailedToFindComponent(_logger, serverComponent.TypeName, serverComponent.AssemblyName);
return default;
}
var componentDescriptor = new ComponentDescriptor
{
ComponentType = componentType,
Sequence = serverComponent.Sequence
};
return (componentDescriptor, serverComponent);
}
private static class Log
{
private static readonly Action<ILogger, Exception> _failedToDeserializeDescriptor =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(1, "FailedToDeserializeDescriptor"),
"Failed to deserialize the component descriptor.");
private static readonly Action<ILogger, string, string, Exception> _failedToFindComponent =
LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(2, "FailedToFindComponent"),
"Failed to find component '{ComponentName}' in assembly '{Assembly}'.");
private static readonly Action<ILogger, Exception> _failedToUnprotectDescriptor =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(3, "FailedToUnprotectDescriptor"),
"Failed to unprotect the component descriptor.");
private static readonly Action<ILogger, string, Exception> _invalidMarkerType =
LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(4, "InvalidMarkerType"),
"Invalid component marker type '{}'.");
private static readonly Action<ILogger, Exception> _missingMarkerDescriptor =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(5, "MissingMarkerDescriptor"),
"The component marker is missing the descriptor.");
private static readonly Action<ILogger, string, string, Exception> _mismatchedInvocationId =
LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(6, "MismatchedInvocationId"),
"The descriptor invocationId is '{invocationId}' and got a descriptor with invocationId '{currentInvocationId}'.");
private static readonly Action<ILogger, int, int, Exception> _outOfSequenceDescriptor =
LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(7, "OutOfSequenceDescriptor"),
"The last descriptor sequence was '{lastSequence}' and got a descriptor with sequence '{receivedSequence}'.");
private static readonly Action<ILogger, int, Exception> _descriptorSequenceMustStartAtZero =
LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(8, "DescriptorSequenceMustStartAtZero"),
"The descriptor sequence '{sequence}' is an invalid start sequence.");
public static void FailedToDeserializeDescriptor(ILogger<ServerComponentDeserializer> logger, Exception e) =>
_failedToDeserializeDescriptor(logger, e);
public static void FailedToFindComponent(ILogger<ServerComponentDeserializer> logger, string assemblyName, string typeName) =>
_failedToFindComponent(logger, assemblyName, typeName, null);
public static void FailedToUnprotectDescriptor(ILogger<ServerComponentDeserializer> logger, Exception e) =>
_failedToUnprotectDescriptor(logger, e);
public static void InvalidMarkerType(ILogger<ServerComponentDeserializer> logger, string markerType) =>
_invalidMarkerType(logger, markerType, null);
public static void MissingMarkerDescriptor(ILogger<ServerComponentDeserializer> logger) =>
_missingMarkerDescriptor(logger, null);
public static void MismatchedInvocationId(ILogger<ServerComponentDeserializer> logger, string invocationId, string currentInvocationId) =>
_mismatchedInvocationId(logger, invocationId, currentInvocationId, null);
public static void OutOfSequenceDescriptor(ILogger<ServerComponentDeserializer> logger, int lastSequence, int sequence) =>
_outOfSequenceDescriptor(logger, lastSequence, sequence, null);
public static void DescriptorSequenceMustStartAtZero(ILogger<ServerComponentDeserializer> logger, int sequence) =>
_descriptorSequenceMustStartAtZero(logger, sequence, null);
}
}
}