150 lines
5.6 KiB
C#
150 lines
5.6 KiB
C#
// Copyright (C) 2026, The Duplicati Team
|
|
// https://duplicati.com, hello@duplicati.com
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the "Software"),
|
|
// to deal in the Software without restriction, including without limitation
|
|
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
// and/or sell copies of the Software, and to permit persons to whom the
|
|
// Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System.Net;
|
|
using System.Net.NetworkInformation;
|
|
using System.Net.Sockets;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Duplicati.Library.Certificates;
|
|
|
|
/// <summary>
|
|
/// Detects local hostnames and IP addresses for SSL certificate SANs.
|
|
/// </summary>
|
|
public static class HostnameDetector
|
|
{
|
|
/// <summary>
|
|
/// The list of always-included hostnames (localhost and loopback addresses).
|
|
/// </summary>
|
|
public static readonly IReadOnlyList<string> AlwaysIncludedHostnames = new[]
|
|
{
|
|
"localhost",
|
|
"127.0.0.1",
|
|
"::1"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Detects all local hostnames and IP addresses that should be included in the server certificate.
|
|
/// Always includes localhost, 127.0.0.1, and ::1.
|
|
/// </summary>
|
|
/// <returns>A list of hostnames and IP addresses.</returns>
|
|
public static List<string> DetectHostnames()
|
|
{
|
|
var hostnames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Always include localhost and loopback addresses
|
|
hostnames.UnionWith(AlwaysIncludedHostnames);
|
|
|
|
// Add the machine name
|
|
try
|
|
{
|
|
var machineName = Environment.MachineName;
|
|
if (IsValidDnsName(machineName))
|
|
hostnames.Add(machineName);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors getting machine name
|
|
}
|
|
|
|
// Add the domain-qualified machine name if available
|
|
try
|
|
{
|
|
var domainName = IPGlobalProperties.GetIPGlobalProperties().DomainName;
|
|
if (IsValidDnsName(domainName))
|
|
{
|
|
var machineName = Environment.MachineName;
|
|
if (IsValidDnsName(machineName))
|
|
hostnames.Add($"{machineName}.{domainName}");
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Ignore errors getting domain name
|
|
}
|
|
|
|
// Explictly do not add any external IP addresses, as we expect a localhost only binding
|
|
// Additional hostnames can be added by the user if needed
|
|
|
|
return hostnames.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates and normalizes a list of hostnames provided by the user.
|
|
/// </summary>
|
|
/// <param name="hostnames">The hostnames to validate.</param>
|
|
/// <returns>A normalized list of valid hostnames.</returns>
|
|
public static List<string> ValidateHostnames(IEnumerable<string> hostnames)
|
|
{
|
|
var validHostnames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var hostname in hostnames)
|
|
{
|
|
var trimmed = hostname.Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a valid IP address
|
|
if (IPAddress.TryParse(trimmed, out var ipAddress))
|
|
{
|
|
// Only support IPv4 and IPv6
|
|
if (ipAddress.AddressFamily == AddressFamily.InterNetwork ||
|
|
ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
|
{
|
|
validHostnames.Add(trimmed);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a valid DNS name
|
|
// Basic validation: must not contain spaces or invalid characters
|
|
if (IsValidDnsName(trimmed))
|
|
validHostnames.Add(trimmed.ToLowerInvariant());
|
|
}
|
|
|
|
return validHostnames.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regex pattern for validating DNS hostnames per RFC 1123.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Pattern breakdown:
|
|
/// -^(?=.{1,253}$) - Ensure total length is 1-253 characters
|
|
/// - [a-zA-Z0-9] - Label must start with alphanumeric
|
|
/// - ([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? - Optional middle part (0-61 chars) ending with alphanumeric
|
|
/// - (\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)* - Additional labels separated by dots
|
|
/// </remarks>
|
|
private static readonly Regex DnsNameRegex = new(
|
|
@"^(?=.{1,253}$)[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
|
|
RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
|
|
/// <summary>
|
|
/// Performs basic validation of a DNS name using regex.
|
|
/// </summary>
|
|
/// <param name="name">The DNS name to validate.</param>
|
|
/// <returns>True if the name appears to be valid.</returns>
|
|
private static bool IsValidDnsName(string name)
|
|
=> !string.IsNullOrWhiteSpace(name) && DnsNameRegex.IsMatch(name);
|
|
}
|