613 lines
16 KiB
C#
613 lines
16 KiB
C#
#if !NoNet
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net;
|
|
using Android.Content;
|
|
using Android.OS;
|
|
using FluentFTP;
|
|
using FluentFTP.Exceptions;
|
|
using KeePass.Util;
|
|
using KeePassLib;
|
|
using KeePassLib.Serialization;
|
|
using KeePassLib.Utility;
|
|
|
|
|
|
namespace keepass2android.Io
|
|
{
|
|
public class NetFtpFileStorage: IFileStorage
|
|
{
|
|
public struct ConnectionSettings
|
|
{
|
|
public FtpEncryptionMode EncryptionMode {get; set; }
|
|
|
|
public string Username
|
|
{
|
|
get;set;
|
|
}
|
|
public string Password
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public static ConnectionSettings FromIoc(IOConnectionInfo ioc)
|
|
{
|
|
if (!string.IsNullOrEmpty(ioc.UserName))
|
|
{
|
|
//legacy support
|
|
return new ConnectionSettings()
|
|
{
|
|
EncryptionMode = FtpEncryptionMode.None,
|
|
Username = ioc.UserName,
|
|
Password = ioc.Password
|
|
};
|
|
}
|
|
|
|
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 = WebUtility.UrlDecode(tokens[0]),
|
|
Password = WebUtility.UrlDecode(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;
|
|
private readonly Func<bool> _debugLogPrefGetter;
|
|
|
|
public MemoryStream traceStream;
|
|
|
|
public NetFtpFileStorage(Context context, ICertificateValidationHandler app, Func<bool> debugLogPrefGetter)
|
|
{
|
|
_app = app;
|
|
_debugLogPrefGetter = debugLogPrefGetter;
|
|
traceStream = new MemoryStream();
|
|
}
|
|
|
|
public IEnumerable<string> SupportedProtocols
|
|
{
|
|
get
|
|
{
|
|
yield return "ftp";
|
|
}
|
|
}
|
|
|
|
public bool UserShouldBackup
|
|
{
|
|
get { return true; }
|
|
}
|
|
|
|
public void Delete(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
using (FtpClient client = GetClient(ioc))
|
|
{
|
|
string localPath = IocToLocalPath(ioc);
|
|
if (client.DirectoryExists(localPath))
|
|
client.DeleteDirectory(localPath);
|
|
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(ExceptionUtil.GetErrorMessage(exception), exception);
|
|
}
|
|
|
|
return exception;
|
|
}
|
|
|
|
|
|
internal FtpClient GetClient(IOConnectionInfo ioc, bool enableCloneClient = true)
|
|
{
|
|
var settings = ConnectionSettings.FromIoc(ioc);
|
|
|
|
FtpClient client = new FtpClient();
|
|
client.Config.RetryAttempts = 3;
|
|
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 = IocToUri(ioc);
|
|
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.Config.EncryptionMode = settings.EncryptionMode;
|
|
|
|
if (_debugLogPrefGetter())
|
|
client.Logger = new Kp2aLogFTPLogger();
|
|
|
|
client.Connect();
|
|
return client;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static Uri IocToUri(IOConnectionInfo ioc)
|
|
{
|
|
if (!string.IsNullOrEmpty(ioc.UserName))
|
|
{
|
|
//legacy support.
|
|
return new Uri(ioc.Path);
|
|
}
|
|
string path = ioc.Path;
|
|
//remove additional stuff like TLS param
|
|
int schemeLength = path.IndexOf("://", StringComparison.Ordinal);
|
|
string scheme = path.Substring(0, schemeLength);
|
|
path = path.Substring(schemeLength + 3);
|
|
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);
|
|
|
|
}
|
|
Kp2aLog.Log("FTP: IocToUri out = " + scheme + "://" + path);
|
|
return new Uri(scheme + "://" + path);
|
|
}
|
|
|
|
private string IocPathFromUri(IOConnectionInfo baseIoc, string 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));
|
|
string result = scheme + "://" + baseSettings + ConnectionSettings.SettingsPostFix + baseHost + uri; //TODO does this contain Query?
|
|
return result;
|
|
}
|
|
|
|
|
|
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))
|
|
{
|
|
var memStream = new MemoryStream();
|
|
cl.OpenRead(IocToLocalPath(ioc), FtpDataType.Binary, 0).CopyTo(memStream);
|
|
memStream.Seek(0, SeekOrigin.Begin);
|
|
return memStream;
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
return UrlUtil.StripExtension(
|
|
UrlUtil.GetFileName(ioc.Path));
|
|
}
|
|
|
|
public string GetFileExtension(IOConnectionInfo ioc)
|
|
{
|
|
return UrlUtil.GetExtension(ioc.Path);
|
|
}
|
|
|
|
public bool RequiresCredentials(IOConnectionInfo ioc)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
|
|
{
|
|
try
|
|
{
|
|
using (var client = GetClient(ioc))
|
|
{
|
|
client.CreateDirectory(IocToLocalPath(GetFilePath(ioc, newDirName)));
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public static string IocToLocalPath(IOConnectionInfo ioc)
|
|
{
|
|
return WebUtility.UrlDecode(IocToUri(ioc).PathAndQuery);
|
|
}
|
|
|
|
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
|
|
{
|
|
try
|
|
{
|
|
using (var client = GetClient(ioc))
|
|
{
|
|
/*
|
|
* For some reason GetListing(path) does not always return the contents of the directory.
|
|
* However, calling SetWorkingDirectory(path) followed by GetListing(null, options) to
|
|
* list the contents of the working directory does consistently work.
|
|
*
|
|
* Similar behavior was confirmed using ncftp client. I suspect this is a strange
|
|
* bug/nuance in the server's implementation of the LIST command?
|
|
*
|
|
* [bug #2423]
|
|
*/
|
|
client.SetWorkingDirectory(IocToLocalPath(ioc));
|
|
|
|
List<FileDescription> files = new List<FileDescription>();
|
|
foreach (FtpListItem item in client.GetListing(null,
|
|
FtpListOption.SizeModify | FtpListOption.AllFiles))
|
|
{
|
|
switch (item.Type)
|
|
{
|
|
case FtpObjectType.Directory:
|
|
files.Add(new FileDescription()
|
|
{
|
|
CanRead = true,
|
|
CanWrite = true,
|
|
DisplayName = item.Name,
|
|
IsDirectory = true,
|
|
LastModified = item.Modified,
|
|
Path = IocPathFromUri(ioc, item.FullName)
|
|
});
|
|
break;
|
|
case FtpObjectType.File:
|
|
files.Add(new FileDescription()
|
|
{
|
|
CanRead = true,
|
|
CanWrite = true,
|
|
DisplayName = item.Name,
|
|
IsDirectory = false,
|
|
LastModified = item.Modified,
|
|
Path = IocPathFromUri(ioc, item.FullName),
|
|
SizeInBytes = item.Size
|
|
});
|
|
break;
|
|
default:
|
|
Kp2aLog.Log("FTP: ListContents item skipped: " + IocToUri(ioc) + ": " + item.FullName + ", type=" + item.Type);
|
|
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))
|
|
{
|
|
|
|
|
|
string path = IocToLocalPath(ioc);
|
|
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 = IocToUri(ioc);
|
|
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(IocToLocalPath(ioc));
|
|
|
|
}
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public static int GetDefaultPort(FtpEncryptionMode encryption)
|
|
{
|
|
var client = new FtpClient();
|
|
client.Config.EncryptionMode = encryption;
|
|
return client.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(NetFtpFileStorage.IocToLocalPath(_iocTemp));
|
|
return _stream;
|
|
}
|
|
catch (FtpCommandException ex)
|
|
{
|
|
throw NetFtpFileStorage.ConvertException(ex);
|
|
}
|
|
}
|
|
|
|
public void CommitWrite()
|
|
{
|
|
try
|
|
{
|
|
Android.Util.Log.Debug("NETFTP","connected: " + _client.IsConnected.ToString());
|
|
_stream.Close();
|
|
_stream.Dispose();
|
|
_client.GetReply();
|
|
|
|
_client.MoveFile(NetFtpFileStorage.IocToLocalPath(_iocTemp),
|
|
NetFtpFileStorage.IocToLocalPath(_ioc));
|
|
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
|
|
class Kp2aLogFTPLogger : IFtpLogger
|
|
{
|
|
public void Log(FtpLogEntry entry)
|
|
{
|
|
Kp2aLog.Log("[FluentFTP] " + entry.Message);
|
|
}
|
|
}
|
|
}
|
|
#endif |