Search code examples
javaftpftps

Java - CopyStreamException due to SSLProtocolException when storing file's contents in FTP server


I'm facing an issue when trying to store a file in an FTP server. When trying to upload a file to the FTP server, the file is created but its contents are not copied.

Context

We use the following configuration to access the FTP server using lftp. I cannot change this configuration, and don't know why do we use FTPS with verify-certificates disabled.

# FTPS_OPTIONS:
set ssl:verify-certificate/testhostname no;
set ftp:ssl-protect-data yes;
set ftp:passive-mode on;

I need to store certain files from a Java application. I'm using apache-commons library. The implemented code looks like this:

@Autowired
public FtpService() {
  ftpsClient = new FTPSClient();
  ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
}

public void uploadFile(String ftpHost, File tempFile, String destination, String filename)
throws UploadException {

  ftpsClient.connect(ftpHost, 990);
  ftpsClient.execPBSZ(0);
  ftpsClient.execPROT("P");
  ftpsClient.enterLocalPassiveMode();
  ftpsClient.setKeepAlive(true);
  ftpsClient.setControlKeepAliveTimeout(3000);

  if(ftpsClient.login("user", "password")) {
    try (InputStream fileStream = new FileInputStream(tempFile)) {

      if (!ftpsClient.changeWorkingDirectory(destination)) {
        throwUploadException("Destination directory not available in FTP server");
      }

      boolean saved = ftpsClient.storeFile(filename, fileStream);
      // Following code is not executed since the exception is thrown in the previous line
      if (!saved) {
        throwUploadException("Unable to save file in FTP server");
      }
      log.info("Saved FTP file: {}/{}", destination, filename);
    }
    catch (UploadException | IOException e)
    {
      throwUploadException(e.getMessage());
    }
    finally
    {
      ftpsClient.disconnect();
      if (!tempFile.delete()) {
        log.warn("Unable to delete '{}' file", tempFile.getAbsolutePath());
      }
    }
  }
}

Problem

I started with a FTPClient (non FTPSClient) but this way I wasn't able to login.

Currently (FTPSClient), I can:

  • change the working directory
  • create directories in the FTP server

I cannot:

  • storeFile: this method throws the following exception, and creates the file in the FTP server, but this is empty

    org.apache.commons.net.io.CopyStreamException: IOException caught while copying.
    Cause: javax.net.ssl.SSLProtocolException: Software caused connection abort: socket write error
    
  • listFiles()/listDirectories(): when executing this command, the obtained list is always empty. The logged user has all the required permissions in the whole FTP server

Following is the FTP's log (note that I have translated the commands to English between parenthesis), corresponding to the code shown before, that raises the exception mentioned before:

er: testhostname:990
USER *******
331 Usuario testuser OK. Clave requerida ( = User testuser OK. Password required)
PASS *******
230 OK. El directorio restringido actual es / ( = OK. The current restricted directory is /)
CWD /test/upload
250 OK. El directorio actual es /test/upload ( = Ok. The current directory is /test/upload)
PASV
227 Entering Passive Mode (<numbers...>)
[Replacing PASV mode reply address <ip_address> with testhostname]
STOR dummyfile.txt
150 Conexi├│n de datos aceptada ( = Data connection accepted)

If there is anything else I can include to improve the description, please let me know. Thanks for your help!


Solution

  • I had a similar problem from python connecting to an FTPS server. The error was that the server required the data channel session to be the same as the control channel session(reuse the session). The solution was to override one of the methods to do that.

    You can test extending FTPClient.java and overriding the next method:

    @Override
    protected void _prepareDataSocket_(final Socket socket) {
         if(preferences.getBoolean("ftp.tls.session.requirereuse")) {
             if(socket instanceof SSLSocket) {
                 // Control socket is SSL
                 final SSLSession session = ((SSLSocket) _socket_).getSession();
                 if(session.isValid()) {
                     final SSLSessionContext context = session.getSessionContext();
                     context.setSessionCacheSize(preferences.getInteger("ftp.ssl.session.cache.size"));
                     try {
                         final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache");
                         sessionHostPortCache.setAccessible(true);
                         final Object cache = sessionHostPortCache.get(context);
                         final Method putMethod = cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
                         putMethod.setAccessible(true);
                         Method getHostMethod;
                         try {
                             getHostMethod = socket.getClass().getMethod("getPeerHost");
                         }
                         catch(NoSuchMethodException e) {
                             // Running in IKVM
                             getHostMethod = socket.getClass().getDeclaredMethod("getHost");
                         }
                         getHostMethod.setAccessible(true);
                         Object peerHost = getHostMethod.invoke(socket);
                         putMethod.invoke(cache, String.format("%s:%s", peerHost, socket.getPort()).toLowerCase(Locale.ROOT), session);
                     }
                     catch(NoSuchFieldException e) {
                         // Not running in expected JRE
                         log.warn("No field sessionHostPortCache in SSLSessionContext", e);
                     }
                     catch(Exception e) {
                         // Not running in expected JRE
                         log.warn(e.getMessage());
                     }
                 }
                 else {
                     log.warn(String.format("SSL session %s for socket %s is not rejoinable", session, socket));
                 }
             }
         }
     }
    

    I found this Java solution here: https://stackoverflow.com/a/32404418/19599290