Squashed commit of not-yet-approved PR #2038
https://github.com/PhilippC/keepass2android/pull/2038 User-defined SFTP connection timeout The addition of SFTP query parameter options are needed to support custom private key functionality. Squashed commits from hyproman:sftp-conn-timeout: commit 9c6b96e8198f1b912acdc1248af775f8fed58e1c commit ebe59d9bc337a46bf0646677eb38b13ddde21f14 commit 69eb0bfd1a7010a2e442c36d10a16d1710c958de commit 9394947c12bedb8667b7b94d0b1457f9e0451e18
This commit is contained in:
		| @@ -97,7 +97,28 @@ public class FileEntry { | ||||
| 			return false; | ||||
| 		return true; | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		StringBuilder s = new StringBuilder("JavaFileStorage.FileEntry{").append(displayName).append("|") | ||||
| 				.append("path=").append(path).append(",sz=").append(sizeInBytes) | ||||
| 				.append(",").append(isDirectory ? "dir" : "file") | ||||
| 				.append(",lastMod=").append(lastModifiedTime); | ||||
|  | ||||
| 		StringBuilder perms = new StringBuilder(); | ||||
| 		if (canRead) | ||||
| 			perms.append("r"); | ||||
| 		if (canWrite) | ||||
| 			perms.append("w"); | ||||
| 		if (perms.length() > 0) { | ||||
| 			s.append(",").append(perms); | ||||
| 		} | ||||
|  | ||||
| 		if (userData != null && userData.length() > 0) | ||||
| 			s.append(",userData=").append(userData); | ||||
|  | ||||
| 		return s.append("}").toString(); | ||||
| 	} | ||||
| 	 | ||||
| } | ||||
| 	 | ||||
|   | ||||
| @@ -8,7 +8,12 @@ import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Comparator; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.TreeSet; | ||||
|  | ||||
| import com.jcraft.jsch.Channel; | ||||
| import com.jcraft.jsch.ChannelSftp; | ||||
| @@ -28,6 +33,9 @@ import android.os.Bundle; | ||||
| public class SftpStorage extends JavaFileStorageBase { | ||||
|  | ||||
| 	public static final int DEFAULT_SFTP_PORT = 22; | ||||
| 	public static final int UNSET_SFTP_CONNECT_TIMEOUT = -1; | ||||
| 	private static final String SFTP_CONNECT_TIMEOUT_OPTION_NAME = "connectTimeout"; | ||||
|  | ||||
| 	JSch jsch; | ||||
|  | ||||
| 	public class ConnectionInfo | ||||
| @@ -37,7 +45,33 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 		public String password; | ||||
| 		public String localPath; | ||||
| 		public int port; | ||||
| 		public int connectTimeoutSec = UNSET_SFTP_CONNECT_TIMEOUT; | ||||
|  | ||||
|  | ||||
| 		public String toString() { | ||||
| 			return "ConnectionInfo{host=" + host + ",port=" + port + ",user=" + username + | ||||
| 					",pwd=<hidden>,path=" + localPath + ",connectTimeout=" + connectTimeoutSec + | ||||
| 					"}"; | ||||
| 		} | ||||
|  | ||||
| 		boolean hasOptions() { | ||||
| 			return connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private static Map<String, String> buildOptionMap(ConnectionInfo ci) { | ||||
| 		return buildOptionMap(ci.connectTimeoutSec); | ||||
| 	} | ||||
|  | ||||
| 	private static Map<String, String> buildOptionMap(int connectTimeoutSec) { | ||||
| 		Map<String, String> opts = new HashMap<>(); | ||||
| 		if (connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) { | ||||
| 			opts.put(SFTP_CONNECT_TIMEOUT_OPTION_NAME, String.valueOf(connectTimeoutSec)); | ||||
| 		} | ||||
| 		return opts; | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	Context _appContext; | ||||
|  | ||||
| 	public SftpStorage(Context appContext) { | ||||
| @@ -64,15 +98,15 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
|  | ||||
| 	@Override | ||||
| 	public InputStream openFileForRead(String path) throws Exception { | ||||
|  | ||||
| 		ChannelSftp c = init(path); | ||||
| 		ConnectionInfo cInfo = splitStringToConnectionInfo(path); | ||||
| 		ChannelSftp c = init(cInfo); | ||||
|  | ||||
| 		try { | ||||
| 			byte[] buff = new byte[8000]; | ||||
|  | ||||
| 			int bytesRead = 0; | ||||
|  | ||||
| 			InputStream in = c.get(extractSessionPath(path)); | ||||
| 			InputStream in = c.get(cInfo.localPath); | ||||
| 			ByteArrayOutputStream bao = new ByteArrayOutputStream(); | ||||
|  | ||||
| 			while ((bytesRead = in.read(buff)) != -1) { | ||||
| @@ -104,14 +138,15 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 	public void uploadFile(String path, byte[] data, boolean writeTransactional) | ||||
| 			throws Exception { | ||||
|  | ||||
| 		ChannelSftp c = init(path); | ||||
| 		ConnectionInfo cInfo = splitStringToConnectionInfo(path); | ||||
| 		ChannelSftp c = init(cInfo); | ||||
| 		try { | ||||
| 			InputStream in = new ByteArrayInputStream(data); | ||||
| 			String targetPath = extractSessionPath(path); | ||||
| 			String targetPath = cInfo.localPath; | ||||
| 			if (writeTransactional) | ||||
| 			{ | ||||
| 				//upload to temporary location: | ||||
| 				String tmpPath = targetPath+".tmp"; | ||||
| 				String tmpPath = targetPath + ".tmp"; | ||||
| 				c.put(in, tmpPath); | ||||
| 				//remove previous file: | ||||
| 				try | ||||
| @@ -127,9 +162,9 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				c.put(in, targetPath);				 | ||||
| 				c.put(in, targetPath); | ||||
| 			} | ||||
| 			 | ||||
|  | ||||
| 			tryDisconnect(c); | ||||
| 		} catch (Exception e) { | ||||
| 			tryDisconnect(c); | ||||
| @@ -141,53 +176,95 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 	@Override | ||||
| 	public String createFolder(String parentPath, String newDirName) | ||||
| 			throws Exception { | ||||
|  | ||||
| 		ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath); | ||||
| 		try { | ||||
| 			ChannelSftp c = init(parentPath); | ||||
| 			String newPath = concatPaths(parentPath, newDirName); | ||||
| 			c.mkdir(extractSessionPath(newPath)); | ||||
| 			ChannelSftp c = init(cInfo); | ||||
| 			String newPath = concatPaths(cInfo.localPath, newDirName); | ||||
| 			c.mkdir(newPath); | ||||
| 			tryDisconnect(c); | ||||
| 			return newPath; | ||||
|  | ||||
| 			return buildFullPath(cInfo.host, cInfo.port, newPath, | ||||
| 					cInfo.username, cInfo.password, cInfo.connectTimeoutSec); | ||||
| 		} catch (Exception e) { | ||||
| 			throw convertException(e); | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	private String extractUserPwdHostPort(String path) { | ||||
| 	    String withoutProtocol = path | ||||
| 				.substring(getProtocolPrefix().length()); | ||||
| 		return withoutProtocol.substring(0, withoutProtocol.indexOf("/")); | ||||
| 	} | ||||
|  | ||||
| 	private String extractSessionPath(String newPath) { | ||||
| 		String withoutProtocol = newPath | ||||
| 				.substring(getProtocolPrefix().length()); | ||||
| 		return withoutProtocol.substring(withoutProtocol.indexOf("/")); | ||||
| 		int pathStartIdx = withoutProtocol.indexOf("/"); | ||||
| 		int pathEndIdx = withoutProtocol.indexOf("?"); | ||||
| 		if (pathEndIdx < 0) { | ||||
| 			pathEndIdx = withoutProtocol.length(); | ||||
| 		} | ||||
| 		return withoutProtocol.substring(pathStartIdx, pathEndIdx); | ||||
| 	} | ||||
| 	 | ||||
| 	private String extractUserPwdHost(String path) { | ||||
|  | ||||
| 	private Map<String, String> extractOptionsMap(String path) throws UnsupportedEncodingException { | ||||
| 		String withoutProtocol = path | ||||
| 				.substring(getProtocolPrefix().length()); | ||||
| 		return withoutProtocol.substring(0,withoutProtocol.indexOf("/")); | ||||
|  | ||||
| 		Map<String, String> options = new HashMap<>(); | ||||
|  | ||||
| 		int extraOptsIdx = withoutProtocol.indexOf("?"); | ||||
| 		if (extraOptsIdx > 0 && extraOptsIdx + 1 < withoutProtocol.length()) { | ||||
| 			String optsString = withoutProtocol.substring(extraOptsIdx + 1); | ||||
| 			String[] parts = optsString.split("&"); | ||||
| 			for (String p : parts) { | ||||
| 				int sepIdx = p.indexOf('='); | ||||
| 				if (sepIdx > 0) { | ||||
| 					String key = decode(p.substring(0, sepIdx)); | ||||
| 					String value = decode(p.substring(sepIdx + 1)); | ||||
| 					options.put(key, value); | ||||
| 				} else { | ||||
| 					options.put(decode(p), "true"); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return options; | ||||
| 	} | ||||
|  | ||||
| 	private String concatPaths(String parentPath, String newDirName) { | ||||
| 		String res = parentPath; | ||||
| 		if (!res.endsWith("/")) | ||||
| 			res += "/"; | ||||
| 		res += newDirName; | ||||
| 		return res; | ||||
| 		StringBuilder fp = new StringBuilder(parentPath); | ||||
| 		if (!parentPath.endsWith("/")) | ||||
| 			fp.append("/"); | ||||
| 		return fp.append(newDirName).toString(); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String createFilePath(String parentPath, String newFileName) | ||||
| 	public String createFilePath(final String parentUri, String newFileName) | ||||
| 			throws Exception { | ||||
| 		if (parentPath.endsWith("/") == false) | ||||
| 			parentPath += "/"; | ||||
| 		return parentPath + newFileName; | ||||
|  | ||||
| 		String parentPath = parentUri; | ||||
| 		String params = null; | ||||
| 		int paramsIdx = parentUri.lastIndexOf("?"); | ||||
| 		if (paramsIdx > 0) { | ||||
| 			params = parentUri.substring(paramsIdx); | ||||
| 			parentPath = parentPath.substring(0, paramsIdx); | ||||
| 		} | ||||
|  | ||||
| 		String newPath = concatPaths(parentPath, newFileName); | ||||
|  | ||||
| 		if (params != null) { | ||||
| 			newPath += params; | ||||
| 		} | ||||
| 		return newPath; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public List<FileEntry> listFiles(String parentPath) throws Exception { | ||||
| 		ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath); | ||||
| 		ChannelSftp c = init(cInfo); | ||||
|  | ||||
| 		ChannelSftp c = init(parentPath); | ||||
| 		return listFiles(parentPath, c); | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	private void setFromAttrs(FileEntry fileEntry, SftpATTRS attrs) { | ||||
| @@ -211,23 +288,27 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 			if (sftpEx.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) | ||||
| 				return new FileNotFoundException(sftpEx.getMessage()); | ||||
| 		} | ||||
| 		 | ||||
|  | ||||
| 		return e; | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public FileEntry getFileEntry(String filename) throws Exception { | ||||
|  | ||||
| 		ChannelSftp c = init(filename); | ||||
| 		ConnectionInfo cInfo = splitStringToConnectionInfo(filename); | ||||
| 		ChannelSftp c = init(cInfo); | ||||
| 		try { | ||||
| 			FileEntry fileEntry = new FileEntry(); | ||||
| 			String sessionPath = extractSessionPath(filename); | ||||
| 			SftpATTRS attr = c.stat(sessionPath); | ||||
| 			SftpATTRS attr = c.stat(cInfo.localPath); | ||||
| 			setFromAttrs(fileEntry, attr); | ||||
|  | ||||
| 			// Full URI | ||||
| 			fileEntry.path = filename; | ||||
| 			fileEntry.displayName = getFilename(sessionPath); | ||||
|  | ||||
| 			fileEntry.displayName = getFilename(cInfo.localPath); | ||||
|  | ||||
| 			tryDisconnect(c); | ||||
|  | ||||
| 			return fileEntry; | ||||
| 		} catch (Exception e) { | ||||
| 			logDebug("Exception in getFileEntry! " + e); | ||||
| @@ -238,8 +319,9 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
|  | ||||
| 	@Override | ||||
| 	public void delete(String path) throws Exception { | ||||
| 		ConnectionInfo cInfo = splitStringToConnectionInfo(path); | ||||
| 		ChannelSftp c = init(cInfo); | ||||
|  | ||||
| 		ChannelSftp c = init(path); | ||||
| 		delete(path, c); | ||||
| 	} | ||||
|  | ||||
| @@ -263,10 +345,11 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 			tryDisconnect(c); | ||||
| 			throw convertException(e); | ||||
| 		} | ||||
| 		 | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	private List<FileEntry> listFiles(String path, ChannelSftp c) throws Exception { | ||||
|  | ||||
| 		try { | ||||
| 			List<FileEntry> res = new ArrayList<FileEntry>(); | ||||
| 			@SuppressWarnings("rawtypes") | ||||
| @@ -282,7 +365,7 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 								||(lsEntry.getFilename().equals("..")) | ||||
| 								) | ||||
| 							continue; | ||||
| 						 | ||||
|  | ||||
| 						FileEntry fileEntry = new FileEntry(); | ||||
| 						fileEntry.displayName = lsEntry.getFilename(); | ||||
| 						fileEntry.path = createFilePath(path, fileEntry.displayName); | ||||
| @@ -312,16 +395,15 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 			throws UnsupportedEncodingException { | ||||
| 		return java.net.URLDecoder.decode(encodedString, UTF_8); | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	protected String encode(final String unencoded) | ||||
| 			throws UnsupportedEncodingException { | ||||
| 		return java.net.URLEncoder.encode(unencoded, UTF_8); | ||||
| 	} | ||||
| 	 | ||||
| 	ChannelSftp init(String filename) throws JSchException, UnsupportedEncodingException { | ||||
|  | ||||
| 	ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException { | ||||
| 		jsch = new JSch(); | ||||
| 		ConnectionInfo ci = splitStringToConnectionInfo(filename); | ||||
|  | ||||
| 		String base_dir = getBaseDir(); | ||||
| 		jsch.setKnownHosts(base_dir + "/known_hosts"); | ||||
| @@ -340,13 +422,13 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
|  | ||||
| 		} | ||||
|  | ||||
| 		Session session = jsch.getSession(ci.username, ci.host, ci.port); | ||||
| 		UserInfo ui = new SftpUserInfo(ci.password,_appContext); | ||||
| 		Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port); | ||||
| 		UserInfo ui = new SftpUserInfo(cInfo.password, _appContext); | ||||
| 		session.setUserInfo(ui); | ||||
|  | ||||
| 		session.setConfig("PreferredAuthentications", "publickey,password"); | ||||
|  | ||||
| 		session.connect(); | ||||
| 		sessionConnect(session, cInfo); | ||||
|  | ||||
| 		Channel channel = session.openChannel("sftp"); | ||||
| 		channel.connect(); | ||||
| @@ -357,6 +439,14 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	private void sessionConnect(Session session, ConnectionInfo ci) throws JSchException { | ||||
| 		if (ci.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) { | ||||
| 			session.connect(ci.connectTimeoutSec * 1000); | ||||
| 		} else { | ||||
| 			session.connect(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private String getBaseDir() { | ||||
| 		return _appContext.getFilesDir().getAbsolutePath(); | ||||
| 	} | ||||
| @@ -389,7 +479,7 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 	public ConnectionInfo splitStringToConnectionInfo(String filename) | ||||
| 			throws UnsupportedEncodingException { | ||||
| 		ConnectionInfo ci = new ConnectionInfo(); | ||||
| 		ci.host = extractUserPwdHost(filename); | ||||
| 		ci.host = extractUserPwdHostPort(filename); | ||||
| 		String userPwd = ci.host.substring(0, ci.host.indexOf('@')); | ||||
| 		ci.username = decode(userPwd.substring(0, userPwd.indexOf(":"))); | ||||
| 		ci.password = decode(userPwd.substring(userPwd.indexOf(":")+1)); | ||||
| @@ -398,10 +488,21 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 		int portSeparatorIndex = ci.host.indexOf(":"); | ||||
| 		if (portSeparatorIndex >= 0) | ||||
| 		{ | ||||
| 			ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex+1)); | ||||
| 			ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex + 1)); | ||||
| 			ci.host = ci.host.substring(0, portSeparatorIndex); | ||||
| 		} | ||||
| 		ci.localPath = extractSessionPath(filename); | ||||
|  | ||||
| 		Map<String, String> options = extractOptionsMap(filename); | ||||
|  | ||||
| 		if (options.containsKey(SFTP_CONNECT_TIMEOUT_OPTION_NAME)) { | ||||
| 			String optVal = options.get(SFTP_CONNECT_TIMEOUT_OPTION_NAME); | ||||
| 			try { | ||||
| 				ci.connectTimeoutSec = Integer.parseInt(optVal); | ||||
| 			} catch (NumberFormatException nan) { | ||||
| 				logDebug(SFTP_CONNECT_TIMEOUT_OPTION_NAME + " option not a number: " + optVal); | ||||
| 			} | ||||
| 		} | ||||
| 		return ci; | ||||
| 	} | ||||
|  | ||||
| @@ -438,12 +539,20 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 		try | ||||
| 		{ | ||||
| 			ConnectionInfo ci = splitStringToConnectionInfo(path); | ||||
| 			return getProtocolPrefix()+ci.username+"@"+ci.host+ci.localPath; | ||||
| 			StringBuilder dName = new StringBuilder(getProtocolPrefix()) | ||||
| 					.append(ci.username) | ||||
| 					.append("@") | ||||
| 					.append(ci.host) | ||||
| 					.append(ci.localPath); | ||||
| 			if (ci.hasOptions()) { | ||||
| 				appendOptions(dName, buildOptionMap(ci)); | ||||
| 			} | ||||
| 			return dName.toString(); | ||||
| 		} | ||||
| 		catch (Exception e) | ||||
| 		{ | ||||
| 			return extractSessionPath(path); | ||||
| 		}		 | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| @@ -465,22 +574,65 @@ public class SftpStorage extends JavaFileStorageBase { | ||||
| 	@Override | ||||
| 	public void onActivityResult(FileStorageSetupActivity activity, | ||||
| 			int requestCode, int resultCode, Intent data) { | ||||
| 		 | ||||
|  | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	public String buildFullPath( String host, int port, String localPath, String username, String password) throws UnsupportedEncodingException | ||||
| 	{ | ||||
| 		if (port != DEFAULT_SFTP_PORT) | ||||
| 			host += ":"+String.valueOf(port); | ||||
| 		return getProtocolPrefix()+encode(username)+":"+encode(password)+"@"+host+localPath; | ||||
| 		 | ||||
| 	public String buildFullPath(String host, int port, String localPath, | ||||
| 										String username, String password, | ||||
| 										int connectTimeoutSec) | ||||
| 			throws UnsupportedEncodingException { | ||||
| 		StringBuilder uri = new StringBuilder(getProtocolPrefix()) | ||||
| 				.append(encode(username)).append(":").append(encode(password)) | ||||
| 				.append("@").append(host); | ||||
|  | ||||
| 		if (port != DEFAULT_SFTP_PORT) { | ||||
| 			uri.append(":").append(port); | ||||
| 		} | ||||
| 		if (localPath != null && localPath.startsWith("/")) { | ||||
| 			uri.append(localPath); | ||||
| 		} | ||||
|  | ||||
| 		Map<String, String> opts = buildOptionMap(connectTimeoutSec); | ||||
| 		appendOptions(uri, opts); | ||||
|  | ||||
| 		return uri.toString(); | ||||
| 	} | ||||
|  | ||||
| 	private void appendOptions(StringBuilder uri, Map<String, String> opts) | ||||
| 			throws UnsupportedEncodingException { | ||||
|  | ||||
| 		boolean first = true; | ||||
| 		// Sort for stability/consistency | ||||
| 		Set<Map.Entry<String, String>> sortedEntries = new TreeSet<>(new EntryComparator<>()); | ||||
| 		sortedEntries.addAll(opts.entrySet()); | ||||
| 		for (Map.Entry<String, String> me : sortedEntries) { | ||||
| 			if (first) { | ||||
| 				uri.append("?"); | ||||
| 				first = false; | ||||
| 			} else { | ||||
| 				uri.append("&"); | ||||
| 			} | ||||
| 			uri.append(encode(me.getKey())).append("=").append(encode(me.getValue())); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	@Override | ||||
| 	public void prepareFileUsage(Context appContext, String path) { | ||||
| 		//nothing to do | ||||
| 		 | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * A comparator that compares Map.Entry objects by their keys, via natural ordering. | ||||
| 	 * | ||||
| 	 * @param <T> the Map.Entry key type, that must implement Comparable. | ||||
| 	 */ | ||||
| 	private static class EntryComparator<T extends Comparable<T>> implements Comparator<Map.Entry<T, ?>> { | ||||
| 		@Override | ||||
| 		public int compare(Map.Entry<T, ?> o1, Map.Entry<T, ?> o2) { | ||||
| 			return o1.getKey().compareTo(o2.getKey()); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -538,12 +538,12 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag | ||||
| 	} | ||||
|  | ||||
| 	static JavaFileStorage createStorageToTest(Context ctx, Context appContext, boolean simulateRestart) { | ||||
| 		//storageToTest = new SftpStorage(ctx.getApplicationContext()); | ||||
| 		storageToTest = new SftpStorage(ctx.getApplicationContext()); | ||||
| 		//storageToTest = new PCloudFileStorage(ctx, "yCeH59Ffgtm"); | ||||
| 		//storageToTest = new SkyDriveFileStorage("000000004010C234", appContext); | ||||
|  | ||||
|  | ||||
| 		storageToTest = new GoogleDriveAppDataFileStorage(); | ||||
| 		//storageToTest = new GoogleDriveAppDataFileStorage(); | ||||
| 		/*storageToTest = new WebDavStorage(new ICertificateErrorHandler() { | ||||
| 			@Override | ||||
| 			public boolean onValidationError(String error) { | ||||
| @@ -743,7 +743,19 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag | ||||
| 								int port = Integer.parseInt(etPort.getText().toString()); | ||||
| 								EditText etInitDir = ((EditText)view.findViewById(R.id.sftp_initial_dir)); | ||||
| 								String initialDir = etInitDir.getText().toString(); | ||||
| 								onReceivePathForFileSelect(requestCode, sftpStorage.buildFullPath( host, port, initialDir, user, pwd)); | ||||
| 								EditText etConnectTimeout = ((EditText)view.findViewById(R.id.sftp_connect_timeout)); | ||||
| 								int connectTimeout = SftpStorage.UNSET_SFTP_CONNECT_TIMEOUT; | ||||
| 								String ctStr = etConnectTimeout.getText().toString(); | ||||
| 								if (!ctStr.isEmpty()) { | ||||
| 									try { | ||||
| 										int ct = Integer.parseInt(ctStr); | ||||
| 										if (connectTimeout != ct) { | ||||
| 											connectTimeout = ct; | ||||
| 										} | ||||
| 									} catch (NumberFormatException parseEx) { | ||||
| 									} | ||||
| 								} | ||||
| 								onReceivePathForFileSelect(requestCode, sftpStorage.buildFullPath( host, port, initialDir, user, pwd, connectTimeout)); | ||||
| 							} catch (UnsupportedEncodingException e) { | ||||
| 								// TODO Auto-generated catch block | ||||
| 								e.printStackTrace(); | ||||
|   | ||||
| @@ -13,6 +13,7 @@ | ||||
|             android:id="@+id/sftp_host" | ||||
|             android:layout_width="fill_parent" | ||||
|             android:layout_height="wrap_content" | ||||
| 			android:layout_weight="1" | ||||
|             android:singleLine="true" | ||||
|             android:inputType="textNoSuggestions" | ||||
|             android:text="" | ||||
| @@ -26,9 +27,10 @@ | ||||
|             android:id="@+id/sftp_port" | ||||
|             android:layout_width="fill_parent" | ||||
|             android:layout_height="wrap_content" | ||||
| 			android:layout_weight="4" | ||||
|             android:singleLine="true" | ||||
| 			android:inputType="number" | ||||
| 			android:text="22"  | ||||
| 			android:text="22" | ||||
| 			android:hint="@string/hint_sftp_port" /> | ||||
|  </LinearLayout> | ||||
|  <EditText | ||||
| @@ -63,6 +65,14 @@ | ||||
|             android:singleLine="true" | ||||
| 			android:text="/home/philipp" | ||||
| 			 /> | ||||
| 		<EditText | ||||
| 			android:id="@+id/sftp_connect_timeout" | ||||
| 			android:layout_width="fill_parent" | ||||
| 			android:layout_height="wrap_content" | ||||
| 			android:singleLine="true" | ||||
| 			android:inputType="number" | ||||
| 			android:text="" | ||||
| 		android:hint="@string/hint_sftp_connect_timeout" /> | ||||
| 		<Button android:id="@+id/send_public_key" | ||||
| 			android:layout_width="fill_parent" | ||||
| 			android:layout_height="wrap_content" | ||||
|   | ||||
| @@ -333,6 +333,7 @@ | ||||
|  | ||||
| 	<string name="hint_sftp_host">host</string> | ||||
| 	<string name="hint_sftp_port">port</string> | ||||
|     <string name="hint_sftp_connect_timeout">connection timeout seconds</string> | ||||
|  | ||||
| 	<string name="select_storage_type">Select the storage type:</string> | ||||
|  | ||||
|   | ||||
| @@ -221,7 +221,6 @@ public class FileChooserActivity extends FragmentActivity { | ||||
|      | ||||
|     public static final String EXTRA_RESULT_FILE_EXISTS = CLASSNAME + ".result_file_exists"; | ||||
|      | ||||
|      | ||||
|  | ||||
|     /* | ||||
|      * CONTROLS | ||||
|   | ||||
| @@ -269,7 +269,6 @@ public class FragmentFiles extends Fragment implements | ||||
|                 FileChooserActivity.EXTRA_MAX_FILE_COUNT, 1000); | ||||
|         mFileAdapter = new BaseFileAdapter(getActivity(), mFilterMode, | ||||
|                 mIsMultiSelection); | ||||
|          | ||||
|  | ||||
|         /* | ||||
|          * History. | ||||
| @@ -2268,12 +2267,15 @@ public class FragmentFiles extends Fragment implements | ||||
|             } | ||||
|  | ||||
|             if (mIsSaveDialog) { | ||||
|                 mTextSaveas.setText(BaseFileProviderUtils.getFileName(cursor)); | ||||
|                 String fileName = BaseFileProviderUtils.getFileName(cursor); | ||||
|                 Uri uri = BaseFileProviderUtils.getUri(cursor); | ||||
|  | ||||
|                 mTextSaveas.setText(fileName); | ||||
|                 /* | ||||
|                  * Always set tag after setting text, or tag will be reset to | ||||
|                  * null. | ||||
|                  */ | ||||
|                 mTextSaveas.setTag(BaseFileProviderUtils.getUri(cursor)); | ||||
|                 mTextSaveas.setTag(uri); | ||||
|             } | ||||
|  | ||||
|             if (mDoubleTapToChooseFiles) { | ||||
| @@ -2286,10 +2288,12 @@ public class FragmentFiles extends Fragment implements | ||||
|                 if (mIsMultiSelection) | ||||
|                     return; | ||||
|  | ||||
|                 if (mIsSaveDialog) | ||||
|                 if (mIsSaveDialog) { | ||||
|                     checkSaveasFilenameAndFinish(); | ||||
|                 else | ||||
|                 } | ||||
|                 else { | ||||
|                     finish(BaseFileProviderUtils.getUri(cursor)); | ||||
|                 } | ||||
|             }// single tap to choose files | ||||
|         }// onItemClick() | ||||
|     };// mViewFilesOnItemClickListener | ||||
|   | ||||
| @@ -15,4 +15,24 @@ public class FileEntry { | ||||
| 		isDirectory = false; | ||||
| 		canRead = canWrite = true; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public String toString() { | ||||
| 		StringBuilder s = new StringBuilder("kp2afilechooser.FileEntry{") | ||||
| 				.append(displayName).append("|") | ||||
| 				.append("path=").append(path).append(",sz=").append(sizeInBytes) | ||||
| 				.append(",").append(isDirectory ? "dir" : "file") | ||||
| 				.append(",lastMod=").append(lastModifiedTime); | ||||
|  | ||||
| 		StringBuilder perms = new StringBuilder(); | ||||
| 		if (canRead) | ||||
| 			perms.append("r"); | ||||
| 		if (canWrite) | ||||
| 			perms.append("w"); | ||||
| 		if (perms.length() > 0) { | ||||
| 			s.append(",").append(perms); | ||||
| 		} | ||||
|  | ||||
| 		return s.append("}").toString(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,7 @@ public class Kp2aFileChooserBridge { | ||||
| 		.buildUpon() | ||||
| 		.appendPath(defaultPath) | ||||
| 		.build()); | ||||
|  | ||||
| 		return intent; | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -306,10 +306,9 @@ public abstract class Kp2aFileProvider extends BaseFileProvider { | ||||
| 	            String parentPath = getParentPath(path); | ||||
| 	             | ||||
|         		 | ||||
|         		if (parentPath == null) | ||||
| 	            { | ||||
|         		if (parentPath == null) { | ||||
|         			if (Utils.doLog()) | ||||
|         				Log.d(CLASSNAME, "parent file is null"); | ||||
|                         Log.d(CLASSNAME, "parent file is null"); | ||||
| 	                return null; | ||||
| 	            } | ||||
|                 FileEntry e; | ||||
| @@ -501,10 +500,10 @@ public abstract class Kp2aFileProvider extends BaseFileProvider { | ||||
| 		RowBuilder newRow = matrixCursor.newRow(); | ||||
| 		newRow.add(id);// _ID | ||||
| 		newRow.add(BaseFile | ||||
| 		        .genContentIdUriBase( | ||||
| 		                getAuthority()) | ||||
| 		        .buildUpon().appendPath(f.path) | ||||
| 		        .build().toString()); | ||||
|                 .genContentIdUriBase( | ||||
|                         getAuthority()) | ||||
|                 .buildUpon().appendPath(f.path) | ||||
|                 .build().toString()); | ||||
| 		newRow.add(f.path); | ||||
| 		if (f.displayName == null) | ||||
| 			Log.w("KP2AJ", "displayName is null for " + f.path); | ||||
| @@ -549,7 +548,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider { | ||||
|     //puts the file entry in the cache for later reuse with retrieveFileInfo | ||||
| 	private void updateFileEntryCache(FileEntry f) { | ||||
| 		if (f != null) | ||||
| 			fileEntryMap.put(f.path, f); | ||||
|             fileEntryMap.put(f.path, f); | ||||
| 	} | ||||
| 	//removes the file entry from the cache (if cached). Should be called whenever the file changes | ||||
| 	private void removeFromCache(String filename, boolean recursive) { | ||||
| @@ -584,7 +583,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider { | ||||
|  | ||||
| 	//returns the file entry from the cache if present or queries the concrete provider method to return the file info | ||||
|     private FileEntry getFileEntryCached(String filename) { | ||||
|     	//check if enry is cached: | ||||
|     	//check if entry is cached: | ||||
|     	FileEntry cachedEntry = fileEntryMap.get(filename); | ||||
|     	if (cachedEntry != null) | ||||
|     	{ | ||||
| @@ -728,7 +727,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider { | ||||
|         if (targetParent != null && targetParent.startsWith(source)) | ||||
|         { | ||||
|         	if (Utils.doLog()) | ||||
|         		Log.d("KP2A_FC_P", source+" is parent of "+target); | ||||
|                 Log.d("KP2A_FC_P", source + " is parent of " + target); | ||||
|             return BaseFileProviderUtils.newClosedCursor(); | ||||
|         } | ||||
|         if (Utils.doLog()) | ||||
| @@ -768,28 +767,37 @@ public abstract class Kp2aFileProvider extends BaseFileProvider { | ||||
|  | ||||
|     private String getParentPath(String path) | ||||
|     { | ||||
|     	path = removeTrailingSlash(path); | ||||
|     	if (path.indexOf("://") == -1) | ||||
|     	{ | ||||
|     		Log.d("KP2A_FC_P", "invalid path: " + path); | ||||
|     		return null;  | ||||
|     	} | ||||
|     	String pathWithoutProtocol = path.substring(path.indexOf("://")+3); | ||||
|     	int lastSlashPos = path.lastIndexOf("/"); | ||||
|     	if (pathWithoutProtocol.indexOf("/") == -1) | ||||
|     	{ | ||||
|     		Log.d("KP2A_FC_P", "parent of " + path +" is null"); | ||||
|     		return null; | ||||
|     	} | ||||
|     	else | ||||
|     	{ | ||||
|     		String parent = path.substring(0, lastSlashPos)+"/"; | ||||
|     		Log.d("KP2A_FC_P", "parent of " + path +" is "+parent); | ||||
|     		return parent; | ||||
|     	} | ||||
|         String params = null; | ||||
|         int paramsIdx = path.lastIndexOf("?"); | ||||
|         if (paramsIdx > 0) { | ||||
|             params = path.substring(paramsIdx); | ||||
|             path = path.substring(0, paramsIdx); | ||||
|         } | ||||
|  | ||||
|         path = removeTrailingSlash(path); | ||||
|         if (path.indexOf("://") == -1) | ||||
|         { | ||||
|             Log.d("KP2A_FC_P", "invalid path: " + path); | ||||
|             return null; | ||||
|         } | ||||
|         String pathWithoutProtocol = path.substring(path.indexOf("://") + 3); | ||||
|         int lastSlashPos = path.lastIndexOf("/"); | ||||
|         if (pathWithoutProtocol.indexOf("/") == -1) | ||||
|         { | ||||
|             Log.d("KP2A_FC_P", "parent of " + path + " is null"); | ||||
|             return null; | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             String parent = path.substring(0, lastSlashPos) + "/"; | ||||
|             if (params != null) { | ||||
|                 parent += params; | ||||
|             } | ||||
|             Log.d("KP2A_FC_P", "parent of " + path +" is " + parent); | ||||
|             return parent; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|      | ||||
|  | ||||
| 	protected abstract FileEntry getFileEntry(String path, StringBuilder errorMessageBuilder) throws Exception; | ||||
|      | ||||
|   | ||||
| @@ -90,13 +90,17 @@ namespace keepass2android | ||||
|  | ||||
|             if (!defaultPath.EndsWith(_schemeSeparator)) | ||||
| 		    { | ||||
| 		        var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext); | ||||
| 		        var fileStorage = new SftpStorage(activity.ApplicationContext); | ||||
|                 SftpStorage.ConnectionInfo ci = fileStorage.SplitStringToConnectionInfo(defaultPath); | ||||
| 		        dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text = ci.Host; | ||||
| 		        dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text = ci.Port.ToString(); | ||||
| 		        dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text = ci.Username; | ||||
| 		        dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text = ci.Password; | ||||
| 		        dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text = ci.LocalPath; | ||||
| 		        if (ci.ConnectTimeoutSec != SftpStorage.UnsetSftpConnectTimeout) | ||||
| 		        { | ||||
| 			        dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text = ci.ConnectTimeoutSec.ToString(); | ||||
| 		        } | ||||
| 		        if (string.IsNullOrEmpty(ci.Password)) | ||||
| 		        { | ||||
| 		            spinner.SetSelection(1); | ||||
| @@ -109,7 +113,7 @@ namespace keepass2android | ||||
| 									  { | ||||
| 										  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; | ||||
| 										  int port = SftpStorage.DefaultSftpPort; | ||||
| 										  if (!string.IsNullOrEmpty(portText)) | ||||
| 											  int.TryParse(portText, out port); | ||||
| 										  string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text; | ||||
| @@ -117,8 +121,14 @@ namespace keepass2android | ||||
| 										  string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text; | ||||
| 									      if (string.IsNullOrEmpty(initialPath)) | ||||
| 									          initialPath = "/"; | ||||
|                                           string sftpPath = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext).BuildFullPath(host, port, initialPath, user, | ||||
| 																										  password); | ||||
| 									      string connectTimeoutText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text; | ||||
| 									      int connectTimeout = SftpStorage.UnsetSftpConnectTimeout; | ||||
| 									      if (!string.IsNullOrEmpty(connectTimeoutText)) { | ||||
| 									          int.TryParse(connectTimeoutText, out connectTimeout); | ||||
| 									      } | ||||
|  | ||||
|                                           string sftpPath = new SftpStorage(activity.ApplicationContext) | ||||
|                                               .BuildFullPath(host, port, initialPath, user, password, connectTimeout); | ||||
| 										  onStartBrowse(sftpPath); | ||||
| 									  }); | ||||
| 			EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel()); | ||||
|   | ||||
| @@ -88,6 +88,18 @@ | ||||
|             android:singleLine="true" | ||||
| 			android:text="/" | ||||
| 			 /> | ||||
| 		 | ||||
|  | ||||
| 		<TextView android:id="@+id/connect_timeout" | ||||
| 		    android:layout_width="wrap_content" | ||||
| 		    android:layout_height="wrap_content" | ||||
| 		    android:layout_marginLeft="4dip" | ||||
| 		    android:layout_marginTop="4dip" | ||||
| 		    android:text="@string/connect_timeout" /> | ||||
| 	    <EditText | ||||
| 			android:id="@+id/sftp_connect_timeout" | ||||
| 			android:layout_width="fill_parent" | ||||
| 			android:layout_height="wrap_content" | ||||
| 			android:singleLine="true" | ||||
| 			android:inputType="number" /> | ||||
|  | ||||
| </LinearLayout> | ||||
| @@ -586,6 +586,7 @@ | ||||
|   <string name="hint_sftp_host">host (ex: 192.168.0.1)</string> | ||||
| 	<string name="hint_sftp_port">port</string> | ||||
| 	<string name="initial_directory">Initial directory (optional):</string> | ||||
|     <string name="connect_timeout">Connection timeout seconds (optional):"</string> | ||||
| 	<string name="enter_sftp_login_title">Enter SFTP login data:</string> | ||||
|   <string name="sftp_auth_mode">Authentication mode</string> | ||||
|   <string name="send_public_key">Send public key...</string> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Rick Brown
					Rick Brown