implement GUI for NetFtpStorage

store user/password for ftp connection in "path" string for compatiblity with file chooser
This commit is contained in:
Philipp Crocoll 2016-11-18 03:24:15 +01:00
parent fe3da55e0d
commit 113d693f7a
9 changed files with 222 additions and 69 deletions

View File

@ -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<string> 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

View File

@ -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<EditText>(Resource.Id.ftp_host).Text;
string portText = dlgContents.FindViewById<EditText>(Resource.Id.ftp_port).Text;
FtpEncryptionMode encryption =
(FtpEncryptionMode) dlgContents.FindViewById<Spinner>(Resource.Id.ftp_encryption).SelectedItemPosition;
int port = NetFtpFileStorage.GetDefaultPort(encryption);
if (!string.IsNullOrEmpty(portText))
int.TryParse(portText, out port);
string user = dlgContents.FindViewById<EditText>(Resource.Id.ftp_user).Text;
string password = dlgContents.FindViewById<EditText>(Resource.Id.ftp_password).Text;
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.ftp_initial_dir).Text;
string ftpPath = new NetFtpFileStorage(_activity, App.Kp2a).BuildFullPath(host, port, initialPath, user,
password, encryption);
onStartBrowse(ftpPath);
});
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((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<string, Dialog, bool> onOpen = (filename, dialog) => OnOpenButton(filename, dialog);
Func<string, Dialog, bool> 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://"))
{

View File

@ -40,46 +40,46 @@ namespace keepass2android
private readonly FileStorageSelectionActivity _context;
private readonly List<string> _protocolIds = new List<string>();
private readonly List<string> _displayedProtocolIds = new List<string>();
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; }
}
}

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="12dip"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/ftp_host"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="85.13.147.153"
android:layout_weight="1"
android:inputType="textNoSuggestions"
android:hint="@string/hint_sftp_host" />
<TextView
android:id="@+id/portsep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":" />
<EditText
android:id="@+id/ftp_port"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="4"
android:singleLine="true"
android:inputType="number"
android:text=""
android:hint="@string/hint_sftp_port" />
</LinearLayout>
<Spinner
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:entries="@array/ftp_encryption_modes"
android:id="@+id/ftp_encryption" />
<EditText
android:id="@+id/ftp_user"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="f00c530c"
android:hint="@string/hint_username" />
<EditText
android:id="@+id/ftp_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:text="2C4vsSt8MhNEaqv5"
android:singleLine="true"
android:hint="@string/hint_pass" />
<TextView android:id="@+id/initial_dir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/initial_directory" />
<EditText
android:id="@+id/ftp_initial_dir"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="/"
/>
</LinearLayout>

View File

@ -944,8 +944,12 @@ Initial public release
</string-array>
<string name="design_title">Design</string>
<string-array name="cred_remember_modes">
<string-array name="ftp_encryption_modes">
<item>No encryption (FTP)</item>
<item>Implicit encryption (FTP over TLS, FTPS)</item>
<item>Explicit encryption (FTP over TLS, FTPS)</item>
</string-array>
<string-array name="cred_remember_modes">
<item>Do not remember username and password</item>
<item>Remember username only</item>
<item>Remember username and password</item>

View File

@ -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<EditText>(Resource.Id.sftp_host).Text;
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
int port = Keepass2android.Javafilestorage.SftpStorage.DefaultSftpPort;
if (!string.IsNullOrEmpty(portText))
int.TryParse(portText, out port);
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
string password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
string sftpPath = new Keepass2android.Javafilestorage.SftpStorage().BuildFullPath(host, port, initialPath, user,
password);
onStartBrowse(sftpPath);
});
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>( (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
{

View File

@ -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);

View File

@ -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<FileEntry> 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)

View File

@ -678,7 +678,9 @@
<AndroidResource Include="Resources\layout\QuickUnlock.xml">
<SubType>Designer</SubType>
</AndroidResource>
<AndroidResource Include="Resources\layout\url_credentials.xml" />
<AndroidResource Include="Resources\layout\url_credentials.xml">
<SubType>Designer</SubType>
</AndroidResource>
<AndroidResource Include="Resources\drawable\section_header.xml" />
<AndroidResource Include="Resources\drawable\extra_string_header.xml" />
<AndroidResource Include="Resources\layout\entry_edit_section.xml">
@ -771,6 +773,10 @@
<Project>{A8779D4D-7C49-4C2F-82BD-2CDC448391DA}</Project>
<Name>Kp2aKeyboardBinding</Name>
</ProjectReference>
<ProjectReference Include="..\netftpandroid\System.Net.FtpClient\System.Net.FtpClient.Android.csproj">
<Project>{146FD497-BA03-4740-B6C5-5C84EA8FCDE2}</Project>
<Name>System.Net.FtpClient.Android</Name>
</ProjectReference>
<ProjectReference Include="..\PluginSdkBinding\PluginSdkBinding.csproj">
<Project>{3DA3911E-36DE-465E-8F15-F1991B6437E5}</Project>
<Name>PluginSdkBinding</Name>
@ -1683,6 +1689,11 @@
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\ic_fp_40px.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\layout\ftpcredentials.xml">
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<Import Project="..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets" Condition="Exists('..\packages\Xamarin.Insights.1.11.3\build\MonoAndroid10\Xamarin.Insights.targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">