Compare commits

..

4 Commits

Author SHA1 Message Date
Leendert de Borst
e5c68c6c6e Bump version to 0.23.1 (#1227) 2025-09-16 13:43:20 +02:00
Leendert de Borst
58c39815e4 Add more browser like behavior to improve FaviconExtractor success rate (#1225) 2025-09-16 13:19:22 +02:00
Leendert de Borst
4b706f466f Improve favicon extractor request handling (#1225) 2025-09-16 13:19:22 +02:00
Leendert de Borst
19f72b1386 Update self-signed SSL cert logic to use correct IP vs DNS name labels (#1223) 2025-09-16 11:40:00 +02:00
15 changed files with 559 additions and 82 deletions

View File

@@ -2,7 +2,7 @@
"name": "aliasvault-browser-extension",
"description": "AliasVault Browser Extension",
"private": true,
"version": "0.23.0",
"version": "0.23.1",
"type": "module",
"scripts": {
"dev:chrome": "wxt -b chrome",

View File

@@ -460,7 +460,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -492,7 +492,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -530,7 +530,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -569,7 +569,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
OTHER_LDFLAGS = (
"-framework",
SafariServices,

View File

@@ -6,7 +6,7 @@ export class AppInfo {
/**
* The current extension version. This should be updated with each release of the extension.
*/
public static readonly VERSION = '0.23.0';
public static readonly VERSION = '0.23.1';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

@@ -20,7 +20,7 @@ export default defineConfig({
return {
name: "AliasVault",
description: "AliasVault Browser AutoFill Extension. Keeping your personal information private.",
version: "0.23.0",
version: "0.23.1",
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},

View File

@@ -94,7 +94,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 230000
versionName "0.23.0"
versionName "0.23.1"
}
signingConfigs {
debug {

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "AliasVault",
"slug": "AliasVault",
"version": "0.23.0",
"version": "0.23.1",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "net.aliasvault.app",

View File

@@ -1218,7 +1218,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1253,7 +1253,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1863,7 +1863,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
@@ -1908,7 +1908,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 0.23.0;
MARKETING_VERSION = 0.23.1;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = net.aliasvault.app.autofill;

View File

@@ -8,7 +8,7 @@ export class AppInfo {
/**
* The current mobile app version. This should be updated with each release of the mobile app.
*/
public static readonly VERSION = '0.23.0';
public static readonly VERSION = '0.23.1';
/**
* The minimum supported AliasVault server (API) version. If the server version is below this, the

View File

@@ -94,7 +94,6 @@
<ProjectReference Include="..\Shared\AliasVault.Shared\AliasVault.Shared.csproj" />
<ProjectReference Include="..\Utilities\Cryptography\AliasVault.Cryptography.Client\AliasVault.Cryptography.Client.csproj" />
<ProjectReference Include="..\Utilities\AliasVault.ImportExport\AliasVault.ImportExport.csproj" />
<ProjectReference Include="..\Utilities\AliasVault.FaviconExtractor\AliasVault.FaviconExtractor.csproj" />
<ProjectReference Include="..\Utilities\AliasVault.TotpGenerator\AliasVault.TotpGenerator.csproj" />
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
<ServiceWorker Include="wwwroot\service-worker.published.js">

View File

@@ -30,7 +30,7 @@ public static class AppInfo
/// <summary>
/// Gets the patch version number.
/// </summary>
public const int VersionPatch = 0;
public const int VersionPatch = 1;
/// <summary>
/// Gets the minimum supported AliasVault client version. Normally the minimum client version is the same

View File

@@ -22,4 +22,73 @@ public class FaviconExtractorTests
var faviconBytes = await FaviconExtractor.FaviconExtractor.GetFaviconAsync("https://adsense.google.com/start/");
Assert.That(faviconBytes, Is.Not.Null);
}
/// <summary>
/// Test that localhost URLs are blocked (SSRF protection).
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task BlockLocalhostUrls()
{
var localhostUrls = new[]
{
"http://localhost/favicon.ico",
"http://127.0.0.1/favicon.ico",
"http://[::1]/favicon.ico",
"http://localhost:8080/favicon.ico",
"https://localhost/favicon.ico",
};
foreach (var url in localhostUrls)
{
var faviconBytes = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
Assert.That(faviconBytes, Is.Null, $"Should block localhost URL: {url}");
}
}
/// <summary>
/// Test that private IP ranges are blocked (SSRF protection).
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task BlockPrivateIpRanges()
{
var privateIpUrls = new[]
{
"http://10.0.0.1/favicon.ico",
"http://10.100.0.1/favicon.ico",
"http://192.168.1.1/favicon.ico",
"http://172.16.0.1/favicon.ico",
"http://169.254.169.254/latest/meta-data/", // AWS metadata endpoint
"http://[fc00::1]/favicon.ico", // IPv6 private
"http://[fe80::1]/favicon.ico", // IPv6 link-local
};
foreach (var url in privateIpUrls)
{
var faviconBytes = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
Assert.That(faviconBytes, Is.Null, $"Should block private IP URL: {url}");
}
}
/// <summary>
/// Test that non-standard ports are blocked.
/// </summary>
/// <returns>Task.</returns>
[Test]
public async Task BlockNonStandardPorts()
{
var nonStandardPortUrls = new[]
{
"http://example.com:8080/favicon.ico",
"https://example.com:8443/favicon.ico",
"http://example.com:3000/favicon.ico",
};
foreach (var url in nonStandardPortUrls)
{
var faviconBytes = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
Assert.That(faviconBytes, Is.Null, $"Should block non-standard port URL: {url}");
}
}
}

View File

@@ -9,7 +9,9 @@ namespace AliasVault.FaviconExtractor;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading.Tasks;
using HtmlAgilityPack;
using SkiaSharp;
@@ -24,7 +26,7 @@ public static class FaviconExtractor
private static readonly string[] _allowedSchemes = { "http", "https" };
/// <summary>
/// Extracts the favicon from a URL.
/// Extracts the favicon from a URL with enhanced browser like behavior.
/// </summary>
/// <param name="url">The URL to extract the favicon for.</param>
/// <returns>Byte array for favicon image.</returns>
@@ -40,63 +42,37 @@ public static class FaviconExtractor
using HttpClient client = CreateHttpClient();
// First attempt
var result = await TryGetFaviconAsync(client, uri);
if (result != null)
// Attempt the operation up to two times to handle common cookiewall redirects or transient issues.
for (int attempt = 0; attempt < 2; attempt++)
{
return result;
var result = await TryGetFaviconAsync(client, uri);
if (result != null)
{
return result;
}
}
return await TryGetFaviconAsync(client, uri);
// Return null if the favicon extraction failed.
return null;
}
/// <summary>
/// Normalizes the URL by adding a scheme if it is missing.
/// Tries to get the favicon from the URL.
/// </summary>
/// <param name="url">The URL to normalize.</param>
/// <returns>The normalized URL.</returns>
private static string NormalizeUrl(string url)
/// <param name="client">The HTTP client.</param>
/// <param name="uri">The URI to get the favicon from.</param>
/// <returns>The favicon bytes.</returns>
private static async Task<byte[]?> TryGetFaviconAsync(HttpClient client, Uri uri)
{
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
var response = await FollowRedirectsAsync(client, uri);
if (response == null || !response.IsSuccessStatusCode)
{
return "https://" + url;
return null;
}
return url;
}
/// <summary>
/// Checks if the URI is valid.
/// </summary>
/// <param name="uri">The URI to check.</param>
/// <returns>True if the URI is valid, false otherwise.</returns>
private static bool IsValidUri(Uri uri)
{
return _allowedSchemes.Contains(uri.Scheme) && uri.IsDefaultPort;
}
/// <summary>
/// Creates a new HTTP client with default headers.
/// </summary>
/// <returns>The HTTP client.</returns>
private static HttpClient CreateHttpClient()
{
var client = new HttpClient(new HttpClientHandler
{
AllowAutoRedirect = true,
MaxAutomaticRedirections = 10,
})
{
Timeout = TimeSpan.FromSeconds(5),
};
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.5");
client.DefaultRequestHeaders.Add("Connection", "keep-alive");
client.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1");
return client;
var faviconNodes = await GetFaviconNodesFromHtml(response, uri);
return await TryExtractFaviconFromNodes(faviconNodes, client, uri);
}
/// <summary>
@@ -114,6 +90,7 @@ public static class FaviconExtractor
var defaultFavicon = new HtmlNode(HtmlNodeType.Element, htmlDoc, 0);
defaultFavicon.Attributes.Add("href", $"{uri.GetLeftPart(UriPartial.Authority)}/favicon.ico");
// Get the favicon nodes from the HTML, in order of preference.
HtmlNodeCollection?[] nodeArray =
[
htmlDoc.DocumentNode.SelectNodes("//link[@rel='icon' and @type='image/svg+xml']"),
@@ -131,18 +108,13 @@ public static class FaviconExtractor
return nodeArray.Where(x => x != null).Cast<HtmlNodeCollection>().ToArray();
}
private static async Task<byte[]?> TryGetFaviconAsync(HttpClient client, Uri uri)
{
HttpResponseMessage response = await client.GetAsync(uri);
if (!response.IsSuccessStatusCode)
{
return null;
}
var faviconNodes = await GetFaviconNodesFromHtml(response, uri);
return await TryExtractFaviconFromNodes(faviconNodes, client, uri);
}
/// <summary>
/// Tries to extract the favicon from the nodes.
/// </summary>
/// <param name="faviconNodes">The favicon nodes.</param>
/// <param name="client">The HTTP client.</param>
/// <param name="baseUri">The base URI.</param>
/// <returns>The favicon bytes.</returns>
private static async Task<byte[]?> TryExtractFaviconFromNodes(HtmlNodeCollection[] faviconNodes, HttpClient client, Uri baseUri)
{
foreach (var nodeCollection in faviconNodes)
@@ -186,8 +158,16 @@ public static class FaviconExtractor
{
try
{
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode)
// Validate the favicon URL before fetching
if (!Uri.TryCreate(url, UriKind.Absolute, out var faviconUri) || !IsValidUri(faviconUri))
{
return null;
}
// Follow redirects with validation
var response = await FollowRedirectsAsync(client, faviconUri);
if (response == null || !response.IsSuccessStatusCode)
{
return null;
}
@@ -223,6 +203,179 @@ public static class FaviconExtractor
}
}
/// <summary>
/// Creates a new HTTP client with enhanced browser-like configuration to handle bot protection.
/// </summary>
/// <returns>The HTTP client.</returns>
private static HttpClient CreateHttpClient()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = false, // Handle redirects manually
UseCookies = true, // Enable cookie handling for session management
CookieContainer = new System.Net.CookieContainer(),
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.Brotli,
};
var client = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(5), // Keep original timeout
};
var random = new Random();
var userAgents = new[]
{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
};
// Use random User-Agent
client.DefaultRequestHeaders.Add("User-Agent", userAgents[random.Next(userAgents.Length)]);
// More comprehensive Accept header with image types prioritized
client.DefaultRequestHeaders.Add(
"Accept",
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
// Additional browser-like headers
client.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.9");
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("DNT", "1");
client.DefaultRequestHeaders.Add("Upgrade-Insecure-Requests", "1");
client.DefaultRequestHeaders.Add("Cache-Control", "max-age=0");
// Add Sec-Fetch headers to mimic modern browsers
if (random.Next(2) == 0)
{
client.DefaultRequestHeaders.Add("Sec-Fetch-Dest", "document");
client.DefaultRequestHeaders.Add("Sec-Fetch-Mode", "navigate");
client.DefaultRequestHeaders.Add("Sec-Fetch-Site", "none");
client.DefaultRequestHeaders.Add("Sec-Fetch-User", "?1");
}
// Add Chrome-specific headers randomly
if (random.Next(3) == 0)
{
client.DefaultRequestHeaders.Add("Sec-CH-UA", "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"");
client.DefaultRequestHeaders.Add("Sec-CH-UA-Mobile", "?0");
client.DefaultRequestHeaders.Add("Sec-CH-UA-Platform", "\"Windows\"");
}
return client;
}
/// <summary>
/// Normalizes the URL by adding a scheme if it is missing.
/// </summary>
/// <param name="url">The URL to normalize.</param>
/// <returns>The normalized URL.</returns>
private static string NormalizeUrl(string url)
{
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return "https://" + url;
}
return url;
}
/// <summary>
/// Checks if the URI is valid and not pointing to internal/private IPs.
/// </summary>
/// <param name="uri">The URI to check.</param>
/// <returns>True if the URI is valid and safe, false otherwise.</returns>
private static bool IsValidUri(Uri uri)
{
// Check scheme and port
if (!_allowedSchemes.Contains(uri.Scheme) || !uri.IsDefaultPort)
{
return false;
}
// Resolve hostname to IP and validate
try
{
var addresses = Dns.GetHostAddresses(uri.Host);
foreach (var address in addresses)
{
if (!IPAddressValidator.IsPublicIPAddress(address))
{
return false;
}
}
}
catch
{
// If DNS resolution fails, block the request
return false;
}
return true;
}
/// <summary>
/// Handles HTTP redirects with validation to prevent SSRF attacks.
/// </summary>
/// <param name="client">The HTTP client.</param>
/// <param name="uri">The initial URI to request.</param>
/// <returns>The final HTTP response after following redirects, or null if blocked/failed.</returns>
private static async Task<HttpResponseMessage?> FollowRedirectsAsync(HttpClient client, Uri uri)
{
var currentUri = uri;
int redirectCount = 0;
const int maxRedirects = 5;
while (redirectCount < maxRedirects)
{
// Create request with referer header to appear more browser-like
var request = new HttpRequestMessage(HttpMethod.Get, currentUri);
if (redirectCount == 0)
{
// First request - add Google referer to appear like navigation
request.Headers.Add("Referer", "https://www.google.com/");
}
else
{
// Subsequent redirects - use original URL as referer
request.Headers.Add("Referer", uri.ToString());
}
var response = await client.SendAsync(request);
if ((int)response.StatusCode >= 300 && (int)response.StatusCode < 400)
{
var location = response.Headers.Location;
if (location == null)
{
return null;
}
// Resolve relative URLs
if (!location.IsAbsoluteUri)
{
location = new Uri(currentUri, location);
}
// Validate the redirect target
if (!IsValidUri(location))
{
return null; // Block redirect to internal IPs
}
currentUri = location;
redirectCount++;
}
else
{
return response;
}
}
return null; // Too many redirects
}
/// <summary>
/// Resizes the image to the target width.
/// </summary>

View File

@@ -0,0 +1,170 @@
//-----------------------------------------------------------------------
// <copyright file="IPAddressValidator.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.FaviconExtractor;
using System;
using System.Net;
using System.Net.Sockets;
/// <summary>
/// Validates IP addresses for public accessibility to prevent SSRF attacks.
/// </summary>
internal static class IPAddressValidator
{
/// <summary>
/// Private IPv4 blocks.
/// </summary>
private static readonly (byte[] Net, int Prefix)[] PrivateV4Blocks = new[]
{
(new byte[] { 10, 0, 0, 0 }, 8), // private
(new byte[] { 172, 16, 0, 0 }, 12), // private
(new byte[] { 192, 168, 0, 0 }, 16), // private
(new byte[] { 169, 254, 0, 0 }, 16), // link-local
(new byte[] { 100, 64, 0, 0 }, 10), // CGNAT
(new byte[] { 192, 0, 0, 0 }, 24), // IETF Protocol Assignments
(new byte[] { 192, 0, 2, 0 }, 24), // TEST-NET-1
(new byte[] { 198, 18, 0, 0 }, 15), // benchmarking
(new byte[] { 198, 51, 100, 0 }, 24), // TEST-NET-2
(new byte[] { 203, 0, 113, 0 }, 24), // TEST-NET-3
(new byte[] { 224, 0, 0, 0 }, 4), // multicast
(new byte[] { 240, 0, 0, 0 }, 4), // reserved
(new byte[] { 0, 0, 0, 0 }, 8), // local
};
/// <summary>
/// Private IPv6 blocks.
/// </summary>
private static readonly (byte[] Net, int Prefix)[] PrivateV6Blocks = new[]
{
(new byte[] { 0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 7), // ULA
(new byte[] { 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 32), // documentation
};
/// <summary>
/// Checks if an IP address is public (routable on the internet).
/// </summary>
/// <param name="address">The IP address to check.</param>
/// <returns>True if the IP is publicly routable, false otherwise.</returns>
public static bool IsPublicIPAddress(IPAddress address)
{
if (address is null)
{
throw new ArgumentNullException(nameof(address));
}
// Normalize IPv4-mapped IPv6 addresses to IPv4 for simpler handling.
if (address.AddressFamily == AddressFamily.InterNetworkV6 && address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
// Loopback / unspecified (0.0.0.0 or ::) are not public.
if (IPAddress.IsLoopback(address) || address.Equals(IPAddress.None) || address.Equals(IPAddress.IPv6None))
{
return false;
}
// IPv4 checks.
if (address.AddressFamily == AddressFamily.InterNetwork)
{
return IsPublicIPv4Address(address);
}
// IPv6 checks.
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
return IsPublicIPv6Address(address);
}
// Unknown family -> treat as non-public.
return false;
}
/// <summary>
/// Checks if an IPv4 address is public.
/// </summary>
/// <param name="address">The IPv4 address to check.</param>
/// <returns>True if the IP is publicly routable, false otherwise.</returns>
private static bool IsPublicIPv4Address(IPAddress address)
{
var bytes = address.GetAddressBytes();
// Broadcast 255.255.255.255
if (bytes[0] == 255 && bytes[1] == 255 && bytes[2] == 255 && bytes[3] == 255)
{
return false;
}
// Check if the IP address is in any of the private IPv4 block.
foreach (var (net, prefix) in PrivateV4Blocks)
{
if (IsInPrefix(bytes, net, prefix))
{
return false;
}
}
return true;
}
/// <summary>
/// Checks if an IPv6 address is public.
/// </summary>
/// <param name="address">The IPv6 address to check.</param>
/// <returns>True if the IP is publicly routable, false otherwise.</returns>
private static bool IsPublicIPv6Address(IPAddress address)
{
// Built-in flags for common non-routable addresses
if (address.IsIPv6LinkLocal || address.IsIPv6SiteLocal || address.IsIPv6Multicast)
{
return false;
}
var bytes = address.GetAddressBytes();
// Check if the IP address is in any of the private IPv6 block.
foreach (var (net, prefix) in PrivateV6Blocks)
{
if (IsInPrefix(bytes, net, prefix))
{
return false;
}
}
return true;
}
/// <summary>
/// Checks if an address is within a CIDR prefix.
/// </summary>
/// <param name="address">The address bytes to check.</param>
/// <param name="network">The network prefix bytes.</param>
/// <param name="prefixLength">The prefix length in bits.</param>
/// <returns>True if the address is within the prefix, false otherwise.</returns>
private static bool IsInPrefix(byte[] address, byte[] network, int prefixLength)
{
int fullBytes = prefixLength / 8;
int remainingBits = prefixLength % 8;
for (int i = 0; i < fullBytes; i++)
{
if (address[i] != network[i])
{
return false;
}
}
if (remainingBits == 0)
{
return true;
}
int mask = 0xFF << (8 - remainingBits) & 0xFF;
return (address[fullBytes] & mask) == (network[fullBytes] & mask);
}
}

View File

@@ -36,6 +36,38 @@ needs_cert_regeneration() {
return 1
}
# Function to check if a string is an IP address (IPv4 or IPv6)
is_ip_address() {
local value="$1"
# Check for IPv4 pattern
if echo "$value" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
# Validate each octet is <= 255
local valid=1
# Use a simple approach to split the IP address
local o1=$(echo "$value" | cut -d. -f1)
local o2=$(echo "$value" | cut -d. -f2)
local o3=$(echo "$value" | cut -d. -f3)
local o4=$(echo "$value" | cut -d. -f4)
for octet in "$o1" "$o2" "$o3" "$o4"; do
if [ "$octet" -gt 255 ]; then
valid=0
break
fi
done
if [ "$valid" -eq 1 ]; then
return 0 # It's a valid IPv4
fi
fi
# Check for IPv6 pattern (simplified check)
if echo "$value" | grep -qE '^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^::1$|^::$'; then
return 0 # It's likely IPv6
fi
return 1 # Not an IP address
}
# Generate self-signed SSL certificate if not exists or hostname changed
if needs_cert_regeneration; then
echo "Generating new SSL certificate (10 years validity)..."
@@ -49,12 +81,23 @@ if needs_cert_regeneration; then
-out /etc/nginx/ssl/cert.pem \
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
else
# Generate certificate with the hostname and include localhost as SAN
# Determine if the hostname is an IP address or a DNS name
if is_ip_address "$HOSTNAME_VALUE"; then
# It's an IP address - use IP: prefix in SAN
SAN_ENTRY="IP:${HOSTNAME_VALUE}"
echo "Detected IP address: ${HOSTNAME_VALUE}"
else
# It's a DNS name - use DNS: prefix in SAN
SAN_ENTRY="DNS:${HOSTNAME_VALUE}"
echo "Detected hostname: ${HOSTNAME_VALUE}"
fi
# Generate certificate with the appropriate SAN entry
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/key.pem \
-out /etc/nginx/ssl/cert.pem \
-subj "/C=US/ST=State/L=City/O=AliasVault/CN=${HOSTNAME_VALUE}" \
-addext "subjectAltName=DNS:${HOSTNAME_VALUE},DNS:localhost,IP:127.0.0.1"
-addext "subjectAltName=${SAN_ENTRY},DNS:localhost,IP:127.0.0.1"
fi
# Set proper permissions

View File

@@ -176,6 +176,38 @@ needs_cert_regeneration() {
return 1
}
# Function to check if a string is an IP address (IPv4 or IPv6)
is_ip_address() {
local value="$1"
# Check for IPv4 pattern
if echo "$value" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
# Validate each octet is <= 255
local valid=1
# Use a simple approach to split the IP address
local o1=$(echo "$value" | cut -d. -f1)
local o2=$(echo "$value" | cut -d. -f2)
local o3=$(echo "$value" | cut -d. -f3)
local o4=$(echo "$value" | cut -d. -f4)
for octet in "$o1" "$o2" "$o3" "$o4"; do
if [ "$octet" -gt 255 ]; then
valid=0
break
fi
done
if [ "$valid" -eq 1 ]; then
return 0 # It's a valid IPv4
fi
fi
# Check for IPv6 pattern (simplified check)
if echo "$value" | grep -qE '^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^::1$|^::$'; then
return 0 # It's likely IPv6
fi
return 1 # Not an IP address
}
# Generate SSL certificates if needed or hostname changed
if needs_cert_regeneration; then
log 0 ""
@@ -199,19 +231,30 @@ if needs_cert_regeneration; then
>/dev/null 2>&1
fi
else
# Generate certificate with the hostname and include localhost as SAN
# Determine if the hostname is an IP address or a DNS name
if is_ip_address "$HOSTNAME_VALUE"; then
# It's an IP address - use IP: prefix in SAN
SAN_ENTRY="IP:${HOSTNAME_VALUE}"
log 1 "[init] Detected IP address: ${HOSTNAME_VALUE}"
else
# It's a DNS name - use DNS: prefix in SAN
SAN_ENTRY="DNS:${HOSTNAME_VALUE}"
log 1 "[init] Detected hostname: ${HOSTNAME_VALUE}"
fi
# Generate certificate with the appropriate SAN entry
if [ "$VERBOSITY" -ge 2 ]; then
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /certificates/ssl/key.pem \
-out /certificates/ssl/cert.pem \
-subj "/C=US/ST=State/L=City/O=AliasVault/CN=${HOSTNAME_VALUE}" \
-addext "subjectAltName=DNS:${HOSTNAME_VALUE},DNS:localhost,IP:127.0.0.1"
-addext "subjectAltName=${SAN_ENTRY},DNS:localhost,IP:127.0.0.1"
else
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /certificates/ssl/key.pem \
-out /certificates/ssl/cert.pem \
-subj "/C=US/ST=State/L=City/O=AliasVault/CN=${HOSTNAME_VALUE}" \
-addext "subjectAltName=DNS:${HOSTNAME_VALUE},DNS:localhost,IP:127.0.0.1" \
-addext "subjectAltName=${SAN_ENTRY},DNS:localhost,IP:127.0.0.1" \
>/dev/null 2>&1
fi
fi