blob: 180800ff16a2522a5ec46581468219229155f7c7 [file] [log] [blame]
/*
* 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.apache.ftpserver.impl;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.ftpserver.DataConnectionConfiguration;
import org.apache.ftpserver.DataConnectionException;
import org.apache.ftpserver.ftplet.DataConnection;
import org.apache.ftpserver.ftplet.FtpException;
import org.apache.ftpserver.ssl.ClientAuth;
import org.apache.ftpserver.ssl.SslConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <strong>Internal class, do not use directly.</strong>
*
* We can get the FTP data connection using this class. It uses either PORT or PASV command.
*
* @author <a href="http://mina.apache.org">Apache MINA Project</a>
*/
public class IODataConnectionFactory implements ServerDataConnectionFactory {
private final Logger LOG = LoggerFactory.getLogger(IODataConnectionFactory.class);
private FtpServerContext serverContext;
private Socket dataSoc;
ServerSocket servSoc;
InetAddress address;
int port = 0;
long requestTime = 0L;
boolean passive = false;
boolean secure = false;
private boolean isZip = false;
InetAddress serverControlAddress;
FtpIoSession session;
public IODataConnectionFactory(final FtpServerContext serverContext, final FtpIoSession session) {
this.session = session;
this.serverContext = serverContext;
if ((session != null) && (session.getListener() != null) && session.getListener().getDataConnectionConfiguration().isImplicitSsl()) {
secure = true;
}
}
/**
* Close data socket. This method must be idempotent as we might call it multiple times during disconnect.
*/
public synchronized void closeDataConnection() {
// close client socket if any
if (dataSoc != null) {
try {
dataSoc.close();
} catch (Exception ex) {
LOG.warn("FtpDataConnection.closeDataSocket()", ex);
}
dataSoc = null;
}
// close server socket if any
if (servSoc != null) {
try {
servSoc.close();
} catch (Exception ex) {
LOG.warn("FtpDataConnection.closeDataSocket()", ex);
}
if (session != null) {
DataConnectionConfiguration dcc = session.getListener().getDataConnectionConfiguration();
if (dcc != null) {
dcc.releasePassivePort(port);
}
}
servSoc = null;
}
// reset request time
requestTime = 0L;
}
/**
* Port command.
*/
public synchronized void initActiveDataConnection(final InetSocketAddress address) {
// close old sockets if any
closeDataConnection();
// set variables
passive = false;
this.address = address.getAddress();
port = address.getPort();
requestTime = System.currentTimeMillis();
}
private SslConfiguration getSslConfiguration() {
DataConnectionConfiguration dataCfg = session.getListener().getDataConnectionConfiguration();
SslConfiguration configuration = dataCfg.getSslConfiguration();
// fall back if no configuration has been provided on the data connection config
if (configuration == null) {
configuration = session.getListener().getSslConfiguration();
}
return configuration;
}
/**
* Initiate a data connection in passive mode (server listening).
*/
public synchronized InetSocketAddress initPassiveDataConnection() throws DataConnectionException {
LOG.debug("Initiating passive data connection");
// close old sockets if any
closeDataConnection();
// get the passive port
int passivePort = session.getListener().getDataConnectionConfiguration().requestPassivePort();
if (passivePort == -1) {
servSoc = null;
throw new DataConnectionException("Cannot find an available passive port.");
}
// open passive server socket and get parameters
try {
DataConnectionConfiguration dataCfg = session.getListener().getDataConnectionConfiguration();
String passiveAddress = dataCfg.getPassiveAddress();
if (passiveAddress == null) {
address = serverControlAddress;
} else {
address = resolveAddress(dataCfg.getPassiveAddress());
}
if (secure) {
LOG.debug("Opening SSL passive data connection on address \"{}\" and port {}", address, passivePort);
SslConfiguration ssl = getSslConfiguration();
if (ssl == null) {
throw new DataConnectionException("Data connection SSL required but not configured.");
}
// this method does not actually create the SSL socket, due to a JVM bug
// (https://issues.apache.org/jira/browse/FTPSERVER-241).
// Instead, it creates a regular
// ServerSocket that will be wrapped as a SSL socket in createDataSocket()
servSoc = new ServerSocket(passivePort, 0, address);
LOG.debug("SSL Passive data connection created on address \"{}\" and port {}", address, passivePort);
} else {
LOG.debug("Opening passive data connection on address \"{}\" and port {}", address, passivePort);
servSoc = new ServerSocket(passivePort, 0, address);
LOG.debug("Passive data connection created on address \"{}\" and port {}", address, passivePort);
}
port = servSoc.getLocalPort();
servSoc.setSoTimeout(dataCfg.getIdleTime() * 1000);
// set different state variables
passive = true;
requestTime = System.currentTimeMillis();
return new InetSocketAddress(address, port);
} catch (Exception ex) {
closeDataConnection();
throw new DataConnectionException("Failed to initate passive data connection: " + ex.getMessage(), ex);
}
}
/*
* (non-Javadoc)
*
* @see org.apache.ftpserver.FtpDataConnectionFactory2#getInetAddress()
*/
public InetAddress getInetAddress() {
return address;
}
/*
* (non-Javadoc)
*
* @see org.apache.ftpserver.FtpDataConnectionFactory2#getPort()
*/
public int getPort() {
return port;
}
/*
* (non-Javadoc)
*
* @see org.apache.ftpserver.FtpDataConnectionFactory2#openConnection()
*/
public DataConnection openConnection() throws Exception {
return new IODataConnection(createDataSocket(), session, this);
}
/**
* Get the data socket. In case of error returns null.
*/
private synchronized Socket createDataSocket() throws Exception {
// get socket depending on the selection
dataSoc = null;
DataConnectionConfiguration dataConfig = session.getListener().getDataConnectionConfiguration();
try {
if (!passive) {
if (secure) {
LOG.debug("Opening secure active data connection");
SslConfiguration ssl = getSslConfiguration();
if (ssl == null) {
throw new FtpException("Data connection SSL not configured");
}
// get socket factory
SSLSocketFactory socFactory = ssl.getSocketFactory();
// create socket
SSLSocket ssoc = (SSLSocket) socFactory.createSocket();
ssoc.setUseClientMode(false);
// initialize socket
if (ssl.getEnabledCipherSuites() != null) {
ssoc.setEnabledCipherSuites(ssl.getEnabledCipherSuites());
}
if (ssl.getEnabledProtocols() != null) {
ssoc.setEnabledProtocols(ssl.getEnabledProtocols());
}
dataSoc = ssoc;
} else {
LOG.debug("Opening active data connection");
dataSoc = new Socket();
}
dataSoc.setReuseAddress(true);
InetAddress localAddr = resolveAddress(dataConfig.getActiveLocalAddress());
// if no local address has been configured, make sure we use the same as the client connects from
if (localAddr == null) {
localAddr = ((InetSocketAddress) session.getLocalAddress()).getAddress();
}
SocketAddress localSocketAddress = new InetSocketAddress(localAddr, dataConfig.getActiveLocalPort());
LOG.debug("Binding active data connection to {}", localSocketAddress);
dataSoc.bind(localSocketAddress);
dataSoc.connect(new InetSocketAddress(address, port));
} else {
if (secure) {
LOG.debug("Opening secure passive data connection");
// this is where we wrap the unsecured socket as a SSLSocket. This is
// due to the JVM bug described in FTPSERVER-241.
// get server socket factory
SslConfiguration ssl = getSslConfiguration();
// we've already checked this, but let's do it again
if (ssl == null) {
throw new FtpException("Data connection SSL not configured");
}
SSLSocketFactory ssocketFactory = ssl.getSocketFactory();
Socket serverSocket = servSoc.accept();
SSLSocket sslSocket = (SSLSocket) ssocketFactory.createSocket(serverSocket, serverSocket.getInetAddress().getHostAddress(), serverSocket.getPort(), true);
sslSocket.setUseClientMode(false);
// initialize server socket
if (ssl.getClientAuth() == ClientAuth.NEED) {
sslSocket.setNeedClientAuth(true);
} else if (ssl.getClientAuth() == ClientAuth.WANT) {
sslSocket.setWantClientAuth(true);
}
if (ssl.getEnabledCipherSuites() != null) {
sslSocket.setEnabledCipherSuites(ssl.getEnabledCipherSuites());
}
if (ssl.getEnabledProtocols() != null) {
sslSocket.setEnabledProtocols(ssl.getEnabledProtocols());
}
dataSoc = sslSocket;
} else {
LOG.debug("Opening passive data connection");
dataSoc = servSoc.accept();
}
if (dataConfig.isPassiveIpCheck()) {
// Let's make sure we got the connection from the same
// client that we are expecting
InetAddress remoteAddress = ((InetSocketAddress) session.getRemoteAddress()).getAddress();
InetAddress dataSocketAddress = dataSoc.getInetAddress();
if (!dataSocketAddress.equals(remoteAddress)) {
LOG.warn("Passive IP Check failed. Closing data connection from " + dataSocketAddress + " as it does not match the expected address " + remoteAddress);
closeDataConnection();
return null;
}
}
DataConnectionConfiguration dataCfg = session.getListener().getDataConnectionConfiguration();
dataSoc.setSoTimeout(dataCfg.getIdleTime() * 1000);
LOG.debug("Passive data connection opened");
}
} catch (Exception ex) {
closeDataConnection();
LOG.warn("FtpDataConnection.getDataSocket()", ex);
throw ex;
}
dataSoc.setSoTimeout(dataConfig.getIdleTime() * 1000);
// Make sure we initiate the SSL handshake, or we'll
// get an error if we turn out not to send any data
// e.g. during the listing of an empty directory
if (dataSoc instanceof SSLSocket) {
((SSLSocket) dataSoc).startHandshake();
}
return dataSoc;
}
/*
* (non-Javadoc) Returns an InetAddress object from a hostname or IP address.
*/
private InetAddress resolveAddress(String host) throws DataConnectionException {
if (host == null) {
return null;
} else {
try {
return InetAddress.getByName(host);
} catch (UnknownHostException ex) {
throw new DataConnectionException("Failed to resolve address", ex);
}
}
}
/*
* (non-Javadoc)
*
* @see org.apache.ftpserver.DataConnectionFactory#isSecure()
*/
public boolean isSecure() {
return secure;
}
/**
* Set the security protocol.
*/
public void setSecure(final boolean secure) {
this.secure = secure;
}
/*
* (non-Javadoc)
*
* @see org.apache.ftpserver.DataConnectionFactory#isZipMode()
*/
public boolean isZipMode() {
return isZip;
}
/**
* Set zip mode.
*/
public void setZipMode(final boolean zip) {
isZip = zip;
}
/**
* Check the data connection idle status.
*/
public synchronized boolean isTimeout(final long currTime) {
// data connection not requested - not a timeout
if (requestTime == 0L) {
return false;
}
// data connection active - not a timeout
if (dataSoc != null) {
return false;
}
// no idle time limit - not a timeout
int maxIdleTime = session.getListener().getDataConnectionConfiguration().getIdleTime() * 1000;
if (maxIdleTime == 0) {
return false;
}
// idle time is within limit - not a timeout
if ((currTime - requestTime) < maxIdleTime) {
return false;
}
return true;
}
/**
* Dispose data connection - close all the sockets.
*/
public void dispose() {
closeDataConnection();
}
/**
* Sets the server's control address.
*/
public void setServerControlAddress(final InetAddress serverControlAddress) {
this.serverControlAddress = serverControlAddress;
}
}