免费版的edtFTPj不支持FTPS等安全协议, 所以不能访问基于TLS/SSL FTP服务器。最近对SSL有了些概念,项目也使用过edtFTPj库,所以尝试给这个库添加TLS/SSL支持,就当是个练习。
1. commons-net的FTP
commons-net包支持TLS/SSL FTP,首先参考它的实现。
FTP:实现了基本的FTP命令,
FTPClient:对FTP中的基本FTP命令进行封装
FTPSClient: 提供 TLS/SSL FTP功能.
FTP、FTP、FTPSClient继承了SocketClient类的connect()方法, 该方法主要是发起到远端的连接并调用_connectAction_()方法。。 这三个子类重写了_connectAction_()方法。
// SocketClient.java
public void connect(InetAddress host, int port,
InetAddress localAddr, int localPort)
throws SocketException, IOException
{
_socket_ = _socketFactory_.createSocket();
if (receiveBufferSize != -1) {
_socket_.setReceiveBufferSize(receiveBufferSize);
}
if (sendBufferSize != -1) {
_socket_.setSendBufferSize(sendBufferSize);
}
_socket_.bind(new InetSocketAddress(localAddr, localPort));
_socket_.connect(new InetSocketAddress(host, port), connectTimeout);
_connectAction_();
}
// FTPSClient.java
protected void _connectAction_() throws IOException {
// Implicit mode.
if (isImplicit) {
sslNegotiation();
}
super._connectAction_();
// Explicit mode.
if (!isImplicit) {
execAUTH();
sslNegotiation();
}
}
FTPSClient重写_connectAction_方法:如果isImplicit为false, 则执行AUTH命令并开始SSL握手;否则直接开始SSL握手。
// FTPSClient
protected void sslNegotiation() throws IOException {
plainSocket = _socket_;
initSslContext();
SSLSocketFactory ssf = context.getSocketFactory();
String ip = _socket_.getInetAddress().getHostAddress();
int port = _socket_.getPort();
SSLSocket socket =
(SSLSocket) ssf.createSocket(_socket_, ip, port, false);
socket.setEnableSessionCreation(isCreation);
socket.setUseClientMode(isClientMode);
// server mode
if (!isClientMode) {
socket.setNeedClientAuth(isNeedClientAuth);
socket.setWantClientAuth(isWantClientAuth);
}
if (protocols != null) {
socket.setEnabledProtocols(protocols);
}
if (suites != null) {
socket.setEnabledCipherSuites(suites);
}
socket.startHandshake();
_socket_ = socket;
_controlInput_ = new BufferedReader(new InputStreamReader(
socket .getInputStream(), getControlEncoding()));
_controlOutput_ = new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream(), getControlEncoding()));
}
FTPSClient的sslNegotiation()方法说白了就是用一个已经连接到远程FTP服务器的Socket来创建一个SSLSocket,并替换原来的_socket_成员变量
2. edtFTPj添加FTPS支持
按照上面的思路给edtFTPj添加 FTPS支持。首先看edtFTPj主要的类,(蓝色是我添加的类)
我们写的应用使用edtFTPj包提供的FileTransferClient作为FTP客户端。一般这样使用
FileTransferClient client = new FileTransferClient();
得到一个不支持TLS/SSL的FTP客户端。
根据以上用法, 主要作以下修改:
1. 实现一个继承自FTPClient的类FTPSClient
2. 实现一个承自FTPControlSocket的类FTPSControlSocket
3. 实现一个SSLSocket的代理MySSLSocket, 这个类同时继承StreamSocket
4. 修改FileTransferClient的构造方法, 通过一个参数来区分是生成FTPClient还是FTPSClient对象
基本上,对原有代码的修改只有一处,其他位置都是扩展。 比较关键的两处代码如下
public class FTPSClient extends FTPClient {
@Override
public void connect() throws IOException, FTPException {
checkConnection(false);
if (remoteAddr == null)
remoteAddr = InetAddress.getByName(remoteHost);
// 创建Socket
FTPSControlSocket sock = new FTPSControlSocket(remoteAddr, controlPort,
timeout, controlEncoding, messageListener);
initialize(sock);
// 在Socket上发出第一条命令AUTH TLS
System.out.println("send AUTH TLS");
lastReply = control.sendCommand("AUTH TLS");
if (!"234".equals(lastReply.getReplyCode())) {
System.err.println("err");
throw new IOException();
}
// 创建SSLSocket
StreamSocket ss = MySSLSocket.createSSLSocket(sock.getControlSock(),
remoteAddr, controlPort, timeout);
// 用SSLSocket替换原来的的Socket
sock.setControlSock(ss);
sock.initStreams();
}
这段代码所做的工作类似于前面提到的sslNegotiation方法
public class MySSLSocket implements StreamSocket {
private SSLSocket sslSocket;
protected String remoteHostname;
private MySSLSocket(SSLSocket sslSocket) {
this.sslSocket = sslSocket;
}
public static MySSLSocket createSSLSocket(Socket socket, InetAddress host,
int port, int timeout) throws IOException {
SSLSocket sock = getSSLSocket(socket, host.getHostName(), port);
if (null == sock) {
throw new IOException("null SSLSocket");
}
// 注意: 已连接, 不用再次连接
// sock.connect(addr, timeout);
return new MySSLSocket(sock);
}
private static SSLSocket getSSLSocket(Socket socket, String host, int port) {
long now = System.currentTimeMillis();
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null,
new TrustManager[] { new MyX509TrustManager() }, null);
SSLSocket sslSocket = (SSLSocket) sslContext.getSocketFactory()
.createSocket(socket, host, port, false);
sslSocket.setEnableSessionCreation(true);
sslSocket.setUseClientMode(true);
System.out.println(System.currentTimeMillis() - now);
now = System.currentTimeMillis();
sslSocket.startHandshake();
System.out.println("shakehand takes"
+ (System.currentTimeMillis() - now));
return sslSocket;
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
这段代码主要是通过一个已经连接到FTP服务器的Socket(控制连接)来创建一个SSLSocket,之后FTPSClient使用这个SSLSocket与FTP服务器交互。
public static void main(String[] args) throws FTPException, IOException {
// 使用自己添加的构造函数, 获取一个支持FTPS的 FileTransferClient
FileTransferClient client = new FileTransferClient(FTPType.FTPS);
client.setRemoteHost("10.204.80.48");
client.setRemotePort(21);
client.setUserName("tom");
client.setPassword("123456");
// 二进制传输模式
client.setContentType(FTPTransferType.BINARY);
// 控制信道上的字符集为utf-8
client.getAdvancedSettings().setControlEncoding("utf-8");
// 不要自动登录
client.getAdvancedSettings().setAutoLogin(false);
client.getAdvancedFTPSettings().setConnectMode(FTPConnectMode.PASV);
try {
client.connect();
client.manualLogin();
// 遍历当前目录
for (String string : client.directoryNameList()) {
System.out.println(string);
}
client.changeDirectory("data");
// 遍历data目录
for (String string : client.directoryNameList()) {
System.out.println(string);
}
client.uploadFile("c:\\a.txt", "cm.txt");
System.out.println("upload!!");
} catch (IOException e) {
e.printStackTrace();
}
}
简单验证了下FTPSClient的功能, 可以登录、切换目录、显示目录列表、上传文件。用Wireshark抓包看了下, 控制连接和数据连接上数据果然都加密了。
3. 遗留的问题
(1) 登录很慢。 对比了下, 用commons-net包里面的FTPSClient,登录非常快。
(2) 数据连接也加密了吗? 还需要再看看代码