thonnor thonnor - 2 months ago 19
Apache Configuration Question

Apache FTP Client decide to use SSL or raw FTP

Where I work we have a vast server estate that transfer files around using in-house software which uses FTP. There is an ongoing plan to upgrade all FTP to use FTPS and SSL certificates.

At the time of writing this question, I am working on a project to modernize our file transfer software (it is quite old) using Java/Apache.

I've written an FTP Client using the Apache software and have also written a very similar FTPS client. Both work as expected.

However, if the FTPS client attempts to connect to a non-FTPS server it throws an SSLException. The FTP client works fine in this situation.

Ultimately I'd like to rationalize both Clients into a single client that can manage FTP & FTPS connections.

My question is simply:

Is there a way using Apache/Java to detect which protocol is in use on the remote server before attempting the SSL connection?

An even better solution would be to emulate cURL and try FTPS and drop down to FTP.

Any help or suggestions would be greatly appreciated.

Code below

package jtm.ftp.client;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLException;

import jtm.common.JtmConstants;

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;
import org.apache.commons.net.util.TrustManagerUtils;
import org.apache.log4j.Logger;

public class JtmFTPSClient
{
static Logger log = Logger.getLogger( JtmFTPSClient.class.getName( ) );

private static int PORT = JtmConstants.FTP_PORT;

private static FTPSClient ftps = null;

private static boolean IS_IMPLICIT = false;


public static boolean getOrPut( String correlationID, String hostname, String user, String pass, String remoteFile, String localFile, String mode, String direction )
{
ftps = new FTPSClient( IS_IMPLICIT ); //look at using a constructor here perhaps - see the SWIPE FTPSClient

boolean ftpsSuccess = false;

log.debug( correlationID + " FTPS GET initiated" );

try
{
if( setSslConfig( ) == false )
{
return false;
}

String fileTermName = new File( remoteFile ).getName();

/*
* Configuration Section - source this from the FTPConfig object ** that needs to be made more flexible
*/

log.info( correlationID + " FTPS Server Address : " + hostname );
log.info( correlationID + " FTPS Port Number : " + PORT );
log.info( correlationID + " FTPS remoteFile : " + remoteFile );
log.info( correlationID + " FTPS localFile : " + localFile );
log.info( correlationID + " FTPS remoteTermName : " + fileTermName );


// try to connect
ftps.connect( hostname, PORT );

/*
* Once connected we need to login to the server via username/password
*/
if ( !ftps.login( user, pass ) )
{
log.error( correlationID + " FTPS Unable to log into : " + hostname + " with supplied credentials" );

return false;
}

int reply = ftps.getReplyCode();

log.debug( correlationID + " FTPS Login Reply Code : " + reply );

/*
* http://forus.com/csm/ftps/ trying to SSL the crap out of this client
*/
ftps.execPBSZ( 0 );
ftps.execPROT( "P" );

/*
* FTPReply stores a set of constants for FTP reply codes.
*/
if ( !FTPReply.isPositiveCompletion( reply ) )
{
log.error( correlationID + " FTPS Not a positive reply from " + hostname + " : " + reply);

return false;
}

log.info( correlationID + " FTPS Logged into to Remote Host : " + hostname );

ftps.enterLocalPassiveMode();

log.debug( correlationID + " FTPS Entered Local Passive Mode");


int xferMode = FTP.BINARY_FILE_TYPE;

if( mode.equalsIgnoreCase("b") ) {

xferMode = FTP.BINARY_FILE_TYPE;

}
else if( mode.equalsIgnoreCase("t")) {

xferMode = FTP.ASCII_FILE_TYPE;
}
else
{
xferMode = FTP.BINARY_FILE_TYPE;
}

log.debug(correlationID + " Mode for FTP : " + xferMode );


// Set the buffer size to cope with larger files
ftps.setBufferSize( 1024 * 1024 );


log.debug( correlationID + " FTPS Set Buffer Size to : " + ftps.getBufferSize( ) );
log.debug( correlationID + " FTPS Remote system type : " + ftps.getSystemType( ) );
log.debug( correlationID + " FTPS Remote directory is : " + ftps.printWorkingDirectory( ) );

log.info( correlationID + " FTPS Remote file is " + remoteFile );

// Get output stream - This is where the file will be downloaded to

if(direction.equalsIgnoreCase( "get" ) )
{
OutputStream downloadedFile = new FileOutputStream( localFile );

/*
* TODO Check that the remote file exists before download
*
* TODO Also a check that a local copy of the file does not already exist.
*/


/*
* GET the file from the remote system ( remoteFile, downloadedFile )
*/
ftpsSuccess = ftps.retrieveFile( remoteFile, downloadedFile );


/*
* close output stream
*/
downloadedFile.close();


log.info( correlationID + " FTPS Retrieval Complete for " + remoteFile );
}
else if( direction.equalsIgnoreCase("put"))
{
FileInputStream file = new FileInputStream( localFile );
ftpsSuccess = ftps.storeFile(remoteFile, file );

file.close();
}


if( ftpsSuccess == false )
{
log.error( correlationID + "FTPS Success is False" );
}
}
catch( SSLException e )
{
log.error("SSLException caught ", e );

/*
* Do we drop down to basic FTP here and try the transfer again?
*/
//return JtmFTPClient.get(correlationID, hostname, user, pass, remoteFile, localFile, mode);

ftpsSuccess = false;

}
catch ( FileNotFoundException e )
{
log.error( correlationID + " FileNotFoundException caught ", e );


ftpsSuccess = false;
}
catch ( GeneralSecurityException e )
{
log.error( correlationID + " GeneralSecurityException caught ", e );

ftpsSuccess = false;
}
catch ( IOException e )
{
log.error( correlationID + " IOException caught ", e );


ftpsSuccess = false;
}
finally
{
try
{
ftps.logout();
ftps.disconnect();

}
catch ( IOException e )
{
log.error(correlationID + " IOException caught closing", e );
}
}

log.debug(correlationID + " FTPS result : " + ftpsSuccess );

return ftpsSuccess;
}




/**
* A Method that configures the SSL requirements when FTP'ing files to/from secure instances of UTM
*
* @param isSSL
* @return boolean
* @throws IOException
* @throws GeneralSecurityException
*/
private static boolean setSslConfig( ) throws IOException, GeneralSecurityException
{

String trustStorePath = JtmConstants.TRUST_STORE_PATH;
String trustStorePass = JtmConstants.TRUST_STORE_PASS;

String keyStorePath = JtmConstants.KEY_STORE_PATH;
String keyStorePass = JtmConstants.KEY_STORE_PASS;

String keyPass = JtmConstants.KEY_PASS;
String keyAlias = JtmConstants.KEY_ALIAS;

boolean isSslRequired = true;


if ( isSslRequired )
{
if ( trustStorePath != null && trustStorePass != null )
{
KeyStore ks = KeyStore.getInstance( "JKS" );
ks.load( new FileInputStream( trustStorePath ), trustStorePass.toCharArray( ) );

ftps.setTrustManager( TrustManagerUtils.getDefaultTrustManager( ks ) );
}
else
{
log.error( "Error setting up Trust Store" );
log.error( "Trust Store path or trust store passord have not been supplied." );

return false;
}
}
else
{
ftps.setTrustManager( TrustManagerUtils.getAcceptAllTrustManager( ) );
}


if ( keyStorePath != null && keyStorePass != null )
{

File keyFile = new File( keyStorePath );

KeyManager keyManager;

if ( keyAlias != null )
{
if ( keyPass != null )
{
keyManager = org.apache.commons.net.util.KeyManagerUtils
.createClientKeyManager( "JKS", keyFile,
keyStorePass,
keyAlias,
keyPass );
}
else
{
keyManager = org.apache.commons.net.util.KeyManagerUtils
.createClientKeyManager( keyFile, keyStorePass, keyAlias );
}
}
else
{
keyManager = org.apache.commons.net.util.KeyManagerUtils
.createClientKeyManager( keyFile, keyStorePass );
}

ftps.setKeyManager( keyManager );

return true;
}
else
{
log.error( "Error setting up Key Store" );
log.error( "Key Store path or key store passord have not been supplied." );

return false;
}
}
}

Answer

Do not do this. You just pretend security.

There are many ways, you can get hacked, if you allow an automatic downgrade to unsecured connection. Two obvious ways at least:

  • An attacker just needs to redirect DNS lookup to malicious unsecured server and you never know that you just lost your credentials.

  • An attacker can just simulate failure of the AUTH command (happening before connection gets secured). You automatically downgrade to unsecured connection, again showing your credentials in plain text to the attacker.


Anyway, just try the FTPSClient, if that fails with the SSLException, use the FTPClient.

If you really need a nice solution (without reconnecting) for an explicit TLS/SSL, see how the FTPSClient._connectAction_() is implemented. You can reimplement it to call base the FTPClient._connectAction_() and just try sendCommand(CMD_AUTH, auth), without throwing. Of course do not call the sslNegotiation(), if the AUTH fails.