/*
* Copyright 2010 netling project <http://netling.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file may incorporate work covered by the following copyright and
* permission notice:
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netling.ftp;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
/**
* FTP over SSL processing. If desired, the JVM property -Djavax.net.debug=all can be used to
* see wire-level SSL details.
*/
public class FTPSClient extends FTPClient {
/**
* Parameters for the <code>PROT</code> command
*/
public enum Prot {
CLEAR("C"),
SAFE("S"),
CONFIDENTIAL("E"),
PRIVATE("P");
private final String code;
Prot(String code) {
this.code = code;
}
String code() { return code; }
}
/** Default protocol name */
private static final String DEFAULT_PROTOCOL = "TLS";
/** The security mode. (True - Implicit Mode / False - Explicit Mode) */
private final boolean isImplicit;
/** The use SSL/TLS protocol. */
private final String protocol;
/** The AUTH Command value */
private String auth = DEFAULT_PROTOCOL;
/** The context object. */
private SSLContext context;
/** For RFC 4217 behaviour */
private boolean autoClose = false;
/** The socket object. */
private Socket plainSocket;
/** SSL-wrapped socket */
private SSLSocket ssock;
/** The established socket flag. */
private boolean isCreation = true;
/** The use client mode flag. */
private boolean isClientMode = true;
/** The need client auth flag. */
private boolean isNeedClientAuth = false;
/** The want client auth flag. */
private boolean isWantClientAuth = false;
/** The cipher suites */
private String[] suites = null;
/** The protocol versions */
private String[] protocols = null;
/** The FTPS {@link TrustManager} implementation. */
private TrustManager trustManager = new FTPSTrustManager();
/** The {@link KeyManager} */
private KeyManager keyManager;
/**
* Constructor for FTPSClient.
* @throws NoSuchAlgorithmException A requested cryptographic algorithm
* is not available in the environment.
*/
public FTPSClient() throws NoSuchAlgorithmException {
this.protocol = DEFAULT_PROTOCOL;
this.isImplicit = false;
}
/**
* Constructor for FTPSClient.
* @param isImplicit The secutiry mode(Implicit/Explicit).
* @throws NoSuchAlgorithmException A requested cryptographic algorithm
* is not available in the environment.
*/
public FTPSClient(boolean isImplicit) throws NoSuchAlgorithmException {
this.protocol = DEFAULT_PROTOCOL;
this.isImplicit = isImplicit;
}
/**
* Constructor for FTPSClient.
* @param protocol the protocol
* @throws NoSuchAlgorithmException A requested cryptographic algorithm
* is not available in the environment.
*/
public FTPSClient(String protocol) throws NoSuchAlgorithmException {
this.protocol = protocol;
this.isImplicit = false;
}
/**
* Constructor for FTPSClient.
* @param protocol the protocol
* @param isImplicit The secutiry mode(Implicit/Explicit).
* @throws NoSuchAlgorithmException A requested cryptographic algorithm
* is not available in the environment.
*/
public FTPSClient(String protocol, boolean isImplicit)
throws NoSuchAlgorithmException {
this.protocol = protocol;
this.isImplicit = isImplicit;
}
/**
* Constructor for FTPSClient.
* @param isImplicit The secutiry mode(Implicit/Explicit).
* @param context A pre-configured SSL Context
*/
public FTPSClient(boolean isImplicit, SSLContext context) {
this.isImplicit = isImplicit;
this.context = context;
this.protocol = DEFAULT_PROTOCOL;
}
/**
* Constructor for FTPSClient.
* @param context A pre-configured SSL Context
*/
public FTPSClient(SSLContext context) {
this(false, context);
}
/**
* Set AUTH command use value.
* This processing is done before connected processing.
* @param auth AUTH command use value.
*/
public void setAuthValue(String auth) {
this.auth = auth;
}
/**
* Return AUTH command use value.
* @return AUTH command use value.
*/
public String getAuthValue() {
return this.auth;
}
/**
* Because there are so many connect() methods,
* the connectAction() method is provided as a means of performing
* some action immediately after establishing a connection,
* rather than reimplementing all of the connect() methods.
* @throws IOException If it throw by connectAction.
* @see org.netling.SocketClient#_connectAction()
*/
@Override
protected void onConnect() throws IOException {
// Implicit mode.
if (isImplicit) sslNegotiation();
super.onConnect();
// Explicit mode.
if (!isImplicit) {
execAUTH();
sslNegotiation();
}
}
/**
* AUTH command.
* @throws SSLException If it server reply code not equal "234" and "334".
* @throws IOException If an I/O error occurs while either sending
* the command.
*/
private void execAUTH() throws SSLException, IOException {
int replyCode = sendCommand(FTPSCommand.AUTH.command(), auth);
if (FTPReply.SECURITY_MECHANISM_IS_OK.code() == replyCode) {
// replyCode = 334
// I carry out an ADAT command.
} else if (FTPReply.SECURITY_DATA_EXCHANGE_COMPLETE.code() != replyCode) {
throw new SSLException(getReplyString());
}
}
/**
* Performs a lazy init of the SSL context
* @throws IOException
*/
private void initSslContext() throws IOException {
if(context == null) {
try {
context = SSLContext.getInstance(protocol);
context.init(new KeyManager[] { getKeyManager() } , new TrustManager[] { getTrustManager() } , null);
} catch (KeyManagementException e) {
throw new IOException("Could not initialize SSL context", e);
} catch (NoSuchAlgorithmException e) {
throw new IOException("Could not initialize SSL context", e);
}
}
}
/**
* SSL/TLS negotiation. Acquires an SSL socket of a control
* connection and carries out handshake processing.
* @throws IOException If server negotiation fails
*/
private void sslNegotiation() throws IOException {
plainSocket = socket;
initSslContext();
SSLSocketFactory ssf = context.getSocketFactory();
String ip = socket.getInetAddress().getHostAddress();
int port = socket.getPort();
ssock =
(SSLSocket) ssf.createSocket(socket, ip, port, true);
ssock.setEnableSessionCreation(isCreation);
ssock.setUseClientMode(isClientMode);
// server mode
if (!isClientMode) {
ssock.setNeedClientAuth(isNeedClientAuth);
ssock.setWantClientAuth(isWantClientAuth);
}
if (protocols != null) ssock.setEnabledProtocols(protocols);
if (suites != null) ssock.setEnabledCipherSuites(suites);
ssock.startHandshake();
this.socket = ssock;
controlInput = new BufferedReader(new InputStreamReader(
ssock .getInputStream(), getControlEncoding()));
controlOutput = new BufferedWriter(new OutputStreamWriter(
ssock.getOutputStream(), getControlEncoding()));
}
/**
* Get the {@link KeyManager} instance.
* @return The {@link KeyManager} instance
*/
private KeyManager getKeyManager() {
return keyManager;
}
/**
* Set a {@link KeyManager} to use
*
* @param keyManager The KeyManager implementation to set.
*/
public void setKeyManager(KeyManager keyManager) {
this.keyManager = keyManager;
}
/**
* Controls whether new a SSL session may be established by this socket.
* @param isCreation The established socket flag.
*/
public void setEnabledSessionCreation(boolean isCreation) {
this.isCreation = isCreation;
}
/**
* Returns true if new SSL sessions may be established by this socket.
* When the underlying {@link Socket} instance is not SSL-enabled (i.e. an
* instance of {@link SSLSocket} with {@link SSLSocket}{@link #getEnableSessionCreation()}) enabled,
* this returns False.
* @return true - Indicates that sessions may be created;
* this is the default.
* false - indicates that an existing session must be resumed.
*/
public boolean getEnableSessionCreation() {
if (socket instanceof SSLSocket)
return ((SSLSocket)socket).getEnableSessionCreation();
return false;
}
/**
* Configures the socket to require client authentication.
* @param isNeedClientAuth The need client auth flag.
*/
public void setNeedClientAuth(boolean isNeedClientAuth) {
this.isNeedClientAuth = isNeedClientAuth;
}
/**
* Returns true if the socket will require client authentication.
* When the underlying {@link Socket} is not an {@link SSLSocket} instance, returns false.
* @return true - If the server mode socket should request
* that the client authenticate itself.
*/
public boolean getNeedClientAuth() {
if (socket instanceof SSLSocket)
return ((SSLSocket)socket).getNeedClientAuth();
return false;
}
/**
* Configures the socket to request client authentication,
* but only if such a request is appropriate to the cipher
* suite negotiated.
* @param isWantClientAuth The want client auth flag.
*/
public void setWantClientAuth(boolean isWantClientAuth) {
this.isWantClientAuth = isWantClientAuth;
}
/**
* Returns true if the socket will request client authentication.
* When the underlying {@link Socket} is not an {@link SSLSocket} instance, returns false.
* @return true - If the server mode socket should request
* that the client authenticate itself.
*/
public boolean getWantClientAuth() {
if (socket instanceof SSLSocket)
return ((SSLSocket)socket).getWantClientAuth();
return false;
}
/**
* Configures the socket to use client (or server) mode in its first
* handshake.
* @param isClientMode The use client mode flag.
*/
public void setUseClientMode(boolean isClientMode) {
this.isClientMode = isClientMode;
}
/**
* Returns true if the socket is set to use client mode
* in its first handshake.
* When the underlying {@link Socket} is not an {@link SSLSocket} instance, returns false.
* @return true - If the socket should start its first handshake
* in "client" mode.
*/
public boolean getUseClientMode() {
if (socket instanceof SSLSocket)
return ((SSLSocket)socket).getUseClientMode();
return false;
}
/**
* Controls which particular cipher suites are enabled for use on this
* connection. Called before server negotiation.
* @param cipherSuites The cipher suites.
*/
public void setEnabledCipherSuites(String[] cipherSuites) {
suites = new String[cipherSuites.length];
System.arraycopy(cipherSuites, 0, suites, 0, cipherSuites.length);
}
/**
* Returns the names of the cipher suites which could be enabled
* for use on this connection.
* When the underlying {@link Socket} is not an {@link SSLSocket} instance, returns null.
* @return An array of cipher suite names, or <code>null</code>
*/
public String[] getEnabledCipherSuites() {
if (socket instanceof SSLSocket)
return ((SSLSocket)socket).getEnabledCipherSuites();
return null;
}
/**
* Controls which particular protocol versions are enabled for use on this
* connection. I perform setting before a server negotiation.
* @param protocolVersions The protocol versions.
*/
public void setEnabledProtocols(String[] protocolVersions) {
protocols = new String[protocolVersions.length];
System.arraycopy(protocolVersions, 0, protocols, 0, protocolVersions.length);
}
/**
* Returns the names of the protocol versions which are currently
* enabled for use on this connection.
* When the underlying {@link Socket} is not an {@link SSLSocket} instance, returns null.
* @return An array of protocols, or <code>null</code>
*/
public String[] getEnabledProtocols() {
if (socket instanceof SSLSocket)
return ((SSLSocket)socket).getEnabledProtocols();
return null;
}
/**
* PBSZ command. pbsz value: 0 to (2^32)-1 decimal integer.
* @param pbsz Protection Buffer Size.
* @throws SSLException If the server reply code does not equal "200".
* @throws IOException If an I/O error occurs while sending
* the command.
*/
public void execPBSZ(long pbsz) throws SSLException, IOException {
if (pbsz < 0 || 4294967295L < pbsz)
throw new IllegalArgumentException();
if (FTPReply.COMMAND_OK.code() != sendCommand(FTPSCommand.PBSZ.command(), String.valueOf(pbsz)))
throw new SSLException(getReplyString());
}
/**
* PROT command.</br>
* C - Clear</br>
* S - Safe(SSL protocol only)</br>
* E - Confidential(SSL protocol only)</br>
* P - Private
* @param prot Data Channel Protection Level.
* @throws SSLException If the server reply code does not equal "200".
* @throws IOException If an I/O error occurs while sending
* the command.
*/
public void execPROT(Prot prot) throws SSLException, IOException {
if (prot == null) prot = Prot.CLEAR;
if (FTPReply.COMMAND_OK.code() != sendCommand(
FTPSCommand.PROT.command(), prot.code()))
throw new SSLException(getReplyString());
if (prot == Prot.CLEAR) {
setSocketFactory(null);
setServerSocketFactory(null);
} else {
setSocketFactory(new FTPSSocketFactory(context));
setServerSocketFactory(new FTPSServerSocketFactory(context));
initSslContext();
}
}
/**
* Send an FTP command.
* The CCC (Clear Command Channel) command causes the underlying {@link SSLSocket} instance to be assigned
* to a plain {@link Socket} instances
* @param command The FTP command.
* @return server reply.
* @throws IOException If an I/O error occurs while sending
* the command.
* @see org.netling.ftp.FTP#sendCommand(java.lang.String)
*/
@Override
public int sendCommand(String command, String args) throws IOException {
int repCode = super.sendCommand(command, args);
/* If CCC is issued, restore socket i/o streams to unsecured versions */
if (FTPSCommand.CCC.command().equals(command)) {
if (FTPReply.COMMAND_OK.code() == repCode) {
if (autoClose)
socket.close();
socket = plainSocket;
controlInput = new BufferedReader(
new InputStreamReader(
socket .getInputStream(), getControlEncoding()));
controlOutput = new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream(), getControlEncoding()));
setSocketFactory(null);
} else {
throw new SSLException(getReplyString());
}
}
return repCode;
}
/**
* Returns a socket of the data connection.
* Wrapped as an {@link SSLSocket}, which carries out handshake processing.
* @param command The textual representation of the FTP command to send.
* @param arg The arguments to the FTP command.
* If this parameter is set to null, then the command is sent with
* no arguments.
* @return corresponding to the established data connection.
* Null is returned if an FTP protocol error is reported at any point
* during the establishment and initialization of the connection.
* @throws IOException If there is any problem with the connection.
* @see FTPClient#openDataConnection(int, String)
*/
@Override
protected Socket openDataConnection(FTPCommand command, String arg)
throws IOException {
Socket socket = super.openDataConnection(command, arg);
if (socket != null && socket instanceof SSLSocket) {
SSLSocket sslSocket = (SSLSocket)socket;
sslSocket.setUseClientMode(isClientMode);
sslSocket.setEnableSessionCreation(isCreation);
// server mode
if (!isClientMode) {
sslSocket.setNeedClientAuth(isNeedClientAuth);
sslSocket.setWantClientAuth(isWantClientAuth);
}
if (suites != null)
sslSocket.setEnabledCipherSuites(suites);
if (protocols != null)
sslSocket.setEnabledProtocols(protocols);
sslSocket.startHandshake();
}
return socket;
}
/**
* Get the currently configured {@link TrustManager}.
*
* @return A TrustManager instance.
*/
public TrustManager getTrustManager() {
return trustManager;
}
/**
* Override the default {@link TrustManager} to use.
*
* @param trustManager The TrustManager implementation to set.
*/
public void setTrustManager(TrustManager trustManager) {
this.trustManager = trustManager;
}
/**
* If set to <code>true</code>, closes the underlying plain socket
* when the SSL socket is closed on transmission of a <code>CCC</code> command.
* For RFC 4217 compliance, this should
* be set to <code>false</code>.
*
* @param autoClose Whether to close the underlying plain socket on a <code>CCC</code>
*/
public void setAutoClose(boolean autoClose) {
this.autoClose = autoClose;
}
}