Skip to content

Commit 06f99a4

Browse files
Sketch out auth utils class
1 parent 2b1450d commit 06f99a4

File tree

7 files changed

+195
-33
lines changed

7 files changed

+195
-33
lines changed

src/ImageSharp.Web.Providers.AWS/AsyncHelper.cs renamed to src/ImageSharp.Web/AsyncHelper.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ namespace SixLabors.ImageSharp.Web
1313
/// </summary>
1414
internal static class AsyncHelper
1515
{
16-
private static readonly TaskFactory TaskFactory = new
17-
(CancellationToken.None,
18-
TaskCreationOptions.None,
19-
TaskContinuationOptions.None,
20-
TaskScheduler.Default);
16+
private static readonly TaskFactory TaskFactory
17+
= new(
18+
CancellationToken.None,
19+
TaskCreationOptions.None,
20+
TaskContinuationOptions.None,
21+
TaskScheduler.Default);
2122

2223
/// <summary>
2324
/// Executes an async <see cref="Task"/> method synchronously.

src/ImageSharp.Web/CaseHandlingUriBuilder.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Web
1414
/// </summary>
1515
public static class CaseHandlingUriBuilder
1616
{
17+
private static readonly Uri FallbackBaseUri = new("http://localhost/");
1718
private static readonly SpanAction<char, (bool LowerInvariant, string Host, string PathBase, string Path, string Query)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);
1819

1920
/// <summary>
@@ -127,13 +128,11 @@ public static string Encode(CaseHandling handling, Uri uri)
127128
}
128129
else
129130
{
130-
string components = uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
131-
if (handling == CaseHandling.LowerInvariant)
132-
{
133-
return components.ToLowerInvariant();
134-
}
135-
136-
return components;
131+
Uri faux = new(FallbackBaseUri, uri);
132+
return BuildRelative(
133+
handling,
134+
pathBase: PathString.FromUriComponent(faux),
135+
query: QueryString.FromUriComponent(faux));
137136
}
138137
}
139138

src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Collections.Generic;
55
using Microsoft.AspNetCore.Http;
6-
using Microsoft.AspNetCore.WebUtilities;
76
using Microsoft.Extensions.Primitives;
87

98
namespace SixLabors.ImageSharp.Web.Commands
@@ -22,11 +21,8 @@ public CommandCollection ParseRequestCommands(HttpContext context)
2221
return new();
2322
}
2423

25-
// TODO: Investigate skipping the double allocation here.
26-
// In .NET 6 we can directly use the QueryStringEnumerable type and enumerate stright to our command collection
27-
Dictionary<string, StringValues> parsed = QueryHelpers.ParseQuery(context.Request.QueryString.ToUriComponent());
2824
CommandCollection transformed = new();
29-
foreach (KeyValuePair<string, StringValues> pair in parsed)
25+
foreach (KeyValuePair<string, StringValues> pair in context.Request.Query)
3026
{
3127
// Use the indexer for both set and query. This replaces any previously parsed values.
3228
transformed[pair.Key] = pair.Value[pair.Value.Count - 1];

src/ImageSharp.Web/FormatUtilities.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ public FormatUtilities(IOptions<ImageSharpMiddlewareOptions> options)
3434
{
3535
string[] extensions = imageFormat.FileExtensions.ToArray();
3636

37-
foreach (string extension in extensions)
38-
{
39-
this.extensions.Add(extension);
40-
}
37+
this.extensions.AddRange(extensions);
4138

4239
this.extensionsByMimeType[imageFormat.DefaultMimeType] = extensions[0];
4340
}

src/ImageSharp.Web/ImageSharp.Web.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
3838
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
39+
<PackageReference Include="Microsoft.AspNetCore.Http" />
3940
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
4041
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
4142
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Runtime.CompilerServices;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Http;
9+
10+
#if !NETCOREAPP3_0_OR_GREATER
11+
using Microsoft.AspNetCore.Http.Internal;
12+
#endif
13+
14+
using Microsoft.AspNetCore.WebUtilities;
15+
using Microsoft.Extensions.Options;
16+
using SixLabors.ImageSharp.Web.Commands;
17+
using SixLabors.ImageSharp.Web.Middleware;
18+
using SixLabors.ImageSharp.Web.Processors;
19+
20+
namespace SixLabors.ImageSharp.Web
21+
{
22+
/// <summary>
23+
/// Contains various helper methods for authorizing image requests.
24+
/// </summary>
25+
public sealed class ImageSharpRequestAuthorizationUtilities
26+
{
27+
/// <summary>
28+
/// The command used by image requests for transporting Hash-based Message Authentication Code (HMAC) tokens.
29+
/// </summary>
30+
public const string TokenCommand = HMACUtilities.TokenCommand;
31+
32+
private static readonly Uri FallbackBaseUri = new("http://localhost/");
33+
private readonly HashSet<string> knownCommands;
34+
private readonly ImageSharpMiddlewareOptions options;
35+
private readonly IRequestParser requestParser;
36+
private readonly IServiceProvider serviceProvider;
37+
38+
/// <summary>
39+
/// Initializes a new instance of the <see cref="ImageSharpRequestAuthorizationUtilities"/> class.
40+
/// </summary>
41+
/// <param name="options">The middleware configuration options.</param>
42+
/// <param name="requestParser">An <see cref="IRequestParser"/> instance used to parse image requests for commands.</param>
43+
/// <param name="processors">A collection of <see cref="IImageWebProcessor"/> instances used to process images.</param>
44+
/// <param name="serviceProvider">The service provider.</param>
45+
public ImageSharpRequestAuthorizationUtilities(
46+
IOptions<ImageSharpMiddlewareOptions> options,
47+
IRequestParser requestParser,
48+
IEnumerable<IImageWebProcessor> processors,
49+
IServiceProvider serviceProvider)
50+
{
51+
Guard.NotNull(options, nameof(options));
52+
Guard.NotNull(requestParser, nameof(requestParser));
53+
Guard.NotNull(processors, nameof(processors));
54+
Guard.NotNull(serviceProvider, nameof(serviceProvider));
55+
56+
HashSet<string> commands = new(StringComparer.OrdinalIgnoreCase);
57+
foreach (IImageWebProcessor processor in processors)
58+
{
59+
foreach (string command in processor.Commands)
60+
{
61+
commands.Add(command);
62+
}
63+
}
64+
65+
this.knownCommands = commands;
66+
this.options = options.Value;
67+
}
68+
69+
/// <summary>
70+
/// Strips any unknown commands from the command collection.
71+
/// </summary>
72+
/// <param name="commands">The unsanitized command collection.</param>
73+
public void StripUnknownCommands(CommandCollection commands)
74+
{
75+
if (commands?.Count > 0)
76+
{
77+
// Strip out any unknown commands, if needed.
78+
var keys = new List<string>(commands.Keys);
79+
for (int i = keys.Count - 1; i >= 0; i--)
80+
{
81+
if (!this.knownCommands.Contains(keys[i]))
82+
{
83+
commands.RemoveAt(i);
84+
}
85+
}
86+
}
87+
}
88+
89+
/// <summary>
90+
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
91+
/// </summary>
92+
/// <param name="uri">The uri to compute the code from.</param>
93+
/// <returns>The computed HMAC.</returns>
94+
public string ComputeHMAC(string uri)
95+
=> this.ComputeHMAC(new Uri(uri));
96+
97+
/// <summary>
98+
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
99+
/// </summary>
100+
/// <param name="uri">The uri to compute the code from.</param>
101+
/// <returns>The computed HMAC.</returns>
102+
public string ComputeHMAC(Uri uri)
103+
=> AsyncHelper.RunSync(() => this.ComputeHMACAsync(uri));
104+
105+
/// <summary>
106+
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
107+
/// </summary>
108+
/// <param name="uri">The uri to compute the code from.</param>
109+
/// <returns>The computed HMAC.</returns>
110+
public Task<string> ComputeHMACAsync(string uri)
111+
=> this.ComputeHMACAsync(new Uri(uri));
112+
113+
/// <summary>
114+
/// Compute a Hash-based Message Authentication Code (HMAC) for request authentication.
115+
/// </summary>
116+
/// <param name="uri">The uri to compute the code from.</param>
117+
/// <returns>The computed HMAC.</returns>
118+
public async Task<string> ComputeHMACAsync(Uri uri)
119+
{
120+
byte[] secret = this.options.HMACSecretKey;
121+
if (secret is null || secret.Length == 0)
122+
{
123+
return null;
124+
}
125+
126+
// We need to generate a HttpRequest to use the rest of the services.
127+
DefaultHttpContext context = new() { RequestServices = this.serviceProvider };
128+
HttpRequest request = context.Request;
129+
130+
ToComponents(
131+
uri,
132+
out HostString host,
133+
out PathString path,
134+
out QueryString queryString,
135+
out QueryCollection query);
136+
137+
request.Host = host;
138+
request.Path = path;
139+
request.QueryString = queryString;
140+
request.Query = query;
141+
142+
CommandCollection commands = this.requestParser.ParseRequestCommands(context);
143+
144+
// The provided URI should not contain invalid commands since image URI geneneration
145+
// should be tightly controlled by the running application but we will strip any out anyway.
146+
this.StripUnknownCommands(commands);
147+
148+
ImageCommandContext imageCommandContext = new(context, commands, null, null);
149+
return await this.options.OnComputeHMACAsync(imageCommandContext, secret);
150+
}
151+
152+
private static void ToComponents(
153+
Uri uri,
154+
out HostString host,
155+
out PathString path,
156+
out QueryString queryString,
157+
out QueryCollection query)
158+
{
159+
if (uri.IsAbsoluteUri)
160+
{
161+
host = HostString.FromUriComponent(uri);
162+
path = PathString.FromUriComponent(uri);
163+
queryString = QueryString.FromUriComponent(uri);
164+
query = GetQueryComponent(queryString);
165+
}
166+
else
167+
{
168+
Uri faux = new(FallbackBaseUri, uri);
169+
host = default;
170+
path = PathString.FromUriComponent(faux);
171+
queryString = QueryString.FromUriComponent(faux);
172+
query = GetQueryComponent(queryString);
173+
}
174+
}
175+
176+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
177+
private static QueryCollection GetQueryComponent(QueryString query)
178+
=> new(QueryHelpers.ParseQuery(query.Value));
179+
}
180+
}

src/ImageSharp.Web/PathUtilities.cs

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

4-
using System;
54
using System.IO;
65

76
namespace SixLabors.ImageSharp.Web
@@ -25,16 +24,5 @@ internal static string EnsureTrailingSlash(string path)
2524

2625
return path;
2726
}
28-
29-
/// <summary>
30-
/// Determines whether the <paramref name="path" /> is located underneath the specified <paramref name="rootPath" />.
31-
/// </summary>
32-
/// <param name="path">The fully qualified path to test.</param>
33-
/// <param name="rootPath">The root path (needs to end with a directory separator).</param>
34-
/// <returns>
35-
/// <c>true</c> if the path is located underneath the specified root path; otherwise, <c>false</c>.
36-
/// </returns>
37-
internal static bool IsUnderneathRoot(string path, string rootPath)
38-
=> path.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase);
3927
}
4028
}

0 commit comments

Comments
 (0)