Skip to content

Commit e42ef8e

Browse files
Wire up utils
1 parent 879f990 commit e42ef8e

File tree

6 files changed

+76
-57
lines changed

6 files changed

+76
-57
lines changed

src/ImageSharp.Web/Commands/PresetOnlyQueryCollectionRequestParser.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ public PresetOnlyQueryCollectionRequestParser(IOptions<PresetOnlyQueryCollection
3333
/// <inheritdoc/>
3434
public CommandCollection ParseRequestCommands(HttpContext context)
3535
{
36-
if (context.Request.Query.Count == 0 || !context.Request.Query.ContainsKey(QueryKey))
36+
IQueryCollection queryCollection = context.Request.Query;
37+
if (queryCollection is null
38+
|| queryCollection.Count == 0
39+
|| !queryCollection.ContainsKey(QueryKey))
3740
{
3841
// We return new here and below to ensure the collection is still mutable via events.
3942
return new();
4043
}
4144

42-
StringValues query = context.Request.Query[QueryKey];
45+
StringValues query = queryCollection[QueryKey];
4346
string requestedPreset = query[query.Count - 1];
4447
if (this.presets.TryGetValue(requestedPreset, out CommandCollection collection))
4548
{

src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public sealed class QueryCollectionRequestParser : IRequestParser
1515
/// <inheritdoc/>
1616
public CommandCollection ParseRequestCommands(HttpContext context)
1717
{
18-
if (context.Request.Query.Count == 0)
18+
IQueryCollection query = context.Request.Query;
19+
if (query is null || query.Count == 0)
1920
{
2021
// We return new to ensure the collection is still mutable via events.
2122
return new();

src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ private static void AddDefaultServices(
6060

6161
builder.SetRequestParser<QueryCollectionRequestParser>();
6262

63+
builder.Services.AddSingleton<ImageSharpRequestAuthorizationUtilities>();
64+
6365
builder.SetCache<PhysicalFileSystemCache>();
6466

6567
builder.SetCacheKey<UriRelativeLowerInvariantCacheKey>();

src/ImageSharp.Web/ImageSharpRequestAuthorizationUtilities.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@ public ImageSharpRequestAuthorizationUtilities(
5454
Guard.NotNull(options, nameof(options));
5555
Guard.NotNull(requestParser, nameof(requestParser));
5656
Guard.NotNull(processors, nameof(processors));
57+
Guard.NotNull(commandParser, nameof(commandParser));
5758
Guard.NotNull(serviceProvider, nameof(serviceProvider));
5859

5960
this.options = options.Value;
61+
this.requestParser = requestParser;
6062
this.commandParser = commandParser;
6163
this.parserCulture = this.options.UseInvariantParsingCulture
6264
? CultureInfo.InvariantCulture
6365
: CultureInfo.CurrentCulture;
66+
this.serviceProvider = serviceProvider;
6467

6568
HashSet<string> commands = new(StringComparer.OrdinalIgnoreCase);
6669
foreach (IImageWebProcessor processor in processors)
@@ -101,7 +104,7 @@ public void StripUnknownCommands(CommandCollection commands)
101104
/// <param name="handling">The command collection handling.</param>
102105
/// <returns>The computed HMAC.</returns>
103106
public string ComputeHMAC(string uri, CommandHandling handling)
104-
=> this.ComputeHMAC(new Uri(uri), handling);
107+
=> this.ComputeHMAC(new Uri(uri, UriKind.RelativeOrAbsolute), handling);
105108

106109
/// <summary>
107110
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
@@ -110,7 +113,7 @@ public string ComputeHMAC(string uri, CommandHandling handling)
110113
/// <param name="handling">The command collection handling.</param>
111114
/// <returns>The computed HMAC.</returns>
112115
public Task<string> ComputeHMACAsync(string uri, CommandHandling handling)
113-
=> this.ComputeHMACAsync(new Uri(uri), handling);
116+
=> this.ComputeHMACAsync(new Uri(uri, UriKind.RelativeOrAbsolute), handling);
114117

115118
/// <summary>
116119
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
@@ -155,7 +158,7 @@ public Task<string> ComputeHMACAsync(Uri uri, CommandHandling handling)
155158
/// <param name="handling">The command collection handling.</param>
156159
/// <returns>The computed HMAC.</returns>
157160
public string ComputeHMAC(HostString host, PathString path, QueryString queryString, CommandHandling handling)
158-
=> this.ComputeHMAC(host, path, queryString, handling);
161+
=> this.ComputeHMAC(host, path, queryString, new(QueryHelpers.ParseQuery(queryString.Value)), handling);
159162

160163
/// <summary>
161164
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
@@ -225,6 +228,29 @@ public async Task<string> ComputeHMACAsync(HttpContext context, CommandHandling
225228
return await this.options.OnComputeHMACAsync(imageCommandContext, secret);
226229
}
227230

231+
/// <summary>
232+
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
233+
/// </summary>
234+
/// <param name="context">Contains information about the current image request and parsed commands.</param>
235+
/// <param name="handling">The command collection handling.</param>
236+
/// <returns>The computed HMAC.</returns>
237+
internal async Task<string> ComputeHMACAsync(ImageCommandContext context, CommandHandling handling)
238+
{
239+
byte[] secret = this.options.HMACSecretKey;
240+
if (secret is null || secret.Length == 0)
241+
{
242+
return null;
243+
}
244+
245+
CommandCollection commands = this.requestParser.ParseRequestCommands(context.Context);
246+
if (handling == CommandHandling.Sanitize)
247+
{
248+
this.StripUnknownCommands(commands);
249+
}
250+
251+
return await this.options.OnComputeHMACAsync(context, secret);
252+
}
253+
228254
private static void ToComponents(
229255
Uri uri,
230256
out HostString host,

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,6 @@ private static readonly ConcurrentTLruCache<string, string> HMACTokenLru
9292
/// </summary>
9393
private readonly ICacheHash cacheHash;
9494

95-
/// <summary>
96-
/// The collection of known commands gathered from the processors.
97-
/// </summary>
98-
private readonly HashSet<string> knownCommands;
99-
10095
/// <summary>
10196
/// Contains various helper methods based on the current configuration.
10297
/// </summary>
@@ -117,6 +112,11 @@ private static readonly ConcurrentTLruCache<string, string> HMACTokenLru
117112
/// </summary>
118113
private readonly AsyncKeyReaderWriterLock<string> asyncKeyLock;
119114

115+
/// <summary>
116+
/// Contains helpers that allow authorization of image requests.
117+
/// </summary>
118+
private readonly ImageSharpRequestAuthorizationUtilities authorizationUtilities;
119+
120120
/// <summary>
121121
/// Initializes a new instance of the <see cref="ImageSharpMiddleware"/> class.
122122
/// </summary>
@@ -131,7 +131,8 @@ private static readonly ConcurrentTLruCache<string, string> HMACTokenLru
131131
/// <param name="cacheHash">An <see cref="ICacheHash"/>instance used for calculating cached file names.</param>
132132
/// <param name="commandParser">The command parser.</param>
133133
/// <param name="formatUtilities">Contains various format helper methods based on the current configuration.</param>
134-
/// <param name="asyncKeyLock">The async key lock</param>
134+
/// <param name="asyncKeyLock">The async key lock.</param>
135+
/// <param name="requestAuthorizationUtilities">Contains helpers that allow authorization of image requests.</param>
135136
public ImageSharpMiddleware(
136137
RequestDelegate next,
137138
IOptions<ImageSharpMiddlewareOptions> options,
@@ -144,7 +145,8 @@ public ImageSharpMiddleware(
144145
ICacheHash cacheHash,
145146
CommandParser commandParser,
146147
FormatUtilities formatUtilities,
147-
AsyncKeyReaderWriterLock<string> asyncKeyLock)
148+
AsyncKeyReaderWriterLock<string> asyncKeyLock,
149+
ImageSharpRequestAuthorizationUtilities requestAuthorizationUtilities)
148150
{
149151
Guard.NotNull(next, nameof(next));
150152
Guard.NotNull(options, nameof(options));
@@ -158,6 +160,7 @@ public ImageSharpMiddleware(
158160
Guard.NotNull(commandParser, nameof(commandParser));
159161
Guard.NotNull(formatUtilities, nameof(formatUtilities));
160162
Guard.NotNull(asyncKeyLock, nameof(asyncKeyLock));
163+
Guard.NotNull(requestAuthorizationUtilities, nameof(requestAuthorizationUtilities));
161164

162165
this.next = next;
163166
this.options = options.Value;
@@ -172,20 +175,10 @@ public ImageSharpMiddleware(
172175
? CultureInfo.InvariantCulture
173176
: CultureInfo.CurrentCulture;
174177

175-
var commands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
176-
foreach (IImageWebProcessor processor in this.processors)
177-
{
178-
foreach (string command in processor.Commands)
179-
{
180-
commands.Add(command);
181-
}
182-
}
183-
184-
this.knownCommands = commands;
185-
186178
this.logger = loggerFactory.CreateLogger<ImageSharpMiddleware>();
187179
this.formatUtilities = formatUtilities;
188180
this.asyncKeyLock = asyncKeyLock;
181+
this.authorizationUtilities = requestAuthorizationUtilities;
189182
}
190183

191184
/// <summary>
@@ -197,31 +190,6 @@ public ImageSharpMiddleware(
197190

198191
private async Task Invoke(HttpContext httpContext, bool retry)
199192
{
200-
CommandCollection commands = this.requestParser.ParseRequestCommands(httpContext);
201-
202-
// First check for a HMAC token and capture before the command is stripped out.
203-
byte[] secret = this.options.HMACSecretKey;
204-
bool checkHMAC = false;
205-
string token = null;
206-
if (secret?.Length > 0)
207-
{
208-
checkHMAC = true;
209-
token = commands.GetValueOrDefault(HMACUtilities.TokenCommand);
210-
}
211-
212-
if (commands.Count > 0)
213-
{
214-
// Strip out any unknown commands, if needed.
215-
var keys = new List<string>(commands.Keys);
216-
for (int i = keys.Count - 1; i >= 0; i--)
217-
{
218-
if (!this.knownCommands.Contains(keys[i]))
219-
{
220-
commands.RemoveAt(i);
221-
}
222-
}
223-
}
224-
225193
// Get the correct provider for the request
226194
IImageProvider provider = null;
227195
foreach (IImageProvider resolver in this.providers)
@@ -240,6 +208,19 @@ private async Task Invoke(HttpContext httpContext, bool retry)
240208
return;
241209
}
242210

211+
CommandCollection commands = this.requestParser.ParseRequestCommands(httpContext);
212+
213+
// First check for a HMAC token and capture before the command is stripped out.
214+
byte[] secret = this.options.HMACSecretKey;
215+
bool checkHMAC = false;
216+
string token = null;
217+
if (secret?.Length > 0)
218+
{
219+
checkHMAC = true;
220+
token = commands.GetValueOrDefault(ImageSharpRequestAuthorizationUtilities.TokenCommand);
221+
}
222+
223+
this.authorizationUtilities.StripUnknownCommands(commands);
243224
ImageCommandContext imageCommandContext = new(httpContext, commands, this.commandParser, this.parserCulture);
244225

245226
// At this point we know that this is an image request so should attempt to compute a validating HMAC.
@@ -253,7 +234,9 @@ private async Task Invoke(HttpContext httpContext, bool retry)
253234
//
254235
// As a rule all image requests should contain valid commands only.
255236
// Key generation uses string.Create under the hood with very low allocation so should be good enough as a cache key.
256-
hmac = await HMACTokenLru.GetOrAddAsync(httpContext.Request.GetEncodedUrl(), _ => this.options.OnComputeHMACAsync(imageCommandContext, secret));
237+
hmac = await HMACTokenLru.GetOrAddAsync(
238+
httpContext.Request.GetEncodedUrl(),
239+
_ => this.authorizationUtilities.ComputeHMACAsync(imageCommandContext, CommandHandling.None));
257240
}
258241

259242
await this.options.OnParseCommandsAsync.Invoke(imageCommandContext);

tests/ImageSharp.Web.Tests/TestUtilities/AuthenticatedServerTestBase.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

4-
using System;
54
using System.Net;
65
using System.Net.Http;
76
using System.Threading.Tasks;
7+
using Microsoft.Extensions.DependencyInjection;
88
using Xunit;
99
using Xunit.Abstractions;
1010

@@ -13,9 +13,16 @@ namespace SixLabors.ImageSharp.Web.Tests.TestUtilities
1313
public abstract class AuthenticatedServerTestBase<TFixture> : ServerTestBase<TFixture>
1414
where TFixture : AuthenticatedTestServerFixture
1515
{
16+
private readonly ImageSharpRequestAuthorizationUtilities authorizationUtilities;
17+
private readonly string relativeImageSouce;
18+
1619
protected AuthenticatedServerTestBase(TFixture fixture, ITestOutputHelper outputHelper, string imageSource)
1720
: base(fixture, outputHelper, imageSource)
1821
{
22+
this.authorizationUtilities =
23+
this.Fixture.Services.GetRequiredService<ImageSharpRequestAuthorizationUtilities>();
24+
25+
this.relativeImageSouce = this.ImageSource.Replace("http://localhost", string.Empty);
1926
}
2027

2128
[Fact]
@@ -38,12 +45,9 @@ public async Task CanRejectUnauthorizedRequestAsync()
3845

3946
protected override string AugmentCommand(string command)
4047
{
41-
// Mimic the lowecase relative url format used by the token and default options.
42-
string uri = (this.ImageSource + command).Replace("http://localhost", string.Empty);
43-
uri = CaseHandlingUriBuilder.Encode(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, uri);
44-
45-
string token = HMACUtilities.ComputeHMACSHA256(uri, AuthenticatedTestServerFixture.HMACSecretKey);
46-
return command + "&" + HMACUtilities.TokenCommand + "=" + token;
48+
string uri = this.relativeImageSouce + command;
49+
string token = this.authorizationUtilities.ComputeHMAC(uri, CommandHandling.Sanitize);
50+
return command + "&" + ImageSharpRequestAuthorizationUtilities.TokenCommand + "=" + token;
4751
}
4852
}
4953
}

0 commit comments

Comments
 (0)