From 7bb38498fed0468a2a58cff8bca192b2d74fa90e Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 12 May 2022 14:23:37 +0200 Subject: [PATCH 1/6] Coorectly parse ETag value --- .../AzureBlobFileSystemMiddleware.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs index 01f85bb..dd382db 100644 --- a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs +++ b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs @@ -164,7 +164,12 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) }; responseHeaders.LastModified = properties.Value.LastModified; - responseHeaders.ETag = new EntityTagHeaderValue(properties.Value.ETag.ToString("H")); + + if (EntityTagHeaderValue.TryParse(properties.Value.ETag.ToString("H"), out EntityTagHeaderValue entityTagHeaderValue)) + { + responseHeaders.ETag = entityTagHeaderValue; + } + responseHeaders.Append(HeaderNames.Vary, "Accept-Encoding"); var requestHeaders = request.GetTypedHeaders(); From 989691b404e528d59ba40b9967ddaa0b92c3f8ff Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 12 May 2022 14:26:56 +0200 Subject: [PATCH 2/6] Use remaining path segment --- .../AzureBlobFileSystemMiddleware.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs index dd382db..fe06c71 100644 --- a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs +++ b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs @@ -83,13 +83,13 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) var request = context.Request; var response = context.Response; - if (!context.Request.Path.StartsWithSegments(_rootPath, StringComparison.InvariantCultureIgnoreCase)) + if (!context.Request.Path.StartsWithSegments(_rootPath, StringComparison.InvariantCultureIgnoreCase, out PathString path)) { await next(context).ConfigureAwait(false); return; } - string containerPath = $"{_containerRootPath.TrimEnd('/')}/{(request.Path.Value.Remove(0, _rootPath.Length)).TrimStart('/')}"; + string containerPath = $"{_containerRootPath.TrimEnd('/')}/{path.Value?.TrimStart('/')}"; var blob = _fileSystemProvider.GetFileSystem(_name).GetBlobClient(containerPath); var blobRequestConditions = GetAccessCondition(context.Request); From c5b394b644b3cd77fd74bb2154af12a8e23a09cc Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 12 May 2022 14:29:47 +0200 Subject: [PATCH 3/6] Correctly parse access conditions --- .../AzureBlobFileSystemMiddleware.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs index fe06c71..d39af58 100644 --- a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs +++ b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs @@ -253,11 +253,12 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) } var ifModifiedSince = request.Headers["If-Modified-Since"]; - if (!string.IsNullOrEmpty(ifModifiedSince)) + if (!string.IsNullOrEmpty(ifModifiedSince) && + DateTimeOffset.TryParse(ifModifiedSince, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset ifModifiedSinceDate)) { return new BlobRequestConditions { - IfModifiedSince = DateTimeOffset.Parse(ifModifiedSince, CultureInfo.InvariantCulture) + IfModifiedSince = ifModifiedSinceDate }; } } @@ -268,24 +269,29 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) var ifRange = request.Headers["If-Range"]; if (!string.IsNullOrEmpty(ifRange)) { - var conditions = new BlobRequestConditions(); - - if (DateTimeOffset.TryParse(ifRange, out var date)) + if (DateTimeOffset.TryParse(ifRange, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset ifRangeDate)) { - conditions.IfUnmodifiedSince = date; + return new BlobRequestConditions() + { + IfUnmodifiedSince = ifRangeDate + }; } else { - conditions.IfMatch = new ETag(ifRange); + return new BlobRequestConditions() + { + IfMatch = new ETag(ifRange) + }; } } var ifUnmodifiedSince = request.Headers["If-Unmodified-Since"]; - if (!string.IsNullOrEmpty(ifUnmodifiedSince)) + if (!string.IsNullOrEmpty(ifUnmodifiedSince) && + DateTimeOffset.TryParse(ifUnmodifiedSince, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset ifUnmodifiedSinceDate)) { return new BlobRequestConditions { - IfUnmodifiedSince = DateTimeOffset.Parse(ifUnmodifiedSince, CultureInfo.InvariantCulture) + IfUnmodifiedSince = ifUnmodifiedSinceDate }; } } From 12828cc89c24353390ecb9b3a2adc1a66c67b05b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 12 May 2022 14:31:57 +0200 Subject: [PATCH 4/6] Use HeaderNames constants and fix formatting --- .../AzureBlobFileSystemMiddleware.cs | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs index d39af58..d7fbdf2 100644 --- a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs +++ b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs @@ -114,7 +114,7 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) // a Content-Range header is needed with the new length ignoreRange = true; properties = await blob.GetPropertiesAsync().ConfigureAwait(false); - response.Headers.Append("Content-Range", $"bytes */{properties.Value.ContentLength}"); + response.Headers.Append(HeaderNames.ContentRange, $"bytes */{properties.Value.ContentLength}"); } catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.NotModified) { @@ -128,15 +128,15 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) // and not a request failed with status NotModified :( catch (Exception ex) when (ex.Message == "The condition specified using HTTP conditional header(s) is not met.") { - if (blobRequestConditions != null - && (blobRequestConditions.IfMatch.HasValue || blobRequestConditions.IfUnmodifiedSince.HasValue)) + if (blobRequestConditions != null && + (blobRequestConditions.IfMatch.HasValue || blobRequestConditions.IfUnmodifiedSince.HasValue)) { // If-Range or If-Unmodified-Since is not met // if the resource has been modified, we need to send the whole file back with a 200 OK // a Content-Range header is needed with the new length ignoreRange = true; properties = await blob.GetPropertiesAsync().ConfigureAwait(false); - response.Headers.Append("Content-Range", $"bytes */{properties.Value.ContentLength}"); + response.Headers.Append(HeaderNames.ContentRange, $"bytes */{properties.Value.ContentLength}"); } else { @@ -155,13 +155,12 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) var responseHeaders = response.GetTypedHeaders(); - responseHeaders.CacheControl = - new CacheControlHeaderValue - { - Public = true, - MustRevalidate = true, - MaxAge = _maxAge, - }; + responseHeaders.CacheControl = new CacheControlHeaderValue + { + Public = true, + MustRevalidate = true, + MaxAge = _maxAge, + }; responseHeaders.LastModified = properties.Value.LastModified; @@ -170,12 +169,10 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) responseHeaders.ETag = entityTagHeaderValue; } - responseHeaders.Append(HeaderNames.Vary, "Accept-Encoding"); + responseHeaders.Append(HeaderNames.Vary, HeaderNames.AcceptEncoding); var requestHeaders = request.GetTypedHeaders(); - var rangeHeader = requestHeaders.Range; - if (!ignoreRange && rangeHeader != null) { if (!ValidateRanges(rangeHeader.Ranges, properties.Value.ContentLength)) @@ -228,6 +225,7 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) return; } } + response.StatusCode = (int)HttpStatusCode.OK; response.ContentType = properties.Value.ContentType; responseHeaders.ContentLength = properties.Value.ContentLength; @@ -239,11 +237,11 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) private static BlobRequestConditions? GetAccessCondition(HttpRequest request) { - var range = request.Headers["Range"]; + var range = request.Headers[HeaderNames.Range]; if (string.IsNullOrEmpty(range)) { // etag - var ifNoneMatch = request.Headers["If-None-Match"]; + var ifNoneMatch = request.Headers[HeaderNames.IfNoneMatch]; if (!string.IsNullOrEmpty(ifNoneMatch)) { return new BlobRequestConditions @@ -252,7 +250,7 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) }; } - var ifModifiedSince = request.Headers["If-Modified-Since"]; + var ifModifiedSince = request.Headers[HeaderNames.IfModifiedSince]; if (!string.IsNullOrEmpty(ifModifiedSince) && DateTimeOffset.TryParse(ifModifiedSince, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset ifModifiedSinceDate)) { @@ -266,7 +264,7 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) { // handle If-Range header, it can be either an etag or a date // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range and https://tools.ietf.org/html/rfc7233#section-3.2 - var ifRange = request.Headers["If-Range"]; + var ifRange = request.Headers[HeaderNames.IfRange]; if (!string.IsNullOrEmpty(ifRange)) { if (DateTimeOffset.TryParse(ifRange, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset ifRangeDate)) @@ -285,7 +283,7 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) } } - var ifUnmodifiedSince = request.Headers["If-Unmodified-Since"]; + var ifUnmodifiedSince = request.Headers[HeaderNames.IfUnmodifiedSince]; if (!string.IsNullOrEmpty(ifUnmodifiedSince) && DateTimeOffset.TryParse(ifUnmodifiedSince, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset ifUnmodifiedSinceDate)) { @@ -302,14 +300,21 @@ private async Task HandleRequestAsync(HttpContext context, RequestDelegate next) private static bool ValidateRanges(ICollection ranges, long length) { if (ranges.Count == 0) + { return false; + } foreach (var range in ranges) { if (range.From > range.To) + { return false; + } + if (range.To >= length) + { return false; + } } return true; @@ -348,8 +353,7 @@ private static ContentRangeHeaderValue GetRangeHeader(BlobProperties properties, return new ContentRangeHeaderValue(from, to, properties.ContentLength); } - private static async Task DownloadRangeToStreamAsync(BlobClient blob, BlobProperties properties, - Stream outputStream, ContentRangeHeaderValue contentRange, CancellationToken cancellationToken) + private static async Task DownloadRangeToStreamAsync(BlobClient blob, BlobProperties properties, Stream outputStream, ContentRangeHeaderValue contentRange, CancellationToken cancellationToken) { var offset = contentRange.From.GetValueOrDefault(0L); var length = properties.ContentLength; @@ -370,8 +374,7 @@ private static async Task DownloadRangeToStreamAsync(BlobClient blob, BlobProper await DownloadRangeToStreamAsync(blob, outputStream, offset, length, cancellationToken).ConfigureAwait(false); } - private static async Task DownloadRangeToStreamAsync(BlobClient blob, Stream outputStream, - long offset, long length, CancellationToken cancellationToken) + private static async Task DownloadRangeToStreamAsync(BlobClient blob, Stream outputStream, long offset, long length, CancellationToken cancellationToken) { try { From e1939fd56f8e7a98d3bd089c769b7ae420df6934 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 8 Jun 2022 21:20:57 +0200 Subject: [PATCH 5/6] Remove extra slash between CDN URL and media path --- src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs b/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs index 5c7c55d..7d54ff2 100644 --- a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs +++ b/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs @@ -43,7 +43,7 @@ public CdnMediaUrlProvider(IOptionsMonitor options, return mediaUrl.IsUrl switch { false => mediaUrl, - _ => UrlInfo.Url($"{_cdnUrl}/{mediaUrl.Text[(_removeMediaFromPath ? "/media/" : "/").Length..]}", culture) + _ => UrlInfo.Url(_cdnUrl + mediaUrl.Text[(_removeMediaFromPath ? "/media/" : "/").Length..], culture) }; } From 105dc0b700ed78c12865d5d9ecdfb30be43402cc Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 8 Jun 2022 21:26:18 +0200 Subject: [PATCH 6/6] Use configured UmbracoMediaPath --- .../CdnMediaUrlProvider.cs | 81 +++++++++++++++---- 1 file changed, 64 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs b/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs index 7d54ff2..48d786e 100644 --- a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs +++ b/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs @@ -1,8 +1,11 @@ using System; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Routing; +using Umbraco.Extensions; namespace Umbraco.StorageProviders.AzureBlob { @@ -14,45 +17,89 @@ public class CdnMediaUrlProvider : DefaultMediaUrlProvider { private bool _removeMediaFromPath; private Uri _cdnUrl; + private string _mediaPath; /// - /// Creates a new instance of . + /// Initializes a new instance of the class. /// /// The options. + /// The global settings. + /// The hosting environment. /// The media path generators. /// The URI utility. - /// options - public CdnMediaUrlProvider(IOptionsMonitor options, - MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) + /// is null. + /// is null. + /// is null. + public CdnMediaUrlProvider(IOptionsMonitor options, IOptionsMonitor globalSettings, IHostingEnvironment hostingEnvironment, MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) + : this(options, mediaPathGenerators, uriUtility, string.Empty) + { + if (globalSettings == null) throw new ArgumentNullException(nameof(globalSettings)); + if (hostingEnvironment == null) throw new ArgumentNullException(nameof(hostingEnvironment)); + + _mediaPath = hostingEnvironment.ToAbsolute(globalSettings.CurrentValue.UmbracoMediaPath).EnsureEndsWith('/'); + + globalSettings.OnChange((options, name) => + { + if (name == Options.DefaultName) + { + _mediaPath = hostingEnvironment.ToAbsolute(options.UmbracoMediaPath).EnsureEndsWith('/'); + } + }); + } + + /// + /// Initializes a new instance of the class. + /// + /// The options. + /// The media path generators. + /// The URI utility. + /// is null. + [Obsolete("This constructor is obsolete and will be removed in a future version. Use another constructor instead.")] + public CdnMediaUrlProvider(IOptionsMonitor options, MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility) + : this(options, mediaPathGenerators, uriUtility, "/media/") + { } + + private CdnMediaUrlProvider(IOptionsMonitor options, MediaUrlGeneratorCollection mediaPathGenerators, UriUtility uriUtility, string mediaPath) : base(mediaPathGenerators, uriUtility) { if (options == null) throw new ArgumentNullException(nameof(options)); _cdnUrl = options.CurrentValue.Url; _removeMediaFromPath = options.CurrentValue.RemoveMediaFromPath; + _mediaPath = mediaPath; - options.OnChange(OptionsOnChange); + options.OnChange((options, name) => + { + if (name == Options.DefaultName) + { + _removeMediaFromPath = options.RemoveMediaFromPath; + _cdnUrl = options.Url; + } + }); } /// public override UrlInfo? GetMediaUrl(IPublishedContent content, string propertyAlias, UrlMode mode, string culture, Uri current) { var mediaUrl = base.GetMediaUrl(content, propertyAlias, UrlMode.Relative, culture, current); - if (mediaUrl == null) return null; - - return mediaUrl.IsUrl switch + if (mediaUrl?.IsUrl == true) { - false => mediaUrl, - _ => UrlInfo.Url(_cdnUrl + mediaUrl.Text[(_removeMediaFromPath ? "/media/" : "/").Length..], culture) - }; - } + string url = mediaUrl.Text; - private void OptionsOnChange(CdnMediaUrlProviderOptions options, string name) - { - if (name != Options.DefaultName) return; + int startIndex = 0; + if (_removeMediaFromPath && url.StartsWith(_mediaPath, StringComparison.OrdinalIgnoreCase)) + { + startIndex = _mediaPath.Length; + } + else if (url.StartsWith('/')) + { + startIndex = 1; + } + + return UrlInfo.Url(_cdnUrl + url[startIndex..], culture); + } - _removeMediaFromPath = options.RemoveMediaFromPath; - _cdnUrl = options.Url; + return mediaUrl; } } }