Skip to content

Commit

Permalink
Add .AddRewriteHandler, .AddPreRewriteAuthorizationHandler and AddPos…
Browse files Browse the repository at this point in the history
…tRewriteAuthorizationHandler
  • Loading branch information
lilith committed Jun 11, 2020
1 parent ca78eff commit 5285540
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 35 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ namespace Imageflow.Server.Example
.SetAllowMemoryCaching(false)
// Cache publicly (including on shared proxies and CDNs) for 30 days
.SetDefaultCacheControlString("public, max-age=2592000")
// Force all paths under "/gallery" to be watermarked
.AddRewriteHandler("/gallery", args =>
{
args.Query["watermark"] = "imazen";
})
// Register a named watermark that floats 10% from the bottom-right corner of the image
// With 70% opacity and some sharpness applied.
.AddWatermark(
Expand All @@ -128,7 +133,8 @@ namespace Imageflow.Server.Example
.WithHints(
new ResampleHints()
.ResampleFilter(InterpolationFilter.Robidoux_Sharp, null)
.Sharpen(7, SharpenWhen.Downscaling)))));
.Sharpen(7, SharpenWhen.Downscaling))
.WithMinCanvasSize(300,300))));


app.UseStaticFiles();
Expand Down
8 changes: 7 additions & 1 deletion examples/Imageflow.Server.Example/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
.SetAllowMemoryCaching(false)
// Cache publicly (including on shared proxies and CDNs) for 30 days
.SetDefaultCacheControlString("public, max-age=2592000")
// Force all paths under "/gallery" to be watermarked
.AddRewriteHandler("/gallery", args =>
{
args.Query["watermark"] = "imazen";
})
// Register a named watermark that floats 10% from the bottom-right corner of the image
// With 70% opacity and some sharpness applied.
.AddWatermark(
Expand All @@ -96,7 +101,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
.WithHints(
new ResampleHints()
.ResampleFilter(InterpolationFilter.Robidoux_Sharp, null)
.Sharpen(7, SharpenWhen.Downscaling)))));
.Sharpen(7, SharpenWhen.Downscaling))
.WithMinCanvasSize(300,300))));


app.UseStaticFiles();
Expand Down
74 changes: 55 additions & 19 deletions src/Imageflow.Server/ImageJobInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,54 +14,60 @@ namespace Imageflow.Server
{
internal class ImageJobInfo
{
public ImageJobInfo(string virtualPath, IQueryCollection query,
IReadOnlyCollection<NamedWatermark> namedWatermarks, BlobProvider blobProvider)
public ImageJobInfo(HttpContext context, ImageflowMiddlewareOptions options, BlobProvider blobProvider)
{
HasParams = PathHelpers.SupportedQuerystringKeys.Any(query.ContainsKey);
Authorized = ProcessRewritesAndAuthorization(context, options);

if (!Authorized) return;

HasParams = PathHelpers.SupportedQuerystringKeys.Any(FinalQuery.ContainsKey);


var extension = Path.GetExtension(virtualPath);
if (query.TryGetValue("format", out var newExtension))
var extension = Path.GetExtension(FinalVirtualPath);
if (FinalQuery.TryGetValue("format", out var newExtension))
{
extension = newExtension;
}

EstimatedFileExtension = PathHelpers.SanitizeImageExtension(extension);

primaryBlob = new BlobFetchCache(virtualPath, blobProvider);
primaryBlob = new BlobFetchCache(FinalVirtualPath, blobProvider);
allBlobs = new List<BlobFetchCache>(1) {primaryBlob};

if (HasParams)
{
CommandString = string.Join("&", PathHelpers.MatchingResizeQueryStringParameters(query));

CommandString = PathHelpers.SerializeCommandString(FinalQuery);
// Look up watermark names
if (query.TryGetValue("watermark", out var watermarkValues))
if (FinalQuery.TryGetValue("watermark", out var watermarkValues))
{
var watermarkNames = watermarkValues.SelectMany(w => w.Split(",")).Select(s => s.Trim(' '));
var watermarkNames = watermarkValues.Split(",").Select(s => s.Trim(' '));

appliedWatermarks = new List<NamedWatermark>();
foreach (var name in watermarkNames)
{
var watermark = namedWatermarks.FirstOrDefault(w =>
var watermark = options.NamedWatermarks.FirstOrDefault(w =>
w.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (watermark == null)
{
throw new ArgumentOutOfRangeException(nameof(query), $"watermark {name} was referenced from the querystring but no watermark by that name is registered with the middleware");
throw new InvalidOperationException($"watermark {name} was referenced from the querystring but no watermark by that name is registered with the middleware");
}

appliedWatermarks.Add(watermark);
allBlobs.Add(new BlobFetchCache(watermark.VirtualPath, blobProvider));
}
}
}

VirtualPath = virtualPath;

provider = blobProvider;
}

public string VirtualPath { get; }
public string FinalVirtualPath { get; private set; }

public Dictionary<string,string> FinalQuery { get; private set; }
public bool HasParams { get; }

public bool Authorized { get; }
public string CommandString { get; }
public string EstimatedFileExtension { get; }

Expand All @@ -71,9 +77,39 @@ public ImageJobInfo(string virtualPath, IQueryCollection query,

private readonly List<BlobFetchCache> allBlobs;
private readonly BlobFetchCache primaryBlob;




private bool ProcessRewritesAndAuthorization(HttpContext context, ImageflowMiddlewareOptions options)
{
var path = context.Request.Path.Value;
var args = new UrlEventArgs(context, context.Request.Path.Value, PathHelpers.ToQueryDictionary(context.Request.Query));
foreach (var handler in options.PreRewriteAuthorization)
{
var matches = string.IsNullOrEmpty(handler.PathPrefix) ||
path.StartsWith(handler.PathPrefix, StringComparison.OrdinalIgnoreCase);
if (matches && !handler.Handler(args)) return false;
}
foreach (var handler in options.Rewrite)
{
var matches = string.IsNullOrEmpty(handler.PathPrefix) ||
path.StartsWith(handler.PathPrefix, StringComparison.OrdinalIgnoreCase);
if (matches)
{
handler.Handler(args);
path = args.VirtualPath;
}
}
foreach (var handler in options.PreRewriteAuthorization)
{
var matches = string.IsNullOrEmpty(handler.PathPrefix) ||
path.StartsWith(handler.PathPrefix, StringComparison.OrdinalIgnoreCase);
if (matches && !handler.Handler(args)) return false;
}

FinalVirtualPath = args.VirtualPath;
FinalQuery = args.Query;
return true;
}


public bool PrimaryBlobMayExist()
Expand Down Expand Up @@ -110,7 +146,7 @@ public async Task<string> GetFastCacheKey()
.Select(async b =>
(await b.GetBlob())?.LastModifiedDateUtc?.ToBinary().ToString()));

return HashStrings(new string[] {VirtualPath, CommandString}.Concat(dateTimes).Concat(SerializeWatermarkConfigs()));
return HashStrings(new string[] {FinalVirtualPath, CommandString}.Concat(dateTimes).Concat(SerializeWatermarkConfigs()));
}

public override string ToString()
Expand All @@ -125,7 +161,7 @@ public async Task<string> GetExactCacheKey()
.Select(async b =>
(await b.GetBlob())?.LastModifiedDateUtc?.ToBinary().ToString()));

return HashStrings(new string[] {VirtualPath, CommandString}.Concat(dateTimes).Concat(SerializeWatermarkConfigs()));
return HashStrings(new string[] {FinalVirtualPath, CommandString}.Concat(dateTimes).Concat(SerializeWatermarkConfigs()));
}

public async Task<ImageData> ProcessUncached()
Expand Down
40 changes: 28 additions & 12 deletions src/Imageflow.Server/ImageflowMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ public async Task Invoke(HttpContext context)
}


var imageJobInfo = new ImageJobInfo(path.Value, context.Request.Query, options.NamedWatermarks, blobProvider);
var imageJobInfo = new ImageJobInfo(context, options, blobProvider);

if (!imageJobInfo.Authorized)
{
await NotAuthorized(context);
return;
}

// If the file is definitely missing hand to the next middleware
// Remote providers will fail late rather than make 2 requests
Expand Down Expand Up @@ -115,13 +121,23 @@ public async Task Invoke(HttpContext context)
}
}

private async Task NotAuthorized(HttpContext context)
{
var s = "You are not authorized to access the given resource.";

context.Response.StatusCode = 403;
context.Response.ContentType = "text/plain; charset=utf-8";
var bytes = Encoding.UTF8.GetBytes(s);
await context.Response.Body.WriteAsync(bytes, 0, bytes.Length);
}

private async Task ProcessWithDiskCache(HttpContext context, string cacheKey, ImageJobInfo info)
{
var cacheResult = await diskCache.GetOrCreate(cacheKey, info.EstimatedFileExtension, async (stream) =>
{
if (info.HasParams)
{
logger.LogInformation($"DiskCache Miss: Processing image {info.VirtualPath}?{info}");
logger.LogInformation($"DiskCache Miss: Processing image {info.FinalVirtualPath}?{info}");


var result = await info.ProcessUncached();
Expand All @@ -131,7 +147,7 @@ await stream.WriteAsync(result.ResultBytes.Array, result.ResultBytes.Offset,
}
else
{
logger.LogInformation($"DiskCache Miss: Proxying image {info.VirtualPath}");
logger.LogInformation($"DiskCache Miss: Proxying image {info.FinalVirtualPath}");

await using var sourceStream = (await info.GetPrimaryBlob()).OpenReadAsync();
await sourceStream.CopyToAsync(stream);
Expand All @@ -150,7 +166,7 @@ await stream.WriteAsync(result.ResultBytes.Array, result.ResultBytes.Offset,
}
else
{
logger.LogInformation("Serving {0}?{1} from disk cache {2}", info.VirtualPath, info.CommandString, cacheResult.RelativePath);
logger.LogInformation("Serving {0}?{1} from disk cache {2}", info.FinalVirtualPath, info.CommandString, cacheResult.RelativePath);
await ServeFileFromDisk(context, cacheResult.PhysicalPath, cacheKey,
PathHelpers.ContentTypeFor(info.EstimatedFileExtension));
}
Expand All @@ -171,22 +187,22 @@ private async Task ProcessWithMemoryCache(HttpContext context, string cacheKey,
var isContentTypeCached = memoryCache.TryGetValue(cacheKey + ".contentType", out string contentType);
if (isCached && isContentTypeCached)
{
logger.LogInformation("Serving {0}?{1} from memory cache", info.VirtualPath, info.CommandString);
logger.LogInformation("Serving {0}?{1} from memory cache", info.FinalVirtualPath, info.CommandString);
}
else
{

if (info.HasParams)
{
logger.LogInformation($"Memory Cache Miss: Processing image {info.VirtualPath}?{info.CommandString}");
logger.LogInformation($"Memory Cache Miss: Processing image {info.FinalVirtualPath}?{info.CommandString}");

var imageData = await info.ProcessUncached();
imageBytes = imageData.ResultBytes;
contentType = imageData.ContentType;
}
else
{
logger.LogInformation($"Memory Cache Miss: Proxying image {info.VirtualPath}?{info.CommandString}");
logger.LogInformation($"Memory Cache Miss: Proxying image {info.FinalVirtualPath}?{info.CommandString}");

contentType = PathHelpers.ContentTypeFor(info.EstimatedFileExtension);
await using var sourceStream = (await info.GetPrimaryBlob()).OpenReadAsync();
Expand Down Expand Up @@ -223,15 +239,15 @@ private async Task ProcessWithDistributedCache(HttpContext context, string cache
var contentType = await distributedCache.GetStringAsync(cacheKey + ".contentType");
if (imageBytes != null && contentType != null)
{
logger.LogInformation("Serving {0}?{1} from distributed cache", info.VirtualPath, info.CommandString);
logger.LogInformation("Serving {0}?{1} from distributed cache", info.FinalVirtualPath, info.CommandString);
}
else
{


if (info.HasParams)
{
logger.LogInformation($"Distributed Cache Miss: Processing image {info.VirtualPath}?{info.CommandString}");
logger.LogInformation($"Distributed Cache Miss: Processing image {info.FinalVirtualPath}?{info.CommandString}");

var imageData = await info.ProcessUncached();
imageBytes = imageData.ResultBytes.Count != imageData.ResultBytes.Array?.Length
Expand All @@ -242,7 +258,7 @@ private async Task ProcessWithDistributedCache(HttpContext context, string cache
}
else
{
logger.LogInformation($"Distributed Cache Miss: Proxying image {info.VirtualPath}?{info.CommandString}");
logger.LogInformation($"Distributed Cache Miss: Proxying image {info.FinalVirtualPath}?{info.CommandString}");

contentType = PathHelpers.ContentTypeFor(info.EstimatedFileExtension);
await using var sourceStream = (await info.GetPrimaryBlob()).OpenReadAsync();
Expand Down Expand Up @@ -284,7 +300,7 @@ private async Task ProcessWithNoCache(HttpContext context, ImageJobInfo info)

if (info.HasParams)
{
logger.LogInformation($"Processing image {info.VirtualPath} with params {info.CommandString}");
logger.LogInformation($"Processing image {info.FinalVirtualPath} with params {info.CommandString}");

var imageData = await info.ProcessUncached();
var imageBytes = imageData.ResultBytes;
Expand All @@ -299,7 +315,7 @@ private async Task ProcessWithNoCache(HttpContext context, ImageJobInfo info)
}
else
{
logger.LogInformation($"Proxying image {info.VirtualPath} with params {info.CommandString}");
logger.LogInformation($"Proxying image {info.FinalVirtualPath} with params {info.CommandString}");

var contentType = PathHelpers.ContentTypeFor(info.EstimatedFileExtension);
await using var sourceStream = (await info.GetPrimaryBlob()).OpenReadAsync();
Expand Down
23 changes: 23 additions & 0 deletions src/Imageflow.Server/ImageflowMiddlewareOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ public ImageflowMiddlewareOptions()
public bool MapWebRoot { get; set; } = true;

public string DefaultCacheControlString { get; set; }

internal readonly List<UrlHandler<Action<UrlEventArgs>>> Rewrite = new List<UrlHandler<Action<UrlEventArgs>>>();

internal readonly List<UrlHandler<Func<UrlEventArgs, bool>>> PreRewriteAuthorization = new List<UrlHandler<Func<UrlEventArgs, bool>>>();

internal readonly List<UrlHandler<Func<UrlEventArgs, bool>>> PostRewriteAuthorization = new List<UrlHandler<Func<UrlEventArgs, bool>>>();

public ImageflowMiddlewareOptions AddRewriteHandler(string pathPrefix, Action<UrlEventArgs> handler)
{
Rewrite.Add(new UrlHandler<Action<UrlEventArgs>>(pathPrefix, handler));
return this;
}
public ImageflowMiddlewareOptions AddPreRewriteAuthorizationHandler(string pathPrefix, Func<UrlEventArgs, bool> handler)
{
PreRewriteAuthorization.Add(new UrlHandler<Func<UrlEventArgs, bool>>(pathPrefix, handler));
return this;
}
public ImageflowMiddlewareOptions AddPostRewriteAuthorizationHandler(string pathPrefix, Func<UrlEventArgs, bool> handler)
{
PostRewriteAuthorization.Add(new UrlHandler<Func<UrlEventArgs, bool>>(pathPrefix, handler));
return this;
}

public ImageflowMiddlewareOptions SetMapWebRoot(bool value)
{
MapWebRoot = value;
Expand Down
21 changes: 19 additions & 2 deletions src/Imageflow.Server/PathHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Imageflow.Server
{
Expand Down Expand Up @@ -89,7 +90,23 @@ internal static string Base64Hash(string data)
.Replace('+', '-')
.Replace('/', '_');
}




public static Dictionary<string, string> ToQueryDictionary(IQueryCollection requestQuery)
{
var dict = new Dictionary<string,string>(requestQuery.Count);
foreach (var pair in requestQuery)
{
dict.Add(pair.Key, pair.Value.ToString());
}

return dict;
}

public static string SerializeCommandString(Dictionary<string, string> finalQuery)
{
var qs = QueryString.Create(finalQuery.Select(p => new KeyValuePair<string, StringValues>(p.Key, p.Value)));
return qs.ToString() ?? "";
}
}
}
20 changes: 20 additions & 0 deletions src/Imageflow.Server/UrlEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;

namespace Imageflow.Server
{
public class UrlEventArgs
{
internal UrlEventArgs(HttpContext context, string virtualPath, Dictionary<string, string> query)
{
VirtualPath = virtualPath;
Context = context;
Query = query;
}
public string VirtualPath { get; set; }

public Dictionary<string,string> Query { get; set; }

public HttpContext Context { get; }
}
}
Loading

0 comments on commit 5285540

Please sign in to comment.