2016-04-27 14:48:11 +02:00
#region Disclaimer / License
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Net.FtpClient ;
using System.Net.Security ;
using System.Security.Authentication ;
using Duplicati.Library.Interface ;
using Uri = System . Uri ;
using CoreUtility = Duplicati . Library . Utility . Utility ;
namespace Duplicati.Library.Backend.AlternativeFTP
{
// ReSharper disable once RedundantExtendsListEntry
public class AlternativeFtpBackend : IBackend , IStreamingBackend
{
private System . Net . NetworkCredential _userInfo ;
private const string OPTION_ACCEPT_SPECIFIED_CERTIFICATE = "accept-specified-ssl-hash" ; // Global option
private const string OPTION_ACCEPT_ANY_CERTIFICATE = "accept-any-ssl-certificate" ; // Global option
private const FtpDataConnectionType DEFAULT_DATA_CONNECTION_TYPE = FtpDataConnectionType . AutoPassive ;
private const FtpEncryptionMode DEFAULT_ENCRYPTION_MODE = FtpEncryptionMode . None ;
private const SslProtocols DEFAULT_SSL_PROTOCOLS = SslProtocols . Default ;
private const string CONFIG_KEY_AFTP_ENCRYPTION_MODE = "aftp-encryption-mode" ;
private const string CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE = "aftp-data-connection-type" ;
private const string CONFIG_KEY_AFTP_SSL_PROTOCOLS = "aftp-ssl-protocols" ;
2017-05-24 01:39:30 +07:00
private const string TEST_FILE_NAME = "duplicati-access-privileges-test.tmp" ;
private const string TEST_FILE_CONTENT = "This file used by Duplicati to test access permissions and could be safely deleted." ;
2016-04-27 14:48:11 +02:00
// ReSharper disable InconsistentNaming
private static readonly string DEFAULT_DATA_CONNECTION_TYPE_STRING = DEFAULT_DATA_CONNECTION_TYPE . ToString ();
private static readonly string DEFAULT_ENCRYPTION_MODE_STRING = DEFAULT_ENCRYPTION_MODE . ToString ();
private static readonly string DEFAULT_SSL_PROTOCOLS_STRING = DEFAULT_SSL_PROTOCOLS . ToString ();
// ReSharper restore InconsistentNaming
private readonly string _url ;
private readonly bool _listVerify = true ;
private readonly FtpEncryptionMode _encryptionMode ;
private readonly FtpDataConnectionType _dataConnectionType ;
private readonly SslProtocols _sslProtocols ;
private readonly byte [] _copybuffer = new byte [ CoreUtility . DEFAULT_BUFFER_SIZE ];
private readonly bool _accepAllCertificates ;
private readonly string [] _validHashes ;
/// <summary>
/// The localized name to display for this backend
/// </summary>
public string DisplayName
{
get { return Strings . DisplayName ; }
}
/// <summary>
/// The protocol key, eg. ftp, http or ssh
/// </summary>
public string ProtocolKey
{
get { return "aftp" ; }
}
2016-12-29 15:00:19 +01:00
private FtpClient Client
{ get ; set ; }
2016-04-27 14:48:11 +02:00
public IList < ICommandLineArgument > SupportedCommands
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
2017-05-24 01:39:30 +07:00
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . DescriptionAuthPasswordShort , Strings . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . DescriptionAuthUsernameShort , Strings . DescriptionAuthUsernameLong ),
new CommandLineArgument ( "disable-upload-verify" , CommandLineArgument . ArgumentType . Boolean , Strings . DescriptionDisableUploadVerifyShort , Strings . DescriptionDisableUploadVerifyLong ),
new CommandLineArgument ( CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE , CommandLineArgument . ArgumentType . Enumeration , Strings . DescriptionFtpDataConnectionTypeShort , Strings . DescriptionFtpDataConnectionTypeLong , DEFAULT_DATA_CONNECTION_TYPE_STRING , null , Enum . GetNames ( typeof ( FtpDataConnectionType ))),
new CommandLineArgument ( CONFIG_KEY_AFTP_ENCRYPTION_MODE , CommandLineArgument . ArgumentType . Enumeration , Strings . DescriptionFtpEncryptionModeShort , Strings . DescriptionFtpEncryptionModeLong , DEFAULT_ENCRYPTION_MODE_STRING , null , Enum . GetNames ( typeof ( FtpEncryptionMode ))),
new CommandLineArgument ( CONFIG_KEY_AFTP_SSL_PROTOCOLS , CommandLineArgument . ArgumentType . Flags , Strings . DescriptionSslProtocolsShort , Strings . DescriptionSslProtocolsLong , DEFAULT_SSL_PROTOCOLS_STRING , null , Enum . GetNames ( typeof ( SslProtocols ))),
});
2016-04-27 14:48:11 +02:00
}
}
/// <summary>
/// Initialize a new instance.
/// </summary>
public AlternativeFtpBackend ()
{
}
/// <summary>
/// Initialize a new instance/
/// </summary>
/// <param name="url">Configured url.</param>
/// <param name="options">Configured options. cannot be null.</param>
public AlternativeFtpBackend ( string url , Dictionary < string , string > options )
{
_accepAllCertificates = CoreUtility . ParseBoolOption ( options , OPTION_ACCEPT_ANY_CERTIFICATE );
string certHash ;
options . TryGetValue ( OPTION_ACCEPT_SPECIFIED_CERTIFICATE , out certHash );
_validHashes = certHash == null ? null : certHash . Split ( new [] { "," , ";" }, StringSplitOptions . RemoveEmptyEntries );
var u = new Utility . Uri ( url );
u . RequireHost ();
if (! string . IsNullOrEmpty ( u . Username ))
{
_userInfo = new System . Net . NetworkCredential ();
_userInfo . UserName = u . Username ;
if (! string . IsNullOrEmpty ( u . Password ))
_userInfo . Password = u . Password ;
else if ( options . ContainsKey ( "auth-password" ))
_userInfo . Password = options [ "auth-password" ];
}
else
{
if ( options . ContainsKey ( "auth-username" ))
{
_userInfo = new System . Net . NetworkCredential ();
_userInfo . UserName = options [ "auth-username" ];
if ( options . ContainsKey ( "auth-password" ))
_userInfo . Password = options [ "auth-password" ];
}
}
//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 ( _userInfo != null )
_userInfo . Domain = "" ;
_url = u . SetScheme ( "ftp" ). SetQuery ( null ). SetCredentials ( null , null ). ToString ();
2017-11-26 10:53:14 -08:00
if (! _url . EndsWith ( "/" , StringComparison . Ordinal ))
2016-04-27 14:48:11 +02:00
{
_url += "/" ;
}
_listVerify = ! CoreUtility . ParseBoolOption ( options , "disable-upload-verify" );
// Process the aftp-data-connection-type option
string dataConnectionTypeString ;
if (! options . TryGetValue ( CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE , out dataConnectionTypeString ) || string . IsNullOrWhiteSpace ( dataConnectionTypeString ))
{
dataConnectionTypeString = null ;
}
if ( dataConnectionTypeString == null || ! Enum . TryParse ( dataConnectionTypeString , true , out _dataConnectionType ))
{
_dataConnectionType = DEFAULT_DATA_CONNECTION_TYPE ;
}
// Process the aftp-encryption-mode option
string encryptionModeString ;
if (! options . TryGetValue ( CONFIG_KEY_AFTP_ENCRYPTION_MODE , out encryptionModeString ) || string . IsNullOrWhiteSpace ( encryptionModeString ))
{
encryptionModeString = null ;
}
if ( encryptionModeString == null || ! Enum . TryParse ( encryptionModeString , true , out _encryptionMode ))
{
_encryptionMode = DEFAULT_ENCRYPTION_MODE ;
}
// Process the aftp-ssl-protocols option
string sslProtocolsString ;
2016-09-15 11:39:27 +02:00
if (! options . TryGetValue ( CONFIG_KEY_AFTP_SSL_PROTOCOLS , out sslProtocolsString ) || string . IsNullOrWhiteSpace ( sslProtocolsString ))
2016-04-27 14:48:11 +02:00
{
sslProtocolsString = null ;
}
if ( sslProtocolsString == null || ! Enum . TryParse ( sslProtocolsString , true , out _sslProtocols ))
{
_sslProtocols = DEFAULT_SSL_PROTOCOLS ;
}
}
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List ()
2016-04-27 14:48:11 +02:00
{
return List ( "" );
}
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List ( string filename )
2016-04-27 14:48:11 +02:00
{
return List ( filename , false );
}
2017-09-25 23:17:45 -06:00
private IEnumerable < IFileEntry > List ( string filename , bool stripFile )
2016-04-27 14:48:11 +02:00
{
var list = new List < IFileEntry >();
string remotePath = filename ;
try
{
2016-12-29 21:20:12 +01:00
var ftpClient = CreateClient ();
// Get the remote path
var url = new Uri ( this . _url );
2017-11-26 10:53:14 -08:00
remotePath = "/" + ( url . AbsolutePath . EndsWith ( "/" , StringComparison . Ordinal ) ? url . AbsolutePath . Substring ( 0 , url . AbsolutePath . Length - 1 ) : url . AbsolutePath );
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
if (! string . IsNullOrEmpty ( filename ))
{
if (! stripFile )
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
// Append the filename
remotePath += filename ;
}
else if ( filename . Contains ( "/" ))
{
2017-09-18 23:23:45 -06:00
remotePath += filename . Substring ( 0 , filename . LastIndexOf ( "/" , StringComparison . Ordinal ));
2016-04-27 14:48:11 +02:00
}
2016-12-29 21:20:12 +01:00
// else: stripping the filename in this case ignoring it
}
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
foreach ( FtpListItem item in ftpClient . GetListing ( remotePath , FtpListOption . Modify | FtpListOption . Size | FtpListOption . DerefLinks ))
{
switch ( item . Type )
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
case FtpFileSystemObjectType . Directory :
2016-04-27 14:48:11 +02:00
{
if ( item . Name == "." || item . Name == ".." )
{
continue ;
}
list . Add ( new FileEntry ( item . Name , - 1 , new DateTime (), item . Modified )
{
IsFolder = true ,
});
break ;
}
2016-12-29 21:20:12 +01:00
case FtpFileSystemObjectType . File :
2016-04-27 14:48:11 +02:00
{
list . Add ( new FileEntry ( item . Name , item . Size , new DateTime (), item . Modified ));
break ;
}
2016-12-29 21:20:12 +01:00
case FtpFileSystemObjectType . Link :
2016-04-27 14:48:11 +02:00
{
if ( item . Name == "." || item . Name == ".." )
{
continue ;
}
if ( item . LinkObject != null )
{
switch ( item . LinkObject . Type )
{
case FtpFileSystemObjectType . Directory :
{
2016-12-29 21:20:12 +01:00
if ( item . Name == "." || item . Name == ".." )
{
continue ;
}
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
list . Add ( new FileEntry ( item . Name , - 1 , new DateTime (), item . Modified )
{
IsFolder = true ,
});
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
break ;
}
2016-04-27 14:48:11 +02:00
case FtpFileSystemObjectType . File :
2016-12-29 21:20:12 +01:00
{
list . Add ( new FileEntry ( item . Name , item . Size , new DateTime (), item . Modified ));
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
break ;
}
2016-04-27 14:48:11 +02:00
}
}
break ;
}
2016-12-29 21:20:12 +01:00
2016-04-27 14:48:11 +02:00
}
}
2016-09-15 11:39:27 +02:00
} // Message "Directory not found." string
2016-04-27 14:48:11 +02:00
catch ( FtpCommandException ex )
{
if ( ex . Message == "Directory not found." )
{
throw new FolderMissingException ( Strings . MissingFolderError ( remotePath , ex . Message ), ex );
}
throw ;
}
return list ;
}
public void Put ( string remotename , System . IO . Stream input )
{
string remotePath = remotename ;
long streamLen = - 1 ;
try
{
2016-12-29 21:20:12 +01:00
var ftpClient = CreateClient ();
try
{
streamLen = input . Length ;
}
// ReSharper disable once EmptyGeneralCatchClause
catch
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
}
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
// Get the remote path
remotePath = "" ;
if (! string . IsNullOrEmpty ( remotename ))
{
// Append the filename
remotePath += remotename ;
}
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
using ( var outputStream = ftpClient . OpenWrite ( remotePath ))
{
try
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
CoreUtility . CopyStream ( input , outputStream , true , _copybuffer );
2016-04-27 14:48:11 +02:00
}
2016-12-29 21:20:12 +01:00
finally
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
outputStream . Close ();
2016-04-27 14:48:11 +02:00
}
}
2016-12-29 21:20:12 +01:00
2016-04-27 14:48:11 +02:00
if ( _listVerify )
{
var fileEntries = List ( remotename , true );
foreach ( var fileEntry in fileEntries )
{
2017-11-26 10:53:14 -08:00
if ( fileEntry . Name . Equals ( remotename ) || fileEntry . Name . EndsWith ( "/" + remotename , StringComparison . Ordinal ) || fileEntry . Name . EndsWith ( "\\" + remotename , StringComparison . Ordinal ))
2016-04-27 14:48:11 +02:00
{
if ( fileEntry . Size < 0 || streamLen < 0 || fileEntry . Size == streamLen )
{
return ;
}
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( Strings . ListVerifySizeFailure ( remotename , fileEntry . Size , streamLen ), "AftpListVerifySizeFailure" );
2016-04-27 14:48:11 +02:00
}
}
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( Strings . ListVerifyFailure ( remotename , fileEntries . Select ( n => n . Name )), "AftpListVerifySizeFailure" );
2016-04-27 14:48:11 +02:00
}
}
catch ( FtpCommandException ex )
{
if ( ex . Message == "Directory not found." )
{
throw new FolderMissingException ( Strings . MissingFolderError ( remotePath , ex . Message ), ex );
}
throw ;
}
}
public void Put ( string remotename , string localname )
{
using ( System . IO . FileStream fs = System . IO . File . Open ( localname , System . IO . FileMode . Open , System . IO . FileAccess . Read , System . IO . FileShare . Read ))
{
Put ( remotename , fs );
}
}
public void Get ( string remotename , System . IO . Stream output )
{
2016-12-29 21:20:12 +01:00
var ftpClient = CreateClient ();
// Get the remote path
var remotePath = "" ;
if (! string . IsNullOrEmpty ( remotename ))
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
// Append the filename
remotePath += remotename ;
}
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
using ( var inputStream = ftpClient . OpenRead ( remotePath ))
{
try
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
CoreUtility . CopyStream ( inputStream , output , false , _copybuffer );
2016-04-27 14:48:11 +02:00
}
2016-12-29 21:20:12 +01:00
finally
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
inputStream . Close ();
2016-04-27 14:48:11 +02:00
}
}
2016-12-29 21:20:12 +01:00
2016-04-27 14:48:11 +02: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 );
}
}
public void Delete ( string remotename )
{
2016-12-29 21:20:12 +01:00
var ftpClient = CreateClient ();
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
// Get the remote path
var remotePath = "" ;
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
if (! string . IsNullOrEmpty ( remotename ))
{
// Append the filename
remotePath += remotename ;
2016-04-27 14:48:11 +02:00
}
2016-12-29 21:20:12 +01:00
ftpClient . DeleteFile ( remotePath );
2016-04-27 14:48:11 +02:00
}
/// <summary>
/// A localized description of the backend, for display in the usage information
/// </summary>
public string Description
{
get
{
return Strings . Description ;
}
}
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 ( _url ). Host }; }
2018-02-21 22:38:01 +01:00
}
2018-02-18 00:18:44 +01:00
2017-05-24 01:39:30 +07:00
private static System . IO . Stream StringToStream ( string str )
{
var stream = new System . IO . MemoryStream ();
var writer = new System . IO . StreamWriter ( stream ) { AutoFlush = true };
writer . Write ( str );
return stream ;
}
/// <summary>
/// Test FTP access permissions.
/// </summary>
2016-04-27 14:48:11 +02:00
public void Test ()
{
2017-05-24 01:39:30 +07:00
var list = List ();
// Delete test file if exists
if ( list . Any ( entry => entry . Name == TEST_FILE_NAME ))
{
try
{
Delete ( TEST_FILE_NAME );
}
catch ( Exception e )
{
throw new Exception ( string . Format ( Strings . ErrorDeleteFile , e . Message ), e );
}
}
// Test write permissions
using ( var testStream = StringToStream ( TEST_FILE_CONTENT ))
{
try
{
Put ( TEST_FILE_NAME , testStream );
}
catch ( Exception e )
{
throw new Exception ( string . Format ( Strings . ErrorWriteFile , e . Message ), e );
}
}
// Test read permissions
using ( var stream = new System . IO . MemoryStream ())
{
try
{
Get ( TEST_FILE_NAME , stream );
}
catch ( Exception e )
{
throw new Exception ( string . Format ( Strings . ErrorReadFile , e . Message ), e );
}
}
// Cleanup
try
{
Delete ( TEST_FILE_NAME );
}
catch ( Exception e )
{
throw new Exception ( string . Format ( Strings . ErrorDeleteFile , e . Message ), e );
}
2016-04-27 14:48:11 +02:00
}
public void CreateFolder ()
{
2016-12-29 21:20:12 +01:00
var client = CreateClient ();
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
var url = new Uri ( _url );
// Get the remote path
2017-11-26 10:53:14 -08:00
var remotePath = url . AbsolutePath . EndsWith ( "/" , StringComparison . Ordinal ) ? url . AbsolutePath . Substring ( 0 , url . AbsolutePath . Length - 1 ) : url . AbsolutePath ;
2016-12-29 21:20:12 +01:00
// Try to create the directory
client . CreateDirectory ( remotePath , true );
2016-04-27 14:48:11 +02:00
}
public void Dispose ()
{
2017-05-24 01:39:30 +07:00
if ( Client != null )
2016-12-29 21:20:12 +01:00
Client . Dispose ();
Client = null ;
2016-04-27 14:48:11 +02:00
_userInfo = null ;
}
private FtpClient CreateClient ()
{
2016-12-29 15:00:19 +01:00
if ( this . Client == null ) // Create connection if it doesn't exist yet
{
2016-04-27 14:48:11 +02:00
2016-12-29 15:00:19 +01:00
var url = _url ;
2016-04-27 14:48:11 +02:00
2016-12-29 15:00:19 +01:00
var uri = new Uri ( url );
var ftpClient = new FtpClient
{
Host = uri . Host ,
Port = uri . Port == - 1 ? 21 : uri . Port ,
Credentials = _userInfo ,
EncryptionMode = _encryptionMode ,
DataConnectionType = _dataConnectionType ,
SslProtocols = _sslProtocols ,
EnableThreadSafeDataConnections = true , // Required to work properly but can result in up to 3 connections being used even when you expect just one..
};
ftpClient . ValidateCertificate += HandleValidateCertificate ;
// Get the remote path
2017-11-26 10:53:14 -08:00
var remotePath = uri . AbsolutePath . EndsWith ( "/" , StringComparison . Ordinal ) ? uri . AbsolutePath . Substring ( 0 , uri . AbsolutePath . Length - 1 ) : uri . AbsolutePath ;
2016-12-29 15:00:19 +01:00
ftpClient . SetWorkingDirectory ( remotePath );
this . Client = ftpClient ;
} // else reuse existing connection
return this . Client ;
2016-04-27 14:48:11 +02:00
}
private void HandleValidateCertificate ( FtpClient control , FtpSslValidationEventArgs e )
{
if ( e . PolicyErrors == SslPolicyErrors . None || _accepAllCertificates )
{
e . Accept = true ;
return ;
}
try
{
var certHash = ( _validHashes != null && _validHashes . Length > 0 ) ? CoreUtility . ByteArrayAsHexString ( e . Certificate . GetCertHash ()) : null ;
if ( certHash != null )
{
2017-09-18 23:23:45 -06:00
if ( _validHashes . Any ( hash => ! string . IsNullOrEmpty ( hash ) && certHash . Equals ( hash , StringComparison . OrdinalIgnoreCase )))
2016-04-27 14:48:11 +02:00
{
e . Accept = true ;
}
}
}
catch
{
e . Accept = false ;
}
}
}
}