Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Update="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Update="Microsoft.AspNetCore.Http" Version="2.2.0"/>
<PackageReference Update="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Update="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Update="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
Expand Down
51 changes: 51 additions & 0 deletions src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCache.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
Expand Down Expand Up @@ -119,5 +122,53 @@ private static async Task<PutBucketResponse> CreateIfNotExistsAsync(

return null;
}

/// <summary>
/// <see href="https://github.com/aspnet/AspNetIdentity/blob/b7826741279450c58b230ece98bd04b4815beabf/src/Microsoft.AspNet.Identity.Core/AsyncHelper.cs"/>
/// </summary>
private static class AsyncHelper
{
private static readonly TaskFactory TaskFactory
= new(
CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);

/// <summary>
/// Executes an async <see cref="Task"/> method synchronously.
/// </summary>
/// <param name="task">The task to excecute.</param>
public static void RunSync(Func<Task> task)
{
CultureInfo cultureUi = CultureInfo.CurrentUICulture;
CultureInfo culture = CultureInfo.CurrentCulture;
TaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return task();
}).Unwrap().GetAwaiter().GetResult();
}

/// <summary>
/// Executes an async <see cref="Task{TResult}"/> method which has
/// a <paramref name="task"/> return type synchronously.
/// </summary>
/// <typeparam name="TResult">The type of result to return.</typeparam>
/// <param name="task">The task to excecute.</param>
/// <returns>The <typeparamref name="TResult"/>.</returns>
public static TResult RunSync<TResult>(Func<Task<TResult>> task)
{
CultureInfo cultureUi = CultureInfo.CurrentUICulture;
CultureInfo culture = CultureInfo.CurrentCulture;
return TaskFactory.StartNew(() =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = cultureUi;
return task();
}).Unwrap().GetAwaiter().GetResult();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ namespace SixLabors.ImageSharp.Web
/// </summary>
internal static class AsyncHelper
{
private static readonly TaskFactory TaskFactory = new
(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
private static readonly TaskFactory TaskFactory
= new(
CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);

/// <summary>
/// Executes an async <see cref="Task"/> method synchronously.
Expand Down
7 changes: 6 additions & 1 deletion src/ImageSharp.Web/Caching/UriAbsoluteCacheKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public class UriAbsoluteCacheKey : ICacheKey
{
/// <inheritdoc/>
public string Create(HttpContext context, CommandCollection commands)
=> CaseHandlingUriBuilder.BuildAbsolute(CaseHandlingUriBuilder.CaseHandling.None, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
=> CaseHandlingUriBuilder.BuildAbsolute(
CaseHandlingUriBuilder.CaseHandling.None,
context.Request.Host,
context.Request.PathBase,
context.Request.Path,
QueryString.Create(commands));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public class UriAbsoluteLowerInvariantCacheKey : ICacheKey
{
/// <inheritdoc/>
public string Create(HttpContext context, CommandCollection commands)
=> CaseHandlingUriBuilder.BuildAbsolute(CaseHandlingUriBuilder.CaseHandling.LowerInvariant, context.Request.Host, context.Request.PathBase, context.Request.Path, QueryString.Create(commands));
=> CaseHandlingUriBuilder.BuildAbsolute(
CaseHandlingUriBuilder.CaseHandling.LowerInvariant,
context.Request.Host,
context.Request.PathBase,
context.Request.Path,
QueryString.Create(commands));
}
}
58 changes: 47 additions & 11 deletions src/ImageSharp.Web/CaseHandlingUriBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ namespace SixLabors.ImageSharp.Web
/// </summary>
public static class CaseHandlingUriBuilder
{
private static readonly SpanAction<char, (bool LowerInvariant, string Host, string PathBase, string Path, string Query)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);
private static readonly Uri FallbackBaseUri = new("http://localhost/");
private static readonly SpanAction<char, (bool LowerInvariant, string Scheme, string Host, string PathBase, string Path, string Query)> InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString);

/// <summary>
/// Provides Uri case handling options.
Expand Down Expand Up @@ -49,30 +50,54 @@ public static string BuildRelative(
// Take any potential performance hit vs concatination for code reading sanity.
=> BuildAbsolute(handling, default, pathBase, path, query);

/// <summary>
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
/// Note that unicode in the HostString will be encoded as punycode and the scheme is not included
/// in the result.
/// </summary>
/// <param name="handling">Determines case handling for the result. <paramref name="query"/> is always converted to invariant lowercase.</param>
/// <param name="host">The host portion of the uri normally included in the Host header. This may include the port.</param>
/// <param name="pathBase">The first portion of the request path associated with application root.</param>
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
/// <param name="query">The query, if any.</param>
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
public static string BuildAbsolute(
CaseHandling handling,
HostString host,
PathString pathBase = default,
PathString path = default,
QueryString query = default)
=> BuildAbsolute(handling, string.Empty, host, pathBase, path, query);

/// <summary>
/// Combines the given URI components into a string that is properly encoded for use in HTTP headers.
/// Note that unicode in the HostString will be encoded as punycode.
/// </summary>
/// <param name="handling">Determines case handling for the result. <paramref name="query"/> is always converted to invariant lowercase.</param>
/// <param name="scheme">http, https, etc.</param>
/// <param name="host">The host portion of the uri normally included in the Host header. This may include the port.</param>
/// <param name="pathBase">The first portion of the request path associated with application root.</param>
/// <param name="path">The portion of the request path that identifies the requested resource.</param>
/// <param name="query">The query, if any.</param>
/// <returns>The combined URI components, properly encoded for use in HTTP headers.</returns>
public static string BuildAbsolute(
CaseHandling handling,
string scheme,
HostString host,
PathString pathBase = default,
PathString path = default,
QueryString query = default)
{
Guard.NotNull(scheme, nameof(scheme));

string hostText = host.ToUriComponent();
string pathBaseText = pathBase.ToUriComponent();
string pathText = path.ToUriComponent();
string queryText = query.ToUriComponent();

// PERF: Calculate string length to allocate correct buffer size for string.Create.
int length =
(scheme.Length > 0 ? scheme.Length + Uri.SchemeDelimiter.Length : 0) +
hostText.Length +
pathBaseText.Length +
pathText.Length +
Expand All @@ -94,7 +119,10 @@ public static string BuildAbsolute(
length--;
}

return string.Create(length, (handling == CaseHandling.LowerInvariant, hostText, pathBaseText, pathText, queryText), InitializeAbsoluteUriStringSpanAction);
return string.Create(
length,
(handling == CaseHandling.LowerInvariant, scheme, hostText, pathBaseText, pathText, queryText),
InitializeAbsoluteUriStringSpanAction);
}

/// <summary>
Expand All @@ -105,7 +133,10 @@ public static string BuildAbsolute(
/// <param name="uri">The Uri to encode.</param>
/// <returns>The encoded string version of <paramref name="uri"/>.</returns>
public static string Encode(CaseHandling handling, string uri)
=> Encode(handling, new Uri(uri, UriKind.RelativeOrAbsolute));
{
Guard.NotNull(uri, nameof(uri));
return Encode(handling, new Uri(uri, UriKind.RelativeOrAbsolute));
}

/// <summary>
/// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in
Expand All @@ -121,19 +152,18 @@ public static string Encode(CaseHandling handling, Uri uri)
{
return BuildAbsolute(
handling,
scheme: uri.Scheme,
host: HostString.FromUriComponent(uri),
pathBase: PathString.FromUriComponent(uri),
query: QueryString.FromUriComponent(uri));
}
else
{
string components = uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped);
if (handling == CaseHandling.LowerInvariant)
{
return components.ToLowerInvariant();
}

return components;
Uri faux = new(FallbackBaseUri, uri);
return BuildRelative(
handling,
path: PathString.FromUriComponent(faux),
query: QueryString.FromUriComponent(faux));
}
}

Expand Down Expand Up @@ -168,7 +198,7 @@ private static int CopyTextToBufferLowerInvariant(Span<char> buffer, int index,
/// </summary>
/// <param name="buffer">The URI <see cref="string"/>'s <see cref="char"/> buffer.</param>
/// <param name="uriParts">The URI parts.</param>
private static void InitializeAbsoluteUriString(Span<char> buffer, (bool Lower, string Host, string PathBase, string Path, string Query) uriParts)
private static void InitializeAbsoluteUriString(Span<char> buffer, (bool Lower, string Scheme, string Host, string PathBase, string Path, string Query) uriParts)
{
int index = 0;
ReadOnlySpan<char> pathBaseSpan = uriParts.PathBase.AsSpan();
Expand All @@ -181,6 +211,12 @@ private static void InitializeAbsoluteUriString(Span<char> buffer, (bool Lower,
pathBaseSpan = pathBaseSpan.Slice(0, pathBaseSpan.Length - 1);
}

if (uriParts.Scheme.Length > 0)
{
index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Scheme.AsSpan());
index = CopyTextToBuffer(buffer, index, Uri.SchemeDelimiter.AsSpan());
}

if (uriParts.Lower)
{
index = CopyTextToBufferLowerInvariant(buffer, index, uriParts.Host.AsSpan());
Expand Down
24 changes: 24 additions & 0 deletions src/ImageSharp.Web/CommandHandling.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using SixLabors.ImageSharp.Web.Commands;

namespace SixLabors.ImageSharp.Web
{
/// <summary>
/// Provides enumeration for handling <see cref="CommandCollection"/> instances
/// when processing a request.
/// </summary>
public enum CommandHandling
{
/// <summary>
/// The command collection will be stripped of any unknown commands.
/// </summary>
Sanitize,

/// <summary>
/// The command collection will be processed unaltered.
/// </summary>
None
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,16 @@ public PresetOnlyQueryCollectionRequestParser(IOptions<PresetOnlyQueryCollection
/// <inheritdoc/>
public CommandCollection ParseRequestCommands(HttpContext context)
{
if (context.Request.Query.Count == 0 || !context.Request.Query.ContainsKey(QueryKey))
IQueryCollection queryCollection = context.Request.Query;
if (queryCollection is null
|| queryCollection.Count == 0
|| !queryCollection.ContainsKey(QueryKey))
{
// We return new here and below to ensure the collection is still mutable via events.
return new();
}

StringValues query = context.Request.Query[QueryKey];
StringValues query = queryCollection[QueryKey];
string requestedPreset = query[query.Count - 1];
if (this.presets.TryGetValue(requestedPreset, out CommandCollection collection))
{
Expand Down
9 changes: 3 additions & 6 deletions src/ImageSharp.Web/Commands/QueryCollectionRequestParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;

namespace SixLabors.ImageSharp.Web.Commands
Expand All @@ -16,17 +15,15 @@ public sealed class QueryCollectionRequestParser : IRequestParser
/// <inheritdoc/>
public CommandCollection ParseRequestCommands(HttpContext context)
{
if (context.Request.Query.Count == 0)
IQueryCollection query = context.Request.Query;
if (query is null || query.Count == 0)
{
// We return new to ensure the collection is still mutable via events.
return new();
}

// TODO: Investigate skipping the double allocation here.
// In .NET 6 we can directly use the QueryStringEnumerable type and enumerate stright to our command collection
Dictionary<string, StringValues> parsed = QueryHelpers.ParseQuery(context.Request.QueryString.ToUriComponent());
CommandCollection transformed = new();
foreach (KeyValuePair<string, StringValues> pair in parsed)
foreach (KeyValuePair<string, StringValues> pair in query)
{
// Use the indexer for both set and query. This replaces any previously parsed values.
transformed[pair.Key] = pair.Value[pair.Value.Count - 1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ private static void AddDefaultServices(

builder.SetRequestParser<QueryCollectionRequestParser>();

builder.Services.AddSingleton<ImageSharpRequestAuthorizationUtilities>();

builder.SetCache<PhysicalFileSystemCache>();

builder.SetCacheKey<UriRelativeLowerInvariantCacheKey>();
Expand Down
5 changes: 1 addition & 4 deletions src/ImageSharp.Web/FormatUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ public FormatUtilities(IOptions<ImageSharpMiddlewareOptions> options)
{
string[] extensions = imageFormat.FileExtensions.ToArray();

foreach (string extension in extensions)
{
this.extensions.Add(extension);
}
this.extensions.AddRange(extensions);

this.extensionsByMimeType[imageFormat.DefaultMimeType] = extensions[0];
}
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp.Web/ImageSharp.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" />
Expand Down
Loading