2016-03-01 23:29:42 -05:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Net ;
using System.Net.FtpClient ;
2016-11-14 06:31:16 -05:00
using System.Reflection ;
using System.Threading ;
2016-03-01 23:29:42 -05:00
using Android.Content ;
using Android.OS ;
using Android.Preferences ;
using KeePassLib ;
using KeePassLib.Serialization ;
using KeePassLib.Utility ;
namespace keepass2android.Io
{
public class NetFtpFileStorage : IFileStorage
{
2016-11-14 06:31:16 -05:00
class RetryConnectFtpClient : FtpClient
{
protected override FtpClient CloneConnection ( )
{
RetryConnectFtpClient conn = new RetryConnectFtpClient ( ) ;
conn . m_isClone = true ;
foreach ( PropertyInfo prop in GetType ( ) . GetProperties ( ) )
{
object [ ] attributes = prop . GetCustomAttributes ( typeof ( FtpControlConnectionClone ) , true ) ;
if ( attributes ! = null & & attributes . Length > 0 )
{
prop . SetValue ( conn , prop . GetValue ( this , null ) , null ) ;
}
}
// always accept certficate no matter what because if code execution ever
// gets here it means the certificate on the control connection object being
// cloned was already accepted.
conn . ValidateCertificate + = new FtpSslValidation (
delegate ( FtpClient obj , FtpSslValidationEventArgs e )
{
e . Accept = true ;
} ) ;
return conn ;
}
private static T DoInRetryLoop < T > ( Func < T > func )
{
double timeout = 30.0 ;
double timePerRequest = 1.0 ;
var startTime = DateTime . Now ;
while ( true )
{
var attemptStartTime = DateTime . Now ;
try
{
return func ( ) ;
}
catch ( System . Net . Sockets . SocketException e )
{
if ( ( e . ErrorCode ! = 10061 ) | | ( DateTime . Now > startTime . AddSeconds ( timeout ) ) )
{
throw ;
}
double secondsSinceAttemptStart = ( DateTime . Now - attemptStartTime ) . TotalSeconds ;
if ( secondsSinceAttemptStart < timePerRequest )
{
Thread . Sleep ( TimeSpan . FromSeconds ( timePerRequest - secondsSinceAttemptStart ) ) ;
}
}
}
}
public override void Connect ( )
{
DoInRetryLoop ( ( ) = >
{
base . Connect ( ) ;
return true ;
}
) ;
}
}
public struct ConnectionSettings
2016-03-01 23:29:42 -05:00
{
public FtpEncryptionMode EncryptionMode { get ; set ; }
2016-11-17 21:24:15 -05:00
public string Username
{
get ; set ;
}
public string Password
{
get ;
set ;
}
2016-03-01 23:29:42 -05:00
public static ConnectionSettings FromIoc ( IOConnectionInfo ioc )
{
2016-11-28 14:41:39 -05:00
if ( ! string . IsNullOrEmpty ( ioc . UserName ) )
{
//legacy support
return new ConnectionSettings ( )
{
EncryptionMode = FtpEncryptionMode . None ,
Username = ioc . UserName ,
Password = ioc . Password
} ;
}
2016-03-01 23:29:42 -05:00
string path = ioc . Path ;
int schemeLength = path . IndexOf ( "://" , StringComparison . Ordinal ) ;
path = path . Substring ( schemeLength + 3 ) ;
2016-11-17 21:24:15 -05:00
string settings = path . Substring ( 0 , path . IndexOf ( SettingsPostFix , StringComparison . Ordinal ) ) ;
if ( ! settings . StartsWith ( SettingsPrefix ) )
throw new Exception ( "unexpected settings in path" ) ;
settings = settings . Substring ( SettingsPrefix . Length ) ;
var tokens = settings . Split ( Separator ) ;
2016-03-01 23:29:42 -05:00
return new ConnectionSettings ( )
{
2016-11-17 21:24:15 -05:00
EncryptionMode = ( FtpEncryptionMode ) int . Parse ( tokens [ 2 ] ) ,
2017-01-31 05:11:17 -05:00
Username = WebUtility . UrlDecode ( tokens [ 0 ] ) ,
Password = WebUtility . UrlDecode ( tokens [ 1 ] )
2016-03-01 23:29:42 -05:00
} ;
}
2016-11-14 06:31:16 -05:00
2016-11-17 21:24:15 -05:00
public const string SettingsPrefix = "SET" ;
2016-11-22 06:39:23 -05:00
public const string SettingsPostFix = "#" ;
2016-11-17 21:24:15 -05:00
public const char Separator = ':' ;
public override string ToString ( )
2016-11-14 06:31:16 -05:00
{
2016-11-17 21:24:15 -05:00
return SettingsPrefix +
System . Net . WebUtility . UrlEncode ( Username ) + Separator +
WebUtility . UrlEncode ( Password ) + Separator +
( int ) EncryptionMode ;
;
2016-11-14 06:31:16 -05:00
}
2016-03-01 23:29:42 -05:00
}
2016-11-14 23:55:11 -05:00
private readonly ICertificateValidationHandler _app ;
2016-03-01 23:29:42 -05:00
2016-11-14 06:31:16 -05:00
public MemoryStream traceStream ;
2016-11-14 23:55:11 -05:00
public NetFtpFileStorage ( Context context , ICertificateValidationHandler app )
2016-03-01 23:29:42 -05:00
{
2016-11-14 23:55:11 -05:00
_app = app ;
2016-11-14 06:31:16 -05:00
traceStream = new MemoryStream ( ) ;
FtpTrace . AddListener ( new System . Diagnostics . TextWriterTraceListener ( traceStream ) ) ;
2016-03-01 23:29:42 -05:00
}
public IEnumerable < string > SupportedProtocols
{
2016-11-17 21:24:15 -05:00
get
{
yield return "ftp" ;
}
2016-03-01 23:29:42 -05:00
}
public void Delete ( IOConnectionInfo ioc )
{
2016-11-14 06:31:16 -05:00
try
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
using ( FtpClient client = GetClient ( ioc ) )
{
2016-11-28 14:41:39 -05:00
string localPath = IocToUri ( ioc ) . PathAndQuery ;
2016-11-14 06:31:16 -05:00
if ( client . DirectoryExists ( localPath ) )
client . DeleteDirectory ( localPath , true ) ;
else
client . DeleteFile ( localPath ) ;
}
2016-03-01 23:29:42 -05:00
}
2016-11-14 06:31:16 -05:00
catch ( FtpCommandException ex )
{
throw ConvertException ( ex ) ;
}
}
public static Exception ConvertException ( Exception exception )
{
if ( exception is FtpCommandException )
{
var ftpEx = ( FtpCommandException ) exception ;
if ( ftpEx . CompletionCode = = "550" )
throw new FileNotFoundException ( exception . Message , exception ) ;
}
return exception ;
2016-03-01 23:29:42 -05:00
}
internal FtpClient GetClient ( IOConnectionInfo ioc , bool enableCloneClient = true )
{
2016-11-17 21:24:15 -05:00
var settings = ConnectionSettings . FromIoc ( ioc ) ;
2016-11-14 06:31:16 -05:00
FtpClient client = new RetryConnectFtpClient ( ) ;
2016-11-17 21:24:15 -05:00
if ( ( settings . Username . Length > 0 ) | | ( settings . Password . Length > 0 ) )
client . Credentials = new NetworkCredential ( settings . Username , settings . Password ) ;
2016-03-01 23:29:42 -05:00
else
client . Credentials = new NetworkCredential ( "anonymous" , "" ) ; //TODO TEST
2016-11-28 14:41:39 -05:00
Uri uri = IocToUri ( ioc ) ;
2016-03-01 23:29:42 -05:00
client . Host = uri . Host ;
if ( ! uri . IsDefaultPort ) //TODO test
client . Port = uri . Port ;
2016-11-14 23:55:11 -05:00
client . ValidateCertificate + = ( control , args ) = >
{
args . Accept = _app . CertificateValidationCallback ( control , args . Certificate , args . Chain , args . PolicyErrors ) ;
} ;
2016-03-01 23:29:42 -05:00
2016-11-17 21:24:15 -05:00
client . EncryptionMode = settings . EncryptionMode ;
2016-11-14 06:31:16 -05:00
2016-11-14 23:55:11 -05:00
client . Connect ( ) ;
return client ;
2016-11-14 06:31:16 -05:00
2016-03-01 23:29:42 -05:00
}
2016-11-14 06:31:16 -05:00
2016-11-28 14:41:39 -05:00
internal Uri IocToUri ( IOConnectionInfo ioc )
2016-03-01 23:29:42 -05:00
{
2016-11-28 14:41:39 -05:00
if ( ! string . IsNullOrEmpty ( ioc . UserName ) )
{
//legacy support.
return new Uri ( ioc . Path ) ;
}
string path = ioc . Path ;
2016-11-14 06:31:16 -05:00
//remove additional stuff like TLS param
2016-03-01 23:29:42 -05:00
int schemeLength = path . IndexOf ( "://" , StringComparison . Ordinal ) ;
string scheme = path . Substring ( 0 , schemeLength ) ;
path = path . Substring ( schemeLength + 3 ) ;
2016-11-28 14:41:39 -05:00
if ( path . StartsWith ( ConnectionSettings . SettingsPrefix ) )
{
//this should always be the case. However, in rare cases we might get an ioc with legacy path but no username set (if they only want to get a display name)
string settings = path . Substring ( 0 , path . IndexOf ( ConnectionSettings . SettingsPostFix , StringComparison . Ordinal ) ) ;
path = path . Substring ( settings . Length + 1 ) ;
}
2016-03-01 23:29:42 -05:00
return new Uri ( scheme + "://" + path ) ;
}
private string IocPathFromUri ( IOConnectionInfo baseIoc , Uri uri )
{
string basePath = baseIoc . Path ;
int schemeLength = basePath . IndexOf ( "://" , StringComparison . Ordinal ) ;
string scheme = basePath . Substring ( 0 , schemeLength ) ;
basePath = basePath . Substring ( schemeLength + 3 ) ;
2016-11-17 21:24:15 -05:00
string baseSettings = basePath . Substring ( 0 , basePath . IndexOf ( ConnectionSettings . SettingsPostFix , StringComparison . Ordinal ) ) ;
2016-11-14 06:31:16 -05:00
basePath = basePath . Substring ( baseSettings . Length + 1 ) ;
string baseHost = basePath . Substring ( 0 , basePath . IndexOf ( "/" , StringComparison . Ordinal ) ) ;
2016-11-17 21:24:15 -05:00
return scheme + "://" + baseSettings + ConnectionSettings . SettingsPostFix + baseHost + uri . AbsolutePath ; //TODO does this contain Query?
2016-03-01 23:29:42 -05:00
}
public bool CheckForFileChangeFast ( IOConnectionInfo ioc , string previousFileVersion )
{
return false ;
}
public string GetCurrentFileVersionFast ( IOConnectionInfo ioc )
{
return null ;
}
public Stream OpenFileForRead ( IOConnectionInfo ioc )
{
2016-11-14 06:31:16 -05:00
try
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
using ( var cl = GetClient ( ioc ) )
{
2016-11-28 14:41:39 -05:00
return cl . OpenRead ( IocToUri ( ioc ) . PathAndQuery , FtpDataType . Binary , 0 ) ;
2016-11-14 06:31:16 -05:00
}
}
catch ( FtpCommandException ex )
{
throw ConvertException ( ex ) ;
2016-03-01 23:29:42 -05:00
}
}
public IWriteTransaction OpenWriteTransaction ( IOConnectionInfo ioc , bool useFileTransaction )
{
2016-11-14 06:31:16 -05:00
try
{
if ( ! useFileTransaction )
return new UntransactedWrite ( ioc , this ) ;
else
return new TransactedWrite ( ioc , this ) ;
}
catch ( FtpCommandException ex )
{
throw ConvertException ( ex ) ;
}
2016-03-01 23:29:42 -05:00
}
public string GetFilenameWithoutPathAndExt ( IOConnectionInfo ioc )
{
return UrlUtil . StripExtension (
UrlUtil . GetFileName ( ioc . Path ) ) ;
}
public bool RequiresCredentials ( IOConnectionInfo ioc )
{
2016-11-17 21:24:15 -05:00
return false ;
2016-03-01 23:29:42 -05:00
}
public void CreateDirectory ( IOConnectionInfo ioc , string newDirName )
{
2016-11-14 06:31:16 -05:00
try
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
using ( var client = GetClient ( ioc ) )
{
2016-11-28 14:41:39 -05:00
client . CreateDirectory ( IocToUri ( GetFilePath ( ioc , newDirName ) ) . PathAndQuery ) ;
2016-11-14 06:31:16 -05:00
}
}
catch ( FtpCommandException ex )
{
throw ConvertException ( ex ) ;
2016-03-01 23:29:42 -05:00
}
}
public IEnumerable < FileDescription > ListContents ( IOConnectionInfo ioc )
{
2016-11-14 06:31:16 -05:00
try
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
using ( var client = GetClient ( ioc ) )
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
List < FileDescription > files = new List < FileDescription > ( ) ;
2016-11-28 14:41:39 -05:00
foreach ( FtpListItem item in client . GetListing ( IocToUri ( ioc ) . PathAndQuery ,
2016-11-14 06:31:16 -05:00
FtpListOption . Modify | FtpListOption . Size | FtpListOption . DerefLinks ) )
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
switch ( item . Type )
{
case FtpFileSystemObjectType . Directory :
files . Add ( new FileDescription ( )
{
CanRead = true ,
CanWrite = true ,
DisplayName = item . Name ,
IsDirectory = true ,
LastModified = item . Modified ,
Path = IocPathFromUri ( ioc , new Uri ( item . FullName ) )
} ) ;
break ;
case FtpFileSystemObjectType . File :
files . Add ( new FileDescription ( )
{
CanRead = true ,
CanWrite = true ,
DisplayName = item . Name ,
IsDirectory = false ,
LastModified = item . Modified ,
Path = IocPathFromUri ( ioc , new Uri ( item . FullName ) ) ,
SizeInBytes = item . Size
} ) ;
break ;
}
2016-03-01 23:29:42 -05:00
}
2016-11-14 06:31:16 -05:00
return files ;
2016-03-01 23:29:42 -05:00
}
2016-11-14 06:31:16 -05:00
}
catch ( FtpCommandException ex )
{
throw ConvertException ( ex ) ;
2016-03-01 23:29:42 -05:00
}
}
public FileDescription GetFileDescription ( IOConnectionInfo ioc )
{
2016-11-14 06:31:16 -05:00
try
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
//TODO when is this called?
//is it very inefficient to connect for each description?
using ( FtpClient client = GetClient ( ioc ) )
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
2016-11-28 14:41:39 -05:00
var uri = IocToUri ( ioc ) ;
2016-11-14 06:31:16 -05:00
string path = uri . PathAndQuery ;
2016-11-17 21:24:15 -05:00
if ( ! client . FileExists ( path ) & & ( ! client . DirectoryExists ( path ) ) )
throw new FileNotFoundException ( ) ;
var fileDesc = new FileDescription ( )
2016-11-14 06:31:16 -05:00
{
CanRead = true ,
CanWrite = true ,
Path = ioc . Path ,
LastModified = client . GetModifiedTime ( path ) ,
SizeInBytes = client . GetFileSize ( path ) ,
2016-11-17 21:24:15 -05:00
DisplayName = UrlUtil . GetFileName ( path )
2016-11-14 06:31:16 -05:00
} ;
2016-11-17 21:24:15 -05:00
fileDesc . IsDirectory = fileDesc . Path . EndsWith ( "/" ) ;
return fileDesc ;
2016-11-14 06:31:16 -05:00
}
}
catch ( FtpCommandException ex )
{
throw ConvertException ( ex ) ;
2016-03-01 23:29:42 -05:00
}
}
public bool RequiresSetup ( IOConnectionInfo ioConnection )
{
return false ;
}
public string IocToPath ( IOConnectionInfo ioc )
{
return ioc . Path ;
}
public void StartSelectFile ( IFileStorageSetupInitiatorActivity activity , bool isForSave , int requestCode , string protocolId )
{
2016-11-14 23:55:11 -05:00
activity . PerformManualFileSelect ( isForSave , requestCode , "ftp" ) ;
2016-03-01 23:29:42 -05:00
}
public void PrepareFileUsage ( IFileStorageSetupInitiatorActivity activity , IOConnectionInfo ioc , int requestCode ,
bool alwaysReturnSuccess )
{
Intent intent = new Intent ( ) ;
activity . IocToIntent ( intent , ioc ) ;
activity . OnImmediateResult ( requestCode , ( int ) FileStorageResults . FileUsagePrepared , intent ) ;
}
public void PrepareFileUsage ( Context ctx , IOConnectionInfo ioc )
{
}
public void OnCreate ( IFileStorageSetupActivity activity , Bundle savedInstanceState )
{
}
public void OnResume ( IFileStorageSetupActivity activity )
{
}
public void OnStart ( IFileStorageSetupActivity activity )
{
}
public void OnActivityResult ( IFileStorageSetupActivity activity , int requestCode , int resultCode , Intent data )
{
}
public string GetDisplayName ( IOConnectionInfo ioc )
{
2016-11-28 14:41:39 -05:00
var uri = IocToUri ( ioc ) ;
2016-03-01 23:29:42 -05:00
return uri . ToString ( ) ; //TODO is this good?
}
public string CreateFilePath ( string parent , string newFilename )
{
if ( ! parent . EndsWith ( "/" ) )
parent + = "/" ;
return parent + newFilename ;
}
public IOConnectionInfo GetParentPath ( IOConnectionInfo ioc )
{
return IoUtil . GetParentPath ( ioc ) ;
}
public IOConnectionInfo GetFilePath ( IOConnectionInfo folderPath , string filename )
{
IOConnectionInfo res = folderPath . CloneDeep ( ) ;
if ( ! res . Path . EndsWith ( "/" ) )
res . Path + = "/" ;
res . Path + = filename ;
return res ;
}
public bool IsPermanentLocation ( IOConnectionInfo ioc )
{
return true ;
}
public bool IsReadOnly ( IOConnectionInfo ioc , OptionalOut < UiStringKey > reason = null )
{
return false ;
}
2016-11-14 06:31:16 -05:00
public Stream OpenWrite ( IOConnectionInfo ioc )
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
try
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
using ( var client = GetClient ( ioc ) )
{
2016-11-28 14:41:39 -05:00
return client . OpenWrite ( IocToUri ( ioc ) . PathAndQuery ) ;
2016-03-01 23:29:42 -05:00
2016-11-14 06:31:16 -05:00
}
}
catch ( FtpCommandException ex )
2016-03-01 23:29:42 -05:00
{
2016-11-14 06:31:16 -05:00
throw ConvertException ( ex ) ;
2016-03-01 23:29:42 -05:00
}
}
2016-11-17 21:24:15 -05:00
public static int GetDefaultPort ( FtpEncryptionMode encryption )
{
return new FtpClient ( ) { EncryptionMode = encryption } . Port ;
}
public string BuildFullPath ( string host , int port , string initialPath , string user , string password , FtpEncryptionMode encryption )
{
var connectionSettings = new ConnectionSettings ( )
{
EncryptionMode = encryption ,
Username = user ,
Password = password
} ;
string scheme = "ftp" ;
string fullPath = scheme + "://" + connectionSettings . ToString ( ) + ConnectionSettings . SettingsPostFix + host ;
if ( port ! = GetDefaultPort ( encryption ) )
fullPath + = ":" + port ;
if ( ! initialPath . StartsWith ( "/" ) )
initialPath = "/" + initialPath ;
fullPath + = initialPath ;
return fullPath ;
}
2016-03-01 23:29:42 -05:00
}
public class TransactedWrite : IWriteTransaction
{
private readonly IOConnectionInfo _ioc ;
private readonly NetFtpFileStorage _fileStorage ;
private readonly IOConnectionInfo _iocTemp ;
private FtpClient _client ;
2016-11-14 06:31:16 -05:00
private Stream _stream ;
2016-03-01 23:29:42 -05:00
public TransactedWrite ( IOConnectionInfo ioc , NetFtpFileStorage fileStorage )
{
_ioc = ioc ;
_iocTemp = _ioc . CloneDeep ( ) ;
_iocTemp . Path + = "." + new PwUuid ( true ) . ToHexString ( ) . Substring ( 0 , 6 ) + ".tmp" ;
_fileStorage = fileStorage ;
}
public void Dispose ( )
{
2016-11-14 06:31:16 -05:00
if ( _stream ! = null )
_stream . Dispose ( ) ;
_stream = null ;
2016-03-01 23:29:42 -05:00
}
public Stream OpenFile ( )
{
2016-11-14 06:31:16 -05:00
try
{
_client = _fileStorage . GetClient ( _ioc , false ) ;
2016-11-28 14:41:39 -05:00
_stream = _client . OpenWrite ( _fileStorage . IocToUri ( _iocTemp ) . PathAndQuery ) ;
2016-11-14 06:31:16 -05:00
return _stream ;
}
catch ( FtpCommandException ex )
{
throw NetFtpFileStorage . ConvertException ( ex ) ;
}
2016-03-01 23:29:42 -05:00
}
public void CommitWrite ( )
{
2016-11-14 06:31:16 -05:00
try
{
Android . Util . Log . Debug ( "NETFTP" , "connected: " + _client . IsConnected . ToString ( ) ) ;
_stream . Close ( ) ;
Android . Util . Log . Debug ( "NETFTP" , "connected: " + _client . IsConnected . ToString ( ) ) ;
//make sure target file does not exist:
//try
{
2016-11-28 14:41:39 -05:00
if ( _client . FileExists ( _fileStorage . IocToUri ( _ioc ) . PathAndQuery ) )
_client . DeleteFile ( _fileStorage . IocToUri ( _ioc ) . PathAndQuery ) ;
2016-11-14 06:31:16 -05:00
}
//catch (FtpCommandException)
{
//TODO get a new clien? might be stale
}
2016-11-28 14:41:39 -05:00
_client . Rename ( _fileStorage . IocToUri ( _iocTemp ) . PathAndQuery ,
_fileStorage . IocToUri ( _ioc ) . PathAndQuery ) ;
2016-11-14 06:31:16 -05:00
}
catch ( FtpCommandException ex )
{
throw NetFtpFileStorage . ConvertException ( ex ) ;
}
2016-03-01 23:29:42 -05:00
}
}
public class UntransactedWrite : IWriteTransaction
{
private readonly IOConnectionInfo _ioc ;
private readonly NetFtpFileStorage _fileStorage ;
2016-11-14 06:31:16 -05:00
private Stream _stream ;
2016-03-01 23:29:42 -05:00
public UntransactedWrite ( IOConnectionInfo ioc , NetFtpFileStorage fileStorage )
{
_ioc = ioc ;
_fileStorage = fileStorage ;
}
public void Dispose ( )
{
2016-11-14 06:31:16 -05:00
if ( _stream ! = null )
_stream . Dispose ( ) ;
_stream = null ;
2016-03-01 23:29:42 -05:00
}
public Stream OpenFile ( )
{
2016-11-14 06:31:16 -05:00
_stream = _fileStorage . OpenWrite ( _ioc ) ;
return _stream ;
2016-03-01 23:29:42 -05:00
}
public void CommitWrite ( )
{
2016-11-14 06:31:16 -05:00
_stream . Close ( ) ;
2016-03-01 23:29:42 -05:00
}
}
}