From eebcbca5a3b5cf3200aa732c6731e48e47d375ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Kottal?= Date: Wed, 22 Jan 2025 14:34:02 +0100 Subject: [PATCH 1/2] adds config and method to verify that a url exists (returns success status code) when selecting provider --- .../RemoteImageProviderSetting.cs | 5 +++ .../RemoteImageProvider.cs | 34 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderSetting.cs b/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderSetting.cs index e3f19a5..2fdaaa7 100644 --- a/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderSetting.cs +++ b/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderSetting.cs @@ -57,5 +57,10 @@ public RemoteImageProviderSetting(string prefix) /// public bool AllowAllDomains { get; set; } + /// + /// Verify that the input url returns a succesful status code. + /// + public bool VerifyUrl { get; set; } = true; + internal string ClientDictionaryKey => $"{HttpClientName}_{UserAgent}_{Timeout}_{MaxBytes}"; } diff --git a/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs b/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs index a65c6f0..3ab68c3 100644 --- a/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs +++ b/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs @@ -1,5 +1,6 @@ using ImageSharpCommunity.Providers.Remote.Configuration; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web.Providers; @@ -13,13 +14,15 @@ public class RemoteImageProvider : IImageProvider private readonly RemoteImageProviderOptions _options; private readonly ILogger _logger; private readonly ILogger _resolverLogger; + private readonly IMemoryCache _cache; - public RemoteImageProvider(IHttpClientFactory clientFactory, IOptions options, ILogger logger, ILogger resolverLogger) + public RemoteImageProvider(IHttpClientFactory clientFactory, IOptions options, ILogger logger, ILogger resolverLogger, IMemoryCache cache) { _clientFactory = clientFactory; _options = options.Value; _logger = logger; _resolverLogger = resolverLogger; + _cache = cache; } public ProcessingBehavior ProcessingBehavior => ProcessingBehavior.All; @@ -37,7 +40,8 @@ public bool IsValidRequest(HttpContext context) context.Request.Path.GetMatchingRemoteImageProviderSetting(_options) is RemoteImageProviderSetting setting && context.Request.Path.GetSourceUrlForRemoteImageProviderUrl(_options) is string url && Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) && uri != null - && uri.IsValidForSetting(setting); + && uri.IsValidForSetting(setting) + && UrlReturnsSuccess(setting, uri); } public Task GetAsync(HttpContext context) @@ -60,4 +64,30 @@ private bool IsMatch(HttpContext context) { return context.Request.Path.GetMatchingRemoteImageProviderSetting(_options) != null; } + + private bool UrlReturnsSuccess(RemoteImageProviderSetting setting, Uri uri) + { + if (setting.VerifyUrl == false) + { + _logger.LogDebug("Skipping verification of URL {Url} as VerifyUrl is set to false", uri); + return true; + } + + if (_cache.TryGetValue(nameof(RemoteImageProvider) + uri, out bool cachedResult)) + { + _logger.LogDebug("Using cached result for URL {Url}", uri); + return cachedResult; + } + + var client = _clientFactory.GetRemoteImageProviderHttpClient(setting); + var request = new HttpRequestMessage(HttpMethod.Head, uri); + var response = client.SendAsync(request).Result; + + if (response.Headers.CacheControl?.MaxAge is not null) + { + _cache.Set(nameof(RemoteImageProvider) + uri, response.IsSuccessStatusCode, response.Headers.CacheControl.MaxAge ?? TimeSpan.Zero); + } + + return response.IsSuccessStatusCode; + } } From 022f522f932c480c580068d5740f518aff8acf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Kottal?= Date: Wed, 29 Jan 2025 12:18:04 +0100 Subject: [PATCH 2/2] Adds configuration option for setting a fallback max age, in case the response doesn't have one --- docs/configuration.md | 4 ++++ .../Configuration/RemoteImageProviderOptions.cs | 9 +++++++-- .../RemoteImageProvider.cs | 4 ++-- .../RemoteImageResolver.cs | 12 +++++++++--- ...ema.umbraco-community-imagesharpremoteimages.json | 10 ++++++++++ 5 files changed, 32 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e319d34..8eef63e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -4,6 +4,8 @@ The `RemoteImageProviderOptions` class provides the following configuration opti - `Settings`: A list of the different allowed sources for images. +- `FallbackMaxAge`: Specifies a fallback max age for the image being loaded. Used if the server does not return a cache-control header. By default, it is set to `0.01:00:00` (1 hour). +- Each setting (`RemoteImageProviderSetting`) provides the following configuration options: - `Prefix`: Specified in the constructor, and defines the local path to prefix all remote image requests with. For example, setting this to `/remote` allows requests like `/remote/https://test.com/test.png` to pass through this provider. @@ -26,6 +28,8 @@ Each setting (`RemoteImageProviderSetting`) provides the following configuration - `AdditionalOptions`: Allows specifying additional `RemoteImageProviderOptions` instances. This can be useful when you have multiple configurations with different prefixes or other settings. +- `VerifyUrl`: Boolean value. If set to true, the URL will be verified before downloading the image. This can be useful to prevent downloading and processing images returning 404 or other error codes. By default, it is set to `truee`.` + Please note that these options provide customization and control over how remote images are loaded and processed. You can adjust these options according to your specific requirements. Don't forget to configure these options in your application's services configuration as shown in the Usage section of this README. diff --git a/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderOptions.cs b/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderOptions.cs index eeef9b7..7733f59 100644 --- a/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderOptions.cs +++ b/src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderOptions.cs @@ -1,8 +1,13 @@ -namespace ImageSharpCommunity.Providers.Remote.Configuration; +namespace ImageSharpCommunity.Providers.Remote.Configuration; public class RemoteImageProviderOptions { /// /// A list of settings for remote image providers. Here you define your url prefixes, and which domains are allowed to fetch images from. /// public List Settings { get; set; } = new List(); -} \ No newline at end of file + + /// + /// Fallback max age for the image. If the server does not return a cache-control header, this value is used. + /// + public TimeSpan FallbackMaxAge { get; set; } = TimeSpan.FromHours(1); +} diff --git a/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs b/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs index 3ab68c3..bf7323d 100644 --- a/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs +++ b/src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs @@ -57,7 +57,7 @@ public bool IsValidRequest(HttpContext context) else { _logger.LogDebug("Found matching remote image provider setting for path: {path}", context.Request.Path); - return Task.FromResult((IImageResolver?)new RemoteImageResolver(_clientFactory, url, options, _resolverLogger)); + return Task.FromResult((IImageResolver?)new RemoteImageResolver(_clientFactory, url, options, _resolverLogger, _options)); } } private bool IsMatch(HttpContext context) @@ -85,7 +85,7 @@ private bool UrlReturnsSuccess(RemoteImageProviderSetting setting, Uri uri) if (response.Headers.CacheControl?.MaxAge is not null) { - _cache.Set(nameof(RemoteImageProvider) + uri, response.IsSuccessStatusCode, response.Headers.CacheControl.MaxAge ?? TimeSpan.Zero); + _cache.Set(nameof(RemoteImageProvider) + uri, response.IsSuccessStatusCode, response.Headers.CacheControl.MaxAge ?? _options.FallbackMaxAge); } return response.IsSuccessStatusCode; diff --git a/src/ImageSharpCommunity.Providers.Remote/RemoteImageResolver.cs b/src/ImageSharpCommunity.Providers.Remote/RemoteImageResolver.cs index b2e8718..30205ab 100644 --- a/src/ImageSharpCommunity.Providers.Remote/RemoteImageResolver.cs +++ b/src/ImageSharpCommunity.Providers.Remote/RemoteImageResolver.cs @@ -9,13 +9,15 @@ public class RemoteImageResolver : IImageResolver private readonly string _url; private readonly RemoteImageProviderSetting _setting; private readonly ILogger _logger; + private readonly RemoteImageProviderOptions _options; - public RemoteImageResolver(IHttpClientFactory clientFactory, string url, RemoteImageProviderSetting setting, ILogger logger) + public RemoteImageResolver(IHttpClientFactory clientFactory, string url, RemoteImageProviderSetting setting, ILogger logger, RemoteImageProviderOptions options) { _clientFactory = clientFactory; _url = url; _setting = setting; _logger = logger; + _options = options; } public async Task GetMetaDataAsync() @@ -38,10 +40,14 @@ public async Task GetMetaDataAsync() if (response.Headers.CacheControl?.MaxAge is null) { - _logger.LogDebug("MaxAge header is null from {Url}", _url); + _logger.LogDebug("MaxAge header is null from {Url}, falling back to configured FallbackMaxAge {FallbackMaxAge}", _url, _options.FallbackMaxAge); } - return new ImageMetadata(response.Content.Headers.LastModified.GetValueOrDefault().UtcDateTime, (response.Headers.CacheControl?.MaxAge).GetValueOrDefault(), response.Content.Headers.ContentLength.GetValueOrDefault()); + return new ImageMetadata( + response.Content.Headers.LastModified.GetValueOrDefault().UtcDateTime, + response.Headers.CacheControl?.MaxAge ?? _options.FallbackMaxAge, + response.Content.Headers.ContentLength.GetValueOrDefault() + ); } public async Task OpenReadAsync() diff --git a/src/Umbraco.Community.ImageSharpRemoteImages/appsettings-schema.umbraco-community-imagesharpremoteimages.json b/src/Umbraco.Community.ImageSharpRemoteImages/appsettings-schema.umbraco-community-imagesharpremoteimages.json index 1214134..fac92a6 100644 --- a/src/Umbraco.Community.ImageSharpRemoteImages/appsettings-schema.umbraco-community-imagesharpremoteimages.json +++ b/src/Umbraco.Community.ImageSharpRemoteImages/appsettings-schema.umbraco-community-imagesharpremoteimages.json @@ -45,6 +45,12 @@ "items": { "$ref": "#/definitions/UmbracoCommunityImageSharpRemoteImagesSettingDefinition" } + }, + "FallbackMaxAge": { + "type": "string", + "description": "Fallback max age for the image. If the server does not return a cache-control header, this value is used.", + "format": "duration", + "default": "0.01:00:00" } } }, @@ -96,6 +102,10 @@ "AllowAllDomains": { "type": "boolean", "description": "Allows all domains to be processed." + }, + "VerifyUrl": { + "type": "boolean", + "description": "Verify that the input url returns a succesful status code." } }, "required": [ "Prefix" ]