Skip to content

Commit de01fd0

Browse files
authored
Merge pull request #4 from skttl/verify-urls
adds config and method to verify that a url exists (returns success s…
2 parents c7ae121 + 022f522 commit de01fd0

File tree

6 files changed

+68
-8
lines changed

6 files changed

+68
-8
lines changed

docs/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ The `RemoteImageProviderOptions` class provides the following configuration opti
44

55
- `Settings`: A list of the different allowed sources for images.
66

7+
- `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).
8+
-
79
Each setting (`RemoteImageProviderSetting`) provides the following configuration options:
810

911
- `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
2628

2729
- `AdditionalOptions`: Allows specifying additional `RemoteImageProviderOptions` instances. This can be useful when you have multiple configurations with different prefixes or other settings.
2830

31+
- `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`.`
32+
2933
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.
3034

3135
Don't forget to configure these options in your application's services configuration as shown in the Usage section of this README.
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
namespace ImageSharpCommunity.Providers.Remote.Configuration;
1+
namespace ImageSharpCommunity.Providers.Remote.Configuration;
22
public class RemoteImageProviderOptions
33
{
44
/// <summary>
55
/// A list of settings for remote image providers. Here you define your url prefixes, and which domains are allowed to fetch images from.
66
/// </summary>
77
public List<RemoteImageProviderSetting> Settings { get; set; } = new List<RemoteImageProviderSetting>();
8-
}
8+
9+
/// <summary>
10+
/// Fallback max age for the image. If the server does not return a cache-control header, this value is used.
11+
/// </summary>
12+
public TimeSpan FallbackMaxAge { get; set; } = TimeSpan.FromHours(1);
13+
}

src/ImageSharpCommunity.Providers.Remote/Configuration/RemoteImageProviderSetting.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,10 @@ public RemoteImageProviderSetting(string prefix)
5757
/// </summary>
5858
public bool AllowAllDomains { get; set; }
5959

60+
/// <summary>
61+
/// Verify that the input url returns a succesful status code.
62+
/// </summary>
63+
public bool VerifyUrl { get; set; } = true;
64+
6065
internal string ClientDictionaryKey => $"{HttpClientName}_{UserAgent}_{Timeout}_{MaxBytes}";
6166
}

src/ImageSharpCommunity.Providers.Remote/RemoteImageProvider.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ImageSharpCommunity.Providers.Remote.Configuration;
22
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Extensions.Caching.Memory;
34
using Microsoft.Extensions.Logging;
45
using Microsoft.Extensions.Options;
56
using SixLabors.ImageSharp.Web.Providers;
@@ -13,13 +14,15 @@ public class RemoteImageProvider : IImageProvider
1314
private readonly RemoteImageProviderOptions _options;
1415
private readonly ILogger<RemoteImageProvider> _logger;
1516
private readonly ILogger<RemoteImageResolver> _resolverLogger;
17+
private readonly IMemoryCache _cache;
1618

17-
public RemoteImageProvider(IHttpClientFactory clientFactory, IOptions<RemoteImageProviderOptions> options, ILogger<RemoteImageProvider> logger, ILogger<RemoteImageResolver> resolverLogger)
19+
public RemoteImageProvider(IHttpClientFactory clientFactory, IOptions<RemoteImageProviderOptions> options, ILogger<RemoteImageProvider> logger, ILogger<RemoteImageResolver> resolverLogger, IMemoryCache cache)
1820
{
1921
_clientFactory = clientFactory;
2022
_options = options.Value;
2123
_logger = logger;
2224
_resolverLogger = resolverLogger;
25+
_cache = cache;
2326
}
2427

2528
public ProcessingBehavior ProcessingBehavior => ProcessingBehavior.All;
@@ -37,7 +40,8 @@ public bool IsValidRequest(HttpContext context)
3740
context.Request.Path.GetMatchingRemoteImageProviderSetting(_options) is RemoteImageProviderSetting setting
3841
&& context.Request.Path.GetSourceUrlForRemoteImageProviderUrl(_options) is string url
3942
&& Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) && uri != null
40-
&& uri.IsValidForSetting(setting);
43+
&& uri.IsValidForSetting(setting)
44+
&& UrlReturnsSuccess(setting, uri);
4145
}
4246

4347
public Task<IImageResolver?> GetAsync(HttpContext context)
@@ -53,11 +57,37 @@ public bool IsValidRequest(HttpContext context)
5357
else
5458
{
5559
_logger.LogDebug("Found matching remote image provider setting for path: {path}", context.Request.Path);
56-
return Task.FromResult((IImageResolver?)new RemoteImageResolver(_clientFactory, url, options, _resolverLogger));
60+
return Task.FromResult((IImageResolver?)new RemoteImageResolver(_clientFactory, url, options, _resolverLogger, _options));
5761
}
5862
}
5963
private bool IsMatch(HttpContext context)
6064
{
6165
return context.Request.Path.GetMatchingRemoteImageProviderSetting(_options) != null;
6266
}
67+
68+
private bool UrlReturnsSuccess(RemoteImageProviderSetting setting, Uri uri)
69+
{
70+
if (setting.VerifyUrl == false)
71+
{
72+
_logger.LogDebug("Skipping verification of URL {Url} as VerifyUrl is set to false", uri);
73+
return true;
74+
}
75+
76+
if (_cache.TryGetValue(nameof(RemoteImageProvider) + uri, out bool cachedResult))
77+
{
78+
_logger.LogDebug("Using cached result for URL {Url}", uri);
79+
return cachedResult;
80+
}
81+
82+
var client = _clientFactory.GetRemoteImageProviderHttpClient(setting);
83+
var request = new HttpRequestMessage(HttpMethod.Head, uri);
84+
var response = client.SendAsync(request).Result;
85+
86+
if (response.Headers.CacheControl?.MaxAge is not null)
87+
{
88+
_cache.Set(nameof(RemoteImageProvider) + uri, response.IsSuccessStatusCode, response.Headers.CacheControl.MaxAge ?? _options.FallbackMaxAge);
89+
}
90+
91+
return response.IsSuccessStatusCode;
92+
}
6393
}

src/ImageSharpCommunity.Providers.Remote/RemoteImageResolver.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ public class RemoteImageResolver : IImageResolver
99
private readonly string _url;
1010
private readonly RemoteImageProviderSetting _setting;
1111
private readonly ILogger<RemoteImageResolver> _logger;
12+
private readonly RemoteImageProviderOptions _options;
1213

13-
public RemoteImageResolver(IHttpClientFactory clientFactory, string url, RemoteImageProviderSetting setting, ILogger<RemoteImageResolver> logger)
14+
public RemoteImageResolver(IHttpClientFactory clientFactory, string url, RemoteImageProviderSetting setting, ILogger<RemoteImageResolver> logger, RemoteImageProviderOptions options)
1415
{
1516
_clientFactory = clientFactory;
1617
_url = url;
1718
_setting = setting;
1819
_logger = logger;
20+
_options = options;
1921
}
2022

2123
public async Task<ImageMetadata> GetMetaDataAsync()
@@ -38,10 +40,14 @@ public async Task<ImageMetadata> GetMetaDataAsync()
3840

3941
if (response.Headers.CacheControl?.MaxAge is null)
4042
{
41-
_logger.LogDebug("MaxAge header is null from {Url}", _url);
43+
_logger.LogDebug("MaxAge header is null from {Url}, falling back to configured FallbackMaxAge {FallbackMaxAge}", _url, _options.FallbackMaxAge);
4244
}
4345

44-
return new ImageMetadata(response.Content.Headers.LastModified.GetValueOrDefault().UtcDateTime, (response.Headers.CacheControl?.MaxAge).GetValueOrDefault(), response.Content.Headers.ContentLength.GetValueOrDefault());
46+
return new ImageMetadata(
47+
response.Content.Headers.LastModified.GetValueOrDefault().UtcDateTime,
48+
response.Headers.CacheControl?.MaxAge ?? _options.FallbackMaxAge,
49+
response.Content.Headers.ContentLength.GetValueOrDefault()
50+
);
4551
}
4652

4753
public async Task<Stream> OpenReadAsync()

src/Umbraco.Community.ImageSharpRemoteImages/appsettings-schema.umbraco-community-imagesharpremoteimages.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
"items": {
4646
"$ref": "#/definitions/UmbracoCommunityImageSharpRemoteImagesSettingDefinition"
4747
}
48+
},
49+
"FallbackMaxAge": {
50+
"type": "string",
51+
"description": "Fallback max age for the image. If the server does not return a cache-control header, this value is used.",
52+
"format": "duration",
53+
"default": "0.01:00:00"
4854
}
4955
}
5056
},
@@ -96,6 +102,10 @@
96102
"AllowAllDomains": {
97103
"type": "boolean",
98104
"description": "Allows all domains to be processed."
105+
},
106+
"VerifyUrl": {
107+
"type": "boolean",
108+
"description": "Verify that the input url returns a succesful status code."
99109
}
100110
},
101111
"required": [ "Prefix" ]

0 commit comments

Comments
 (0)