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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,8 @@ healthchecksdb

# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
tools
.idea
build-tools
*.swp
*.un~
tmp
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ the parameters sent to the resize service. This can be useful for
testing what defaults to use and ensuring that domain whitelists
are appropriately set.

For comparing the output of many option combinations side-by-side,
open [tools/iraas-compare.html](tools/iraas-compare.html) directly in a
browser (no web server required) and point it at an IRAAS instance with
`EnableTestPage` set to `true`.

## Requirements
- Node (for build)
- dotnet sdk 7.0 or better
Expand All @@ -43,7 +48,7 @@ MaxInputImageSize | 41943040 (40mb) | The maximum siz
MaxOutputImageSize | 41943040 (40mb) | The maximum output size, in bytes, to allow. Outputs which are larger than this are discarded
UseDeveloperExceptionPage | false | Flag: when enabled, if IRAAS encounters an error, it will show the developer exception page instead of the default short response
UseHttps | false | Flag: when enabled, requests coming in on http will be redirected to the https url you configure in `Urls`
EnableTestPage | false | Flag: when enabled, the path `/test` will serve a (very basic) web page to test resizing images
EnableTestPage | false | Flag: when enabled, the path `/test` will serve a (very basic) web page to test resizing images. GET resize and `/size` responses also allow cross-origin reads (`Access-Control-Allow-Origin: *`, `Access-Control-Expose-Headers: *`, `Timing-Allow-Origin: *`) so that standalone test pages, like `tools/iraas-compare.html`, can read sizes and headers
DomainWhitelist | * | Comma-delimited list of domains or domain globs which are allowed as image sources, eg `*.mydomain.com`. Setting this to "*" allows all domains.
MaxConcurrency | 0 | The maximum number of image resize operations to perform concurrently. When set to 0, will default to using the number of processors on the host
MaxImageFetchTimeInMilliseconds | 10000 (10s) | The maximum time to wait for a remote service to provide a requested image, in milliseconds
Expand All @@ -54,6 +59,7 @@ SuppressErrorDiagnostics | false | Flag: when set
MaxUrlFetchRetries | 1 | The number of times to _retry_ a failed image fetch request. If set to zero, only the initial request is performed. Setting this to a low number like 1 or 2 may work around transient errors from remote image servers
AllowInvalidSSLCertificates | false | Flag: when set to true, invalid ssl certificates are ignored when fetching images to resize (useful for dev-testing against self-signed certificates)
AllowPostRequests | false | Flag: when set to true, clients may post raw image data and resize parameters to IRAAS and receive the resize result, if they pass other limitations
Verbose | false | Flag: when enabled, resize responses include `IRAAS-Timing-*` headers (milliseconds per processing step), `IRAAS-Resize-Parameter-*` headers (the final parameters used, after defaults are applied) and an `IRAAS-Source-Size` header (source image size, in bytes). The shipped `appsettings.json` enables this
PostAuthTokens | "" | A comma-separated list of allowed tokens for Bearer authentication when POST requests are allowed. When it's empty, no POST request will be allowed. You may allow all POST requests by setting this to '*'.

## Operation notes
Expand Down
65 changes: 65 additions & 0 deletions src/IRAAS.Tests/ImageProcessing/TestImageResizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,50 @@ public async Task ShouldIncludeTimingHeader_(string header)
}
}

[TestFixture]
public class SourceSizeHeader
{
[Test]
public async Task ShouldIncludeSourceSizeWhenVerbose()
{
// Arrange
var data = Resources.Data.FluffyCatBmp;
var fetcher = CreateFetcherFor(data);
var sut = Create(fetcher);
var options = new ImageUrlResizeParameters()
{
Url = GetRandomHttpUrlWithPath(),
Format = "PNG"
};
// Act
var result = await sut.Resize(options, NoHeaders);
// Assert
Expect(result.Headers)
.To.Contain.Key(SizeHeaders.SourceSize)
.With.Value($"{data.Length}");
}

[Test]
public async Task ShouldNotIncludeSourceSizeWhenNotVerbose()
{
// Arrange
var fetcher = CreateFetcherFor(Resources.Data.FluffyCatBmp);
var appSettings = CreateDefaultAppSettingsWithVerboseEnabled();
appSettings.Verbose.Returns(false);
var sut = Create(fetcher, appSettings);
var options = new ImageUrlResizeParameters()
{
Url = GetRandomHttpUrlWithPath(),
Format = "PNG"
};
// Act
var result = await sut.Resize(options, NoHeaders);
// Assert
Expect(result.Headers)
.Not.To.Contain.Key(SizeHeaders.SourceSize);
}
}

[Test]
public void ShouldThrowIfOutputWillExceedMaxOutputSize()
{
Expand Down Expand Up @@ -875,6 +919,27 @@ private static IUrlFetcher CreateFetcherFor(
[TestFixture]
public class ResizingImageViaPostedData
{
[Test]
public async Task ShouldIncludeSourceSizeWhenVerbose()
{
// Arrange
var data = Resources.Data.FluffyCatBmp;
var sut = Create();
var resizeParameters = new ImageDataResizeParameters()
{
ImageData = data,
Format = "PNG"
};

// Act
var result = await sut.Resize(resizeParameters);

// Assert
Expect(result.Headers)
.To.Contain.Key(SizeHeaders.SourceSize)
.With.Value($"{data.Length}");
}

[Test]
public void ShouldThrowForNullResizeParameters()
{
Expand Down
185 changes: 185 additions & 0 deletions src/IRAAS.Tests/Middleware/TestTestPageCorsMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using IRAAS.Middleware;
using IRAAS.Tests.ImageProcessing;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using NUnit.Framework;
using PeanutButter.TestUtils.AspNetCore.Builders;
using PeanutButter.TestUtils.AspNetCore.Utils;

namespace IRAAS.Tests.Middleware;

[TestFixture]
public class TestTestPageCorsMiddleware
{
[Test]
public void ShouldImplementIMiddleware()
{
// Arrange
// Act
Expect(typeof(TestPageCorsMiddleware))
.To.Implement<IMiddleware>();
// Assert
}

[TestFixture]
public class WhenTestPageEnabled
{
[TestCase("/")]
[TestCase("/size")]
[TestCase("/size/")]
public async Task ShouldAllowAnyOriginToReadGetResponsesFrom_(
string path
)
{
// Arrange
HttpContext captured = null;
var (ctx, next) = Arena(HttpMethods.Get, path, c => captured = c, out var startResponse);
var sut = Create(TestPageEnabled());

// Act
await sut.InvokeAsync(ctx, next);
await startResponse();

// Assert
Expect(captured)
.To.Be(ctx);
var headers = ctx.Response.Headers;
Expect(headers["Access-Control-Allow-Origin"].ToString())
.To.Equal("*");
Expect(headers["Access-Control-Expose-Headers"].ToString())
.To.Equal("*");
Expect(headers["Timing-Allow-Origin"].ToString())
.To.Equal("*");
}

[Test]
public async Task ShouldOverrideCorsHeadersEchoedFromTheImageSource()
{
// Arrange
var (ctx, next) = Arena(
HttpMethods.Get,
"/",
c => c.Response.Headers["Access-Control-Allow-Origin"] = "https://elsewhere.com",
out var startResponse
);
var sut = Create(TestPageEnabled());

// Act
await sut.InvokeAsync(ctx, next);
await startResponse();

// Assert
Expect(ctx.Response.Headers["Access-Control-Allow-Origin"].ToString())
.To.Equal("*");
}

[TestCase("POST", "/")]
[TestCase("GET", "/test")]
[TestCase("GET", "/health")]
[TestCase("GET", "/config")]
public async Task ShouldNotAddCorsHeadersFor_(
string method,
string path
)
{
// Arrange
HttpContext captured = null;
var (ctx, next) = Arena(method, path, c => captured = c, out var startResponse);
var sut = Create(TestPageEnabled());

// Act
await sut.InvokeAsync(ctx, next);
await startResponse();

// Assert
Expect(captured)
.To.Be(ctx);
Expect(ctx.Response.Headers.Keys)
.Not.To.Contain("Access-Control-Allow-Origin");
}
}

[TestFixture]
public class WhenTestPageDisabled
{
[TestCase("/")]
[TestCase("/size")]
public async Task ShouldNotAddCorsHeadersFor_(
string path
)
{
// Arrange
HttpContext captured = null;
var (ctx, next) = Arena(HttpMethods.Get, path, c => captured = c, out var startResponse);
var appSettings = Substitute.For<IAppSettings>()
.WithDefaultSettings()
.WithTestPageDisabled();
var sut = Create(appSettings);

// Act
await sut.InvokeAsync(ctx, next);
await startResponse();

// Assert
Expect(captured)
.To.Be(ctx);
var headers = ctx.Response.Headers;
Expect(headers.Keys)
.Not.To.Contain("Access-Control-Allow-Origin");
Expect(headers.Keys)
.Not.To.Contain("Access-Control-Expose-Headers");
Expect(headers.Keys)
.Not.To.Contain("Timing-Allow-Origin");
}
}

private static RequestDelegateTestArena Arena(
string method,
string path,
Action<HttpContext> logic,
out Func<Task> startResponse
)
{
// the fake response doesn't run OnStarting handlers itself, so
// capture them and let the test say when the response starts
var onStarting = new List<(Func<object, Task> callback, object state)>();
startResponse = async () =>
{
foreach (var (callback, state) in onStarting)
{
await callback(state);
}
};
return RequestDelegateTestArenaBuilder.Create()
.WithContext(
HttpContextBuilder.Create()
.WithRequestMethod(method)
.WithRequestPath(path)
.WithRequestQueryParameter("url", GetRandomHttpsUrlWithPath())
.WithResponse(
HttpResponseBuilder.Create()
.WithOnStartingHandler((callback, state) => onStarting.Add((callback, state)))
.Build()
)
.Build()
).WithDelegateLogic(logic)
.Build();
}

private static IAppSettings TestPageEnabled()
{
return Substitute.For<IAppSettings>()
.WithDefaultSettings()
.WithTestPageEnabled();
}

private static TestPageCorsMiddleware Create(
IAppSettings appSettings
)
{
return new TestPageCorsMiddleware(appSettings);
}
}
1 change: 1 addition & 0 deletions src/IRAAS.Tests/TestStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ public class MiddlewareTests : TestBase
/// </summary>
private static readonly Type[] ExpectedPipeline =
[
typeof(TestPageCorsMiddleware),
typeof(ProductionFallbackExceptionHandlerMiddleware),
typeof(MaxClientsMiddleware),
typeof(BadHttpRequestExceptionMiddleware),
Expand Down
14 changes: 14 additions & 0 deletions src/IRAAS/ImageProcessing/ImageResizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ private StreamAndHeaders ResizeImage(
StreamAndHeaders src
)
{
var sourceSizeHeaders = SourceSizeHeadersFor(src.Stream);
IImageFormat sourceFormat = null;
try
{
Expand Down Expand Up @@ -210,11 +211,24 @@ resizeParameters.ReplaceTransparencyWith is not null &&
new MergeDictionary<string, string>(
src.Headers,
timer.Timings,
sourceSizeHeaders,
resizeParameters.DumpIf(_appSettings.Verbose)
)
);
}

private Dictionary<string, string> SourceSizeHeadersFor(
Stream source
)
{
return _appSettings.Verbose
? new Dictionary<string, string>
{
[SizeHeaders.SourceSize] = $"{source.Length}"
}
: new Dictionary<string, string>();
}

private IResampler DetermineSamplerFor(
IActiveImageResizeParameters resizeParameters
)
Expand Down
6 changes: 6 additions & 0 deletions src/IRAAS/ImageProcessing/SizeHeaders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace IRAAS.ImageProcessing;

public static class SizeHeaders
{
public const string SourceSize = "IRAAS-Source-Size";
}
11 changes: 11 additions & 0 deletions src/IRAAS/Middleware/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ HttpRequest request
);
}

public static bool HasResizeEndpointPath(
HttpRequest request
)
{
return AnyPathMatches(
request,
"",
"/"
);
}

public static bool HasSizeEndpointPath(
HttpRequest request
)
Expand Down
Loading
Loading