Refactor to use central ClientHeaderInfo model with parsing built-in (#1967)

This commit is contained in:
Leendert de Borst
2026-04-26 17:31:59 +02:00
committed by Leendert de Borst
parent 459bd6afe0
commit 65ad2dd5cb
3 changed files with 66 additions and 39 deletions

View File

@@ -100,34 +100,13 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
// Check client version compatibility if header is provided
var clientSupported = false;
if (!string.IsNullOrEmpty(clientHeader))
var clientInfo = ClientHeaderInfo.Parse(clientHeader);
if (!string.IsNullOrEmpty(clientInfo.ClientVersion)
&& AppInfo.MinimumClientVersions.TryGetValue(clientInfo.ClientName, out var minimumVersion))
{
// Client header format should be "{platform}-{version}" e.g. "chrome-1.4.0"
var parts = clientHeader.Split('-');
if (parts.Length == 2)
{
var platform = parts[0].ToLowerInvariant();
var clientVersion = parts[1];
if (AppInfo.MinimumClientVersions.TryGetValue(platform, out var minimumVersion))
{
// Check if version meets minimum requirement AND is not in blocked list
var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientVersion, minimumVersion);
var isBlocked = VersionHelper.IsVersionBlocked(platform, clientVersion, AppInfo.UnsupportedClientVersions);
clientSupported = meetsMinimum && !isBlocked;
}
else
{
// Unknown platform
clientSupported = false;
}
}
else
{
// Invalid header format
clientSupported = false;
}
var meetsMinimum = VersionHelper.IsVersionEqualOrNewer(clientInfo.ClientVersion, minimumVersion);
var isBlocked = VersionHelper.IsVersionBlocked(clientInfo.ClientName, clientInfo.ClientVersion, AppInfo.UnsupportedClientVersions);
clientSupported = meetsMinimum && !isBlocked;
}
return Ok(new StatusResponse

View File

@@ -106,19 +106,14 @@ public static class AuthHelper
var userAgent = request.Headers.UserAgent.ToString();
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
// Client header is formatted like "[client name]-[version]" or "[client name]-[version]-[app-instance-id]"
// Examples: "chrome-0.25.0", "android-0.29.0-550e8400e29b41d4a716446655440000"
var clientHeader = request.Headers["X-AliasVault-Client"].ToString();
var clientParts = clientHeader?.Split('-') ?? [];
var clientName = clientParts.Length > 0 ? clientParts[0] : "unknown";
var clientInfo = ClientHeaderInfo.Parse(request.Headers[ClientHeaderInfo.HeaderName].ToString());
// For Android, extract app instance ID if present (UUID without dashes as 3rd part)
var appInstanceSuffix = string.Empty;
if (clientName == "android" && clientParts.Length >= 3)
{
appInstanceSuffix = $"|{clientParts[2]}";
}
// Only Android appends an app instance id, used to keep device identifiers unique
// across multiple Android User Profiles on the same physical device.
var appInstanceSuffix = clientInfo.ClientName == "android" && clientInfo.AppInstanceId is not null
? $"|{clientInfo.AppInstanceId}"
: string.Empty;
return $"{clientName}|{userAgent}|{acceptLanguage}{appInstanceSuffix}";
return $"{clientInfo.ClientName}|{userAgent}|{acceptLanguage}{appInstanceSuffix}";
}
}

View File

@@ -0,0 +1,53 @@
//-----------------------------------------------------------------------
// <copyright file="ClientHeaderInfo.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.Helpers;
/// <summary>
/// Parsed components of the X-AliasVault-Client request header.
///
/// Header format is "{client}-{version}", optionally followed by additional dash-separated
/// segments.
///
/// Note: currently only the Android app appends a third segment with a per-install
/// app instance id (UUID without dashes) to support multiple Android User Profiles.
///
/// Examples:
/// - "chrome-0.29.0"
/// - "ios-0.29.0"
/// - "android-0.29.0-550e8400e29b41d4a716446655440000".
/// </summary>
/// <param name="ClientName">Lowercased client/platform identifier (e.g. "chrome", "android"). "unknown" when the header is missing or empty.</param>
/// <param name="ClientVersion">Client version string (e.g. "0.29.0"), or null when not present.</param>
/// <param name="AppInstanceId">Per-install app instance identifier, or null when not present.</param>
public sealed record ClientHeaderInfo(string ClientName, string? ClientVersion, string? AppInstanceId)
{
/// <summary>
/// Header name used by AliasVault clients to identify themselves.
/// </summary>
public const string HeaderName = "X-AliasVault-Client";
/// <summary>
/// Parse a raw X-AliasVault-Client header value into its components.
/// </summary>
/// <param name="headerValue">Raw header value, may be null or empty.</param>
/// <returns>Parsed ClientHeaderInfo. Missing trailing segments are returned as null.</returns>
public static ClientHeaderInfo Parse(string? headerValue)
{
if (string.IsNullOrEmpty(headerValue))
{
return new ClientHeaderInfo("unknown", null, null);
}
var parts = headerValue.Split('-');
var clientName = parts[0].ToLowerInvariant();
var clientVersion = parts.Length > 1 ? parts[1] : null;
var appInstanceId = parts.Length > 2 ? parts[2] : null;
return new ClientHeaderInfo(clientName, clientVersion, appInstanceId);
}
}