FTPS传输文件:解决TLS session of data connection not resumed

背景

        最近要做一个通过FTPS上传文件到FTP服务端的功能,FTP服务端是搭建在Linux系统上的vsftpd。目前项目是有通过FTP传输文件的功能的,但没有走SSL/TLS加密协议,也就是不支持FTPS。

        本以为是个比较简单的修改,因为依赖的commons-io库,里面有现成的FTPSClient类。但实际开发调试过程中遇到一个比较棘手的问题:使用FTPClient连接FTP和登录都没有问题,但是上传文件失败,FTP服务端的报错是

425 Unable to build data connection: TLS session of data connection not resumed。(我使用的FileZilla Server作为服务端)

或者 522 SSL connection failed; session reuse required: see require_ssl_reuse option in vsftpd.conf man page。(这个是vsftpd的报错)

        经过研究这是由于FTP服务端开启SSL/TLS加密后,默认要求所有SSL数据连接都需要显示SSL会话重用,如果把这个设置关掉,就可以正常传输。(我用的FileZilla Server 1.11.1版本,并没有控制这个功能开启/关闭的设置项。后来换了FileZilla Server中文版,对应的设置项是“在SSL/TLS模式强制使用“PORT P”加密数据通道传输文件”。vsftpd在配置文件中用require_ssl_reuse控制。)

        “显示SSL会话重用”的大概的意思就连接和上传文件都得用一个SSL会话,而目前的FTPSClient在上传文件的时候根据FTP服务端新分配的IP和PORT重新创建了一个SSL会话,导致了报错。感兴趣的可以自行了解。

        目前项目依赖的commons-io库版本是2.6,FTPSClient不支持ssl通道重用。因为我们的项目sdk、jdk(1.8)版本比较老,暂不考虑升级版本的情况下需要解决此问题(我没研究commons-io新版本是否仍有此问题)。 

        网上可以查到通过重写FTPSClient类来解决此问题的方案,大概逻辑是通过反射的方法将连接用的IP和PORT赋值给FTPSClient的一个缓存,这样FTPSClient在上传文件的时候就不会新建SSL会话了。但是在我这里行不通,原因可能有很多,比如sdk版本等,结果就是需要反射获取的字段在我的源码里就不存在。

        但知道方案大概逻辑后,就可以自己再加工一下了。我先附上网上查到的解决方案(方案一),后面是我自己加工的方案(方案二),也是最终解决了我的问题的方案。

       方案一:

public class CustomFTPSClient extends FTPSClient {
    private static final String TAG = "CustomFTPSClient";


    public CustomFTPSClient(boolean isImplicit, SSLContext sslContext) {
        super(isImplicit, sslContext);
    }

    @Override
    protected void _prepareDataSocket_(final Socket socket) throws IOException {
        if (socket instanceof SSLSocket) {
            // Control socket is SSL
            final SSLSession session = ((SSLSocket) _socket_).getSession();
            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);
                final String key = String.format("%s:%s", socket.getInetAddress().getHostName(), String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT);
                method.invoke(cache, key, session);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                // Not running in expected JRE
                System.out.println("No field sessionHostPortCache in SSLSessionContext");
            } catch (Exception e) {
                // Not running in expected JRE
                e.printStackTrace();
            }
        }
    }
}

        方案二:

public class CustomFTPSClient extends FTPSClient {
    private static final String TAG = "CustomFTPSClient";


    public CustomFTPSClient(boolean isImplicit, SSLContext sslContext) {
        super(isImplicit, sslContext);
    }

    @Override
    protected void _prepareDataSocket_(final Socket socket) throws IOException {
        if (socket instanceof SSLSocket) {
            LogUtil.v(TAG, "Start cached session");
            // com.android.org.conscrypt.OpenSSLSessionImpl
            final SSLSession session = ((SSLSocket) _socket_).getSession();
            // com.android.org.conscrypt.ClientSessionContext
            final SSLSessionContext sessionContext = session.getSessionContext();
            try {
                // SSLSessionContext中有一个sessionsByHostAndPort字段,是一个Map类型
                // sessionsByHostAndPort的key值是ClientSessionContext$HostAndPort类型,value是SSLSession类型
                // HostAndPort中有host和port字段(这两个字段是服务端的IP和服务端监听FTP服务的端口号)
                // 现在需要做的是在sessionsByHostAndPort对象中,将服务端的IP和给客户端分配的端口号,使用上述相同的value(SSLSession)

                // 1. 获取 sessionsByHostAndPort 字段
                final Field sessionsByHostAndPortField = sessionContext.getClass().getDeclaredField("sessionsByHostAndPort");
                sessionsByHostAndPortField.setAccessible(true);
                final Map<?, ?> cache = (Map<?, ?>) sessionsByHostAndPortField.get(sessionContext);

                // 2. 获取 HostAndPort 内部类
                Class<?> hostAndPortClass = null;
                for (Class<?> innerClass : sessionContext.getClass().getDeclaredClasses()) {
                    if (innerClass.getSimpleName().equals("HostAndPort")) {
                        hostAndPortClass = innerClass;
                        break;
                    }
                }

                if (hostAndPortClass == null) {
                    LogUtil.e(TAG, "HostAndPort inner class not found");
                    return;
                }

                // 3. 查找或创建合适的 HostAndPort 键
                Object targetKey = null;
                // 服务端的IP
                String targetHost = getPassiveHost();
                // 服务端给客户端随机分配的端口号
                int targetPort = getPassivePort();

                // 如果没找到匹配的键,尝试创建新键
                if (targetKey == null) {
                    try {
                        Constructor<?> constructor = hostAndPortClass.getDeclaredConstructor(String.class, int.class);
                        constructor.setAccessible(true);
                        targetKey = constructor.newInstance(targetHost, targetPort);
                    } catch (Exception e) {
                        LogUtil.cat(e);
                        return;
                    }
                }
                // 4. 将会话放入缓存
                final Method putMethod = cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
                putMethod.setAccessible(true);
                putMethod.invoke(cache, targetKey, session);
                LogUtil.v(TAG, "Successfully cached session for " + targetHost + ":" + targetPort);
            } catch (Exception e) {
                LogUtil.cat(e);
            }
        }
    }
}

     再附上创建FTPSClient实例的代码:

        //信任所有证书的方式
        TrustManager[] trustAllCerts = new TrustManager[]{
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    }

                    public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
                    }
                }
        };
        SSLContext sc = SSLContext.getInstance("TLSv1.2");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        CustomFTPSClient ftpsClient = new CustomFTPSClient(false, sc);

还有一点需要注意的是,成功登录FTP后,需要设置以下参数:

      // 设置SSL/TLS参数
        ftpsClient.execPBSZ(0);  // 必须设置保护缓冲区大小
        ftpsClient.execPROT("P"); // 设置数据通道保护级别为私有

其他就跟FTPClient一样了,本文章不做介绍。

关于FTP服务端的几个配置:

Protocol:Require explicit FTP over TLS

Minimum allowed TLS version: v1.2

TLS credentials: Use a self-signed X.509 certificate

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值