Merge remote-tracking branch 'origin/master' into feature/dynamic-toggle-of-streaming

This commit is contained in:
Kenneth Skovhede
2025-11-03 19:25:15 +01:00
2 changed files with 187 additions and 34 deletions
@@ -39,5 +39,7 @@ Error message: {3}", foldername, filename, extension, errormessage); }
public static string UsernameRequired { get { return LC.L(@"Digest authentication requires a username to be set"); } }
public static string DescriptionUseExtendedPropfindShort { get { return LC.L(@"Use extended PROPFIND body"); } }
public static string DescriptionUseExtendedPropfindLong { get { return LC.L(@"Use an extended PROPFIND body to request listings of files and folders. This is required for some servers"); } }
public static string DescriptionUseLegacyPropfindParsingShort { get { return LC.L(@"Use legacy PROPFIND parsing"); } }
public static string DescriptionUseLegacyPropfindParsingLong { get { return LC.L(@"Use the legacy PROPFIND response parsing logic."); } }
}
}
+185 -34
View File
@@ -46,6 +46,10 @@ namespace Duplicati.Library.Backend
/// The use extended propfind option name
/// </summary>
private const string USE_EXTENDED_PROPFIND_OPTION = "use-extended-propfind";
/// <summary>
/// The use legacy propfind parsing option name
/// </summary>
private const string USE_LEGACY_PROPFIND_PARSING_OPTION = "use-legacy-parsing";
/// <summary>
/// The credentials to use for the connection
@@ -95,6 +99,18 @@ namespace Duplicati.Library.Backend
/// The options for the SSL certificate validation
/// </summary>
private readonly SslOptionsHelper.SslCertificateOptions m_certificateOptions;
/// <summary>
/// Flag indicating if the legacy PROPFIND parsing should be used
/// </summary>
private readonly bool m_useLegacyParsing;
/// <summary>
/// The base URI that represents the configured WebDAV root
/// </summary>
private readonly System.Uri m_baseUri;
/// <summary>
/// Known path prefixes used to interpret PROPFIND href responses
/// </summary>
private readonly List<string> m_propfindPathPrefixes;
/// <summary>
/// A list of files seen in the last List operation.
@@ -136,6 +152,9 @@ namespace Duplicati.Library.Backend
m_dnsName = string.Empty;
m_certificateOptions = null!;
m_timeouts = null!;
m_useLegacyParsing = false;
m_baseUri = null!;
m_propfindPathPrefixes = null!;
}
public WEBDAV(string url, Dictionary<string, string?> options)
@@ -156,6 +175,7 @@ namespace Duplicati.Library.Backend
m_useIntegratedAuthentication = Utility.Utility.ParseBoolOption(options, INTEGRATED_AUTHENTICATION_OPTION);
m_forceDigestAuthentication = Utility.Utility.ParseBoolOption(options, FORCE_DIGEST_AUTHENTICATION_OPTION);
m_useExtendedPropfind = Utility.Utility.ParseBoolOption(options, USE_EXTENDED_PROPFIND_OPTION);
m_useLegacyParsing = Utility.Utility.ParseBoolOption(options, USE_LEGACY_PROPFIND_PARSING_OPTION);
m_debugPropfindFile = options.GetValueOrDefault(DEBUG_PROPFIND_FILE_OPTION);
// Explicitly support setups with no username and password
@@ -181,6 +201,159 @@ namespace Duplicati.Library.Backend
m_sanitizedUrl = new Utility.Uri(m_certificateOptions.UseSSL ? "https" : "http", u.Host, m_path).ToString();
m_reverseProtocolUrl = new Utility.Uri(m_certificateOptions.UseSSL ? "http" : "https", u.Host, m_path).ToString();
m_timeouts = TimeoutOptionsHelper.Parse(options);
m_baseUri = new System.Uri(m_url, System.UriKind.Absolute);
m_propfindPathPrefixes = BuildPropfindPrefixes();
}
private List<string> BuildPropfindPrefixes()
{
var prefixes = new HashSet<string>(StringComparer.Ordinal);
AddPropfindPrefix(prefixes, "/");
AddPropfindPrefix(prefixes, m_baseUri.AbsolutePath);
AddPropfindPrefix(prefixes, m_path);
AddPropfindPrefixFromUri(prefixes, m_url);
AddPropfindPrefixFromUri(prefixes, m_rawurl);
AddPropfindPrefixFromUri(prefixes, m_rawurlPort);
AddPropfindPrefixFromUri(prefixes, m_sanitizedUrl);
AddPropfindPrefixFromUri(prefixes, m_reverseProtocolUrl);
var ordered = new List<string>(prefixes);
ordered.Sort((left, right) => right.Length.CompareTo(left.Length));
return ordered;
}
private static void AddPropfindPrefixFromUri(HashSet<string> prefixes, string? uri)
{
if (string.IsNullOrWhiteSpace(uri))
return;
if (System.Uri.TryCreate(uri, System.UriKind.Absolute, out var parsed))
AddPropfindPrefix(prefixes, parsed.AbsolutePath);
}
private static void AddPropfindPrefix(HashSet<string> prefixes, string? path)
{
if (path == null)
return;
var normalized = Utility.Uri.UrlDecode(path.Replace("+", "%2B"));
if (!normalized.StartsWith("/", StringComparison.Ordinal))
normalized = "/" + normalized;
if (!normalized.EndsWith("/", StringComparison.Ordinal))
normalized += "/";
prefixes.Add(normalized);
}
private string? ExtractNameFromPropfindHref(string hrefValue)
{
if (string.IsNullOrWhiteSpace(hrefValue))
return null;
var trimmed = hrefValue.Trim();
if (!System.Uri.TryCreate(trimmed, System.UriKind.Absolute, out var hrefUri))
{
if (System.Uri.TryCreate(trimmed, System.UriKind.Relative, out var relativeUri))
{
hrefUri = new System.Uri(m_baseUri, relativeUri);
}
else
{
var decodedValue = Utility.Uri.UrlDecode(trimmed.Replace("+", "%2B"));
return ExtractRelativeFromDecodedPath(decodedValue);
}
}
var decodedPath = Utility.Uri.UrlDecode(hrefUri.AbsolutePath.Replace("+", "%2B"));
var name = ExtractRelativeFromDecodedPath(decodedPath);
if (!string.IsNullOrEmpty(name))
return name;
var decodedHref = Utility.Uri.UrlDecode(trimmed.Replace("+", "%2B"));
return ExtractRelativeFromDecodedPath(decodedHref);
}
private string? ExtractRelativeFromDecodedPath(string decodedPath)
{
if (string.IsNullOrEmpty(decodedPath))
return null;
var comparisonPath = decodedPath.StartsWith("/", StringComparison.Ordinal)
? decodedPath
: "/" + decodedPath;
foreach (var prefix in m_propfindPathPrefixes)
{
if (comparisonPath.Equals(prefix, StringComparison.Ordinal))
return null;
if (comparisonPath.StartsWith(prefix, StringComparison.Ordinal))
{
var remainder = comparisonPath.Substring(prefix.Length);
return remainder.Length == 0 ? null : remainder;
}
var trimmedPrefix = prefix.EndsWith("/", StringComparison.Ordinal)
? prefix.Substring(0, prefix.Length - 1)
: prefix;
if (trimmedPrefix.Length > 0)
{
if (comparisonPath.Equals(trimmedPrefix, StringComparison.Ordinal))
return null;
if (comparisonPath.StartsWith(trimmedPrefix + "/", StringComparison.Ordinal))
{
var remainder = comparisonPath.Substring(trimmedPrefix.Length + 1);
return remainder.Length == 0 ? null : remainder;
}
}
}
if (comparisonPath.StartsWith("/", StringComparison.Ordinal))
{
var remainder = comparisonPath.Substring(1);
return remainder.Length == 0 ? null : remainder;
}
return comparisonPath.Length == 0 ? null : comparisonPath;
}
private string? ExtractNameFromPropfindHrefLegacy(string hrefValue)
{
if (string.IsNullOrWhiteSpace(hrefValue))
return null;
string name = Utility.Uri.UrlDecode(hrefValue.Replace("+", "%2B"));
string cmp_path;
if (name.StartsWith(m_url, StringComparison.Ordinal))
cmp_path = m_url;
else if (name.StartsWith(m_rawurl, StringComparison.Ordinal))
cmp_path = m_rawurl;
else if (name.StartsWith(m_rawurlPort, StringComparison.Ordinal))
cmp_path = m_rawurlPort;
else if (name.StartsWith(m_path, StringComparison.Ordinal))
cmp_path = m_path;
else if (name.StartsWith("/" + m_path, StringComparison.Ordinal))
cmp_path = "/" + m_path;
else if (name.StartsWith(m_sanitizedUrl, StringComparison.Ordinal))
cmp_path = m_sanitizedUrl;
else if (name.StartsWith(m_reverseProtocolUrl, StringComparison.Ordinal))
cmp_path = m_reverseProtocolUrl;
else
return null;
if (name.Length <= cmp_path.Length)
return null;
return name.Substring(cmp_path.Length);
}
/// <summary>
@@ -286,47 +459,24 @@ namespace Duplicati.Library.Backend
}
var entries = new List<IFileEntry>();
foreach (System.Xml.XmlNode n in lst)
foreach (var n in lst.Cast<System.Xml.XmlNode>())
{
//IIS uses %20 for spaces and %2B for +
//Apache uses %20 for spaces and + for +
string name = Utility.Uri.UrlDecode(n.InnerText.Replace("+", "%2B"));
var name = m_useLegacyParsing
? ExtractNameFromPropfindHrefLegacy(n.InnerText)
: ExtractNameFromPropfindHref(n.InnerText) ?? ExtractNameFromPropfindHrefLegacy(n.InnerText);
string cmp_path;
//TODO: This list is getting ridiculous, should change to regexps
if (name.StartsWith(m_url, StringComparison.Ordinal))
cmp_path = m_url;
else if (name.StartsWith(m_rawurl, StringComparison.Ordinal))
cmp_path = m_rawurl;
else if (name.StartsWith(m_rawurlPort, StringComparison.Ordinal))
cmp_path = m_rawurlPort;
else if (name.StartsWith(m_path, StringComparison.Ordinal))
cmp_path = m_path;
else if (name.StartsWith("/" + m_path, StringComparison.Ordinal))
cmp_path = "/" + m_path;
else if (name.StartsWith(m_sanitizedUrl, StringComparison.Ordinal))
cmp_path = m_sanitizedUrl;
else if (name.StartsWith(m_reverseProtocolUrl, StringComparison.Ordinal))
cmp_path = m_reverseProtocolUrl;
else
if (string.IsNullOrEmpty(name))
continue;
if (name.Length <= cmp_path.Length)
continue;
var size = -1L;
var lastAccess = new DateTime();
var lastModified = new DateTime();
var isCollection = false;
name = name.Substring(cmp_path.Length);
long size = -1;
DateTime lastAccess = new DateTime();
DateTime lastModified = new DateTime();
bool isCollection = false;
System.Xml.XmlNode? stat = n.ParentNode?.SelectSingleNode("D:propstat/D:prop", nm);
var stat = n.ParentNode?.SelectSingleNode("D:propstat/D:prop", nm);
if (stat != null)
{
System.Xml.XmlNode? s = stat.SelectSingleNode("D:getcontentlength", nm);
var s = stat.SelectSingleNode("D:getcontentlength", nm);
if (s != null)
size = long.Parse(s.InnerText);
s = stat.SelectSingleNode("D:getlastmodified", nm);
@@ -409,6 +559,7 @@ namespace Duplicati.Library.Backend
new CommandLineArgument(FORCE_DIGEST_AUTHENTICATION_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.WEBDAV.DescriptionForceDigestShort, Strings.WEBDAV.DescriptionForceDigestLong),
new CommandLineArgument(DEBUG_PROPFIND_FILE_OPTION, CommandLineArgument.ArgumentType.Path, Strings.WEBDAV.DescriptionDebugPropfindShort, Strings.WEBDAV.DescriptionDebugPropfindLong),
new CommandLineArgument(USE_EXTENDED_PROPFIND_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.WEBDAV.DescriptionUseExtendedPropfindShort, Strings.WEBDAV.DescriptionUseExtendedPropfindLong),
new CommandLineArgument(USE_LEGACY_PROPFIND_PARSING_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.WEBDAV.DescriptionUseLegacyPropfindParsingShort, Strings.WEBDAV.DescriptionUseLegacyPropfindParsingLong, "false"),
.. SslOptionsHelper.GetOptions(),
.. TimeoutOptionsHelper.GetOptions()
];