diff --git a/src/Icons/IconsSettings.cs b/src/Icons/IconsSettings.cs index 7cfd64d1127f..d12337678c19 100644 --- a/src/Icons/IconsSettings.cs +++ b/src/Icons/IconsSettings.cs @@ -5,4 +5,5 @@ public class IconsSettings public virtual bool CacheEnabled { get; set; } public virtual int CacheHours { get; set; } public virtual long? CacheSizeLimit { get; set; } + public virtual bool GoogleFaviconEnabled { get; set; } } diff --git a/src/Icons/Services/IconFetchingService.cs b/src/Icons/Services/IconFetchingService.cs index b2b8d016a5ad..baeae149cc21 100644 --- a/src/Icons/Services/IconFetchingService.cs +++ b/src/Icons/Services/IconFetchingService.cs @@ -8,26 +8,74 @@ namespace Bit.Icons.Services; public class IconFetchingService : IIconFetchingService { + private const int GoogleFaviconSize = 64; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly IHtmlParser _parser; private readonly IUriService _uriService; + private readonly IconsSettings _iconsSettings; - public IconFetchingService(ILogger logger, IHttpClientFactory httpClientFactory, IHtmlParser parser, IUriService uriService) + public IconFetchingService( + ILogger logger, + IHttpClientFactory httpClientFactory, + IHtmlParser parser, + IUriService uriService, + IconsSettings iconsSettings) { _logger = logger; _httpClientFactory = httpClientFactory; _parser = parser; _uriService = uriService; + _iconsSettings = iconsSettings; } public async Task GetIconAsync(string domain) { + if (_iconsSettings.GoogleFaviconEnabled) + { + var googleIcon = await GetGoogleFaviconAsync(domain); + if (googleIcon != null) + { + return googleIcon; + } + } + var domainIcons = await DomainIcons.FetchAsync(domain, _logger, _httpClientFactory, _parser, _uriService); var result = domainIcons.Where(result => result != null).FirstOrDefault(); return result ?? await GetFaviconAsync(domain); } + private async Task GetGoogleFaviconAsync(string domain) + { + // Google's undocumented favicon endpoint. Returns a PNG, 301-redirects to + // t0.gstatic.com/faviconV2. + var googleUriBuilder = new UriBuilder + { + Scheme = "https", + Host = "www.google.com", + Path = "/s2/favicons", + Query = $"domain={Uri.EscapeDataString(domain)}&sz={GoogleFaviconSize}" + }; + + if (!googleUriBuilder.TryBuild(out var googleUri)) + { + return null; + } + + try + { + // A 404 from Google indicates no favicon exists for the domain; the existing + // IconLink/IconHttpRequest pipeline propagates that as a null return. + return await new IconLink(googleUri!).FetchAsync(_logger, _httpClientFactory, _uriService); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Google favicon lookup failed for {domain}.", domain); + return null; + } + } + private async Task GetFaviconAsync(string domain) { // Fall back to favicon diff --git a/src/Icons/appsettings.SelfHosted.json b/src/Icons/appsettings.SelfHosted.json index 37faf24b5980..4091f81a1fa7 100644 --- a/src/Icons/appsettings.SelfHosted.json +++ b/src/Icons/appsettings.SelfHosted.json @@ -15,5 +15,8 @@ "internalSso": null, "internalScim": null } + }, + "iconsSettings": { + "googleFaviconEnabled": false } } diff --git a/src/Icons/appsettings.json b/src/Icons/appsettings.json index 5e1113b1505a..8b401898d65b 100644 --- a/src/Icons/appsettings.json +++ b/src/Icons/appsettings.json @@ -5,7 +5,8 @@ "iconsSettings": { "cacheEnabled": true, "cacheHours": 24, - "cacheSizeLimit": null + "cacheSizeLimit": null, + "googleFaviconEnabled": true }, "changePasswordUriSettings": { "cacheEnabled": true, diff --git a/test/Icons.Test/Services/ServiceTestBase.cs b/test/Icons.Test/Services/ServiceTestBase.cs index 37d816972eaa..d3197ea0ee06 100644 --- a/test/Icons.Test/Services/ServiceTestBase.cs +++ b/test/Icons.Test/Services/ServiceTestBase.cs @@ -20,6 +20,7 @@ public ServiceTestBase() _services.ConfigureHttpClients(); _services.AddHtmlParsing(); + _services.AddSingleton(new IconsSettings()); _services.AddServices(); _provider = _services.BuildServiceProvider();