621 lines
15 KiB
C#
621 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.FtpClient;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using Android.Content;
|
|
using Android.OS;
|
|
using Android.Preferences;
|
|
using KeePassLib;
|
|
using KeePassLib.Serialization;
|
|
using KeePassLib.Utility;
|
|
|
|
namespace keepass2android.Io
|
|
{
|
|
public class NetFtpFileStorage: IFileStorage
|
|
{
|
|
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
|
|
{
|
|
public FtpEncryptionMode EncryptionMode {get; set; }
|
|
|
|
public string Username
|
|
{
|
|
get;set;
|
|
}
|
|
public string Password
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public static ConnectionSettings FromIoc(IOConnectionInfo ioc)
|
|
{
|
|
string path = ioc.Path;
|
|
int schemeLength = path.IndexOf("://", StringComparison.Ordinal);
|
|
path = path.Substring(schemeLength + 3);
|
|
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);
|
|
return new ConnectionSettings()
|
|
{
|
|
EncryptionMode = (FtpEncryptionMode) int.Parse(tokens[2]),
|
|
Username = tokens[0],
|
|
Password = tokens[1]
|
|
};
|
|
|
|
}
|
|
|
|
public const string SettingsPrefix = "SET";
|
|
public const string SettingsPostFix = "%";
|
|
public const char Separator = ':';
|
|
public override string ToString()
|
|
{
|
|
return SettingsPrefix +
|
|
System.Net.WebUtility.UrlEncode(Username) + Separator +
|
|
WebUtility.UrlEncode(Password) + Separator +
|
|
(int) EncryptionMode;
|
|
;
|
|
}
|
|
}
|
|
|
|
private readonly ICertificateValidationHandler _app;
|
|
|
|
public MemoryStream traceStream;
|
|
|
|
public NetFtpFileStorage(Context context, ICertificateValidationHandler app)
|
|
{
|
|
_app = app;
|
|
traceStream = new MemoryStream();
|
|
FtpTrace.AddListener(new System.Diagnostics.TextWriterTraceListener(traceStream));
|
|
|
|
}
|
|
|
|
public IEnumerable<string> SupportedProtocols
|
|
{
|
|
get
|
|
{
|
|
yield return "ftp";
|
|
}
|
|
}
|
|
|
|
public void Delete(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
using (FtpClient client = GetClient(ioc))
|
|
{
|
|
string localPath = IocPathToUri(ioc.Path).PathAndQuery;
|
|
if (client.DirectoryExists(localPath))
|
|
client.DeleteDirectory(localPath, true);
|
|
else
|
|
client.DeleteFile(localPath);
|
|
}
|
|
|
|
}
|
|
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;
|
|
}
|
|
|
|
|
|
internal FtpClient GetClient(IOConnectionInfo ioc, bool enableCloneClient = true)
|
|
{
|
|
var settings = ConnectionSettings.FromIoc(ioc);
|
|
|
|
FtpClient client = new RetryConnectFtpClient();
|
|
if ((settings.Username.Length > 0) || (settings.Password.Length > 0))
|
|
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
|
|
else
|
|
client.Credentials = new NetworkCredential("anonymous", ""); //TODO TEST
|
|
|
|
Uri uri = IocPathToUri(ioc.Path);
|
|
client.Host = uri.Host;
|
|
if (!uri.IsDefaultPort) //TODO test
|
|
client.Port = uri.Port;
|
|
|
|
client.ValidateCertificate += (control, args) =>
|
|
{
|
|
args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors);
|
|
};
|
|
|
|
client.EncryptionMode = settings.EncryptionMode;
|
|
|
|
client.Connect();
|
|
return client;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal Uri IocPathToUri(string path)
|
|
{
|
|
//remove additional stuff like TLS param
|
|
int schemeLength = path.IndexOf("://", StringComparison.Ordinal);
|
|
string scheme = path.Substring(0, schemeLength);
|
|
path = path.Substring(schemeLength + 3);
|
|
string settings = path.Substring(0, path.IndexOf(ConnectionSettings.SettingsPostFix, StringComparison.Ordinal));
|
|
path = path.Substring(settings.Length + 1);
|
|
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);
|
|
string baseSettings = basePath.Substring(0, basePath.IndexOf(ConnectionSettings.SettingsPostFix, StringComparison.Ordinal));
|
|
basePath = basePath.Substring(baseSettings.Length+1);
|
|
string baseHost = basePath.Substring(0, basePath.IndexOf("/", StringComparison.Ordinal));
|
|
return scheme + "://" + baseSettings + ConnectionSettings.SettingsPostFix + baseHost + uri.AbsolutePath; //TODO does this contain Query?
|
|
}
|
|
|
|
|
|
public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public Stream OpenFileForRead(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
using (var cl = GetClient(ioc))
|
|
{
|
|
return cl.OpenRead(IocPathToUri(ioc.Path).PathAndQuery, FtpDataType.Binary, 0);
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
|
|
{
|
|
try
|
|
{
|
|
|
|
|
|
if (!useFileTransaction)
|
|
return new UntransactedWrite(ioc, this);
|
|
else
|
|
return new TransactedWrite(ioc, this);
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
|
|
{
|
|
//TODO does this work when flags are encoded in the iocPath?
|
|
return UrlUtil.StripExtension(
|
|
UrlUtil.GetFileName(ioc.Path));
|
|
}
|
|
|
|
public bool RequiresCredentials(IOConnectionInfo ioc)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
|
|
{
|
|
try
|
|
{
|
|
using (var client = GetClient(ioc))
|
|
{
|
|
client.CreateDirectory(IocPathToUri(GetFilePath(ioc, newDirName).Path).PathAndQuery);
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
using (var client = GetClient(ioc))
|
|
{
|
|
List<FileDescription> files = new List<FileDescription>();
|
|
foreach (FtpListItem item in client.GetListing(IocPathToUri(ioc.Path).PathAndQuery,
|
|
FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks))
|
|
{
|
|
|
|
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;
|
|
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
|
|
public FileDescription GetFileDescription(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
//TODO when is this called?
|
|
//is it very inefficient to connect for each description?
|
|
|
|
using (FtpClient client = GetClient(ioc))
|
|
{
|
|
|
|
var uri = IocPathToUri(ioc.Path);
|
|
string path = uri.PathAndQuery;
|
|
if (!client.FileExists(path) && (!client.DirectoryExists(path)))
|
|
throw new FileNotFoundException();
|
|
var fileDesc = new FileDescription()
|
|
{
|
|
CanRead = true,
|
|
CanWrite = true,
|
|
Path = ioc.Path,
|
|
LastModified = client.GetModifiedTime(path),
|
|
SizeInBytes = client.GetFileSize(path),
|
|
DisplayName = UrlUtil.GetFileName(path)
|
|
};
|
|
fileDesc.IsDirectory = fileDesc.Path.EndsWith("/");
|
|
return fileDesc;
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
activity.PerformManualFileSelect(isForSave, requestCode, "ftp");
|
|
}
|
|
|
|
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)
|
|
{
|
|
var uri = IocPathToUri(ioc.Path);
|
|
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;
|
|
}
|
|
public Stream OpenWrite(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
using (var client = GetClient(ioc))
|
|
{
|
|
return client.OpenWrite(IocPathToUri(ioc.Path).PathAndQuery);
|
|
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
public class TransactedWrite : IWriteTransaction
|
|
{
|
|
private readonly IOConnectionInfo _ioc;
|
|
private readonly NetFtpFileStorage _fileStorage;
|
|
private readonly IOConnectionInfo _iocTemp;
|
|
private FtpClient _client;
|
|
private Stream _stream;
|
|
|
|
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()
|
|
{
|
|
if (_stream != null)
|
|
_stream.Dispose();
|
|
_stream = null;
|
|
}
|
|
|
|
public Stream OpenFile()
|
|
{
|
|
try
|
|
{
|
|
|
|
_client = _fileStorage.GetClient(_ioc, false);
|
|
_stream = _client.OpenWrite(_fileStorage.IocPathToUri(_iocTemp.Path).PathAndQuery);
|
|
return _stream;
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw NetFtpFileStorage.ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public void CommitWrite()
|
|
{
|
|
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
|
|
{
|
|
if (_client.FileExists(_fileStorage.IocPathToUri(_ioc.Path).PathAndQuery))
|
|
_client.DeleteFile(_fileStorage.IocPathToUri(_ioc.Path).PathAndQuery);
|
|
|
|
}
|
|
//catch (FtpCommandException)
|
|
{
|
|
//TODO get a new clien? might be stale
|
|
}
|
|
|
|
_client.Rename(_fileStorage.IocPathToUri(_iocTemp.Path).PathAndQuery,
|
|
_fileStorage.IocPathToUri(_ioc.Path).PathAndQuery);
|
|
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw NetFtpFileStorage.ConvertException(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class UntransactedWrite : IWriteTransaction
|
|
{
|
|
private readonly IOConnectionInfo _ioc;
|
|
private readonly NetFtpFileStorage _fileStorage;
|
|
private Stream _stream;
|
|
|
|
public UntransactedWrite(IOConnectionInfo ioc, NetFtpFileStorage fileStorage)
|
|
{
|
|
_ioc = ioc;
|
|
_fileStorage = fileStorage;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_stream != null)
|
|
_stream.Dispose();
|
|
_stream = null;
|
|
}
|
|
|
|
public Stream OpenFile()
|
|
{
|
|
_stream = _fileStorage.OpenWrite(_ioc);
|
|
return _stream;
|
|
}
|
|
|
|
public void CommitWrite()
|
|
{
|
|
_stream.Close();
|
|
}
|
|
}
|
|
} |