diff --git a/Duplicati/Library/Backend/WEBDAV/Strings.cs b/Duplicati/Library/Backend/WEBDAV/Strings.cs
index 56fb3372d..70661bda3 100644
--- a/Duplicati/Library/Backend/WEBDAV/Strings.cs
+++ b/Duplicati/Library/Backend/WEBDAV/Strings.cs
@@ -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."); } }
}
}
diff --git a/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs b/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs
index 4662a8ffa..4eda14dac 100644
--- a/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs
+++ b/Duplicati/Library/Backend/WEBDAV/WEBDAV.cs
@@ -46,6 +46,10 @@ namespace Duplicati.Library.Backend
/// The use extended propfind option name
///
private const string USE_EXTENDED_PROPFIND_OPTION = "use-extended-propfind";
+ ///
+ /// The use legacy propfind parsing option name
+ ///
+ private const string USE_LEGACY_PROPFIND_PARSING_OPTION = "use-legacy-parsing";
///
/// The credentials to use for the connection
@@ -95,6 +99,18 @@ namespace Duplicati.Library.Backend
/// The options for the SSL certificate validation
///
private readonly SslOptionsHelper.SslCertificateOptions m_certificateOptions;
+ ///
+ /// Flag indicating if the legacy PROPFIND parsing should be used
+ ///
+ private readonly bool m_useLegacyParsing;
+ ///
+ /// The base URI that represents the configured WebDAV root
+ ///
+ private readonly System.Uri m_baseUri;
+ ///
+ /// Known path prefixes used to interpret PROPFIND href responses
+ ///
+ private readonly List m_propfindPathPrefixes;
///
/// 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 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 BuildPropfindPrefixes()
+ {
+ var prefixes = new HashSet(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(prefixes);
+ ordered.Sort((left, right) => right.Length.CompareTo(left.Length));
+ return ordered;
+ }
+
+ private static void AddPropfindPrefixFromUri(HashSet 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 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);
}
///
@@ -286,47 +459,24 @@ namespace Duplicati.Library.Backend
}
var entries = new List();
- foreach (System.Xml.XmlNode n in lst)
+ foreach (var n in lst.Cast())
{
- //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()
];