8

Окружающая среда: Я использую Sun Java JDK 1.8.0_60 на 64-разрядной Windows 7, используя Spring Integration 4.1.6 (который, как предполагается, использует Apache Commons Net 3.3 для доступа к FTPS).Как подключиться к серверу FTPS с подключением к данным, используя ту же сессию TLS?

Я пытаюсь интегрировать с нашим приложением автоматическую загрузку с сервера FTPS нашего клиента. Я успешно справился с SFTP-серверами с помощью Spring Integration без каких-либо проблем для других клиентов без проблем, но это первый раз, когда клиент потребовал от нас использовать FTPS, и подключение к нему было очень озадачивающим. Хотя в моем реальном приложении я настраиваю Spring Integration с использованием XML-компонентов, чтобы попытаться понять, что не работает, я использую следующий тестовый код (хотя я анонимирую фактический хост/имя пользователя/пароль здесь):

final DefaultFtpsSessionFactory sessionFactory = new DefaultFtpsSessionFactory(); 
sessionFactory.setHost("XXXXXXXXX"); 
sessionFactory.setPort(990); 
sessionFactory.setUsername("XXXXXXX"); 
sessionFactory.setPassword("XXXXXXX"); 
sessionFactory.setClientMode(2); 
sessionFactory.setFileType(2); 
sessionFactory.setUseClientMode(true); 
sessionFactory.setImplicit(true); 
sessionFactory.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager()); 
sessionFactory.setProt("P"); 
sessionFactory.setProtocol("TLSv1.2"); 
sessionFactory.setProtocols(new String[]{"TLSv1.2"}); 
sessionFactory.setSessionCreation(true); 
sessionFactory.setCipherSuites(new String[]{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}); 

final FtpSession session = sessionFactory.getSession(); 
//try { 
    final FTPFile[] ftpFiles = session.list("/"); 
    logger.debug("FtpFiles: {}", (Object[]) ftpFiles); 
//} catch (Exception ignored) {} 
session.close(); 

Я запускаю этот код с -Djavax.net.debug=all, чтобы распечатать всю отладочную информацию TLS.

Основное «управляющее» соединение с сервером FTPS прекрасно работает, но когда он пытается открыть соединение для передачи данных (или любое другое соединение, которое я пытался), я получаю javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake, вызванный java.io.EOFException: SSL peer shut down incorrectly. Если я раскомментировать глотательный-исключения поймать блок вокруг команды session.list, то я могу увидеть (хотя выход javax.net.debug), что сервер послал следующее сообщение после отказа от соединения для передачи данных SSL рукопожатия:

main, READ: TLSv1.2 Application Data, length = 129 
Padded plaintext after DECRYPTION: len = 105 
0000: 34 35 30 20 54 4C 53 20 73 65 73 73 69 6F 6E 20 450 TLS session 
0010: 6F 66 20 64 61 74 61 20 63 6F 6E 6E 65 63 74 69 of data connecti 
0020: 6F 6E 20 68 61 73 20 6E 6F 74 20 72 65 73 75 6D on has not resum 
0030: 65 64 20 6F 72 20 74 68 65 20 73 65 73 73 69 6F ed or the sessio 
0040: 6E 20 64 6F 65 73 20 6E 6F 74 20 6D 61 74 63 68 n does not match 
0050: 20 74 68 65 20 63 6F 6E 74 72 6F 6C 20 63 6F 6E the control con 
0060: 6E 65 63 74 69 6F 6E 0D 0A      nection.. 

Что, как представляется, происходит (и это мой первый опыт работы с FTPS, хотя раньше я имел дело с простым FTP) заключается в том, что способ обеспечения подлинности и шифрования сервера как для управления, так и для данных - это то, что после «нормального» TLS-соединение для установления соединения управления и аутентификации происходит там, каждое подключение к данным требует, чтобы клиент подключался к одному сеансу TLS. Для меня это имеет смысл, так как это должно быть работа, но реализация Apache Commons Net FTPS, похоже, не делает этого. Кажется, он пытается установить новый сеанс TLS, и поэтому сервер отклоняет попытку.

На основании this question about resuming SSL sessions in JSSE, похоже, что Java предполагает или требует другого сеанса для каждой комбинации хоста/сообщения. Моя гипотеза заключается в том, что, поскольку соединение данных FTPS находится на другом порте, чем контрольное соединение, оно не находит существующий сеанс и пытается установить новый, поэтому соединение терпит неудачу.

Я вижу три основных возможностей:

  1. Сервера не ниже стандарта FTPS в требующем тот же сеанс TLS на порт данных, как на порт управления. Я могу подключиться к серверу отлично (используя тот же хост/пользователь/пароль, который я пытаюсь использовать в своем коде) с помощью FileZilla 3.13.1. Сервер идентифицирует себя как «FileZilla Server 0.9.53 beta» при входе в систему, поэтому, возможно, это какой-то проприетарный способ FileZilla делать вещи, и есть что-то странное, что мне нужно сделать, чтобы убедить Java использовать один и тот же сеанс TLS.
  2. Клиент Apache Commons Net фактически не соответствует стандарту FTPS и разрешает только некоторые подмножества, которые не позволяют обеспечить подключение к данным. Это показалось бы странным, поскольку он, по-видимому, является стандартным способом подключения к FTPS из Java.
  3. Я полностью что-то пропустил и неправильно разобрал это.

Буду признателен за любое направление, которое вы можете сообщить о том, как подключиться к этому серверу FTPS. Спасибо.

+0

См. эту ссылку для получения более подробной информации https://stackoverflow.com/questions/46631315/when-using-java-apache-ftpclient-for-ftp-tls-getting-remote-host-closed -connect/48616779 # 48616779 –

ответ

13

Действительно, некоторые серверы FTP (S) требуют повторного использования сеанса TLS/SSL для подключения к данным. Это мера безопасности, с помощью которой сервер может проверить, что соединение с данными используется тем же клиентом, что и управляющее соединение.

Некоторые ссылки на общих серверах FTP:


Что может помочь вам с реализацией т Cyberduck FTP (S) клиент не поддерживает TLS/SSL повторного сеанса и использует Apache Commons Net библиотеки:

  • https://trac.cyberduck.io/ticket/5087 - ключ Повторного использования сеанса подключения для передачи данных

  • См своего FTPClient.java кода (расширяющий Commons Net FTPSClient), в частности, его override of _prepareDataSocket_ method:

    @Override 
    protected void _prepareDataSocket_(final Socket socket) throws IOException { 
        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 method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class); 
            method.setAccessible(true); 
            method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostName(), 
              String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session); 
            method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostAddress(), 
              String.valueOf(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)); 
          } 
         } 
        } 
    } 
    
  • кажется, что метод _prepareDataSocket_ был добавлен в Commons Net FTPSClient специально, чтобы позволить реализацию TLS/SSL повторного сеанса:
    https://issues.apache.org/jira/browse/NET-426

    Уроженец поддержка повторного использования еще продолжается:
    https://issues.apache.org/jira/browse/NET-408

  • Вам, очевидно, нужно будет переопределить интеграцию Spring DefaultFtpsSessionFactory.createClientInstance(), чтобы вернуть вашу реализацию FTPSClient с поддержкой повторного использования сеанса.

+0

Большое вам спасибо. Я просматривал чистую JIRA Apache, но, по-видимому, не копал, насколько вы это делали. И пример кода из рабочего проекта выглядит отлично. –

+0

Добро пожаловать. У меня на самом деле была (та же проблема) (http://stackoverflow.com/q/7786352/850848) в прошлом (только в C++/OpenSSL, я не занимаюсь Java), поэтому я знал, для чего нужно google. –

+0

Не знаю почему, но это работает с версией openjdk «1.8.0_151» и не работает с версией openjdk «1.8.0_161» –

2

Чтобы сделать рекомендации работы Мартина Přikryl для меня должен был хранить ключ не только под socket.getInetAddress().getHostName(), но и под socket.getInetAddress().getHostAddress(). (Решение украдена из here.)

0

Вы можете использовать этот SSLSessionReuseFTPSClient класс:

import java.io.IOException; 
import java.lang.reflect.Field; 
import java.lang.reflect.Method; 
import java.net.Socket; 
import java.util.Locale; 

import javax.net.ssl.SSLSession; 
import javax.net.ssl.SSLSessionContext; 
import javax.net.ssl.SSLSocket; 

import org.apache.commons.net.ftp.FTPSClient; 

public class SSLSessionReuseFTPSClient extends FTPSClient { 

    // adapted from: 
    // https://trac.cyberduck.io/browser/trunk/ftp/src/main/java/ch/cyberduck/core/ftp/FTPClient.java 
    @Override 
    protected void _prepareDataSocket_(final Socket socket) throws IOException { 
     if (socket instanceof SSLSocket) { 
      // Control socket is SSL 
      final SSLSession session = ((SSLSocket) _socket_).getSession(); 
      if (session.isValid()) { 
       final SSLSessionContext context = session.getSessionContext(); 
       try { 
        final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache"); 
        sessionHostPortCache.setAccessible(true); 
        final Object cache = sessionHostPortCache.get(context); 
        final Method method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class); 
        method.setAccessible(true); 
        method.invoke(cache, String 
          .format("%s:%s", socket.getInetAddress().getHostName(), String.valueOf(socket.getPort())) 
          .toLowerCase(Locale.ROOT), session); 
        method.invoke(cache, String 
          .format("%s:%s", socket.getInetAddress().getHostAddress(), String.valueOf(socket.getPort())) 
          .toLowerCase(Locale.ROOT), session); 
       } catch (NoSuchFieldException e) { 
        throw new IOException(e); 
       } catch (Exception e) { 
        throw new IOException(e); 
       } 
      } else { 
       throw new IOException("Invalid SSL Session"); 
      } 
     } 
    } 
} 

И с OpenJDK 1.8.0_161:

Мы должны установить:

System.setProperty("jdk.tls.useExtendedMasterSecret", "false"); 

согласно http://www.oracle.com/technetwork/java/javase/8u161-relnotes-4021379.html

Добавлена ​​поддержка хэша сеанса TLS и расширенная поддержка расширенного основного таймера

В случае проблем с совместимостью приложение может отключить согласование этого расширения, установив свойство системы jdk.tls.useExtendedMasterSecret to false в JDK

 Смежные вопросы

  • Нет связанных вопросов^_^