2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, 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.
2019-05-26 19:46:47 -07:00
using Duplicati.Library.Common.IO ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Interface ;
2009-01-14 17:29:30 +00:00
using System ;
using System.Collections.Generic ;
2015-01-20 21:07:24 +01:00
using System.Linq ;
2019-02-22 21:58:40 -06:00
using System.Text.RegularExpressions ;
using System.Threading ;
using System.Threading.Tasks ;
2009-01-14 17:29:30 +00:00
namespace Duplicati.Library.Backend
{
2019-05-26 19:46:47 -07:00
// ReSharper disable once UnusedMember.Global
// This class is instantiated dynamically in the BackendLoader.
2013-05-05 17:07:24 +02:00
public class FTP : IBackend , IStreamingBackend
2009-01-14 17:29:30 +00:00
{
2009-01-15 12:57:52 +00:00
private System . Net . NetworkCredential m_userInfo ;
2012-01-19 19:04:56 +00:00
private readonly string m_url ;
2009-01-14 17:29:30 +00:00
2012-01-19 19:04:56 +00:00
private readonly bool m_useSSL = false ;
2018-06-28 23:10:35 +02:00
private readonly bool m_passiveMode = false ;
2012-01-19 19:04:56 +00:00
private readonly bool m_listVerify = true ;
2010-02-10 20:44:48 +00:00
2014-11-10 18:38:18 +01:00
private readonly byte [] m_copybuffer = new byte [ Duplicati . Library . Utility . Utility . DEFAULT_BUFFER_SIZE ];
2009-01-14 17:29:30 +00:00
public FTP ()
{
}
public FTP ( string url , Dictionary < string , string > options )
{
2011-09-24 11:39:50 +00:00
//This can be made better by keeping a single ftp stream open,
//unfortunately the .Net model does not allow this as the request is
//bound to a single url (path+file).
//
//To fix this, a thirdparty FTP library is required,
//this would also allow a fix for the FTP servers
//that only support SSL during authentication, not during transfers
//
//If you have experience with a stable open source .Net FTP library,
//please let the Duplicati authors know
2013-05-05 23:46:00 +02:00
var u = new Utility . Uri ( url );
2013-05-06 20:36:26 +02:00
u . RequireHost ();
2018-06-28 21:07:23 +02:00
string username = null ;
string password = null ;
2009-01-15 12:57:52 +00:00
2013-05-05 23:46:00 +02:00
if (! string . IsNullOrEmpty ( u . Username ))
2009-01-15 12:57:52 +00:00
{
2018-06-28 21:07:23 +02:00
username = u . Username ;
2009-01-15 12:57:52 +00:00
}
2019-05-26 19:46:47 -07:00
else if ( options . ContainsKey ( "auth-username" ))
{
username = options [ "auth-username" ];
}
2018-06-28 21:07:23 +02:00
if (! string . IsNullOrEmpty ( u . Username ) && ! string . IsNullOrEmpty ( u . Password )) {
password = u . Password ;
2009-07-16 20:36:49 +00:00
}
2018-06-28 21:07:23 +02:00
else if ( options . ContainsKey ( "auth-password" )) {
password = options [ "auth-password" ];
}
m_userInfo = new System . Net . NetworkCredential
{
UserName = username ,
Password = password
};
2009-01-15 12:57:52 +00:00
2012-04-19 18:49:37 +00:00
//Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code
if ( m_userInfo != null )
m_userInfo . Domain = "" ;
2013-05-05 23:46:00 +02:00
m_url = u . SetScheme ( "ftp" ). SetQuery ( null ). SetCredentials ( null , null ). ToString ();
2018-10-27 12:17:07 +02:00
m_url = Util . AppendDirSeparator ( m_url , "/" );
2019-05-26 19:46:47 -07:00
2011-10-19 05:42:32 +00:00
m_useSSL = Utility . Utility . ParseBoolOption ( options , "use-ssl" );
2012-01-19 19:04:56 +00:00
m_listVerify = ! Utility . Utility . ParseBoolOption ( options , "disable-upload-verify" );
2011-06-26 10:22:03 +00:00
2011-10-19 05:42:32 +00:00
if ( Utility . Utility . ParseBoolOption ( options , "ftp-passive" ))
2011-09-27 06:59:17 +00:00
{
2018-06-28 23:10:35 +02:00
m_passiveMode = true ;
2018-07-17 21:35:55 +02:00
} else m_passiveMode = ! Utility . Utility . ParseBoolOption ( options , "ftp-regular" );
2009-01-14 17:29:30 +00:00
}
#region Regular expression to parse list lines
//Regexps found here: http://www.dotnetfunda.com/articles/article125.aspx
2010-06-09 10:54:58 +00:00
//Modified to allow hyphens in username and groupname
2009-01-14 17:29:30 +00:00
internal readonly static Regex [] PARSEFORMATS =
{
2010-06-09 10:54:58 +00:00
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+(?<groupname>\S+)\s+(?<username>\S+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)" ),
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+(?<groupname>\d+)\s+(?<username>\d+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{4})\s+(?<name>.+)" ),
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+(?<groupname>\d+)\s+(?<username>\d+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)" ),
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})\s+\d+\s+(?<groupname>\S+)\s+(?<username>\S+)\s+(?<size>\d+)\s+(?<timestamp>\w+\s+\d+\s+\d{1,2}:\d{2})\s+(?<name>.+)" ),
2009-01-14 17:29:30 +00:00
new Regex ( @"(?<dir>[\-d])(?<permission>([\-r][\-w][\-xs]){3})(\s+)(?<size>(\d+))(\s+)(?<ctbit>(\w+\s\w+))(\s+)(?<size2>(\d+))\s+(?<timestamp>\w+\s+\d+\s+\d{2}:\d{2})\s+(?<name>.+)" ),
new Regex ( @"(?<timestamp>\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+(?<dir>\<\w+\>){0,1}(?<size>\d+){0,1}\s+(?<name>.+)" ),
new Regex ( @"([<timestamp>]*\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}[Aa|Pp][mM])\s+([<dir>]*\<\w+\>){0,1}([<size>]*\d+){0,1}\s+([<name>]*.+)" )
};
#endregion
2010-06-09 10:54:58 +00:00
2009-01-15 14:47:05 +00:00
private static Match MatchLine ( string line )
2009-01-14 17:29:30 +00:00
{
Match m = null ;
foreach ( Regex s in PARSEFORMATS )
if (( m = s . Match ( line )). Success )
return m ;
return null ;
}
2009-01-15 14:47:05 +00:00
public static FileEntry ParseLine ( string line )
2009-01-14 17:29:30 +00:00
{
Match m = MatchLine ( line );
if ( m == null )
return null ;
FileEntry f = new FileEntry ( m . Groups [ "name" ]. Value );
string time = m . Groups [ "timestamp" ]. Value ;
string dir = m . Groups [ "dir" ]. Value ;
2019-05-26 19:46:47 -07:00
if ( dir != "" && dir != "-" )
{
f . IsFolder = true ;
}
else
{
f . Size = long . Parse ( m . Groups [ "size" ]. Value );
}
if ( DateTime . TryParse ( time , out DateTime t ))
{
f . LastAccess = f . LastModification = t ;
}
2009-01-14 17:29:30 +00:00
return f ;
}
#region IBackendInterface Members
public string DisplayName
{
2009-06-14 21:51:06 +00:00
get { return Strings . FTPBackend . DisplayName ; }
2009-01-14 17:29:30 +00:00
}
public string ProtocolKey
{
get { return "ftp" ; }
}
2017-09-25 23:19:31 -06:00
private T HandleListExceptions < T >( Func < T > func , System . Net . FtpWebRequest req )
2009-01-14 17:29:30 +00:00
{
2017-09-25 23:19:31 -06:00
T ret = default ( T );
2017-10-14 09:17:19 -06:00
Action action = () => ret = func ();
HandleListExceptions ( action , req );
2017-09-25 23:19:31 -06:00
return ret ;
2012-01-19 19:04:56 +00:00
}
2017-09-25 23:17:45 -06:00
2017-09-25 23:19:31 -06:00
private void HandleListExceptions ( Action action , System . Net . FtpWebRequest req )
2012-01-19 19:04:56 +00:00
{
2011-03-10 19:08:16 +00:00
try
{
2017-09-25 23:19:31 -06:00
action ();
2011-03-10 19:08:16 +00:00
}
catch ( System . Net . WebException wex )
{
if ( wex . Response as System . Net . FtpWebResponse != null && ( wex . Response as System . Net . FtpWebResponse ). StatusCode == System . Net . FtpStatusCode . ActionNotTakenFileUnavailable )
2015-01-20 21:07:24 +01:00
throw new Interface . FolderMissingException ( Strings . FTPBackend . MissingFolderError ( req . RequestUri . PathAndQuery , wex . Message ), wex );
2011-03-10 19:08:16 +00:00
else
throw ;
2010-01-17 20:22:14 +00:00
}
2009-01-14 17:29:30 +00:00
}
2017-09-25 23:19:31 -06:00
public IEnumerable < IFileEntry > List ()
{
return List ( "" );
}
public IEnumerable < IFileEntry > List ( string filename )
{
var req = CreateRequest ( filename );
req . Method = System . Net . WebRequestMethods . Ftp . ListDirectoryDetails ;
req . UseBinary = false ;
System . Net . WebResponse resp = null ;
System . IO . Stream rs = null ;
System . IO . StreamReader sr = null ;
try
2017-09-25 23:17:45 -06:00
{
2017-09-25 23:19:31 -06:00
HandleListExceptions (
() =>
{
var areq = new Utility . AsyncHttpRequest ( req );
resp = areq . GetResponse ();
rs = areq . GetResponseStream ();
sr = new System . IO . StreamReader ( new StreamReadHelper ( rs ));
},
req );
2017-09-25 23:17:45 -06:00
string line ;
2018-06-28 23:16:28 +02:00
while (( line = HandleListExceptions ( sr . ReadLine , req )) != null )
2017-09-25 23:17:45 -06:00
{
FileEntry f = ParseLine ( line );
if ( f != null )
yield return f ;
}
}
2017-09-25 23:19:31 -06:00
finally
{
try
{
if ( sr != null )
{
sr . Dispose ();
}
}
finally
{
try
{
if ( rs != null )
{
rs . Dispose ();
}
}
finally
{
if ( resp != null )
{
resp . Dispose ();
}
}
}
}
2017-09-25 23:17:45 -06:00
}
2019-03-17 18:20:14 -05:00
public async Task PutAsync ( string remotename , System . IO . Stream input , CancellationToken cancelToken )
2009-01-14 17:29:30 +00:00
{
2010-10-23 11:40:43 +00:00
System . Net . FtpWebRequest req = null ;
try
2010-02-10 20:44:48 +00:00
{
2011-03-10 19:08:16 +00:00
req = CreateRequest ( remotename );
req . Method = System . Net . WebRequestMethods . Ftp . UploadFile ;
req . UseBinary = true ;
2012-01-19 19:04:56 +00:00
long streamLen = - 1 ;
try { streamLen = input . Length ; }
catch {}
2010-10-23 11:40:43 +00:00
2012-06-06 12:55:48 +00:00
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
2017-05-17 10:59:07 +02:00
using ( System . IO . Stream rs = areq . GetRequestStream ( streamLen ))
2019-02-23 09:17:45 -06:00
await Utility . Utility . CopyStreamAsync ( input , rs , true , cancelToken , m_copybuffer ). ConfigureAwait ( false );
2012-01-19 19:04:56 +00:00
2019-02-23 09:17:45 -06:00
if ( m_listVerify )
2012-01-19 19:04:56 +00:00
{
2017-09-25 23:17:45 -06:00
IEnumerable < IFileEntry > files = List ( remotename );
2012-01-19 19:04:56 +00:00
foreach ( IFileEntry fe in files )
2017-11-26 10:53:14 -08:00
if ( fe . Name . Equals ( remotename ) || fe . Name . EndsWith ( "/" + remotename , StringComparison . Ordinal ) || fe . Name . EndsWith ( "\\" + remotename , StringComparison . Ordinal ))
2012-01-19 19:04:56 +00:00
{
if ( fe . Size < 0 || streamLen < 0 || fe . Size == streamLen )
2019-02-23 09:17:45 -06:00
return ;
2019-02-22 21:58:40 -06:00
2015-01-20 21:07:24 +01:00
throw new Exception ( Strings . FTPBackend . ListVerifySizeFailure ( remotename , fe . Size , streamLen ));
2012-01-19 19:04:56 +00:00
}
2015-01-20 21:07:24 +01:00
throw new Exception ( Strings . FTPBackend . ListVerifyFailure ( remotename , files . Select ( n => n . Name )));
2012-01-19 19:04:56 +00:00
}
2010-10-23 11:40:43 +00:00
}
catch ( System . Net . WebException wex )
{
if ( req != null && wex . Response as System . Net . FtpWebResponse != null && ( wex . Response as System . Net . FtpWebResponse ). StatusCode == System . Net . FtpStatusCode . ActionNotTakenFileUnavailable )
2015-01-20 21:07:24 +01:00
throw new Interface . FolderMissingException ( Strings . FTPBackend . MissingFolderError ( req . RequestUri . PathAndQuery , wex . Message ), wex );
2010-10-23 11:40:43 +00:00
else
throw ;
2010-02-10 20:44:48 +00:00
}
2009-01-14 17:29:30 +00:00
}
2021-06-12 11:30:08 -07:00
public async Task PutAsync ( string remotename , string localname , CancellationToken cancelToken )
2009-01-25 19:33:36 +00:00
{
using ( System . IO . FileStream fs = System . IO . File . Open ( localname , System . IO . FileMode . Open , System . IO . FileAccess . Read , System . IO . FileShare . Read ))
2021-06-12 11:30:08 -07:00
await PutAsync ( remotename , fs , cancelToken );
2009-01-25 19:33:36 +00:00
}
public void Get ( string remotename , System . IO . Stream output )
2009-01-14 17:29:30 +00:00
{
2016-04-16 11:12:05 +02:00
var req = CreateRequest ( remotename );
2011-03-10 19:08:16 +00:00
req . Method = System . Net . WebRequestMethods . Ftp . DownloadFile ;
req . UseBinary = true ;
2009-01-15 12:57:52 +00:00
2016-04-16 11:12:05 +02:00
var areq = new Utility . AsyncHttpRequest ( req );
using ( var resp = areq . GetResponse ())
2016-09-15 11:39:27 +02:00
using ( var rs = areq . GetResponseStream ())
2014-11-10 18:38:18 +01:00
Utility . Utility . CopyStream ( rs , output , false , m_copybuffer );
2009-01-25 19:33:36 +00:00
}
public void Get ( string remotename , string localname )
{
using ( System . IO . FileStream fs = System . IO . File . Open ( localname , System . IO . FileMode . Create , System . IO . FileAccess . Write , System . IO . FileShare . None ))
Get ( remotename , fs );
2009-01-14 17:29:30 +00:00
}
public void Delete ( string remotename )
{
2011-03-10 19:08:16 +00:00
System . Net . FtpWebRequest req = CreateRequest ( remotename );
req . Method = System . Net . WebRequestMethods . Ftp . DeleteFile ;
2012-06-06 12:55:48 +00:00
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
2016-09-15 11:39:27 +02:00
using ( areq . GetResponse ())
2011-03-10 19:08:16 +00:00
{ }
2009-01-14 17:29:30 +00:00
}
2009-03-21 12:46:54 +00:00
public IList < ICommandLineArgument > SupportedCommands
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
2009-06-14 21:51:06 +00:00
new CommandLineArgument ( "ftp-passive" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionFTPPassiveShort , Strings . FTPBackend . DescriptionFTPPassiveLong , "false" ),
new CommandLineArgument ( "ftp-regular" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionFTPActiveShort , Strings . FTPBackend . DescriptionFTPActiveLong , "true" ),
2013-05-05 16:40:37 +02:00
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . FTPBackend . DescriptionAuthPasswordShort , Strings . FTPBackend . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . FTPBackend . DescriptionAuthUsernameShort , Strings . FTPBackend . DescriptionAuthUsernameLong ),
2010-02-10 20:44:48 +00:00
new CommandLineArgument ( "use-ssl" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionUseSSLShort , Strings . FTPBackend . DescriptionUseSSLLong ),
2012-01-19 19:04:56 +00:00
new CommandLineArgument ( "disable-upload-verify" , CommandLineArgument . ArgumentType . Boolean , Strings . FTPBackend . DescriptionDisableUploadVerifyShort , Strings . FTPBackend . DescriptionDisableUploadVerifyLong ),
2009-03-21 12:46:54 +00:00
});
}
}
public string Description
{
get
{
2009-06-14 21:51:06 +00:00
return Strings . FTPBackend . Description ;
2009-03-21 12:46:54 +00:00
}
}
2018-02-26 11:37:10 +01:00
public string [] DNSName
2018-02-21 22:38:01 +01:00
{
2018-02-26 11:37:10 +01:00
get { return new string [] { new Uri ( m_url ). Host }; }
2018-02-21 22:38:01 +01:00
}
2018-02-18 00:18:44 +01:00
2013-05-05 17:07:24 +02:00
public void Test ()
{
2017-09-25 23:17:45 -06:00
this . TestList ();
2013-05-05 17:07:24 +02:00
}
public void CreateFolder ()
{
System . Net . FtpWebRequest req = CreateRequest ( "" , true );
req . Method = System . Net . WebRequestMethods . Ftp . MakeDirectory ;
req . KeepAlive = false ;
Utility . AsyncHttpRequest areq = new Utility . AsyncHttpRequest ( req );
using ( areq . GetResponse ())
{ }
}
2009-01-14 17:29:30 +00:00
#endregion
2009-01-25 19:33:36 +00:00
#region IDisposable Members
public void Dispose ()
2019-05-26 19:46:47 -07:00
{
2018-06-28 23:10:35 +02:00
m_userInfo = null ;
2009-01-25 19:33:36 +00:00
}
#endregion
2012-01-19 19:04:56 +00:00
2009-01-14 17:29:30 +00:00
private System . Net . FtpWebRequest CreateRequest ( string remotename )
{
2012-01-19 19:04:56 +00:00
return CreateRequest ( remotename , false );
}
private System . Net . FtpWebRequest CreateRequest ( string remotename , bool createFolder )
{
string url = m_url ;
2017-11-26 10:53:14 -08:00
if ( createFolder && url . EndsWith ( "/" , StringComparison . Ordinal ))
2012-01-19 19:04:56 +00:00
url = url . Substring ( 0 , url . Length - 1 );
2018-06-28 21:07:23 +02:00
System . Net . FtpWebRequest req = ( System . Net . FtpWebRequest ) System . Net . WebRequest . Create ( url + remotename );
2009-03-21 12:46:54 +00:00
2019-05-26 19:46:47 -07:00
if ( m_userInfo != null )
{
req . Credentials = m_userInfo ;
}
2009-03-11 20:56:01 +00:00
req . KeepAlive = false ;
2018-06-28 23:10:35 +02:00
req . UsePassive = m_passiveMode ;
2009-01-15 12:57:52 +00:00
2010-02-10 20:44:48 +00:00
if ( m_useSSL )
req . EnableSsl = m_useSSL ;
2009-01-14 17:29:30 +00:00
return req ;
}
2010-05-27 19:37:14 +00:00
/// <summary>
/// Private helper class to fix a bug with the StreamReader
/// </summary>
2010-11-21 11:08:15 +00:00
private class StreamReadHelper : Utility . OverrideableStream
2010-05-27 19:37:14 +00:00
{
/// <summary>
/// Once the stream has returned 0 as the read count it is disposed
/// in the FtpRequest, and subsequent read requests will throw an ObjectDisposedException
/// </summary>
private bool m_empty = false ;
/// <summary>
/// Basic initialization, just pass the stream to the super class
/// </summary>
/// <param name="stream"></param>
public StreamReadHelper ( System . IO . Stream stream )
: base ( stream )
{
}
/// <summary>
/// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted
/// </summary>
/// <param name="buffer">The buffer to place data in</param>
/// <param name="offset">The offset into the buffer to start at</param>
/// <param name="count">The number of bytes to read</param>
/// <returns>The number of bytes read</returns>
public override int Read ( byte [] buffer , int offset , int count )
{
int readCount = 0 ;
int a ;
while (! m_empty && count > 0 )
{
a = base . Read ( buffer , offset , count );
readCount += a ;
count -= a ;
offset += a ;
m_empty = a == 0 ;
}
return readCount ;
}
}
2009-01-14 17:29:30 +00:00
}
}