FTPClient TLS 与 FTP 进行数据传输异常:Remote host closed connection during handshake
环境:java JDK 1.8、org.apache.commons-net-3.6.jar、端口已放开
FTPClient ftpClient = new FTPClient(protocol, false);
ftpClient.setRemoteVerificationEnable(false);
ftpClient.setControlKeepAliveTimeout(300);
ftpClient.setDataTimeout(300);
InputStream fin = null;
try {
ftpClient.connect(host, port);
int reply = ftpClient.getReplyCode();
if (ftpClient.isPositiveComletion(reply)) {
if (ftpClient.login(username, password)) {
ftpClient.feat();
ftpClient.execPBSZ(0);
ftpClient.execPROT("p");
ftpClient.setControlEncoding("UTF-8");
ftpClient.setFileType(ftpClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
try {
fin = new FileInoutStream(new File("C:\\doc_home\\test1.txt"));
} catch (FileNotFoundException e) {
System.out.println("---file not found");
}
String remoteFile = "test1.txt";
ftpClient.mlsd();
if (ftpClient.storeFile(remoteFile, fin)) {
fin.close();
} else {
System.out.println("could not store file");
}
fin.close();
} else {
System.out.println("FTP login failed");
}
} else {
System.out.println("FTP connect to host failed");
}
} catch (IOException ioe) {
ioe.printStackTrace();
System.out.println("FTP client received network error");
} finally {
if (fin != null) {
try {
fin.close();
} catch (IOException ioe) {
//do nothing
}
}
}
异常:
javax.net.ssl.SSLHandshakeException:Remote host closed connection during handshake
FTP client received network error
at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source)
at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
at org.apache.commons.net.ftp.FTPSClient.openDataConnection(FTPClient.java:646)
at org.apache.commons.net.ftp.FTPSClient.storeFile(FTPClient.java:653)
at org.apache.commons.net.ftp.FTPSClient.storeFile(FTPClient.java:2030)
at ibgdashboardtest.Demo4.main(Demo4.java:75)
Caused by:java.io.EOFException:SSL peer shut down incorrectly
at sun.security.ssl.InputRecord.read(Unknown Source)
... 9 more
解决方法:自己定义一个类继承FTPSClient,重载_prepareDataSocket_(final Socket socket)方法,添加了TLS的session hash支持并扩展了密钥,使用时用该类来替代FTPSClient的使用
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");
}
}
}
}
如果出现兼容问题,应用程序可能会通过JDK中将系统设置属性jdk.tls.useExtendedMasterSecret设置为false来禁用此扩展,在JDK 1.8.0_161中设置:
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");