2011-03-07 20:15:29 +00:00
#region Disclaimer / License
2015-01-20 21:44:52 +01:00
// Copyright (C) 2015, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
2011-03-07 20:15:29 +00:00
//
// 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
2018-11-02 21:34:07 +01:00
using Duplicati.Library.Common.IO ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Interface ;
2011-11-18 23:47:27 +00:00
using Renci.SshNet ;
2014-11-14 14:41:36 +01:00
using Renci.SshNet.Common ;
2019-02-22 21:58:40 -06:00
using System ;
using System.Collections.Generic ;
2018-12-11 21:07:30 -08:00
using System.Globalization ;
2019-02-22 21:58:40 -06:00
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2011-03-07 20:15:29 +00:00
namespace Duplicati.Library.Backend
{
2016-06-11 22:11:33 +02:00
public class SSHv2 : IStreamingBackend , IRenameEnabledBackend , IBackend , IDisposable
2011-03-07 20:15:29 +00:00
{
public const string SSH_KEYFILE_OPTION = "ssh-keyfile" ;
2014-03-18 22:17:31 +01:00
public const string SSH_KEYFILE_INLINE = "ssh-key" ;
2016-06-24 15:03:08 +02:00
public const string SSH_FINGERPRINT_OPTION = "ssh-fingerprint" ;
2016-08-30 10:00:48 +02:00
public const string SSH_FINGERPRINT_ACCEPT_ANY_OPTION = "ssh-accept-any-fingerprints" ;
2014-03-12 01:43:17 +01:00
public const string KEYFILE_URI = "sshkey://" ;
2017-09-18 23:23:45 -06:00
public const string SSH_TIMEOUT_OPTION = "ssh-operation-timeout" ;
public const string SSH_KEEPALIVE_OPTION = "ssh-keepalive" ;
2018-05-23 21:18:01 -07:00
readonly Dictionary < string , string > m_options ;
2011-03-07 20:15:29 +00:00
2018-05-23 21:18:01 -07:00
private readonly string m_server ;
private readonly string m_path ;
private readonly string m_username ;
private readonly string m_password ;
private readonly string m_fingerprint ;
private readonly bool m_fingerprintallowall ;
private readonly TimeSpan m_operationtimeout ;
private readonly TimeSpan m_keepaliveinterval ;
2017-09-18 23:23:45 -06:00
2018-05-23 21:18:01 -07:00
private readonly int m_port = 22 ;
2016-06-11 22:11:33 +02:00
private SftpClient m_con ;
2011-03-07 20:15:29 +00:00
public SSHv2 ()
{
}
public SSHv2 ( string url , Dictionary < string , string > options )
: this ()
{
m_options = options ;
2013-05-05 23:46:00 +02:00
var uri = new Utility . Uri ( url );
uri . RequireHost ();
2016-06-11 22:11:33 +02:00
2013-05-05 23:46:00 +02:00
if ( options . ContainsKey ( "auth-username" ))
m_username = options [ "auth-username" ];
if ( options . ContainsKey ( "auth-password" ))
m_password = options [ "auth-password" ];
2016-06-24 15:03:08 +02:00
if ( options . ContainsKey ( SSH_FINGERPRINT_OPTION ))
m_fingerprint = options [ SSH_FINGERPRINT_OPTION ];
if (! string . IsNullOrEmpty ( uri . Username ))
2013-05-05 23:46:00 +02:00
m_username = uri . Username ;
2016-06-24 15:03:08 +02:00
if (! string . IsNullOrEmpty ( uri . Password ))
2013-05-05 23:46:00 +02:00
m_password = uri . Password ;
2016-06-27 19:57:36 +02:00
m_fingerprintallowall = Utility . Utility . ParseBoolOption ( options , SSH_FINGERPRINT_ACCEPT_ANY_OPTION );
2013-05-05 23:46:00 +02:00
m_path = uri . Path ;
2011-03-07 20:15:29 +00:00
2018-09-21 16:26:30 -07:00
if (! string . IsNullOrWhiteSpace ( m_path ))
{
2018-10-27 12:17:07 +02:00
m_path = Util . AppendDirSeparator ( m_path , "/" );
2018-09-21 16:26:30 -07:00
}
2011-03-07 20:15:29 +00:00
2017-11-26 10:53:14 -08:00
if (! m_path . StartsWith ( "/" , StringComparison . Ordinal ))
2016-06-11 22:11:33 +02:00
m_path = "/" + m_path ;
2013-05-05 23:46:00 +02:00
m_server = uri . Host ;
2011-03-07 20:15:29 +00:00
2013-05-06 20:36:26 +02:00
if ( uri . Port > 0 )
2013-05-05 23:46:00 +02:00
m_port = uri . Port ;
2017-08-06 17:48:09 +02:00
string timeoutstr ;
options . TryGetValue ( SSH_TIMEOUT_OPTION , out timeoutstr );
if (! string . IsNullOrWhiteSpace ( timeoutstr ))
2017-09-18 23:23:45 -06:00
m_operationtimeout = Library . Utility . Timeparser . ParseTimeSpan ( timeoutstr );
options . TryGetValue ( SSH_KEEPALIVE_OPTION , out timeoutstr );
if (! string . IsNullOrWhiteSpace ( timeoutstr ))
m_keepaliveinterval = Library . Utility . Timeparser . ParseTimeSpan ( timeoutstr );
}
2011-03-07 20:15:29 +00:00
2013-05-05 17:07:24 +02:00
#region IBackend Members
2016-06-11 22:11:33 +02:00
2011-03-07 20:15:29 +00:00
public void Test ()
{
2017-09-25 23:17:45 -06:00
this . TestList ();
2011-03-07 20:15:29 +00:00
}
public void CreateFolder ()
{
2016-06-11 22:11:33 +02:00
CreateConnection ();
2018-11-21 20:25:41 -08:00
// Since the SftpClient.CreateDirectory method does not create all the parent directories
// as needed, this has to be done manually.
string partialPath = String . Empty ;
foreach ( string part in m_path . Split ( '/' ). Where ( x => ! String . IsNullOrEmpty ( x )))
{
partialPath += $"/{part}" ;
2018-11-21 21:03:09 -08:00
if ( this . m_con . Exists ( partialPath ))
2018-11-21 20:25:41 -08:00
{
2018-11-21 21:21:38 -08:00
if (! this . m_con . GetAttributes ( partialPath ). IsDirectory )
2018-11-21 20:25:41 -08:00
{
throw new ArgumentException ( $"The path {partialPath} already exists and is not a directory." );
}
}
else
{
2018-11-21 21:03:09 -08:00
this . m_con . CreateDirectory ( partialPath );
2018-11-21 20:25:41 -08:00
}
}
2011-03-07 20:15:29 +00:00
}
public string DisplayName
{
get { return Strings . SSHv2Backend . DisplayName ; }
}
public string ProtocolKey
{
get { return "ssh" ; }
}
2019-03-17 18:20:14 -05:00
public Task PutAsync ( string remotename , string filename , CancellationToken cancelToken )
2011-03-07 20:15:29 +00:00
{
using ( System . IO . FileStream fs = System . IO . File . Open ( filename , System . IO . FileMode . Open , System . IO . FileAccess . Read , System . IO . FileShare . Read ))
2019-03-17 18:20:14 -05:00
return PutAsync ( remotename , fs , cancelToken );
2011-03-07 20:15:29 +00:00
}
public void Get ( string remotename , string filename )
{
using ( System . IO . FileStream fs = System . IO . File . Open ( filename , System . IO . FileMode . Create , System . IO . FileAccess . Write , System . IO . FileShare . None ))
Get ( remotename , fs );
}
public void Delete ( string remotename )
{
2014-11-14 14:41:36 +01:00
try
{
2016-06-11 22:11:33 +02:00
CreateConnection ();
ChangeDirectory ( m_path );
m_con . DeleteFile ( remotename );
2014-11-14 14:41:36 +01:00
}
catch ( SftpPathNotFoundException ex )
{
2014-11-17 12:01:11 +01:00
throw new FileMissingException ( ex );
2014-11-14 14:41:36 +01:00
}
2016-06-11 22:11:33 +02:00
2011-03-07 20:15:29 +00:00
}
public IList < ICommandLineArgument > SupportedCommands
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
2013-05-05 16:40:37 +02:00
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . SSHv2Backend . DescriptionAuthPasswordShort , Strings . SSHv2Backend . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . SSHv2Backend . DescriptionAuthUsernameShort , Strings . SSHv2Backend . DescriptionAuthUsernameLong ),
2016-06-24 15:03:08 +02:00
new CommandLineArgument ( SSH_FINGERPRINT_OPTION , CommandLineArgument . ArgumentType . String , Strings . SSHv2Backend . DescriptionFingerprintShort , Strings . SSHv2Backend . DescriptionFingerprintLong ),
2016-10-13 23:21:01 +02:00
new CommandLineArgument ( SSH_FINGERPRINT_ACCEPT_ANY_OPTION , CommandLineArgument . ArgumentType . Boolean , Strings . SSHv2Backend . DescriptionAnyFingerprintShort , Strings . SSHv2Backend . DescriptionAnyFingerprintLong ),
2011-03-07 20:15:29 +00:00
new CommandLineArgument ( SSH_KEYFILE_OPTION , CommandLineArgument . ArgumentType . Path , Strings . SSHv2Backend . DescriptionSshkeyfileShort , Strings . SSHv2Backend . DescriptionSshkeyfileLong ),
2017-09-18 23:23:45 -06:00
new CommandLineArgument ( SSH_KEYFILE_INLINE , CommandLineArgument . ArgumentType . Password , Strings . SSHv2Backend . DescriptionSshkeyShort , Strings . SSHv2Backend . DescriptionSshkeyLong ( KEYFILE_URI )),
new CommandLineArgument ( SSH_TIMEOUT_OPTION , CommandLineArgument . ArgumentType . Timespan , Strings . SSHv2Backend . DescriptionSshtimeoutShort , Strings . SSHv2Backend . DescriptionSshtimeoutLong , "0" ),
2017-08-06 17:48:09 +02:00
new CommandLineArgument ( SSH_KEEPALIVE_OPTION , CommandLineArgument . ArgumentType . Timespan , Strings . SSHv2Backend . DescriptionSshkeepaliveShort , Strings . SSHv2Backend . DescriptionSshkeepaliveLong , "0" ),
2017-09-18 23:23:45 -06:00
});
2011-03-07 20:15:29 +00:00
}
}
public string Description
{
get { return Strings . SSHv2Backend . Description ; }
}
#endregion
#region IDisposable Members
public void Dispose ()
{
2016-06-11 22:11:33 +02:00
if ( m_con != null )
{
try
{
m_con . Dispose ();
}
catch ( System . Net . Sockets . SocketException )
{
//If the operating system sometimes close socket before disposal of connection following exception is thrown
//System.Net.Sockets.SocketException (0x80004005): An existing connection was forcibly closed by the remote host
}
finally
{
m_con = null ;
}
}
2011-03-07 20:15:29 +00:00
}
#endregion
#region IStreamingBackend Implementation
2019-03-17 18:20:14 -05:00
public Task PutAsync ( string remotename , System . IO . Stream stream , CancellationToken cancelToken )
2011-03-07 20:15:29 +00:00
{
2016-06-11 22:11:33 +02:00
CreateConnection ();
ChangeDirectory ( m_path );
m_con . UploadFile ( stream , remotename );
2019-02-22 21:58:40 -06:00
return Task . FromResult ( true );
2011-03-07 20:15:29 +00:00
}
public void Get ( string remotename , System . IO . Stream stream )
{
2016-06-11 22:11:33 +02:00
CreateConnection ();
ChangeDirectory ( m_path );
m_con . DownloadFile ( remotename , stream );
2011-03-07 20:15:29 +00:00
}
#endregion
2014-08-21 20:59:21 +02:00
#region IRenameEnabledBackend Implementation
public void Rename ( string source , string target )
{
2016-06-11 22:11:33 +02:00
CreateConnection ();
ChangeDirectory ( m_path );
m_con . RenameFile ( source , target );
2014-08-21 20:59:21 +02:00
}
#endregion
2011-03-07 20:15:29 +00:00
#region Implementation
2016-06-11 22:11:33 +02:00
private void CreateConnection ()
2011-03-07 20:15:29 +00:00
{
2016-06-11 22:11:33 +02:00
if ( m_con != null && m_con . IsConnected )
return ;
if ( m_con != null && ! m_con . IsConnected )
{
m_con . Connect ();
return ;
}
2011-03-07 20:15:29 +00:00
SftpClient con ;
string keyfile ;
m_options . TryGetValue ( SSH_KEYFILE_OPTION , out keyfile );
2014-03-18 22:17:31 +01:00
if ( string . IsNullOrWhiteSpace ( keyfile ))
m_options . TryGetValue ( SSH_KEYFILE_INLINE , out keyfile );
2011-03-07 20:15:29 +00:00
2014-03-18 22:17:31 +01:00
if (! string . IsNullOrWhiteSpace ( keyfile ))
con = new SftpClient ( m_server , m_port , m_username , ValidateKeyFile ( keyfile , m_password ));
2011-03-07 20:15:29 +00:00
else
2017-09-23 00:15:53 +02:00
con = new SftpClient ( m_server , m_port , m_username , m_password ?? string . Empty );
2011-03-07 20:15:29 +00:00
2016-06-24 15:03:08 +02:00
con . HostKeyReceived += delegate ( object sender , HostKeyEventArgs e )
{
e . CanTrust = false ;
2016-06-27 19:57:36 +02:00
if ( m_fingerprintallowall )
2016-06-24 15:03:08 +02:00
{
e . CanTrust = true ;
return ;
}
string hostFingerprint = e . HostKeyName + " " + e . KeyLength . ToString () + " " + BitConverter . ToString ( e . FingerPrint ). Replace ( '-' , ':' );
2016-06-27 19:57:36 +02:00
if ( string . IsNullOrEmpty ( m_fingerprint ))
2018-12-11 21:07:30 -08:00
throw new Library . Utility . HostKeyException ( Strings . SSHv2Backend . FingerprintNotSpecifiedManagedError ( hostFingerprint . ToLower ( CultureInfo . InvariantCulture ), SSH_FINGERPRINT_OPTION , SSH_FINGERPRINT_ACCEPT_ANY_OPTION ), hostFingerprint , m_fingerprint );
2016-06-27 19:57:36 +02:00
2018-09-19 14:29:40 -07:00
if (! String . Equals ( hostFingerprint , m_fingerprint , StringComparison . OrdinalIgnoreCase ))
2018-12-11 21:07:30 -08:00
throw new Library . Utility . HostKeyException ( Strings . SSHv2Backend . FingerprintNotMatchManagedError ( hostFingerprint . ToLower ( CultureInfo . InvariantCulture )), hostFingerprint , m_fingerprint );
2016-06-24 15:03:08 +02:00
else
e . CanTrust = true ;
};
2017-08-06 17:48:09 +02:00
if ( m_operationtimeout . Ticks != 0 )
2017-09-18 23:23:45 -06:00
con . OperationTimeout = m_operationtimeout ;
if ( m_keepaliveinterval . Ticks != 0 )
con . KeepAliveInterval = m_keepaliveinterval ;
2017-08-06 17:48:09 +02:00
2011-03-07 20:15:29 +00:00
con . Connect ();
2016-06-11 22:11:33 +02:00
m_con = con ;
}
private void ChangeDirectory ( string path )
{
if ( string . IsNullOrEmpty ( path ))
return ;
2018-10-27 12:17:07 +02:00
string working_dir = Util . AppendDirSeparator ( m_con . WorkingDirectory , "/" );
2016-06-11 22:11:33 +02:00
if ( working_dir == path )
return ;
2011-03-07 20:15:29 +00:00
try
{
2016-06-11 22:11:33 +02:00
m_con . ChangeDirectory ( path );
2011-03-07 20:15:29 +00:00
}
catch ( Exception ex )
{
2016-06-11 22:11:33 +02:00
throw new Interface . FolderMissingException ( Strings . SSHv2Backend . FolderNotFoundManagedError ( path , ex . Message ), ex );
2011-03-07 20:15:29 +00:00
}
}
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List ()
2011-03-07 20:15:29 +00:00
{
2011-12-16 19:40:50 +00:00
string path = "." ;
2011-03-07 20:15:29 +00:00
2016-06-11 22:11:33 +02:00
CreateConnection ();
ChangeDirectory ( m_path );
foreach ( Renci . SshNet . Sftp . SftpFile ls in m_con . ListDirectory ( path ))
2011-12-16 19:40:50 +00:00
if ( ls . Name . ToString () != "." && ls . Name . ToString () != ".." )
2017-11-26 15:24:53 -08:00
yield return new FileEntry ( ls . Name , ls . Length , ls . LastAccessTime , ls . LastWriteTime ) { IsFolder = ls . Attributes . IsDirectory };
2011-03-07 20:15:29 +00:00
}
2011-11-18 23:47:27 +00:00
public static Renci . SshNet . PrivateKeyFile ValidateKeyFile ( string filename , string password )
2011-03-07 20:15:29 +00:00
{
2017-11-19 17:28:51 +01:00
try
2014-03-12 01:43:17 +01:00
{
2017-11-19 17:28:51 +01:00
if ( filename . StartsWith ( KEYFILE_URI , StringComparison . OrdinalIgnoreCase ))
{
using ( var ms = new System . IO . MemoryStream ())
using ( var sr = new System . IO . StreamWriter ( ms ))
{
sr . Write ( Duplicati . Library . Utility . Uri . UrlDecode ( filename . Substring ( KEYFILE_URI . Length )));
sr . Flush ();
ms . Position = 0 ;
if ( String . IsNullOrEmpty ( password ))
return new Renci . SshNet . PrivateKeyFile ( ms );
else
return new Renci . SshNet . PrivateKeyFile ( ms , password );
}
}
else
2014-03-12 01:43:17 +01:00
{
if ( String . IsNullOrEmpty ( password ))
2017-11-19 17:28:51 +01:00
return new Renci . SshNet . PrivateKeyFile ( filename );
2014-03-12 01:43:17 +01:00
else
2017-11-19 17:28:51 +01:00
return new Renci . SshNet . PrivateKeyFile ( filename , password );
2014-03-12 01:43:17 +01:00
}
}
2017-11-19 17:28:51 +01:00
catch ( Exception ex )
2014-03-12 01:43:17 +01:00
{
2018-03-12 14:07:11 +01:00
throw new UserInformationException ( string . Format ( "Failed to parse the keyfile, check the key format and passphrase. Error message was {0}" , ex . Message ), "SSHFailedToParseKeyFile" , ex );
2014-03-12 01:43:17 +01:00
}
2011-03-07 20:15:29 +00:00
}
2016-06-11 22:11:33 +02:00
2011-03-07 20:15:29 +00:00
#endregion
2016-06-11 22:11:33 +02:00
2014-03-12 16:45:54 +01:00
internal SftpClient Client
{
get
{
2016-06-11 22:11:33 +02:00
return m_con ;
2014-03-12 16:45:54 +01:00
}
}
2018-02-18 00:18:44 +01: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 [] { m_server }; }
2018-02-21 22:38:01 +01:00
}
2011-03-07 20:15:29 +00:00
}
2016-10-13 23:21:01 +02:00
}