尝试从 Linux 机器到 IIS 服务器执行列表文件时,我遇到了同样的问题。该代码在我的开发人员工作站上运行良好,但在服务器上运行时会挂起,特别是由于防火墙阻塞了混合。
必须按顺序执行这些操作,并且需要您扩展 FTPSClient 3.5
- 连接(隐式 = true,SSLContext = TLS)
- 检查 isPositiveCompletion
- 验证(当然)
- 执行PBSZ(0)
- execPROT("P")
- 设置布尔值以指示跳过被动 IP(自定义 FTPSClient 类)
- 设置保存连接IP地址(自定义FTPSClient类)
- setUseEPSVwithIPv4(false)
- enterLocalPassiveMode() 或 enterRemotePassiveMode()
- initialListParsing() 或任何列表命令 a.) 此时将执行openDataConnection,请务必保存此处正在使用的端口 b.) 执行 PASV 命令 c.) 执行 _parsePassiveModeReply,此处将打开套接字使用您用于连接的 IP 地址和保存的端口。
- 断开连接(总是)
更多信息:我的问题特定于 Linux 机器和 IIS 服务器之间的防火墙。
我的问题的根源在于,在被动模式下,进行数据连接时用于打开套接字的 IP 地址与用于进行初始连接的 IP 地址不同。因此,由于 APACHE commons-net 3.5 的两个问题(见下文),很难弄清楚。我的解决方案:扩展 FTPSClient 以便我可以覆盖方法 _parsePassiveModeReply 和openDataConnection。我的 parsePassiveModeReply 实际上只是从回复中保存端口,因为回复表明正在使用哪个端口。我的 openDataConnection 方法使用保存的端口和连接期间使用的原始 IP。
APACHE FTPCLient 3.5 的问题
- 数据连接不会超时(挂起),因此问题所在并不明显。
- FTPSClient 类不会跳过被动 IP 地址。将passiveNatWorkaround 设置为true 并没有像我预期的那样工作,或者它根本没有跳过IP。
需要注意的事项:
- 通过防火墙时,您必须有权访问 IIS 定义的端口范围(请参阅配置 Microsoft IIS 防火墙)。
- 您还应该确保您的密钥库中有任何适当的证书或在运行时指定的证书。
将以下内容添加到您的课程中,这对于了解正在执行的 FTP 命令非常有帮助。
ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
- 检查 FTP 服务器日志,因为它会告诉您正在执行的操作以及您遇到问题的可能原因。在执行列表之前,您应该始终看到打开的数据通道。将您的应用程序的结果与成功的 curl 命令执行的结果进行比较。
- 回复代码,因为它们将指示问题发生的位置。
使用 curl 命令验证您是否已连接,以下是一个好的开始,如果一切顺利,将列出根目录中的内容。
curl -3 ftps://[user id]:[password][ftp server ip]:990/ -1 -v --disable-epsv --ftp-skip-pasv-ip --ftp-ssl --insecure
FTPSClient 扩展(示例代码)
import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import javax.net.ssl.SSLContext;
import org.apache.commons.net.MalformedServerReplyException;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;
/**
* TODO Document Me!
*/
public class PassiveFTPSClient extends FTPSClient {
private String passiveSkipToHost;
private int passiveSkipToPort;
private boolean skipPassiveIP;
/** Pattern for PASV mode responses. Groups: (n,n,n,n),(n),(n) */
private static final java.util.regex.Pattern PARMS_PAT;
static {
PARMS_PAT = java.util.regex.Pattern.compile(
"(\\d{1,3},\\d{1,3},\\d{1,3},\\d{1,3}),(\\d{1,3}),(\\d{1,3})");
}
/**
* @param b
* @param sslContext
*/
public PassiveFTPSClient(boolean b, SSLContext sslContext) {
super(b, sslContext);
}
protected void _parsePassiveModeReply(String reply) throws MalformedServerReplyException {
if (isSkipPassiveIP()) {
System.out.println( "================> _parsePassiveModeReply" + getPassiveSkipToHost());
java.util.regex.Matcher m = PARMS_PAT.matcher(reply);
if (!m.find()) {
throw new MalformedServerReplyException(
"Could not parse passive host information.\nServer Reply: " + reply);
}
try {
int oct1 = Integer.parseInt(m.group(2));
int oct2 = Integer.parseInt(m.group(3));
passiveSkipToPort = (oct1 << 8) | oct2;
}
catch (NumberFormatException e) {
throw new MalformedServerReplyException(
"Could not parse passive port information.\nServer Reply: " + reply);
}
//do nothing
} else {
super._parsePassiveModeReply(reply);
}
}
protected Socket _openDataConnection_(String command, String arg) throws IOException {
System.out.println( "================> _openDataConnection_" + getPassiveSkipToHost());
System.out.println( "================> _openDataConnection_ isSkipPassiveIP: " + isSkipPassiveIP());
if (!isSkipPassiveIP()) {
return super._openDataConnection_(command, arg);
}
System.out.println( "================> getDataConnectionMode: " + getDataConnectionMode());
if (getDataConnectionMode() != ACTIVE_LOCAL_DATA_CONNECTION_MODE &&
getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) {
return null;
}
final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address;
Socket socket;
if (getDataConnectionMode() == ACTIVE_LOCAL_DATA_CONNECTION_MODE) {
return super._openDataConnection_(command, arg);
}
else
{ // We must be in PASSIVE_LOCAL_DATA_CONNECTION_MODE
// Try EPSV command first on IPv6 - and IPv4 if enabled.
// When using IPv4 with NAT it has the advantage
// to work with more rare configurations.
// E.g. if FTP server has a static PASV address (external network)
// and the client is coming from another internal network.
// In that case the data connection after PASV command would fail,
// while EPSV would make the client succeed by taking just the port.
boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address;
if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE)
{
System.out.println( "================> _parseExtendedPassiveModeReply a: ");
_parseExtendedPassiveModeReply(_replyLines.get(0));
}
else
{
if (isInet6Address) {
return null; // Must use EPSV for IPV6
}
// If EPSV failed on IPV4, revert to PASV
if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) {
return null;
}
System.out.println( "================> _parseExtendedPassiveModeReply b: ");
_parsePassiveModeReply(_replyLines.get(0));
}
// hardcode fore testing
//__passiveHost = "10.180.255.181";
socket = _socketFactory_.createSocket();
if (getReceiveDataSocketBufferSize() > 0) {
socket.setReceiveBufferSize(getReceiveDataSocketBufferSize());
}
if (getSendDataSocketBufferSize() > 0) {
socket.setSendBufferSize(getSendDataSocketBufferSize() );
}
if (getPassiveLocalIPAddress() != null) {
System.out.println( "================> socket.bind: " + getPassiveSkipToHost());
socket.bind(new InetSocketAddress(getPassiveSkipToHost(), 0));
}
// For now, let's just use the data timeout value for waiting for
// the data connection. It may be desirable to let this be a
// separately configurable value. In any case, we really want
// to allow preventing the accept from blocking indefinitely.
// if (__dataTimeout >= 0) {
// socket.setSoTimeout(__dataTimeout);
// }
System.out.println( "================> socket connect: " + getPassiveSkipToHost() + ":" + passiveSkipToPort);
socket.connect(new InetSocketAddress(getPassiveSkipToHost(), passiveSkipToPort), connectTimeout);
if ((getRestartOffset() > 0) && !restart(getRestartOffset()))
{
socket.close();
return null;
}
if (!FTPReply.isPositivePreliminary(sendCommand(command, arg)))
{
socket.close();
return null;
}
}
if (isRemoteVerificationEnabled() && !verifyRemote(socket))
{
socket.close();
throw new IOException(
"Host attempting data connection " + socket.getInetAddress().getHostAddress() +
" is not same as server " + getRemoteAddress().getHostAddress());
}
return socket;
}
/**
* Enable or disable passive mode NAT workaround.
* If enabled, a site-local PASV mode reply address will be replaced with the
* remote host address to which the PASV mode request was sent
* (unless that is also a site local address).
* This gets around the problem that some NAT boxes may change the
* reply.
*
* The default is true, i.e. site-local replies are replaced.
* @param enabled true to enable replacing internal IP's in passive
* mode.
*/
public void setSkipPassiveIP(boolean enabled) {
super.setPassiveNatWorkaround(enabled);
this.skipPassiveIP = enabled;
System.out.println( "================> skipPassiveIP: " + skipPassiveIP);
}
/**
* Return the skipPassiveIP.
* @return the skipPassiveIP
*/
public boolean isSkipPassiveIP() {
return skipPassiveIP;
}
/**
* Return the passiveSkipToHost.
* @return the passiveSkipToHost
*/
public String getPassiveSkipToHost() {
return passiveSkipToHost;
}
/**
* Set the passiveSkipToHost.
* @param passiveSkipToHost the passiveSkipToHost to set
*/
public void setPassiveSkipToHost(String passiveSkipToHost) {
this.passiveSkipToHost = passiveSkipToHost;
System.out.println( "================> setPassiveSkipToHost: " + passiveSkipToHost);
}
}