Files
aliasvault/apps/server/AliasVault.Api/Controllers/FaviconController.cs
2026-05-05 10:27:20 +02:00

130 lines
4.3 KiB
C#

//-----------------------------------------------------------------------
// <copyright file="FaviconController.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Api.Controllers;
using AliasServerDb;
using AliasVault.Api.Controllers.Abstracts;
using AliasVault.Api.Services;
using AliasVault.Shared.Models.WebApi.Favicon;
using Asp.Versioning;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// Controller for retrieving favicons from external websites.
/// </summary>
/// <param name="userManager">UserManager instance.</param>
/// <param name="rateLimitService">In-memory per-user favicon extraction rate limiter.</param>
/// <param name="logger">Logger instance.</param>
[ApiVersion("1")]
public class FaviconController(
UserManager<AliasVaultUser> userManager,
FaviconRateLimitService rateLimitService,
ILogger<FaviconController> logger) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Maximum number of URLs accepted in a single batch request.
/// </summary>
public const int MaxBatchSize = 10;
/// <summary>
/// Extracts the favicon from a single URL.
/// </summary>
/// <param name="url">URL to extract the favicon from.</param>
/// <returns>Favicon image bytes, or null if extraction failed.</returns>
[HttpGet("Extract")]
public async Task<IActionResult> Extract(string url)
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return Unauthorized();
}
if (!rateLimitService.TryConsume(user.Id, 1))
{
return StatusCode(StatusCodes.Status429TooManyRequests);
}
try
{
var image = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
return Ok(new FaviconExtractModel { Image = image });
}
catch (Exception ex)
{
logger.LogInformation(ex, "Failed to extract favicon from {Url}", AnonymizeUrl(url));
}
return Ok(new FaviconExtractModel { Image = null });
}
/// <summary>
/// Extracts favicons for multiple URLs in parallel server-side. Cuts down on round trips when
/// the client needs to fetch many favicons (initial vault import, bulk re-download from the
/// storage insights page).
/// </summary>
/// <param name="request">The batch request payload.</param>
/// <returns>A list of favicon results, one per requested URL, in the same order.</returns>
[HttpPost("ExtractBatch")]
public async Task<IActionResult> ExtractBatch([FromBody] FaviconExtractBatchRequest request)
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return Unauthorized();
}
if (request.Urls.Count == 0)
{
return Ok(new FaviconExtractBatchResponse());
}
if (request.Urls.Count > MaxBatchSize)
{
return BadRequest(new ProblemDetails
{
Title = "Too many URLs",
Detail = $"Batch size is capped at {MaxBatchSize} URLs per request.",
});
}
if (!rateLimitService.TryConsume(user.Id, request.Urls.Count))
{
return StatusCode(StatusCodes.Status429TooManyRequests);
}
var images = await FaviconExtractor.FaviconExtractor.GetFaviconsAsync(request.Urls);
var response = new FaviconExtractBatchResponse
{
Results = new List<FaviconExtractBatchResult>(request.Urls.Count),
};
for (int i = 0; i < request.Urls.Count; i++)
{
response.Results.Add(new FaviconExtractBatchResult
{
Url = request.Urls[i],
Image = images[i],
});
}
return Ok(response);
}
/// <summary>
/// Anonymizes a URL by replacing letters with 'x'. Lets us log host structure without
/// recording the actual domain a user was browsing.
/// </summary>
private static string AnonymizeUrl(string url)
{
return new string(url.Select(c => char.IsLetter(c) ? 'x' : c).ToArray());
}
}