1
0
mirror of https://github.com/bitwarden/server synced 2026-02-27 01:43:46 +00:00
Files
server/src/Icons/Models/IconHttpRequest.cs
Matt Gibson 4377c7a897 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
2023-08-08 19:29:40 +00:00

111 lines
3.6 KiB
C#

#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;
}
}
}