将文件上传到 testcontainer FTP 服务器失败,连接后连接被拒绝
Uploading a file to testcontainer FTP server fails with Connection refused after being connected
我正在使用 FTPClient 对 FTP 服务器使用 Testcontainers
。
这里有一个可重现的代码示例:
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
class FtpUtilsTest {
private static final int PORT = 21;
private static final String USER = "user";
private static final String PASSWORD = "password";
private static final int FTP_TIMEOUT_IN_MILLISECONDS = 1000 * 60;
private static final GenericContainer ftp = new GenericContainer(
new ImageFromDockerfile()
.withDockerfileFromBuilder(builder ->
builder
.from("delfer/alpine-ftp-server:latest")
.build()
)
)
.withExposedPorts(PORT)
.withEnv("USERS", USER + "|" + PASSWORD);
@BeforeAll
public static void staticSetup() throws IOException {
ftp.start();
}
@AfterAll
static void afterAll() {
ftp.stop();
}
@Test
void test() throws IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.setDataTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
ftpClient.setConnectTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
ftpClient.setDefaultTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
// Log
ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
// Connect
try {
ftpClient.connect("localhost", ftp.getMappedPort(PORT));
ftpClient.setSoTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect();
throw new AssertionError();
}
// Login
boolean loginSuccess = ftpClient.login(USER, PASSWORD);
if (!loginSuccess) {
throw new AssertionError();
}
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
} catch (IOException e) {
throw new AssertionError(e);
}
String remoteFile = "fileonftp";
try (InputStream targetStream = new ByteArrayInputStream("Hello FTP".getBytes())) {
assertThat(ftpClient.isConnected()).isTrue();
ftpClient.storeFile(remoteFile, targetStream);
}
}
}
这会打印:
220 Welcome Alpine ftp server https://hub.docker.com/r/delfer/alpine-ftp-server/
USER *******
331 Please specify the password.
PASS *******
230 Login successful.
TYPE I
200 Switching to Binary mode.
PASV
227 Entering Passive Mode (172,17,0,3,82,15).
[Replacing PASV mode reply address 172.17.0.3 with 127.0.0.1]
然后失败:
Connection refused (Connection refused)
java.net.ConnectException: Connection refused (Connection refused)
...
at java.base/java.net.Socket.connect(Socket.java:609)
at org.apache.commons.net.ftp.FTPClient._openDataConnection_(FTPClient.java:866)
at org.apache.commons.net.ftp.FTPClient._storeFile(FTPClient.java:1053)
at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:3816)
at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:3846)
我不明白的是连接成功后登录失败,返回true
for isConnected
.
事实证明,当删除 ftpClient.enterLocalPassiveMode();
时它可以工作,但我需要它才能在被动模式下工作。
我猜失败与切换到不同的端口进行被动调用有关。
但是当尝试将端口添加到 withExposedPorts
时,容器无法启动:
Caused by: org.testcontainers.containers.ContainerLaunchException: Timed out waiting for container port to open (localhost ports: [55600, 55601, 55602, 55603, 55604, 55605, 55606, 55607, 55608, 55609, 55598, 55599] should be listening)
运行 对抗 docker (docker run -d -p 21:21 -p 21000-21010:21000-21010 -e USERS="user|password" delfer/alpine-ftp-server
) 有效。
本地 docker 版本:
- Docker 版本 20.10.11,构建 dea9396
- Docker 桌面版 4.3.1
测试容器 - 似乎在 1.16.2 和 1.15.3 上表现相同
Link 测试容器 discussion
正如您在评论中已经了解到的那样,FTP 被动模式的棘手部分是服务器使用另一个端口(不是 21)进行通信。
在您使用的 docker 图像中,默认情况下它是 21000-21010
范围内的一个端口。所以你需要发布(公开)这些额外的容器端口。在 docker run
命令中,您为此使用了 -p 21000-21010:21000-21010
。
但是,Testcontainers 库旨在发布到随机主机端口,以避免在主机端已占用所需的固定端口(或一系列端口)时出现问题。
如果主机端的 FTP 被动模式随机端口会导致问题,因为 afaik 你不能指示 ftp 客户端覆盖端口,FTP 服务器返回被动模式.您需要 ftpClient.connect("localhost", ftp.getMappedPort(PORT));
之类的东西,但也需要被动模式端口。
因此我在这里看到的唯一解决方案是使用 FixedHostPortContainer。尽管它被标记为已弃用并且由于提到的端口占用问题而不推荐使用,但我认为这是一个有效的用例。 FixedHostPortGenericContainer
允许在主机端发布固定端口。类似于:
private static final int PASSIVE_MODE_PORT = 21000;
...
private static final FixedHostPortGenericContainer ftp = new FixedHostPortGenericContainer<>(
"delfer/alpine-ftp-server:latest")
.withFixedExposedPort(PASSIVE_MODE_PORT, PASSIVE_MODE_PORT)
.withExposedPorts(PORT)
.withEnv("USERS", USER + "|" + PASSWORD)
.withEnv("MIN_PORT", String.valueOf(PASSIVE_MODE_PORT))
.withEnv("MAX_PORT", String.valueOf(PASSIVE_MODE_PORT));
请记住,此解决方案依赖于 21000
端口始终空闲的假设。如果你要在无法保证的环境中 运行 这个,那么你需要先调整它以找到一个空闲的主机端口。喜欢:
private static FixedHostPortGenericContainer ftp = new FixedHostPortGenericContainer<>(
"delfer/alpine-ftp-server:latest")
.withExposedPorts(PORT)
.withEnv("USERS", USER + "|" + PASSWORD);
@BeforeAll
public static void staticSetup() throws Exception {
Integer freePort = 0;
try (ServerSocket socket = new ServerSocket(0)) {
freePort = socket.getLocalPort();
}
ftp = (FixedHostPortGenericContainer)ftp.withFixedExposedPort(freePort, freePort)
.withEnv("MIN_PORT", String.valueOf(freePort))
.withEnv("MAX_PORT", String.valueOf(freePort));
ftp.start();
}
我正在使用 FTPClient 对 FTP 服务器使用 Testcontainers
。
这里有一个可重现的代码示例:
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
class FtpUtilsTest {
private static final int PORT = 21;
private static final String USER = "user";
private static final String PASSWORD = "password";
private static final int FTP_TIMEOUT_IN_MILLISECONDS = 1000 * 60;
private static final GenericContainer ftp = new GenericContainer(
new ImageFromDockerfile()
.withDockerfileFromBuilder(builder ->
builder
.from("delfer/alpine-ftp-server:latest")
.build()
)
)
.withExposedPorts(PORT)
.withEnv("USERS", USER + "|" + PASSWORD);
@BeforeAll
public static void staticSetup() throws IOException {
ftp.start();
}
@AfterAll
static void afterAll() {
ftp.stop();
}
@Test
void test() throws IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.setDataTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
ftpClient.setConnectTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
ftpClient.setDefaultTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
// Log
ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
// Connect
try {
ftpClient.connect("localhost", ftp.getMappedPort(PORT));
ftpClient.setSoTimeout(FTP_TIMEOUT_IN_MILLISECONDS);
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect();
throw new AssertionError();
}
// Login
boolean loginSuccess = ftpClient.login(USER, PASSWORD);
if (!loginSuccess) {
throw new AssertionError();
}
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
} catch (IOException e) {
throw new AssertionError(e);
}
String remoteFile = "fileonftp";
try (InputStream targetStream = new ByteArrayInputStream("Hello FTP".getBytes())) {
assertThat(ftpClient.isConnected()).isTrue();
ftpClient.storeFile(remoteFile, targetStream);
}
}
}
这会打印:
220 Welcome Alpine ftp server https://hub.docker.com/r/delfer/alpine-ftp-server/
USER *******
331 Please specify the password.
PASS *******
230 Login successful.
TYPE I
200 Switching to Binary mode.
PASV
227 Entering Passive Mode (172,17,0,3,82,15).
[Replacing PASV mode reply address 172.17.0.3 with 127.0.0.1]
然后失败:
Connection refused (Connection refused)
java.net.ConnectException: Connection refused (Connection refused)
...
at java.base/java.net.Socket.connect(Socket.java:609)
at org.apache.commons.net.ftp.FTPClient._openDataConnection_(FTPClient.java:866)
at org.apache.commons.net.ftp.FTPClient._storeFile(FTPClient.java:1053)
at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:3816)
at org.apache.commons.net.ftp.FTPClient.storeFile(FTPClient.java:3846)
我不明白的是连接成功后登录失败,返回true
for isConnected
.
事实证明,当删除 ftpClient.enterLocalPassiveMode();
时它可以工作,但我需要它才能在被动模式下工作。
我猜失败与切换到不同的端口进行被动调用有关。
但是当尝试将端口添加到 withExposedPorts
时,容器无法启动:
Caused by: org.testcontainers.containers.ContainerLaunchException: Timed out waiting for container port to open (localhost ports: [55600, 55601, 55602, 55603, 55604, 55605, 55606, 55607, 55608, 55609, 55598, 55599] should be listening)
运行 对抗 docker (docker run -d -p 21:21 -p 21000-21010:21000-21010 -e USERS="user|password" delfer/alpine-ftp-server
) 有效。
本地 docker 版本:
- Docker 版本 20.10.11,构建 dea9396
- Docker 桌面版 4.3.1
测试容器 - 似乎在 1.16.2 和 1.15.3 上表现相同
Link 测试容器 discussion
正如您在评论中已经了解到的那样,FTP 被动模式的棘手部分是服务器使用另一个端口(不是 21)进行通信。
在您使用的 docker 图像中,默认情况下它是 21000-21010
范围内的一个端口。所以你需要发布(公开)这些额外的容器端口。在 docker run
命令中,您为此使用了 -p 21000-21010:21000-21010
。
但是,Testcontainers 库旨在发布到随机主机端口,以避免在主机端已占用所需的固定端口(或一系列端口)时出现问题。
如果主机端的 FTP 被动模式随机端口会导致问题,因为 afaik 你不能指示 ftp 客户端覆盖端口,FTP 服务器返回被动模式.您需要 ftpClient.connect("localhost", ftp.getMappedPort(PORT));
之类的东西,但也需要被动模式端口。
因此我在这里看到的唯一解决方案是使用 FixedHostPortContainer。尽管它被标记为已弃用并且由于提到的端口占用问题而不推荐使用,但我认为这是一个有效的用例。 FixedHostPortGenericContainer
允许在主机端发布固定端口。类似于:
private static final int PASSIVE_MODE_PORT = 21000;
...
private static final FixedHostPortGenericContainer ftp = new FixedHostPortGenericContainer<>(
"delfer/alpine-ftp-server:latest")
.withFixedExposedPort(PASSIVE_MODE_PORT, PASSIVE_MODE_PORT)
.withExposedPorts(PORT)
.withEnv("USERS", USER + "|" + PASSWORD)
.withEnv("MIN_PORT", String.valueOf(PASSIVE_MODE_PORT))
.withEnv("MAX_PORT", String.valueOf(PASSIVE_MODE_PORT));
请记住,此解决方案依赖于 21000
端口始终空闲的假设。如果你要在无法保证的环境中 运行 这个,那么你需要先调整它以找到一个空闲的主机端口。喜欢:
private static FixedHostPortGenericContainer ftp = new FixedHostPortGenericContainer<>(
"delfer/alpine-ftp-server:latest")
.withExposedPorts(PORT)
.withEnv("USERS", USER + "|" + PASSWORD);
@BeforeAll
public static void staticSetup() throws Exception {
Integer freePort = 0;
try (ServerSocket socket = new ServerSocket(0)) {
freePort = socket.getLocalPort();
}
ftp = (FixedHostPortGenericContainer)ftp.withFixedExposedPort(freePort, freePort)
.withEnv("MIN_PORT", String.valueOf(freePort))
.withEnv("MAX_PORT", String.valueOf(freePort));
ftp.start();
}