diff --git a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs index 8296781c..62ddf6cc 100644 --- a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs @@ -87,22 +87,45 @@ namespace keepass2android.Io { 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("/", StringComparison.Ordinal)); + 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(settings) + EncryptionMode = (FtpEncryptionMode) int.Parse(tokens[2]), + Username = tokens[0], + Password = tokens[1] }; } - public string ToString() + public const string SettingsPrefix = "SET"; + public const string SettingsPostFix = "%"; + public const char Separator = ':'; + public override string ToString() { - return ((int) EncryptionMode).ToString(); + return SettingsPrefix + + System.Net.WebUtility.UrlEncode(Username) + Separator + + WebUtility.UrlEncode(Password) + Separator + + (int) EncryptionMode; + ; } } @@ -120,7 +143,10 @@ namespace keepass2android.Io public IEnumerable SupportedProtocols { - get { yield return "ftp"; } + get + { + yield return "ftp"; + } } public void Delete(IOConnectionInfo ioc) @@ -160,9 +186,11 @@ namespace keepass2android.Io internal FtpClient GetClient(IOConnectionInfo ioc, bool enableCloneClient = true) { + var settings = ConnectionSettings.FromIoc(ioc); + FtpClient client = new RetryConnectFtpClient(); - if ((ioc.UserName.Length > 0) || (ioc.Password.Length > 0)) - client.Credentials = new NetworkCredential(ioc.UserName, ioc.Password); + 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 @@ -176,7 +204,7 @@ namespace keepass2android.Io args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors); }; - client.EncryptionMode = ConnectionSettings.FromIoc(ioc).EncryptionMode; + client.EncryptionMode = settings.EncryptionMode; client.Connect(); return client; @@ -192,7 +220,7 @@ namespace keepass2android.Io int schemeLength = path.IndexOf("://", StringComparison.Ordinal); string scheme = path.Substring(0, schemeLength); path = path.Substring(schemeLength + 3); - string settings = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); + string settings = path.Substring(0, path.IndexOf(ConnectionSettings.SettingsPostFix, StringComparison.Ordinal)); path = path.Substring(settings.Length + 1); return new Uri(scheme + "://" + path); } @@ -203,10 +231,10 @@ namespace keepass2android.Io int schemeLength = basePath.IndexOf("://", StringComparison.Ordinal); string scheme = basePath.Substring(0, schemeLength); basePath = basePath.Substring(schemeLength + 3); - string baseSettings = basePath.Substring(0, basePath.IndexOf("/", StringComparison.Ordinal)); + 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 + "/" + baseHost + uri.AbsolutePath; //TODO does this contain Query? + return scheme + "://" + baseSettings + ConnectionSettings.SettingsPostFix + baseHost + uri.AbsolutePath; //TODO does this contain Query? } @@ -261,7 +289,7 @@ namespace keepass2android.Io public bool RequiresCredentials(IOConnectionInfo ioc) { - return ioc.CredSaveMode != IOCredSaveMode.SaveCred; + return false; } public void CreateDirectory(IOConnectionInfo ioc, string newDirName) @@ -340,16 +368,19 @@ namespace keepass2android.Io var uri = IocPathToUri(ioc.Path); string path = uri.PathAndQuery; - return new FileDescription() + 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), - IsDirectory = false + DisplayName = UrlUtil.GetFileName(path) }; + fileDesc.IsDirectory = fileDesc.Path.EndsWith("/"); + return fileDesc; } } catch (FtpCommandException ex) @@ -457,6 +488,34 @@ namespace keepass2android.Io 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 diff --git a/src/keepass2android/FileSelectHelper.cs b/src/keepass2android/FileSelectHelper.cs index 2bfcc3c2..dbedd41d 100644 --- a/src/keepass2android/FileSelectHelper.cs +++ b/src/keepass2android/FileSelectHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.FtpClient; using System.Text; using Android.App; @@ -62,13 +63,49 @@ namespace keepass2android #endif } + private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel) + { +#if !NoNet + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.ftpcredentials, null); + builder.SetView(dlgContents); + builder.SetPositiveButton(Android.Resource.String.Ok, + (sender, args) => + { + string host = dlgContents.FindViewById(Resource.Id.ftp_host).Text; + string portText = dlgContents.FindViewById(Resource.Id.ftp_port).Text; + FtpEncryptionMode encryption = + (FtpEncryptionMode) dlgContents.FindViewById(Resource.Id.ftp_encryption).SelectedItemPosition; + int port = NetFtpFileStorage.GetDefaultPort(encryption); + if (!string.IsNullOrEmpty(portText)) + int.TryParse(portText, out port); + string user = dlgContents.FindViewById(Resource.Id.ftp_user).Text; + string password = dlgContents.FindViewById(Resource.Id.ftp_password).Text; + string initialPath = dlgContents.FindViewById(Resource.Id.ftp_initial_dir).Text; + string ftpPath = new NetFtpFileStorage(_activity, App.Kp2a).BuildFullPath(host, port, initialPath, user, + password, encryption); + onStartBrowse(ftpPath); + }); + EventHandler evtH = new EventHandler((sender, e) => onCancel()); + + builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); + builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title)); + Dialog dialog = builder.Create(); + + dialog.Show(); +#endif + } + + public void PerformManualFileSelect(string defaultPath) { if (defaultPath.StartsWith("sftp://")) ShowSftpDialog(_activity, StartFileChooser, ReturnCancel); + else if ((defaultPath.StartsWith("ftp://")) || (defaultPath.StartsWith("ftps://"))) + ShowFtpDialog(_activity, StartFileChooser, ReturnCancel); else { - Func onOpen = (filename, dialog) => OnOpenButton(filename, dialog); + Func onOpen = OnOpenButton; Util.ShowFilenameDialog(_activity, !_isForSave ? onOpen : null, _isForSave ? onOpen : null, @@ -202,7 +239,6 @@ namespace keepass2android public bool StartFileChooser(string defaultPath) { #if !EXCLUDE_FILECHOOSER - Kp2aLog.Log("FSA: defaultPath=" + defaultPath); string fileProviderAuthority = FileChooserFileProvider.TheAuthority; if (defaultPath.StartsWith("file://")) { diff --git a/src/keepass2android/FileStorageSelectionActivity.cs b/src/keepass2android/FileStorageSelectionActivity.cs index 2b74e904..e6c52b90 100644 --- a/src/keepass2android/FileStorageSelectionActivity.cs +++ b/src/keepass2android/FileStorageSelectionActivity.cs @@ -40,46 +40,46 @@ namespace keepass2android private readonly FileStorageSelectionActivity _context; - private readonly List _protocolIds = new List(); + private readonly List _displayedProtocolIds = new List(); public FileStorageAdapter(FileStorageSelectionActivity context) { _context = context; //show all supported protocols: foreach (IFileStorage fs in App.Kp2a.FileStorages) - _protocolIds.AddRange(fs.SupportedProtocols); + _displayedProtocolIds.AddRange(fs.SupportedProtocols); //special handling for local files: if (!Util.IsKitKatOrLater) { //put file:// to the top - _protocolIds.Remove("file"); - _protocolIds.Insert(0, "file"); + _displayedProtocolIds.Remove("file"); + _displayedProtocolIds.Insert(0, "file"); //remove "content" (covered by androidget) //On KitKat, content is handled by AndroidContentStorage taking advantage //of persistable permissions and ACTION_OPEN/CREATE_DOCUMENT - _protocolIds.Remove("content"); + _displayedProtocolIds.Remove("content"); } else { - _protocolIds.Remove("file"); + _displayedProtocolIds.Remove("file"); } if (context.Intent.GetBooleanExtra(AllowThirdPartyAppGet, false)) - _protocolIds.Add("androidget"); + _displayedProtocolIds.Add("androidget"); if (context.Intent.GetBooleanExtra(AllowThirdPartyAppSend, false)) - _protocolIds.Add("androidsend"); + _displayedProtocolIds.Add("androidsend"); #if NoNet - _protocolIds.Add("kp2a"); + _displayedProtocolIds.Add("kp2a"); #endif } public override Object GetItem(int position) { - return _protocolIds[position]; + return _displayedProtocolIds[position]; } public override long GetItemId(int position) @@ -121,7 +121,7 @@ namespace keepass2android btn = (Button)convertView; } - var protocolId = _protocolIds[position]; + var protocolId = _displayedProtocolIds[position]; btn.Tag = protocolId; @@ -143,7 +143,7 @@ namespace keepass2android public override int Count { - get { return _protocolIds.Count; } + get { return _displayedProtocolIds.Count; } } } diff --git a/src/keepass2android/Resources/layout/ftpcredentials.xml b/src/keepass2android/Resources/layout/ftpcredentials.xml new file mode 100644 index 00000000..199439d7 --- /dev/null +++ b/src/keepass2android/Resources/layout/ftpcredentials.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 65c6c161..fbd9aede 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -944,8 +944,12 @@ Initial public release Design - - + + No encryption (FTP) + Implicit encryption (FTP over TLS, FTPS) + Explicit encryption (FTP over TLS, FTPS) + + Do not remember username and password Remember username only Remember username and password diff --git a/src/keepass2android/Utils/Util.cs b/src/keepass2android/Utils/Util.cs index e436c76c..fea6ad2f 100644 --- a/src/keepass2android/Utils/Util.cs +++ b/src/keepass2android/Utils/Util.cs @@ -319,36 +319,7 @@ namespace keepass2android public delegate bool FileSelectedHandler(string filename); - public static void ShowSftpDialog(Activity activity, FileSelectedHandler onStartBrowse, Action onCancel) - { -#if !EXCLUDE_JAVAFILESTORAGE && !NoNet - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.sftpcredentials, null); - builder.SetView(dlgContents); - builder.SetPositiveButton(Android.Resource.String.Ok, - (sender, args) => - { - string host = dlgContents.FindViewById(Resource.Id.sftp_host).Text; - string portText = dlgContents.FindViewById(Resource.Id.sftp_port).Text; - int port = Keepass2android.Javafilestorage.SftpStorage.DefaultSftpPort; - if (!string.IsNullOrEmpty(portText)) - int.TryParse(portText, out port); - string user = dlgContents.FindViewById(Resource.Id.sftp_user).Text; - string password = dlgContents.FindViewById(Resource.Id.sftp_password).Text; - string initialPath = dlgContents.FindViewById(Resource.Id.sftp_initial_dir).Text; - string sftpPath = new Keepass2android.Javafilestorage.SftpStorage().BuildFullPath(host, port, initialPath, user, - password); - onStartBrowse(sftpPath); - }); - EventHandler evtH = new EventHandler( (sender, e) => onCancel()); - - builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); - builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title)); - Dialog dialog = builder.Create(); - - dialog.Show(); -#endif - } + public class DismissListener: Java.Lang.Object, IDialogInterfaceOnDismissListener { diff --git a/src/keepass2android/app/App.cs b/src/keepass2android/app/App.cs index 3daf9ae6..56a478a8 100644 --- a/src/keepass2android/app/App.cs +++ b/src/keepass2android/app/App.cs @@ -511,7 +511,7 @@ namespace keepass2android new GoogleDriveFileStorage(Application.Context, this), new SkyDriveFileStorage(Application.Context, this), new SftpFileStorage(this), - new NetFtpFileStorage(Application.Context), + new NetFtpFileStorage(Application.Context, this), #endif #endif new LocalFileStorage(this) @@ -684,7 +684,7 @@ namespace keepass2android public void ClearOfflineCache() { - new CachingFileStorage(new BuiltInFileStorage(this), Application.Context.CacheDir.Path, this).ClearCache(); + new CachingFileStorage(new LocalFileStorage(this), Application.Context.CacheDir.Path, this).ClearCache(); } public IFileStorage GetFileStorage(string protocolId) @@ -700,7 +700,7 @@ namespace keepass2android { if (iocInfo.IsLocalFile()) - return new BuiltInFileStorage(this); + return new LocalFileStorage(this); else { IFileStorage innerFileStorage = GetCloudFileStorage(iocInfo); diff --git a/src/keepass2android/fileselect/FileChooserFileProvider.cs b/src/keepass2android/fileselect/FileChooserFileProvider.cs index a3ea294e..eec9d5ff 100644 --- a/src/keepass2android/fileselect/FileChooserFileProvider.cs +++ b/src/keepass2android/fileselect/FileChooserFileProvider.cs @@ -66,7 +66,6 @@ namespace keepass2android { try { - Kp2aLog.Log("Provider.GetFileEntry " + filename); return ConvertFileDescription(App.Kp2a.GetFileStorage(filename).GetFileDescription(ConvertPathToIoc(filename))); } catch (Exception e) @@ -80,14 +79,12 @@ namespace keepass2android protected override void ListFiles(int taskId, string dirName, bool showHiddenFiles, int filterMode, int limit, string positiveRegex, string negativeRegex, IList fileList, bool[] hasMoreFiles) { - Kp2aLog.Log("Provider.ListFiles " + dirName); try { var dirContents = App.Kp2a.GetFileStorage(dirName).ListContents(ConvertPathToIoc(dirName)); foreach (FileDescription e in dirContents) { - fileList.Add(ConvertFileDescription(e) - ); + fileList.Add(ConvertFileDescription(e)); } } catch (Exception e) diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index d84db71f..1d51b4d4 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -678,7 +678,9 @@ Designer - + + Designer + @@ -771,6 +773,10 @@ {A8779D4D-7C49-4C2F-82BD-2CDC448391DA} Kp2aKeyboardBinding + + {146FD497-BA03-4740-B6C5-5C84EA8FCDE2} + System.Net.FtpClient.Android + {3DA3911E-36DE-465E-8F15-F1991B6437E5} PluginSdkBinding @@ -1683,6 +1689,11 @@ + + + Designer + +