Skip to content

Commit 6441025

Browse files
committed
Breaking: Replace .SetRequireRequestSignature() and .AddSigningKey with .SetRequestSignatureOptions()
1 parent 5b4ef80 commit 6441025

File tree

6 files changed

+130
-33
lines changed

6 files changed

+130
-33
lines changed

README.md

+8-4
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,14 @@ namespace Imageflow.Server.Example
198198
.SetCommand("width", "1024")
199199
.SetCommand("height", "1024")
200200
.SetCommand("mode", "max"))
201-
//When set to true, this only allows urls with a &signature, returning 403 if missing/invalid.
202-
//Use Imazen.Common.Helpers.Signatures.SignRequest(string pathAndQuery, string signingKey) to generate
203-
.SetRequireRequestSignature(false)
204-
.AddRequestSigningKey("test key")
201+
// When set, this only allows urls with a &signature, returning 403 if missing/invalid.
202+
// Use Imazen.Common.Helpers.Signatures.SignRequest(string pathAndQuery, string signingKey) to generate
203+
//.ForPrefix allows you to set less restrictive rules for subfolders.
204+
// For example, you may want to allow unmodified requests through with SignatureRequired.ForQuerystringRequests
205+
.SetRequestSignatureOptions(
206+
new RequestSignatureOptions(SignatureRequired.ForAllRequests, new []{"test key"})
207+
.ForPrefix("/logos/", StringComparison.Ordinal,
208+
SignatureRequired.ForQuerystringRequests, new []{"test key"}))
205209
// It's a good idea to limit image sizes for security. Requests causing these to be exceeded will fail
206210
// The last argument to FrameSizeLimit() is the maximum number of megapixels
207211
.SetJobSecurityOptions(new SecurityOptions()

examples/Imageflow.Server.Example/Startup.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
127127
.SetCommand("width", "1024")
128128
.SetCommand("height", "1024")
129129
.SetCommand("mode", "max"))
130-
//When set to true, this only allows urls with a &signature, returning 403 if missing/invalid.
131-
//Use Imazen.Common.Helpers.Signatures.SignRequest(string pathAndQuery, string signingKey) to generate
132-
.SetRequireRequestSignature(false)
133-
.AddRequestSigningKey("test key")
130+
// When set, this only allows urls with a &signature, returning 403 if missing/invalid.
131+
// Use Imazen.Common.Helpers.Signatures.SignRequest(string pathAndQuery, string signingKey) to generate
132+
//.ForPrefix allows you to set less restrictive rules for subfolders.
133+
// For example, you may want to allow unmodified requests through with SignatureRequired.ForQuerystringRequests
134+
.SetRequestSignatureOptions(
135+
new RequestSignatureOptions(SignatureRequired.ForAllRequests, new []{"test key"})
136+
.ForPrefix("/logos/", StringComparison.Ordinal,
137+
SignatureRequired.ForQuerystringRequests, new []{"test key"}))
134138
// It's a good idea to limit image sizes for security. Requests causing these to be exceeded will fail
135139
// The last argument to FrameSizeLimit() is the maximum number of megapixels
136140
.SetJobSecurityOptions(new SecurityOptions()

src/Imageflow.Server/ImageJobInfo.cs

+20-5
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,22 @@ private bool ProcessRewritesAndAuthorization(HttpContext context, ImageflowMiddl
219219

220220
private bool VerifySignature(HttpContext context, ImageflowMiddlewareOptions options)
221221
{
222+
if (options.RequestSignatureOptions == null) return true;
223+
224+
var (requirement, signingKeys) = options.RequestSignatureOptions
225+
.GetRequirementForPath(context.Request.Path.Value);
226+
227+
var queryString = context.Request.QueryString.ToString();
228+
222229
var pathAndQuery = context.Request.PathBase.HasValue
223230
? "/" + context.Request.PathBase.Value.TrimStart('/')
224231
: "";
225-
pathAndQuery += context.Request.Path.ToString() + context.Request.QueryString.ToString();
232+
pathAndQuery += context.Request.Path.ToString() + queryString;
226233

227234
pathAndQuery = Signatures.NormalizePathAndQueryForSigning(pathAndQuery);
228235
if (context.Request.Query.TryGetValue("signature", out var actualSignature))
229236
{
230-
foreach (var key in options.SigningKeys)
237+
foreach (var key in signingKeys)
231238
{
232239
var expectedSignature = Signatures.SignString(pathAndQuery, key, 16);
233240
if (expectedSignature == actualSignature) return true;
@@ -238,9 +245,17 @@ private bool VerifySignature(HttpContext context, ImageflowMiddlewareOptions opt
238245

239246
}
240247

241-
// A missing signature is only a problem if they are required
242-
if (!options.RequireRequestSignature) return true;
243-
248+
if (requirement == SignatureRequired.Never)
249+
{
250+
return true;
251+
}
252+
if (requirement == SignatureRequired.ForQuerystringRequests)
253+
{
254+
if (queryString.Length <= 0) return true;
255+
256+
AuthorizedMessage = "Image processing requests must be signed. No &signature query key found. ";
257+
return false;
258+
}
244259
AuthorizedMessage = "Image requests must be signed. No &signature query key found. ";
245260
return false;
246261

src/Imageflow.Server/ImageflowMiddlewareOptions.cs

+3-12
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,10 @@ public ImageflowMiddlewareOptions()
3333
public bool MapWebRoot { get; set; }
3434

3535
public bool UsePresetsExclusively { get; set; }
36-
37-
public bool RequireRequestSignature { get; set; }
3836

3937
public string DefaultCacheControlString { get; set; }
4038

39+
public RequestSignatureOptions RequestSignatureOptions { get; set; }
4140
public SecurityOptions JobSecurityOptions { get; set; }
4241

4342
internal readonly List<UrlHandler<Action<UrlEventArgs>>> Rewrite = new List<UrlHandler<Action<UrlEventArgs>>>();
@@ -52,8 +51,6 @@ public ImageflowMiddlewareOptions()
5251
internal readonly Dictionary<string, string> CommandDefaults = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
5352

5453
internal readonly Dictionary<string, PresetOptions> Presets = new Dictionary<string, PresetOptions>(StringComparer.OrdinalIgnoreCase);
55-
56-
internal readonly List<string> SigningKeys = new List<string>();
5754

5855
internal readonly List<ExtensionlessPath> ExtensionlessPaths = new List<ExtensionlessPath>();
5956
/// <summary>
@@ -84,9 +81,9 @@ public ImageflowMiddlewareOptions HandleExtensionlessRequestsUnder(string prefix
8481
return this;
8582
}
8683

87-
public ImageflowMiddlewareOptions AddRequestSigningKey(string key)
84+
public ImageflowMiddlewareOptions SetRequestSignatureOptions(RequestSignatureOptions options)
8885
{
89-
SigningKeys.Add(key);
86+
RequestSignatureOptions = options;
9087
return this;
9188
}
9289

@@ -112,12 +109,6 @@ public ImageflowMiddlewareOptions AddWatermarkingHandler(string pathPrefix, Acti
112109
return this;
113110
}
114111

115-
public ImageflowMiddlewareOptions SetRequireRequestSignature(bool value)
116-
{
117-
RequireRequestSignature = value;
118-
return this;
119-
}
120-
121112
public ImageflowMiddlewareOptions SetMapWebRoot(bool value)
122113
{
123114
MapWebRoot = value;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Imageflow.Server
6+
{
7+
public enum SignatureRequired
8+
{
9+
ForAllRequests,
10+
ForQuerystringRequests,
11+
Never
12+
}
13+
14+
internal struct SignaturePrefix
15+
{
16+
internal string Prefix { get; set; }
17+
internal StringComparison PrefixComparison { get; set; }
18+
internal SignatureRequired Requirement { get; set; }
19+
internal List<string> SigningKeys { get; set; }
20+
21+
22+
}
23+
public class RequestSignatureOptions
24+
{
25+
internal SignatureRequired DefaultRequirement { get; }
26+
internal List<string> DefaultSigningKeys { get; }
27+
28+
internal List<SignaturePrefix> Prefixes { get; } = new List<SignaturePrefix>();
29+
30+
31+
public RequestSignatureOptions(SignatureRequired defaultRequirement, IEnumerable<string> defaultSigningKeys)
32+
{
33+
DefaultRequirement = defaultRequirement;
34+
DefaultSigningKeys = defaultSigningKeys.ToList();
35+
}
36+
37+
public RequestSignatureOptions ForPrefix(string prefix, StringComparison prefixComparison,
38+
SignatureRequired requirement, IEnumerable<string> signingKeys)
39+
{
40+
Prefixes.Add(new SignaturePrefix()
41+
{
42+
Prefix = prefix,
43+
PrefixComparison = prefixComparison,
44+
Requirement = requirement,
45+
SigningKeys = signingKeys.ToList()
46+
});
47+
return this;
48+
}
49+
50+
internal Tuple<SignatureRequired, ICollection<string>> GetRequirementForPath(string path)
51+
{
52+
if (path != null)
53+
{
54+
foreach (var p in Prefixes)
55+
{
56+
if (path.StartsWith(p.Prefix, p.PrefixComparison))
57+
{
58+
return new Tuple<SignatureRequired, ICollection<string>>(p.Requirement, p.SigningKeys);
59+
}
60+
}
61+
}
62+
return new Tuple<SignatureRequired, ICollection<string>>(DefaultRequirement, DefaultSigningKeys);
63+
}
64+
65+
}
66+
}

tests/Imageflow.Server.Tests/IntegrationTest.cs

+25-8
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,9 @@ public async void TestRequestSigning()
361361
{
362362
const string key = "test key";
363363
using (var contentRoot = new TempContentRoot()
364-
.AddResource("images/fire umbrella.jpg", "TestFiles.fire-umbrella-small.jpg"))
364+
.AddResource("images/fire umbrella.jpg", "TestFiles.fire-umbrella-small.jpg")
365+
.AddResource("images/query/umbrella.jpg", "TestFiles.fire-umbrella-small.jpg")
366+
.AddResource("images/never/umbrella.jpg", "TestFiles.fire-umbrella-small.jpg"))
365367
{
366368

367369
var hostBuilder = new HostBuilder()
@@ -375,8 +377,13 @@ public async void TestRequestSigning()
375377
.SetMapWebRoot(false)
376378
// Maps / to ContentRootPath/images
377379
.MapPath("/", Path.Combine(contentRoot.PhysicalPath, "images"))
378-
.SetRequireRequestSignature(true)
379-
.AddRequestSigningKey(key)
380+
.SetRequestSignatureOptions(
381+
new RequestSignatureOptions(SignatureRequired.ForAllRequests,
382+
new []{key})
383+
.ForPrefix("/query/", StringComparison.Ordinal,
384+
SignatureRequired.ForQuerystringRequests, new []{key})
385+
.ForPrefix("/never/", StringComparison.Ordinal, SignatureRequired.Never,
386+
new string[]{}))
380387
);
381388
});
382389
});
@@ -394,11 +401,21 @@ public async void TestRequestSigning()
394401
using var signedEncodedUnmodifiedResponse = await client.GetAsync(signedEncodedUnmodifiedUrl);
395402
signedEncodedUnmodifiedResponse.EnsureSuccessStatusCode();
396403

397-
// var unsignedUnmodifiedUrl = "/fire%20umbrella.jpg";
398-
// using var unsignedUnmodifiedResponse = await client.GetAsync(unsignedUnmodifiedUrl);
399-
// unsignedUnmodifiedResponse.EnsureSuccessStatusCode();
400-
//
401-
//
404+
var unsignedUnmodifiedUrl = "/query/umbrella.jpg";
405+
using var unsignedUnmodifiedResponse = await client.GetAsync(unsignedUnmodifiedUrl);
406+
unsignedUnmodifiedResponse.EnsureSuccessStatusCode();
407+
408+
using var unsignedResponse2 = await client.GetAsync("/query/umbrella.jpg?width=1");
409+
Assert.Equal(HttpStatusCode.Forbidden,unsignedResponse2.StatusCode);
410+
411+
var unsignedUnmodifiedUrl2 = "/never/umbrella.jpg";
412+
using var unsignedUnmodifiedResponse2 = await client.GetAsync(unsignedUnmodifiedUrl2);
413+
unsignedUnmodifiedResponse2.EnsureSuccessStatusCode();
414+
415+
var unsignedModifiedUrl = "/never/umbrella.jpg?width=1";
416+
using var unsignedModifiedResponse = await client.GetAsync(unsignedModifiedUrl);
417+
unsignedModifiedResponse.EnsureSuccessStatusCode();
418+
402419
var signedEncodedUrl = Imazen.Common.Helpers.Signatures.SignRequest("/fire%20umbrella.jpg?width=1", key);
403420
using var signedEncodedResponse = await client.GetAsync(signedEncodedUrl);
404421
signedEncodedResponse.EnsureSuccessStatusCode();

0 commit comments

Comments
 (0)