Skip to content

Commit 8d50c3d

Browse files
committed
Add ImageflowMiddlewareOptions.HandleExtensionlessRequestsUnder(prefix, comparison)
1 parent ed6341f commit 8d50c3d

File tree

8 files changed

+81
-15
lines changed

8 files changed

+81
-15
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ namespace Imageflow.Server.Example
182182
.SetAllowMemoryCaching(false)
183183
// Cache publicly (including on shared proxies and CDNs) for 30 days
184184
.SetDefaultCacheControlString("public, max-age=2592000")
185+
// Allows extensionless images to be served within the given directory(ies)
186+
.HandleExtensionlessRequestsUnder("/customblobs/", StringComparison.OrdinalIgnoreCase)
185187
// Force all paths under "/gallery" to be watermarked
186188
.AddRewriteHandler("/gallery", args =>
187189
{

examples/Imageflow.Server.Example/Startup.cs

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
110110
.SetAllowMemoryCaching(false)
111111
// Cache publicly (including on shared proxies and CDNs) for 30 days
112112
.SetDefaultCacheControlString("public, max-age=2592000")
113+
// Allows extensionless images to be served within the given directory(ies)
114+
.HandleExtensionlessRequestsUnder("/customblobs/", StringComparison.OrdinalIgnoreCase)
113115
// Force all paths under "/gallery" to be watermarked
114116
.AddRewriteHandler("/gallery", args =>
115117
{
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System;
2+
3+
namespace Imageflow.Server
4+
{
5+
internal struct ExtensionlessPath
6+
{
7+
internal string Prefix { get; set; }
8+
9+
internal StringComparison PrefixComparison { get; set; }
10+
}
11+
}

src/Imageflow.Server/ImageJobInfo.cs

+32-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,36 @@ namespace Imageflow.Server
1313
{
1414
internal class ImageJobInfo
1515
{
16+
public static bool ShouldHandleRequest(HttpContext context, ImageflowMiddlewareOptions options,
17+
BlobProvider blobProvider)
18+
{
19+
// If the path is empty or null we don't handle it
20+
var pathValue = context.Request.Path;
21+
if (pathValue == null || !pathValue.HasValue)
22+
return false;
23+
24+
var path = pathValue.Value;
25+
if (path == null)
26+
return false;
27+
28+
// We handle image request extensions
29+
if (PathHelpers.IsImagePath(path))
30+
{
31+
return true;
32+
}
33+
34+
// Don't do string parsing unless there are actually prefixes configured
35+
if (options.ExtensionlessPaths.Count == 0) return false;
36+
37+
// If there's no extension, then we can see if it's one of the prefixes we should handle
38+
var extension = Path.GetExtension(path);
39+
// If there's a non-image extension, we shouldn't handle the request
40+
if (!string.IsNullOrEmpty(extension)) return false;
41+
42+
// Return true if any of the prefixes match
43+
return options.ExtensionlessPaths
44+
.Any(extensionlessPath => path.StartsWith(extensionlessPath.Prefix, extensionlessPath.PrefixComparison));
45+
}
1646
public ImageJobInfo(HttpContext context, ImageflowMiddlewareOptions options, BlobProvider blobProvider)
1747
{
1848
this.options = options;
@@ -210,7 +240,7 @@ private bool VerifySignature(HttpContext context, ImageflowMiddlewareOptions opt
210240

211241
// A missing signature is only a problem if they are required
212242
if (!options.RequireRequestSignature) return true;
213-
243+
214244
AuthorizedMessage = "Image requests must be signed. No &signature query key found. ";
215245
return false;
216246

@@ -219,6 +249,7 @@ private bool VerifySignature(HttpContext context, ImageflowMiddlewareOptions opt
219249

220250
public bool PrimaryBlobMayExist()
221251
{
252+
// Just returns a lambda for performing the actual fetch, does not actually call .Fetch() on providers
222253
return primaryBlob.GetBlobResult() != null;
223254
}
224255

src/Imageflow.Server/ImageflowMiddleware.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,15 @@ public async Task Invoke(HttpContext context)
7474
return;
7575
}
7676

77-
// We only handle requests with an image extension, period.
78-
if (!PathHelpers.IsImagePath(path))
77+
// We only handle requests with an image extension or if we configured a path prefix for which to handle
78+
// extensionless requests
79+
80+
if (!ImageJobInfo.ShouldHandleRequest(context, options, blobProvider))
7981
{
8082
await next.Invoke(context);
8183
return;
8284
}
83-
84-
85+
8586
var imageJobInfo = new ImageJobInfo(context, options, blobProvider);
8687

8788
if (!imageJobInfo.Authorized)

src/Imageflow.Server/ImageflowMiddlewareOptions.cs

+8
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ public ImageflowMiddlewareOptions()
5454
internal readonly Dictionary<string, PresetOptions> Presets = new Dictionary<string, PresetOptions>(StringComparer.OrdinalIgnoreCase);
5555

5656
internal readonly List<string> SigningKeys = new List<string>();
57+
58+
internal readonly List<ExtensionlessPath> ExtensionlessPaths = new List<ExtensionlessPath>();
5759
/// <summary>
5860
/// Use this to add default command values if they are missing. Does not affect image requests with no querystring.
5961
/// Example: AddCommandDefault("down.colorspace", "srgb") reverts to ImageResizer's legacy behavior in scaling shadows and highlights.
@@ -75,6 +77,12 @@ public ImageflowMiddlewareOptions AddPreset(PresetOptions preset)
7577
Presets[preset.Name] = preset;
7678
return this;
7779
}
80+
81+
public ImageflowMiddlewareOptions HandleExtensionlessRequestsUnder(string prefix, StringComparison prefixComparison = StringComparison.Ordinal)
82+
{
83+
ExtensionlessPaths.Add(new ExtensionlessPath() { Prefix = prefix, PrefixComparison = prefixComparison});
84+
return this;
85+
}
7886

7987
public ImageflowMiddlewareOptions AddRequestSigningKey(string key)
8088
{

src/Imageflow.Server/PathHelpers.cs

+2-5
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,9 @@ public static class PathHelpers
4141
public static IEnumerable<string> AcceptedImageExtensions => suffixes;
4242
public static IEnumerable<string> SupportedQuerystringKeys => querystringKeys;
4343

44-
internal static bool IsImagePath(PathString path)
44+
internal static bool IsImagePath(string path)
4545
{
46-
if (path == null || !path.HasValue)
47-
return false;
48-
49-
return suffixes.Any(suffix => path.Value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase));
46+
return suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.OrdinalIgnoreCase));
5047
}
5148

5249
public static string SanitizeImageExtension(string extension)

tests/Imageflow.Server.Tests/IntegrationTest.cs

+19-5
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public async void TestLocalFiles()
2929
.AddResource("images/fire umbrella.jpg", "TestFiles.fire-umbrella-small.jpg")
3030
.AddResource("images/logo.png", "TestFiles.imazen_400.png")
3131
.AddResource("images/wrong.webp", "TestFiles.imazen_400.png")
32-
.AddResource("images/wrong.jpg", "TestFiles.imazen_400.png"))
32+
.AddResource("images/wrong.jpg", "TestFiles.imazen_400.png")
33+
.AddResource("images/extensionless/file", "TestFiles.imazen_400.png"))
3334
{
3435

3536
var hostBuilder = new HostBuilder()
@@ -45,6 +46,7 @@ public async void TestLocalFiles()
4546
.MapPath("/", Path.Combine(contentRoot.PhysicalPath, "images"))
4647
.MapPath("/insensitive", Path.Combine(contentRoot.PhysicalPath, "images"), true)
4748
.MapPath("/sensitive", Path.Combine(contentRoot.PhysicalPath, "images"), false)
49+
.HandleExtensionlessRequestsUnder("/extensionless/")
4850
.AddWatermark(new NamedWatermark("imazen", "/logo.png", new WatermarkOptions()))
4951
.AddWatermark(new NamedWatermark("broken", "/not_there.png", new WatermarkOptions()))
5052
.AddWatermarkingHandler("/", args =>
@@ -88,7 +90,10 @@ await Assert.ThrowsAsync<InvalidOperationException>(async () =>
8890
wrongImageExtension2.EnsureSuccessStatusCode();
8991
Assert.Equal("image/png", wrongImageExtension2.Content.Headers.ContentType.MediaType);
9092

91-
93+
using var extensionlessRequest = await client.GetAsync("/extensionless/file");
94+
extensionlessRequest.EnsureSuccessStatusCode();
95+
Assert.Equal("image/png", extensionlessRequest.Content.Headers.ContentType.MediaType);
96+
9297

9398
using var response2 = await client.GetAsync("/fire.jpg?width=1");
9499
response2.EnsureSuccessStatusCode();
@@ -128,7 +133,8 @@ public async void TestDiskCache()
128133
.AddResource("images/fire.jpg", "TestFiles.fire-umbrella-small.jpg")
129134
.AddResource("images/logo.png", "TestFiles.imazen_400.png")
130135
.AddResource("images/wrong.webp", "TestFiles.imazen_400.png")
131-
.AddResource("images/wrong.jpg", "TestFiles.imazen_400.png"))
136+
.AddResource("images/wrong.jpg", "TestFiles.imazen_400.png")
137+
.AddResource("images/extensionless/file", "TestFiles.imazen_400.png"))
132138
{
133139

134140
var diskCacheDir = Path.Combine(contentRoot.PhysicalPath, "diskcache");
@@ -147,6 +153,7 @@ public async void TestDiskCache()
147153
app.UseImageflow(new ImageflowMiddlewareOptions()
148154
.SetMapWebRoot(false)
149155
.SetAllowDiskCaching(true)
156+
.HandleExtensionlessRequestsUnder("/extensionless/")
150157
// Maps / to ContentRootPath/images
151158
.MapPath("/", Path.Combine(contentRoot.PhysicalPath, "images")));
152159
});
@@ -179,6 +186,9 @@ public async void TestDiskCache()
179186
wrongImageExtension2.EnsureSuccessStatusCode();
180187
Assert.Equal("image/png", wrongImageExtension2.Content.Headers.ContentType.MediaType);
181188

189+
using var extensionlessRequest = await client.GetAsync("/extensionless/file");
190+
extensionlessRequest.EnsureSuccessStatusCode();
191+
Assert.Equal("image/png", extensionlessRequest.Content.Headers.ContentType.MediaType);
182192

183193

184194
await host.StopAsync(CancellationToken.None);
@@ -383,8 +393,12 @@ public async void TestRequestSigning()
383393
var signedEncodedUnmodifiedUrl = Imazen.Common.Helpers.Signatures.SignRequest("/fire%20umbrella.jpg", key);
384394
using var signedEncodedUnmodifiedResponse = await client.GetAsync(signedEncodedUnmodifiedUrl);
385395
signedEncodedUnmodifiedResponse.EnsureSuccessStatusCode();
386-
387-
396+
397+
// var unsignedUnmodifiedUrl = "/fire%20umbrella.jpg";
398+
// using var unsignedUnmodifiedResponse = await client.GetAsync(unsignedUnmodifiedUrl);
399+
// unsignedUnmodifiedResponse.EnsureSuccessStatusCode();
400+
//
401+
//
388402
var signedEncodedUrl = Imazen.Common.Helpers.Signatures.SignRequest("/fire%20umbrella.jpg?width=1", key);
389403
using var signedEncodedResponse = await client.GetAsync(signedEncodedUrl);
390404
signedEncodedResponse.EnsureSuccessStatusCode();

0 commit comments

Comments
 (0)