Skip to content
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

[release/7.0-preview6] Make RateLimitingMiddleware endpoint aware #42471

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
<UsingToolNetFrameworkReferenceAssemblies Condition="'$(OS)' != 'Windows_NT'">true</UsingToolNetFrameworkReferenceAssemblies>
<!-- Disable XLIFF tasks -->
<UsingToolXliff>false</UsingToolXliff>
<!-- Use custom version of Roslyn Compiler -->
<UsingToolMicrosoftNetCompilers>true</UsingToolMicrosoftNetCompilers>
</PropertyGroup>
<!--

Expand Down Expand Up @@ -194,6 +196,10 @@
framework in current .NET SDKs.
-->
<MicrosoftNETTestSdkVersion>17.1.0-preview-20211109-03</MicrosoftNETTestSdkVersion>
<!--
Use a compiler new enough to use required keyword
-->
<MicrosoftNetCompilersToolsetVersion>4.4.0-1.22315.13</MicrosoftNetCompilersToolsetVersion>
<!--
Versions of Microsoft.CodeAnalysis packages referenced by analyzers shipped in the SDK.
This need to be pinned since they're used in 3.1 apps and need to be loadable in VS 2019.
Expand Down
98 changes: 98 additions & 0 deletions src/Middleware/RateLimiting/src/DefaultCombinedLease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;

namespace Microsoft.AspNetCore.RateLimiting;

internal sealed class DefaultCombinedLease : RateLimitLease
{
private readonly RateLimitLease? _globalLease;
private readonly RateLimitLease _endpointLease;
private HashSet<string>? _metadataNames;

public DefaultCombinedLease(RateLimitLease? globalLease, RateLimitLease endpointLease)
{
_globalLease = globalLease;
_endpointLease = endpointLease;
}

public override bool IsAcquired => true;

public override IEnumerable<string> MetadataNames
{
get
{
if (_metadataNames is null)
{
_metadataNames = new HashSet<string>();
if (_globalLease is not null)
{
foreach (string metadataName in _globalLease.MetadataNames)
{
_metadataNames.Add(metadataName);
}
}
foreach (string metadataName in _endpointLease.MetadataNames)
{
_metadataNames.Add(metadataName);
}
}
return _metadataNames;
}
}

public override bool TryGetMetadata(string metadataName, out object? metadata)
{
// Use the first metadata item of a given name, ignore duplicates, we can't reliably merge arbitrary metadata
// Creating an object[] if there are multiple of the same metadataName could work, but makes consumption of metadata messy
// and makes MetadataName.Create<T>(...) uses no longer work
if (_endpointLease.TryGetMetadata(metadataName, out metadata))
{
return true;
}
if (_globalLease is not null)
{
if (_globalLease.TryGetMetadata(metadataName, out metadata))
{
return true;
}
}

metadata = null;
return false;
}

protected override void Dispose(bool disposing)
{
List<Exception>? exceptions = null;

// Dispose endpoint lease first, then global lease (reverse order of when they were acquired)
// Avoids issues where dispose might unblock a queued acquire and then the acquire fails when acquiring the next limiter.
// When disposing in reverse order there wont be any issues of unblocking an acquire that affects acquires on limiters in the chain after it
try
{
_endpointLease.Dispose();
}
catch (Exception ex)
{
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}

try
{
_globalLease?.Dispose();
}
catch (Exception ex)
{
exceptions ??= new List<Exception>();
exceptions.Add(ex);
}

if (exceptions is not null)
{
throw new AggregateException(exceptions);
}
}
}
19 changes: 19 additions & 0 deletions src/Middleware/RateLimiting/src/DefaultKeyType.T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.RateLimiting;

internal sealed class DefaultKeyType<TKey>: DefaultKeyType
{
private readonly TKey _key;

public DefaultKeyType(TKey key)
{
_key = key;
}

public override object? GetKey()
{
return _key;
}
}
9 changes: 9 additions & 0 deletions src/Middleware/RateLimiting/src/DefaultKeyType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.RateLimiting;

internal abstract class DefaultKeyType
{
public abstract object? GetKey();
}
44 changes: 44 additions & 0 deletions src/Middleware/RateLimiting/src/DefaultKeyTypeEqualityComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.AspNetCore.RateLimiting;
internal class DefaultKeyTypeEqualityComparer : IEqualityComparer<DefaultKeyType>
{
public bool Equals(DefaultKeyType? x, DefaultKeyType? y)
{
if (x == null && y == null)
{
return true;
}
else if (x == null || y == null)
{
return false;
}

var xKey = x.GetKey();
var yKey = y.GetKey();
if (xKey == null && yKey == null)
{
return true;
}
else if (xKey == null || yKey == null)
{
return false;
}

return xKey.Equals(yKey);
}

public int GetHashCode([DisallowNull] DefaultKeyType obj)
{
var key = obj.GetKey();
if (key is not null)
{
return key.GetHashCode();
}
// REVIEW - is this reasonable?
return default;
}
}
25 changes: 25 additions & 0 deletions src/Middleware/RateLimiting/src/DefaultRateLimiterPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.RateLimiting;
internal sealed class DefaultRateLimiterPolicy : IRateLimiterPolicy<DefaultKeyType>
{
private readonly Func<HttpContext, RateLimitPartition<DefaultKeyType>> _partitioner;
private readonly Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected;

public DefaultRateLimiterPolicy(Func<HttpContext, RateLimitPartition<DefaultKeyType>> partitioner, Func<OnRejectedContext, CancellationToken, ValueTask>? onRejected)
{
_partitioner = partitioner;
_onRejected = onRejected;
}

public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => _onRejected;

public RateLimitPartition<DefaultKeyType> GetPartition(HttpContext httpContext)
{
return _partitioner(httpContext);
}
}
15 changes: 15 additions & 0 deletions src/Middleware/RateLimiting/src/IRateLimiterMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// An interface which can be used to identify a type which provides metadata needed for enabling request rate limiting support.
/// </summary>
internal interface IRateLimiterMetadata
{
/// <summary>
/// The name of the limiter which needs to be applied.
/// </summary>
string PolicyName { get; }
}
24 changes: 24 additions & 0 deletions src/Middleware/RateLimiting/src/IRateLimiterPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// An interface which is used to represent a RateLimiter policy.
/// </summary>
public interface IRateLimiterPolicy<TPartitionKey>
{
/// <summary>
/// Gets the <see cref="Func{OnRejectedContext, CancellationToken, ValueTask}"/> that handles requests rejected by this middleware.
/// </summary>
Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; }

/// <summary>
/// Gets the <see cref="RateLimitPartition{TPartitionKey}"/> that applies to the given <see cref="HttpContext"/>.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/> to get the partition for.</param>
RateLimitPartition<TPartitionKey> GetPartition(HttpContext httpContext);
}
17 changes: 17 additions & 0 deletions src/Middleware/RateLimiting/src/LeaseContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;

namespace Microsoft.AspNetCore.RateLimiting;
internal struct LeaseContext : IDisposable
{
public bool? GlobalRejected { get; init; }

public required RateLimitLease Lease { get; init; }

public void Dispose()
{
Lease.Dispose();
}
}
36 changes: 0 additions & 36 deletions src/Middleware/RateLimiting/src/NoLimiter.cs

This file was deleted.

23 changes: 23 additions & 0 deletions src/Middleware/RateLimiting/src/OnRejectedContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Holds state needed for the OnRejected callback in the RateLimitingMiddleware.
/// </summary>
public sealed class OnRejectedContext
{
/// <summary>
/// Gets or sets the <see cref="HttpContext"/> that the OnRejected callback will have access to
/// </summary>
public required HttpContext HttpContext { get; init; }

/// <summary>
/// Gets or sets the failed <see cref="RateLimitLease"/> that the OnRejected callback will have access to
/// </summary>
public required RateLimitLease Lease { get; init; }
}
22 changes: 22 additions & 0 deletions src/Middleware/RateLimiting/src/PolicyNameKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.RateLimiting;
internal sealed class PolicyNameKey
{
public required string PolicyName { get; init; }

public override bool Equals(object? obj)
{
if (obj is PolicyNameKey key)
{
return PolicyName.Equals(key.PolicyName);
}
return false;
}

public override int GetHashCode()
{
return PolicyName.GetHashCode();
}
}
12 changes: 12 additions & 0 deletions src/Middleware/RateLimiting/src/PolicyTypeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.AspNetCore.RateLimiting;
internal sealed class PolicyTypeInfo
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public required Type PolicyType { get; init; }
public required Type PartitionKeyType { get; init; }
}
Loading