From d61931fc04a80602c5371f5d5bb9b882bcbef78b Mon Sep 17 00:00:00 2001 From: junjunni <82033250+xinyuzjj@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:33:39 +0800 Subject: [PATCH] feat: extract credentials from FTP URL (RFC 1738) --- src/Files.App.Storage/Ftp/FtpHelpers.cs | 44 +++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Files.App.Storage/Ftp/FtpHelpers.cs b/src/Files.App.Storage/Ftp/FtpHelpers.cs index 9a0f630e2502..d48ec5302ef0 100644 --- a/src/Files.App.Storage/Ftp/FtpHelpers.cs +++ b/src/Files.App.Storage/Ftp/FtpHelpers.cs @@ -3,6 +3,7 @@ using Files.Shared.Extensions; using FluentFTP; +using System.Net; namespace Files.App.Storage { @@ -10,7 +11,7 @@ internal static class FtpHelpers { public static string GetFtpPath(string path) { - path = path.Replace('\\', '/'); + path = path.Replace('\', '/'); var schemaIndex = path.IndexOf("://", StringComparison.Ordinal) + 3; var hostIndex = path.IndexOf('/', schemaIndex); @@ -26,25 +27,34 @@ public static Task EnsureConnectedAsync(this AsyncFtpClient ftpClient, Cancellat public static string GetFtpHost(string path) { var authority = GetFtpAuthority(path); - var index = authority.IndexOf(':', StringComparison.Ordinal); + var atIndex = authority.IndexOf('@', StringComparison.Ordinal); + if (atIndex != -1) + { + var hostPart = authority.Substring(atIndex + 1); + var colonIndex = hostPart.IndexOf(':', StringComparison.Ordinal); + return colonIndex == -1 ? hostPart : hostPart.Substring(0, colonIndex); + } + var index = authority.IndexOf(':', StringComparison.Ordinal); return index == -1 ? authority : authority.Substring(0, index); } public static ushort GetFtpPort(string path) { var authority = GetFtpAuthority(path); - var index = authority.IndexOf(':', StringComparison.Ordinal); + var atIndex = authority.IndexOf('@', StringComparison.Ordinal); + var hostPart = atIndex != -1 ? authority.Substring(atIndex + 1) : authority; + var index = hostPart.IndexOf(':', StringComparison.Ordinal); if (index != -1) - return ushort.Parse(authority.Substring(index + 1)); + return ushort.Parse(hostPart.Substring(index + 1)); return path.StartsWith("ftps://", StringComparison.OrdinalIgnoreCase) ? (ushort)990 : (ushort)21; } public static string GetFtpAuthority(string path) { - path = path.Replace('\\', '/'); + path = path.Replace('\', '/'); var schemaIndex = path.IndexOf("://", StringComparison.Ordinal) + 3; var hostIndex = path.IndexOf('/', schemaIndex); @@ -53,12 +63,34 @@ public static string GetFtpAuthority(string path) return path.Substring(schemaIndex, hostIndex - schemaIndex); } + public static NetworkCredential? GetFtpCredentials(string path) + { + var authority = GetFtpAuthority(path); + var atIndex = authority.IndexOf('@', StringComparison.Ordinal); + + if (atIndex == -1) + return null; // No credentials in URL + + var credentialsPart = authority.Substring(0, atIndex); + var colonIndex = credentialsPart.IndexOf(':', StringComparison.Ordinal); + + if (colonIndex == -1) + { + // Only username, no password + return new NetworkCredential(Uri.UnescapeDataString(credentialsPart), ""); + } + + var username = Uri.UnescapeDataString(credentialsPart.Substring(0, colonIndex)); + var password = Uri.UnescapeDataString(credentialsPart.Substring(colonIndex + 1)); + + return new NetworkCredential(username, password); + } public static AsyncFtpClient GetFtpClient(string ftpPath) { var host = GetFtpHost(ftpPath); var port = GetFtpPort(ftpPath); - var credentials = FtpManager.Credentials.Get(host, FtpManager.Anonymous); + var credentials = GetFtpCredentials(ftpPath) ?? FtpManager.Credentials.Get(host, FtpManager.Anonymous); return new(host, credentials, port); }