mirror of
https://github.com/bitwarden/server
synced 2026-02-27 01:43:46 +00:00
Rewrite Icon fetching (#3023)
* Rewrite Icon fetching * Move validation to IconUri, Uri, or UriBuilder * `dotnet format` 🤖 * PR suggestions * Add not null compiler hint * Add twitter to test case * Move Uri manipulation to UriService * Implement MockedHttpClient Presents better, fluent handling of message matching and response building. * Add redirect handling tests * Add testing to models * More aggressively dispose content in icon link * Format 🤖 * Update icon lockfile * Convert to cloned stream for HttpResponseBuilder Content was being disposed when HttResponseMessage was being disposed. This avoids losing our reference to our content and allows multiple usages of the same `MockedHttpMessageResponse` * Move services to extension Extension is shared by testing and allows access to services from our service tests * Remove unused `using` * Prefer awaiting asyncs for better exception handling * `dotnet format` 🤖 * Await async * Update tests to use test TLD and ip ranges * Remove unused interfaces * Make assignments static when possible * Prefer invariant comparer to downcasing * Prefer injecting interface services to implementations * Prefer comparer set in HashSet initialization * Allow SVG icons * Filter out icons with unknown formats * Seek to beginning of MemoryStream after writing it * More appropriate to not return icon if it's invalid * Add svg icon test
This commit is contained in:
110
src/Icons/Models/IconHttpRequest.cs
Normal file
110
src/Icons/Models/IconHttpRequest.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Net;
|
||||
using Bit.Icons.Extensions;
|
||||
using Bit.Icons.Services;
|
||||
|
||||
namespace Bit.Icons.Models;
|
||||
|
||||
public class IconHttpRequest
|
||||
{
|
||||
private const int _maxRedirects = 2;
|
||||
|
||||
private static readonly HttpStatusCode[] _redirectStatusCodes = new HttpStatusCode[] { HttpStatusCode.Redirect, HttpStatusCode.MovedPermanently, HttpStatusCode.RedirectKeepVerb, HttpStatusCode.SeeOther };
|
||||
|
||||
private readonly ILogger<IIconFetchingService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IUriService _uriService;
|
||||
private readonly int _redirectsCount;
|
||||
private readonly Uri _uri;
|
||||
private static HttpResponseMessage NotFound => new(HttpStatusCode.NotFound);
|
||||
|
||||
private IconHttpRequest(Uri uri, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService, int redirectsCount)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_httpClient = _httpClientFactory.CreateClient("Icons");
|
||||
_uriService = uriService;
|
||||
_redirectsCount = redirectsCount;
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public static async Task<IconHttpResponse> FetchAsync(Uri uri, ILogger<IIconFetchingService> logger, IHttpClientFactory httpClientFactory, IUriService uriService)
|
||||
{
|
||||
var pageIcons = new IconHttpRequest(uri, logger, httpClientFactory, uriService, 0);
|
||||
var httpResponse = await pageIcons.FetchAsync();
|
||||
return new IconHttpResponse(httpResponse, logger, httpClientFactory, uriService);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> FetchAsync()
|
||||
{
|
||||
if (!_uriService.TryGetUri(_uri, out var iconUri) || !iconUri!.IsValid)
|
||||
{
|
||||
return NotFound;
|
||||
}
|
||||
|
||||
var response = await GetAsync(iconUri);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
using var responseForRedirect = response;
|
||||
return await FollowRedirectsAsync(responseForRedirect, iconUri);
|
||||
}
|
||||
|
||||
|
||||
private async Task<HttpResponseMessage> GetAsync(IconUri iconUri)
|
||||
{
|
||||
using var message = new HttpRequestMessage();
|
||||
message.RequestUri = iconUri.InnerUri;
|
||||
message.Headers.Host = iconUri.Host;
|
||||
message.Method = HttpMethod.Get;
|
||||
|
||||
try
|
||||
{
|
||||
return await _httpClient.SendAsync(message);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> FollowRedirectsAsync(HttpResponseMessage response, IconUri originalIconUri)
|
||||
{
|
||||
if (_redirectsCount >= _maxRedirects || response.Headers.Location == null ||
|
||||
!_redirectStatusCodes.Contains(response.StatusCode))
|
||||
{
|
||||
return NotFound;
|
||||
}
|
||||
|
||||
using var responseForRedirect = response;
|
||||
var redirectUri = DetermineRedirectUri(responseForRedirect.Headers.Location, originalIconUri);
|
||||
|
||||
return await new IconHttpRequest(redirectUri, _logger, _httpClientFactory, _uriService, _redirectsCount + 1).FetchAsync();
|
||||
}
|
||||
|
||||
private static Uri DetermineRedirectUri(Uri responseUri, IconUri originalIconUri)
|
||||
{
|
||||
if (responseUri.IsAbsoluteUri)
|
||||
{
|
||||
if (!responseUri.IsHypertext())
|
||||
{
|
||||
return responseUri.ChangeScheme("https");
|
||||
}
|
||||
return responseUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new UriBuilder
|
||||
{
|
||||
Scheme = originalIconUri.Scheme,
|
||||
Host = originalIconUri.Host,
|
||||
Path = responseUri.ToString()
|
||||
}.Uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user