Skip to content

Commit 884f76e

Browse files
Split out HMAC helper, remove HMAC from request with no commands
1 parent 5c76bfa commit 884f76e

File tree

7 files changed

+1160
-855
lines changed

7 files changed

+1160
-855
lines changed

samples/ImageSharp.Web.Sample/Pages/Index.cshtml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@
6060
</div>
6161
</section>
6262
<section>
63-
<h2>HMAC - No Commands</h2>
63+
<h2>No Commands</h2>
6464
<div>
6565
<p>
66-
<code>sixlabors.imagesharp.web.png?hmac=...</code>
66+
<code>sixlabors.imagesharp.web.png</code>
6767
</p>
6868
<p>
69-
<img src="sixlabors.imagesharp.web.png" width="300" imagesharp-hmac />
69+
<img src="sixlabors.imagesharp.web.png" width="300" />
7070
</p>
7171
</div>
7272
</section>

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,10 @@ private async Task Invoke(HttpContext httpContext, bool retry)
244244

245245
// At this point we know that this is an image request designed for processing via this middleware.
246246
// Check for a token if required and reject if invalid.
247-
if (checkHMAC)
247+
if (checkHMAC && hmac != token)
248248
{
249-
if (token == null || hmac != token)
250-
{
251-
SetBadRequest(httpContext);
252-
return;
253-
}
249+
SetBadRequest(httpContext);
250+
return;
254251
}
255252

256253
IImageResolver? sourceImageResolver = await provider.GetAsync(httpContext);

src/ImageSharp.Web/RequestAuthorizationUtilities.cs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -216,22 +216,13 @@ public void StripUnknownCommands(CommandCollection commands)
216216
this.StripUnknownCommands(commands);
217217
}
218218

219-
ImageCommandContext imageCommandContext = new(context, commands, this.commandParser, this.parserCulture);
220-
return await this.options.OnComputeHMACAsync(imageCommandContext, secret);
221-
}
222-
223-
internal string ComputeHMAC(string uri, CommandCollection commands, byte[] secret)
224-
{
225-
ToComponents(
226-
new Uri(uri, UriKind.RelativeOrAbsolute),
227-
out HostString host,
228-
out PathString path,
229-
out QueryString queryString);
219+
if (commands.Count == 0)
220+
{
221+
return null;
222+
}
230223

231-
HttpContext context = this.ToHttpContext(host, path, queryString, new(QueryHelpers.ParseQuery(queryString.Value)));
232224
ImageCommandContext imageCommandContext = new(context, commands, this.commandParser, this.parserCulture);
233-
234-
return AsyncHelper.RunSync(() => this.options.OnComputeHMACAsync(imageCommandContext, secret));
225+
return await this.options.OnComputeHMACAsync(imageCommandContext, secret);
235226
}
236227

237228
/// <summary>
@@ -243,8 +234,15 @@ internal string ComputeHMAC(string uri, CommandCollection commands, byte[] secre
243234
/// </remarks>
244235
/// <param name="context">Contains information about the current image request and parsed commands.</param>
245236
/// <returns>The computed HMAC.</returns>
246-
internal Task<string> ComputeHMACAsync(ImageCommandContext context)
247-
=> this.options.OnComputeHMACAsync(context, this.options.HMACSecretKey);
237+
internal async Task<string?> ComputeHMACAsync(ImageCommandContext context)
238+
{
239+
if (context.Commands.Count == 0)
240+
{
241+
return null;
242+
}
243+
244+
return await this.options.OnComputeHMACAsync(context, this.options.HMACSecretKey);
245+
}
248246

249247
private static void ToComponents(
250248
Uri uri,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Text;
5+
using System.Text.Encodings.Web;
6+
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
7+
using Microsoft.AspNetCore.Mvc.Routing;
8+
using Microsoft.AspNetCore.Mvc.TagHelpers;
9+
using Microsoft.AspNetCore.Razor.TagHelpers;
10+
using Microsoft.Extensions.Options;
11+
using SixLabors.ImageSharp.Web.Middleware;
12+
13+
namespace SixLabors.ImageSharp.Web.TagHelpers;
14+
15+
/// <summary>
16+
/// A <see cref="TagHelper"/> implementation targeting &lt;img&gt; element that allows the automatic generation of HMAC image processing protection tokens.
17+
/// </summary>
18+
[HtmlTargetElement("img", Attributes = SrcAttributeName, TagStructure = TagStructure.WithoutEndTag)]
19+
public class HmacTokenTagHelper : UrlResolutionTagHelper
20+
{
21+
private const string SrcAttributeName = "src";
22+
23+
private readonly ImageSharpMiddlewareOptions options;
24+
private readonly RequestAuthorizationUtilities authorizationUtilities;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="HmacTokenTagHelper" /> class.
28+
/// </summary>
29+
/// <param name="options">The middleware configuration options.</param>
30+
/// <param name="authorizationUtilities">Contains helpers that allow authorization of image requests.</param>
31+
/// <param name="urlHelperFactory">The URL helper factory.</param>
32+
/// <param name="htmlEncoder">The HTML encorder.</param>
33+
public HmacTokenTagHelper(
34+
IOptions<ImageSharpMiddlewareOptions> options,
35+
RequestAuthorizationUtilities authorizationUtilities,
36+
IUrlHelperFactory urlHelperFactory,
37+
HtmlEncoder htmlEncoder)
38+
: base(urlHelperFactory, htmlEncoder)
39+
{
40+
Guard.NotNull(options, nameof(options));
41+
Guard.NotNull(authorizationUtilities, nameof(authorizationUtilities));
42+
43+
this.options = options.Value;
44+
this.authorizationUtilities = authorizationUtilities;
45+
}
46+
47+
/// <inheritdoc/>
48+
public override int Order => 2;
49+
50+
/// <summary>
51+
/// Gets or sets the source of the image.
52+
/// </summary>
53+
/// <remarks>
54+
/// Passed through to the generated HTML in all cases.
55+
/// </remarks>
56+
[HtmlAttributeName(SrcAttributeName)]
57+
public string? Src { get; set; }
58+
59+
/// <inheritdoc />
60+
public override void Process(TagHelperContext context, TagHelperOutput output)
61+
{
62+
Guard.NotNull(context, nameof(context));
63+
Guard.NotNull(output, nameof(output));
64+
65+
output.CopyHtmlAttribute(SrcAttributeName, context);
66+
this.ProcessUrlAttribute(SrcAttributeName, output);
67+
68+
byte[] secret = this.options.HMACSecretKey;
69+
if (secret is null || secret.Length == 0)
70+
{
71+
return;
72+
}
73+
74+
// Retrieve the TagHelperOutput variation of the "src" attribute in case other TagHelpers in the
75+
// pipeline have touched the value. If the value is already encoded this ImageTagHelper may
76+
// not function properly.
77+
string? src = output.Attributes[SrcAttributeName]?.Value as string;
78+
if (string.IsNullOrWhiteSpace(src))
79+
{
80+
return;
81+
}
82+
83+
string? hmac = this.authorizationUtilities.ComputeHMAC(src, CommandHandling.Sanitize);
84+
if (hmac is not null)
85+
{
86+
this.Src = AddQueryString(src, hmac);
87+
output.Attributes.SetAttribute(SrcAttributeName, this.Src);
88+
}
89+
}
90+
91+
private static string AddQueryString(
92+
ReadOnlySpan<char> uri,
93+
string hmac)
94+
{
95+
ReadOnlySpan<char> uriToBeAppended = uri;
96+
ReadOnlySpan<char> anchorText = default;
97+
98+
// If there is an anchor, then the query string must be inserted before its first occurrence.
99+
int anchorIndex = uri.IndexOf('#');
100+
if (anchorIndex != -1)
101+
{
102+
anchorText = uri[anchorIndex..];
103+
uriToBeAppended = uri[..anchorIndex];
104+
}
105+
106+
int queryIndex = uriToBeAppended.IndexOf('?');
107+
bool hasQuery = queryIndex != -1;
108+
109+
StringBuilder sb = new();
110+
111+
sb.Append(uriToBeAppended)
112+
.Append(hasQuery ? '&' : '?')
113+
.Append(UrlEncoder.Default.Encode(RequestAuthorizationUtilities.TokenCommand))
114+
.Append('=')
115+
.Append(UrlEncoder.Default.Encode(hmac))
116+
.Append(anchorText);
117+
118+
return sb.ToString();
119+
}
120+
}

src/ImageSharp.Web/TagHelpers/ImageTagHelper.cs

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,41 @@
1616
namespace SixLabors.ImageSharp.Web.TagHelpers;
1717

1818
/// <summary>
19-
/// A TagHelper implementation targeting &lt;img&gt; element that allows the automatic generation of HMAC protected processing commands.
19+
/// A <see cref="TagHelper"/> implementation targeting &lt;img&gt; element that allows the automatic generation image processing commands.
2020
/// </summary>
2121
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + WidthAttributeName, TagStructure = TagStructure.WithoutEndTag)]
2222
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + HeightAttributeName, TagStructure = TagStructure.WithoutEndTag)]
2323
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + AnchorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
24-
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + RModeAttributeName, TagStructure = TagStructure.WithoutEndTag)]
24+
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + ModeAttributeName, TagStructure = TagStructure.WithoutEndTag)]
2525
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + XyAttributeName, TagStructure = TagStructure.WithoutEndTag)]
26-
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + RColorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
26+
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + ColorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
2727
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + CompandAttributeName, TagStructure = TagStructure.WithoutEndTag)]
2828
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + OrientAttributeName, TagStructure = TagStructure.WithoutEndTag)]
2929
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + AutoOrientAttributeName, TagStructure = TagStructure.WithoutEndTag)]
3030
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + FormatAttributeName, TagStructure = TagStructure.WithoutEndTag)]
3131
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + BgColorAttributeName, TagStructure = TagStructure.WithoutEndTag)]
3232
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + QualityAttributeName, TagStructure = TagStructure.WithoutEndTag)]
33-
[HtmlTargetElement("img", Attributes = SrcAttributeName + "," + HMACAttributeName, TagStructure = TagStructure.WithoutEndTag)]
3433
public class ImageTagHelper : UrlResolutionTagHelper
3534
{
3635
private const string SrcAttributeName = "src";
3736
private const string AttributePrefix = "imagesharp-";
3837
private const string WidthAttributeName = AttributePrefix + ResizeWebProcessor.Width;
3938
private const string HeightAttributeName = AttributePrefix + ResizeWebProcessor.Height;
4039
private const string AnchorAttributeName = AttributePrefix + ResizeWebProcessor.Anchor;
41-
private const string RModeAttributeName = AttributePrefix + ResizeWebProcessor.Mode;
40+
private const string ModeAttributeName = AttributePrefix + ResizeWebProcessor.Mode;
4241
private const string XyAttributeName = AttributePrefix + ResizeWebProcessor.Xy;
43-
private const string RColorAttributeName = AttributePrefix + ResizeWebProcessor.Color;
42+
private const string ColorAttributeName = AttributePrefix + ResizeWebProcessor.Color;
4443
private const string CompandAttributeName = AttributePrefix + ResizeWebProcessor.Compand;
4544
private const string OrientAttributeName = AttributePrefix + ResizeWebProcessor.Orient;
4645
private const string SamplerAttributeName = AttributePrefix + ResizeWebProcessor.Sampler;
4746
private const string AutoOrientAttributeName = AttributePrefix + AutoOrientWebProcessor.AutoOrient;
4847
private const string FormatAttributeName = AttributePrefix + FormatWebProcessor.Format;
4948
private const string BgColorAttributeName = AttributePrefix + BackgroundColorWebProcessor.Color;
5049
private const string QualityAttributeName = AttributePrefix + QualityWebProcessor.Quality;
51-
private const string HMACAttributeName = AttributePrefix + RequestAuthorizationUtilities.TokenCommand;
5250

5351
private readonly ImageSharpMiddlewareOptions options;
5452
private readonly CultureInfo parserCulture;
5553
private readonly char separator;
56-
private readonly RequestAuthorizationUtilities authorizationUtilities;
5754

5855
/// <summary>
5956
/// Initializes a new instance of the <see cref="ImageTagHelper"/> class.
@@ -77,10 +74,11 @@ public ImageTagHelper(
7774
? CultureInfo.InvariantCulture
7875
: CultureInfo.CurrentCulture;
7976
this.separator = this.parserCulture.TextInfo.ListSeparator[0];
80-
81-
this.authorizationUtilities = authorizationUtilities;
8277
}
8378

79+
/// <inheritdoc/>
80+
public override int Order => 1;
81+
8482
/// <summary>
8583
/// Gets or sets the src.
8684
/// </summary>
@@ -111,7 +109,7 @@ public ImageTagHelper(
111109
/// <summary>
112110
/// Gets or sets the resize mode.
113111
/// </summary>
114-
[HtmlAttributeName(RModeAttributeName)]
112+
[HtmlAttributeName(ModeAttributeName)]
115113
public ResizeMode? ResizeMode { get; set; }
116114

117115
/// <summary>
@@ -129,7 +127,7 @@ public ImageTagHelper(
129127
/// <summary>
130128
/// Gets or sets the color to use as a background when padding an image.
131129
/// </summary>
132-
[HtmlAttributeName(RColorAttributeName)]
130+
[HtmlAttributeName(ColorAttributeName)]
133131
public Color? PadColor { get; set; }
134132

135133
/// <summary>
@@ -180,24 +178,17 @@ public ImageTagHelper(
180178
[HtmlAttributeName(QualityAttributeName)]
181179
public int? Quality { get; set; }
182180

183-
/// <summary>
184-
/// Gets or sets a value indicating whether to append a HMAC token to the request.
185-
/// This value is always <see langword="true"/>. HMAC token usage is controlled by populating the
186-
/// <see cref="ImageSharpMiddlewareOptions.HMACSecretKey"/> property.
187-
/// </summary>
188-
[HtmlAttributeName(HMACAttributeName)]
189-
#pragma warning disable CA1822 // Mark members as static
190-
public bool AppendHMAC { get => true; set => _ = true; }
191-
#pragma warning restore CA1822 // Mark members as static
192-
193181
/// <inheritdoc />
194182
public override void Process(TagHelperContext context, TagHelperOutput output)
195183
{
196184
Guard.NotNull(context, nameof(context));
197185
Guard.NotNull(output, nameof(output));
198186

199187
string? src = output.Attributes[SrcAttributeName]?.Value as string ?? this.Src;
200-
if (string.IsNullOrWhiteSpace(src) || src.StartsWith("data", StringComparison.OrdinalIgnoreCase))
188+
if (string.IsNullOrWhiteSpace(src)
189+
|| src.StartsWith("http", StringComparison.OrdinalIgnoreCase)
190+
|| src.StartsWith("ftp", StringComparison.OrdinalIgnoreCase)
191+
|| src.StartsWith("data", StringComparison.OrdinalIgnoreCase))
201192
{
202193
base.Process(context, output);
203194
return;
@@ -209,24 +200,16 @@ public override void Process(TagHelperContext context, TagHelperOutput output)
209200
CommandCollection commands = new();
210201
this.AddProcessingCommands(context, output, commands, this.parserCulture);
211202

212-
byte[] secret = this.options.HMACSecretKey;
213-
if (commands.Count > 0 || secret?.Length > 0)
203+
if (commands.Count > 0)
214204
{
215205
// Retrieve the TagHelperOutput variation of the "src" attribute in case other TagHelpers in the
216206
// pipeline have touched the value. If the value is already encoded this helper may
217207
// not function properly.
218-
src = output.Attributes[SrcAttributeName].Value as string;
219-
if (secret?.Length > 0)
220-
{
221-
string hash = this.authorizationUtilities.ComputeHMAC(src!, commands, secret);
222-
commands.Add(RequestAuthorizationUtilities.TokenCommand, hash);
223-
}
224-
208+
src = output.Attributes[SrcAttributeName]?.Value as string;
225209
src = AddQueryString(src, commands);
210+
output.Attributes.SetAttribute(SrcAttributeName, src);
211+
this.Src = src;
226212
}
227-
228-
this.Src = src;
229-
output.Attributes.SetAttribute(SrcAttributeName, src);
230213
}
231214

232215
/// <summary>

0 commit comments

Comments
 (0)