432 lines
10 KiB
Java
432 lines
10 KiB
Java
package keepass2android.javafilestorage;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.InputStream;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import com.jcraft.jsch.Channel;
|
|
import com.jcraft.jsch.ChannelSftp;
|
|
import com.jcraft.jsch.ChannelSftp.LsEntry;
|
|
import com.jcraft.jsch.JSch;
|
|
import com.jcraft.jsch.JSchException;
|
|
import com.jcraft.jsch.Session;
|
|
import com.jcraft.jsch.SftpATTRS;
|
|
import com.jcraft.jsch.SftpException;
|
|
import com.jcraft.jsch.UserInfo;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.os.Bundle;
|
|
|
|
public class SftpStorage extends JavaFileStorageBase {
|
|
|
|
public static final int DEFAULT_SFTP_PORT = 22;
|
|
JSch jsch;
|
|
|
|
class ConnectionInfo
|
|
{
|
|
String host;
|
|
String username;
|
|
String password;
|
|
String localPath;
|
|
int port;
|
|
}
|
|
|
|
public SftpStorage() {
|
|
|
|
}
|
|
|
|
private static final String SFTP_PROTOCOL_ID = "sftp";
|
|
|
|
@Override
|
|
public boolean checkForFileChangeFast(String path,
|
|
String previousFileVersion) throws Exception {
|
|
String currentVersion = getCurrentFileVersionFast(path);
|
|
if (currentVersion == null)
|
|
return false;
|
|
return currentVersion.equals(previousFileVersion) == false;
|
|
}
|
|
|
|
@Override
|
|
public String getCurrentFileVersionFast(String path) {
|
|
|
|
return null; // no simple way to get the version "fast"
|
|
}
|
|
|
|
@Override
|
|
public InputStream openFileForRead(String path) throws Exception {
|
|
|
|
ChannelSftp c = init(path);
|
|
|
|
try {
|
|
byte[] buff = new byte[8000];
|
|
|
|
int bytesRead = 0;
|
|
|
|
InputStream in = c.get(extractSessionPath(path));
|
|
ByteArrayOutputStream bao = new ByteArrayOutputStream();
|
|
|
|
while ((bytesRead = in.read(buff)) != -1) {
|
|
bao.write(buff, 0, bytesRead);
|
|
}
|
|
|
|
byte[] data = bao.toByteArray();
|
|
|
|
ByteArrayInputStream bin = new ByteArrayInputStream(data);
|
|
c.getSession().disconnect();
|
|
|
|
return bin;
|
|
|
|
} catch (Exception e) {
|
|
tryDisconnect(c);
|
|
throw convertException(e);
|
|
}
|
|
}
|
|
|
|
private void tryDisconnect(ChannelSftp c) {
|
|
try {
|
|
c.getSession().disconnect();
|
|
} catch (JSchException je) {
|
|
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void uploadFile(String path, byte[] data, boolean writeTransactional)
|
|
throws Exception {
|
|
|
|
ChannelSftp c = init(path);
|
|
try {
|
|
InputStream in = new ByteArrayInputStream(data);
|
|
String targetPath = extractSessionPath(path);
|
|
if (writeTransactional)
|
|
{
|
|
//upload to temporary location:
|
|
String tmpPath = targetPath+".tmp";
|
|
c.put(in, tmpPath);
|
|
//remove previous file:
|
|
try
|
|
{
|
|
c.rm(targetPath);
|
|
}
|
|
catch (SftpException e)
|
|
{
|
|
//ignore. Can happen if file didn't exist before
|
|
}
|
|
//rename tmp to target path:
|
|
c.rename(tmpPath, targetPath);
|
|
}
|
|
else
|
|
{
|
|
c.put(in, targetPath);
|
|
}
|
|
|
|
tryDisconnect(c);
|
|
} catch (Exception e) {
|
|
tryDisconnect(c);
|
|
throw e;
|
|
}
|
|
|
|
}
|
|
|
|
@Override
|
|
public String createFolder(String parentPath, String newDirName)
|
|
throws Exception {
|
|
|
|
try {
|
|
ChannelSftp c = init(parentPath);
|
|
String newPath = concatPaths(parentPath, newDirName);
|
|
c.mkdir(extractSessionPath(newPath));
|
|
tryDisconnect(c);
|
|
return newPath;
|
|
} catch (Exception e) {
|
|
throw convertException(e);
|
|
}
|
|
|
|
}
|
|
|
|
private String extractSessionPath(String newPath) {
|
|
String withoutProtocol = newPath
|
|
.substring(getProtocolPrefix().length());
|
|
return withoutProtocol.substring(withoutProtocol.indexOf("/"));
|
|
}
|
|
|
|
private String extractUserPwdHost(String path) {
|
|
String withoutProtocol = path
|
|
.substring(getProtocolPrefix().length());
|
|
return withoutProtocol.substring(0,withoutProtocol.indexOf("/"));
|
|
}
|
|
|
|
private String concatPaths(String parentPath, String newDirName) {
|
|
String res = parentPath;
|
|
if (!res.endsWith("/"))
|
|
res += "/";
|
|
res += newDirName;
|
|
return res;
|
|
}
|
|
|
|
@Override
|
|
public String createFilePath(String parentPath, String newFileName)
|
|
throws Exception {
|
|
if (parentPath.endsWith("/") == false)
|
|
parentPath += "/";
|
|
return parentPath + newFileName;
|
|
}
|
|
|
|
@Override
|
|
public List<FileEntry> listFiles(String parentPath) throws Exception {
|
|
|
|
ChannelSftp c = init(parentPath);
|
|
return listFiles(parentPath, c);
|
|
|
|
}
|
|
|
|
private void setFromAttrs(FileEntry fileEntry, SftpATTRS attrs) {
|
|
fileEntry.isDirectory = attrs.isDir();
|
|
fileEntry.canRead = true; // currently not inferred from the
|
|
// permissions.
|
|
fileEntry.canWrite = true; // currently not inferred from the
|
|
// permissions.
|
|
fileEntry.lastModifiedTime = ((long) attrs.getMTime()) * 1000;
|
|
if (fileEntry.isDirectory)
|
|
fileEntry.sizeInBytes = 0;
|
|
else
|
|
fileEntry.sizeInBytes = attrs.getSize();
|
|
}
|
|
|
|
private Exception convertException(Exception e) {
|
|
|
|
if (SftpException.class.isAssignableFrom(e.getClass()) )
|
|
{
|
|
SftpException sftpEx = (SftpException)e;
|
|
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);
|
|
try {
|
|
FileEntry fileEntry = new FileEntry();
|
|
String sessionPath = extractSessionPath(filename);
|
|
SftpATTRS attr = c.stat(sessionPath);
|
|
setFromAttrs(fileEntry, attr);
|
|
fileEntry.path = filename;
|
|
fileEntry.displayName = getFilename(sessionPath);
|
|
tryDisconnect(c);
|
|
return fileEntry;
|
|
} catch (Exception e) {
|
|
logDebug("Exception in getFileEntry! " + e);
|
|
tryDisconnect(c);
|
|
throw convertException(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void delete(String path) throws Exception {
|
|
|
|
ChannelSftp c = init(path);
|
|
delete(path, c);
|
|
}
|
|
|
|
private void delete(String path, ChannelSftp c) throws Exception {
|
|
String sessionLocalPath = extractSessionPath(path);
|
|
try {
|
|
if (c.stat(sessionLocalPath).isDir())
|
|
{
|
|
List<FileEntry> contents = listFiles(path, c);
|
|
for (FileEntry fe: contents)
|
|
{
|
|
delete(fe.path, c);
|
|
}
|
|
c.rmdir(sessionLocalPath);
|
|
}
|
|
else
|
|
{
|
|
c.rm(sessionLocalPath);
|
|
}
|
|
} catch (Exception e) {
|
|
tryDisconnect(c);
|
|
throw convertException(e);
|
|
}
|
|
|
|
}
|
|
|
|
private List<FileEntry> listFiles(String path, ChannelSftp c) throws Exception {
|
|
try {
|
|
List<FileEntry> res = new ArrayList<FileEntry>();
|
|
@SuppressWarnings("rawtypes")
|
|
java.util.Vector vv = c.ls(extractSessionPath(path));
|
|
if (vv != null) {
|
|
for (int ii = 0; ii < vv.size(); ii++) {
|
|
|
|
Object obj = vv.elementAt(ii);
|
|
if (obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry) {
|
|
LsEntry lsEntry = (com.jcraft.jsch.ChannelSftp.LsEntry) obj;
|
|
|
|
if ((lsEntry.getFilename().equals("."))
|
|
||(lsEntry.getFilename().equals(".."))
|
|
)
|
|
continue;
|
|
|
|
FileEntry fileEntry = new FileEntry();
|
|
fileEntry.displayName = lsEntry.getFilename();
|
|
fileEntry.path = createFilePath(path, fileEntry.displayName);
|
|
SftpATTRS attrs = lsEntry.getAttrs();
|
|
setFromAttrs(fileEntry, attrs);
|
|
res.add(fileEntry);
|
|
}
|
|
|
|
}
|
|
}
|
|
return res;
|
|
} catch (Exception e) {
|
|
tryDisconnect(c);
|
|
throw convertException(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void startSelectFile(
|
|
JavaFileStorage.FileStorageSetupInitiatorActivity activity,
|
|
boolean isForSave, int requestCode) {
|
|
activity.performManualFileSelect(isForSave, requestCode, getProtocolId());
|
|
}
|
|
|
|
@Override
|
|
protected String decode(String encodedString)
|
|
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 {
|
|
jsch = new JSch();
|
|
ConnectionInfo ci = splitStringToConnectionInfo(filename);
|
|
|
|
Session session = jsch.getSession(ci.username, ci.host, ci.port);
|
|
UserInfo ui = new SftpUserInfo(ci.password);
|
|
session.setUserInfo(ui);
|
|
|
|
session.connect();
|
|
|
|
Channel channel = session.openChannel("sftp");
|
|
channel.connect();
|
|
ChannelSftp c = (ChannelSftp) channel;
|
|
|
|
logDebug("success: init Sftp");
|
|
return c;
|
|
|
|
}
|
|
|
|
private ConnectionInfo splitStringToConnectionInfo(String filename)
|
|
throws UnsupportedEncodingException {
|
|
ConnectionInfo ci = new ConnectionInfo();
|
|
ci.host = extractUserPwdHost(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));
|
|
ci.host = ci.host.substring(ci.host.indexOf('@') + 1);
|
|
ci.port = DEFAULT_SFTP_PORT;
|
|
int portSeparatorIndex = ci.host.indexOf(":");
|
|
if (portSeparatorIndex >= 0)
|
|
{
|
|
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex+1));
|
|
ci.host = ci.host.substring(0, portSeparatorIndex);
|
|
}
|
|
return ci;
|
|
}
|
|
|
|
@Override
|
|
public void prepareFileUsage(JavaFileStorage.FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
|
|
Intent intent = new Intent();
|
|
intent.putExtra(EXTRA_PATH, path);
|
|
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
|
|
}
|
|
|
|
@Override
|
|
public String getProtocolId() {
|
|
return SFTP_PROTOCOL_ID;
|
|
}
|
|
|
|
@Override
|
|
public void onResume(JavaFileStorage.FileStorageSetupActivity setupAct) {
|
|
|
|
}
|
|
|
|
@Override
|
|
public boolean requiresSetup(String path) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(FileStorageSetupActivity activity,
|
|
Bundle savedInstanceState) {
|
|
|
|
}
|
|
|
|
@Override
|
|
public String getDisplayName(String path) {
|
|
try
|
|
{
|
|
ConnectionInfo ci = splitStringToConnectionInfo(path);
|
|
return getProtocolPrefix()+ci.username+"@"+ci.host+extractSessionPath(path);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
return extractSessionPath(path);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String getFilename(String path) throws Exception {
|
|
if (path.endsWith("/"))
|
|
path = path.substring(0, path.length()-1);
|
|
int lastIndex = path.lastIndexOf("/");
|
|
if (lastIndex >= 0)
|
|
return path.substring(lastIndex + 1);
|
|
else
|
|
return path;
|
|
}
|
|
|
|
@Override
|
|
public void onStart(FileStorageSetupActivity activity) {
|
|
|
|
}
|
|
|
|
@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;
|
|
|
|
}
|
|
|
|
@Override
|
|
public void prepareFileUsage(Context appContext, String path) {
|
|
//nothing to do
|
|
|
|
}
|
|
}
|