/******************************************************************************
*
* Copyright (c) 1999-2003 AppGate Network Security AB. All Rights Reserved.
*
* This file contains Original Code and/or Modifications of Original Code as
* defined in and that are subject to the MindTerm Public Source License,
* Version 2.0, (the 'License'). You may not use this file except in compliance
* with the License.
*
* You should have received a copy of the MindTerm Public Source License
* along with this software; see the file LICENSE. If not, write to
* AppGate Network Security AB, Otterhallegatan 2, SE-41118 Goteborg, SWEDEN
*
*****************************************************************************/
package ru.naumen.servacc;
import com.mindbright.net.ftp.FTPException;
import com.mindbright.net.ftp.FTPServer;
import com.mindbright.net.ftp.FTPServerEventHandler;
import com.mindbright.ssh2.SSH2Connection;
import com.mindbright.ssh2.SSH2SFTP;
import com.mindbright.ssh2.SSH2SFTPClient;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a proxy which proxies between an ftp client and an sftp server.
*/
public class FTP2SFTPProxy implements FTPServerEventHandler
{
// FTP error codes
public static final int CANNOT_OPEN_CONNECTION = 425;
public static final int BAD_SEQUENCE_OF_COMMANDS = 503;
public static final int FILE_UNAVAILABLE = 550;
public static final int FILE_NAME_NOT_ALLOWED = 553;
private static final Logger LOG = LoggerFactory.getLogger(FTP2SFTPProxy.class);
private SSH2Connection connection;
private SSH2SFTPClient sftp;
private FTPServer ftp;
private String remoteDir;
private String renameFrom;
private String user;
private SSH2SFTP.FileAttributes attrs;
public FTP2SFTPProxy(SSH2Connection connection, InputStream ftpInput, OutputStream ftpOutput, String identity) throws SSH2SFTP.SFTPException
{
initSFTP(connection);
initFTP(ftpInput, ftpOutput, identity, false);
}
/**
* Connect this instance with an <code>SSH2Connection</code> which is
* connected to the server we want to transfer files to/from.
*
* @param connection Established connection to the server.
*/
private void initSFTP(SSH2Connection connection) throws SSH2SFTP.SFTPException
{
this.connection = connection;
this.attrs = null;
try
{
this.sftp = new SSH2SFTPClient(connection, false);
}
catch (SSH2SFTP.SFTPException e)
{
if (ftp != null)
{
ftp.terminate();
}
throw e;
}
}
/**
* Initialize the FTP server portion of this class.
*
* @param ftpInput The ftp command input stream.
* @param ftpOutput The ftp command output stream.
* @param identity Username to log in as
* @param needPassword Tells the instance if it should request a password or not from
* the user. The actual password the user then gives is ignored.
*/
private void initFTP(InputStream ftpInput, OutputStream ftpOutput, String identity, boolean needPassword)
{
this.ftp = new FTPServer(identity, this, ftpInput, ftpOutput, needPassword);
}
/**
* Login to server. This is actually a null operation for this class since
* the user is already authenticated as part of the SSH connection.
*
* @param user Username to login as.
* @param pass Password.
* @return Returns true if the login was successful.
*/
@Override
public boolean login(String user, String pass)
{
connection.getLog().notice("SSH2FTPOverSFTP", "user " + user + " login");
try
{
attrs = sftp.realpath(".");
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to login as '%s'", user), e);
// !!! TODO, should disconnect ???
return false;
}
remoteDir = "/";
this.user = user;
return true;
}
@Override
public void quit()
{
connection.getLog().notice("SSH2FTPOverSFTP", "user " + user + " logout");
sftp.terminate();
}
@Override
public boolean isPlainFile(String file)
{
try
{
attrs = sftp.lstat(expandRemote(file));
return attrs.isFile();
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to check file '%s", file), e);
return false;
}
}
@Override
public void changeDirectory(String dir) throws FTPException
{
if (dir != null)
{
String newDir = expandRemote(dir);
try
{
attrs = sftp.realpath(newDir);
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to get realpath of directory '%s'", newDir), e);
throw new FTPException(FILE_UNAVAILABLE, dir + ": No such directory.");
}
newDir = attrs.lname;
try
{
SSH2SFTP.FileHandle f = sftp.opendir(newDir);
sftp.close(f);
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to open directory '%s'", newDir), e);
throw new FTPException(FILE_UNAVAILABLE, dir + ": Not a directory.");
}
remoteDir = newDir;
}
}
@Override
public void renameFrom(String from) throws FTPException
{
String fPath = "";
try
{
fPath = expandRemote(from);
attrs = sftp.lstat(fPath);
renameFrom = fPath;
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to rename file '%s' into '%s'", from, fPath), e);
throw new FTPException(FILE_UNAVAILABLE, from + ": No such file or directory.");
}
}
@Override
public void renameTo(String to) throws FTPException
{
if (renameFrom != null)
{
try
{
sftp.rename(renameFrom, expandRemote(to));
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to rename file '%s' into '%s'", renameFrom, to), e);
throw new FTPException(FILE_UNAVAILABLE, "rename: Operation failed.");
}
finally
{
renameFrom = null;
}
}
else
{
throw new FTPException(BAD_SEQUENCE_OF_COMMANDS, "Bad sequence of commands.");
}
}
@Override
public void delete(String file) throws FTPException
{
try
{
sftp.remove(expandRemote(file));
}
catch (SSH2SFTP.SFTPPermissionDeniedException e)
{
LOG.error(String.format("Failed to delete file '%s'. Access denied", file), e);
throw new FTPException(FILE_UNAVAILABLE, "access denied");
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to delete file '%s'. No such file", file), e);
throw new FTPException(FILE_UNAVAILABLE, file + ": no such file.");
}
}
@Override
public void rmdir(String dir) throws FTPException
{
try
{
sftp.rmdir(expandRemote(dir));
}
catch (SSH2SFTP.SFTPPermissionDeniedException e)
{
LOG.error(String.format("Failed to delete directory '%s'. Permission denied", dir), e);
throw new FTPException(FILE_UNAVAILABLE, "access denied");
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to delete directory '%s'. No such directory", dir), e);
throw new FTPException(FILE_UNAVAILABLE, dir + ": no such directory.");
}
}
@Override
public void mkdir(String dir) throws FTPException
{
try
{
sftp.mkdir(expandRemote(dir), new SSH2SFTP.FileAttributes());
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to create directory '%s'", dir), e);
// TODO: should we throw new exception here?
}
}
@Override
public String pwd()
{
return remoteDir;
}
@Override
public String system()
{
return "UNIX Type: L8";
}
@Override
public long modTime(String file) throws FTPException
{
return (timeAndSize(file))[0];
}
@Override
public long size(String file) throws FTPException
{
return (timeAndSize(file))[1];
}
private long[] timeAndSize(String file) throws FTPException
{
try
{
long[] ts = new long[2];
String fPath = expandRemote(file);
attrs = sftp.lstat(fPath);
if (!attrs.hasSize || !attrs.hasModTime)
{
throw new FTPException(FILE_UNAVAILABLE, "SFTP server don't return time/size.");
}
ts[0] = attrs.mtime * 1000L;
ts[1] = attrs.size;
return ts;
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to get file attributes for '%s'. No such file or directory", file), e);
throw new FTPException(FILE_UNAVAILABLE, file + ": No such file or directory.");
}
}
@Override
public void store(String file, InputStream data, boolean binary) throws FTPException
{
try
{
String expandedFile = expandRemote(file);
SSH2SFTP.FileHandle handle = sftp.open(expandedFile, SSH2SFTP.SSH_FXF_WRITE | SSH2SFTP.SSH_FXF_TRUNC |
SSH2SFTP.SSH_FXF_CREAT, new SSH2SFTP.FileAttributes());
sftp.writeFully(handle, data);
}
catch (IOException e)
{
LOG.error(String.format("Failed to store file '%s'. Error writing to data connection", file), e);
throw new FTPException(CANNOT_OPEN_CONNECTION, "Error writing to data connection: " + e.getMessage());
}
catch (SSH2SFTP.SFTPPermissionDeniedException e)
{
LOG.error(String.format("Failed to store file '%s'. Permission denied", file), e);
throw new FTPException(FILE_NAME_NOT_ALLOWED, file + ": Permission denied.");
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to store file '%s'. Error in SFTP connection", file), e);
throw new FTPException(FILE_UNAVAILABLE, file + ": Error in SFTP connection, " + e.getMessage());
}
finally
{
try
{
data.close();
}
catch (Exception e)
{
LOG.warn("Unexpected error while closing chanel", e);
}
}
}
@Override
public void retrieve(String file, OutputStream data, boolean binary) throws FTPException
{
try
{
String expandedFile = expandRemote(file);
SSH2SFTP.FileHandle handle = sftp.open(expandedFile, SSH2SFTP.SSH_FXF_READ, new SSH2SFTP.FileAttributes());
sftp.readFully(handle, data);
}
catch (SSH2SFTP.SFTPNoSuchFileException e)
{
LOG.error(String.format("Failed to retrieve file '%s'. No such file or directory", file), e);
throw new FTPException(FILE_UNAVAILABLE, file + ": No such file or directory.");
}
catch (SSH2SFTP.SFTPException | IOException e)
{
LOG.error(String.format("Failed to retrieve file '%s'. Error in SFTP connection", file), e);
throw new FTPException(FILE_UNAVAILABLE, file + ": Error in SFTP connection, " + e.getMessage());
}
finally
{
try
{
data.close();
}
catch (Exception e)
{
LOG.warn("Unexpected error while closing chanel", e);
}
}
}
private static String rightJustify(String s, int width)
{
String res = s;
while (res.length() < width)
{
res = " " + res;
}
return res;
}
@Override
public void list(String path, OutputStream data) throws FTPException
{
try
{
SSH2SFTP.FileAttributes[] list = dirList(path);
for (SSH2SFTP.FileAttributes attributes : list)
{
if (".".equals(attributes.name) || "..".equals(attributes.name))
{
continue;
}
StringBuilder str = new StringBuilder();
str.append(attributes.permString());
str.append(" 1 ");
str.append(rightJustify(Integer.toString(attributes.uid), 8));
str.append(" ");
str.append(rightJustify(Integer.toString(attributes.gid), 8));
str.append(" ");
str.append(rightJustify(Long.toString(attributes.size), 16));
str.append(" ");
str.append(attributes.mtime);
str.append(" ");
str.append(attributes.name);
String row = str.toString();
if (row.endsWith("/"))
{
row = row.substring(0, row.length() - 1);
}
row += "\r\n";
data.write(row.getBytes());
}
}
catch (IOException e)
{
LOG.error(String.format("Failed to list content of directory '%s'", path), e);
throw new FTPException(CANNOT_OPEN_CONNECTION, "Error writing to data connection: " + e.getMessage());
}
}
@Override
public void nameList(String path, OutputStream data) throws FTPException
{
try
{
SSH2SFTP.FileAttributes[] list = dirList(path);
for (SSH2SFTP.FileAttributes attributes : list)
{
if (".".equals(attributes.name) || "..".equals(attributes.name))
{
continue;
}
String row = attributes.name + "\r\n";
data.write(row.getBytes());
}
}
catch (IOException e)
{
LOG.error(String.format("Failed to name list at '%s'", path), e);
throw new FTPException(CANNOT_OPEN_CONNECTION, "Error writing to data connection: " + e.getMessage());
}
}
private SSH2SFTP.FileAttributes[] dirList(String path) throws FTPException
{
SSH2SFTP.FileHandle handle = null;
SSH2SFTP.FileAttributes[] list;
try
{
String fPath = expandRemote(path);
attrs = sftp.lstat(fPath);
if (attrs.isDirectory())
{
handle = sftp.opendir(fPath);
list = sftp.readdir(handle);
if (list != null)
{
for (SSH2SFTP.FileAttributes attributes : list)
{
attributes.lname = attributes.toString(attributes.name);
}
}
}
else
{
list = new SSH2SFTP.FileAttributes[1];
list[0] = new SSH2SFTP.FileAttributes();
list[0].name = path;
list[0].lname = attrs.toString(path);
}
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to list '%s'. Not a directory", path), e);
throw new FTPException(FILE_UNAVAILABLE, path + ": Not a directory.");
}
finally
{
try
{
if (handle != null)
{
sftp.close(handle);
}
}
catch (Exception e)
{
LOG.warn("Unexpected error while closing chanel", e);
}
}
return list;
}
@Override
public void abort()
{
}
private String expandRemote(String name)
{
if (name == null || name.length() == 0)
{
return remoteDir;
}
if (name.charAt(0) != '/')
{
name = remoteDir + "/" + name;
}
return name;
}
@Override
public void chmod(int mod, String file) throws FTPException
{
try
{
SSH2SFTP.FileAttributes fa = new SSH2SFTP.FileAttributes();
fa.permissions = mod;
fa.hasPermissions = true;
sftp.setstat(expandRemote(file), fa);
}
catch (SSH2SFTP.SFTPException e)
{
LOG.error(String.format("Failed to change mode bits of file '%s'", file), e);
}
}
}