diff --git a/src/main/java/org/apache/commons/net/ftp/FTP.java b/src/main/java/org/apache/commons/net/ftp/FTP.java
index 0c0b3044d..a956e6a15 100644
--- a/src/main/java/org/apache/commons/net/ftp/FTP.java
+++ b/src/main/java/org/apache/commons/net/ftp/FTP.java
@@ -151,6 +151,13 @@ public class FTP extends SocketClient {
*/
public static final int COMPRESSED_TRANSFER_MODE = 12;
+ /**
+ * A constant used to indicate a file is to be transferred as FTP compressed
+ * data with MODE Z (zlib). All constants ending in TRANSFER_MODE
are used to indicate
+ * file transfer modes.
+ */
+ public static final int COMPRESSED_MODE_Z_TRANSFER_MODE = 13;
+
// We have to ensure that the protocol communication is in ASCII,
// but we use ISO-8859-1 just in case 8-bit characters cross
// the wire.
@@ -163,7 +170,7 @@ public class FTP extends SocketClient {
/** Length of the FTP reply code (3 alphanumerics) */
public static final int REPLY_CODE_LEN = 3;
- private static final String modes = "AEILNTCFRPSBC";
+ private static final String modes = "AEILNTCFRPSBCZ";
protected int _replyCode;
protected ArrayList _replyLines;
protected boolean _newReplyString;
diff --git a/src/main/java/org/apache/commons/net/ftp/FTPClient.java b/src/main/java/org/apache/commons/net/ftp/FTPClient.java
index f808f6f8d..5babcb6fb 100644
--- a/src/main/java/org/apache/commons/net/ftp/FTPClient.java
+++ b/src/main/java/org/apache/commons/net/ftp/FTPClient.java
@@ -507,7 +507,6 @@ static String parsePathname(final String reply) {
private int fileFormat;
@SuppressWarnings("unused") // field is written, but currently not read
private int fileStructure;
- @SuppressWarnings("unused") // field is written, but currently not read
private int fileTransferMode;
private boolean remoteVerificationEnabled;
@@ -713,7 +712,8 @@ protected Socket _openDataConnection_(final String command, final String arg) th
if (soTimeoutMillis >= 0) {
server.setSoTimeout(soTimeoutMillis);
}
- socket = server.accept();
+
+ socket = wrapSocketIfModeZisEnabled(server.accept());
// Ensure the timeout is set before any commands are issued on the new socket
if (soTimeoutMillis >= 0) {
@@ -749,7 +749,8 @@ protected Socket _openDataConnection_(final String command, final String arg) th
_parsePassiveModeReply(_replyLines.get(0));
}
- socket = _socketFactory_.createSocket();
+ socket = wrapSocketIfModeZisEnabled(_socketFactory_.createSocket());
+
if (receiveDataSocketBufferSize > 0) {
socket.setReceiveBufferSize(receiveDataSocketBufferSize);
}
@@ -793,6 +794,14 @@ protected Socket _openDataConnection_(final String command, final String arg) th
return socket;
}
+ private Socket wrapSocketIfModeZisEnabled(final Socket plainSocket) {
+ if (fileTransferMode == COMPRESSED_MODE_Z_TRANSFER_MODE) {
+ return ModeZSocket.wrap(plainSocket);
+ } else {
+ return plainSocket;
+ }
+ }
+
protected void _parseExtendedPassiveModeReply(String reply) throws MalformedServerReplyException {
reply = reply.substring(reply.indexOf('(') + 1, reply.indexOf(')')).trim();
diff --git a/src/main/java/org/apache/commons/net/ftp/ModeZSocket.java b/src/main/java/org/apache/commons/net/ftp/ModeZSocket.java
new file mode 100644
index 000000000..2d79b634d
--- /dev/null
+++ b/src/main/java/org/apache/commons/net/ftp/ModeZSocket.java
@@ -0,0 +1,252 @@
+/*
+ * 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.commons.net.ftp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.nio.channels.SocketChannel;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * Wrapper class for FTP data channel sockets when using MODE Z. All methods
+ * except of {@link #getInputStream()} and {@link #getOutputStream()} are
+ * calling the delegate methods directly.
+ */
+public class ModeZSocket extends Socket {
+
+ private final Socket delegate;
+
+ private ModeZSocket(final Socket delegate) {
+ this.delegate = delegate;
+ }
+
+ static Socket wrap(final Socket plain) {
+ return new ModeZSocket(plain);
+ }
+
+ @Override
+ public void connect(final SocketAddress endpoint) throws IOException {
+ delegate.connect(endpoint);
+ }
+
+ @Override
+ public void connect(final SocketAddress endpoint, final int timeout) throws IOException {
+ delegate.connect(endpoint, timeout);
+ }
+
+ @Override
+ public void bind(final SocketAddress bindpoint) throws IOException {
+ delegate.bind(bindpoint);
+ }
+
+ @Override
+ public InetAddress getInetAddress() {
+ return delegate.getInetAddress();
+ }
+
+ @Override
+ public InetAddress getLocalAddress() {
+ return delegate.getLocalAddress();
+ }
+
+ @Override
+ public int getPort() {
+ return delegate.getPort();
+ }
+
+ @Override
+ public int getLocalPort() {
+ return delegate.getLocalPort();
+ }
+
+ @Override
+ public SocketAddress getRemoteSocketAddress() {
+ return delegate.getRemoteSocketAddress();
+ }
+
+ @Override
+ public SocketAddress getLocalSocketAddress() {
+ return delegate.getLocalSocketAddress();
+ }
+
+ @Override
+ public SocketChannel getChannel() {
+ return delegate.getChannel();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return new InflaterInputStream(delegate.getInputStream());
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return new DeflaterOutputStream(delegate.getOutputStream());
+ }
+
+ @Override
+ public void setTcpNoDelay(final boolean on) throws SocketException {
+ delegate.setTcpNoDelay(on);
+ }
+
+ @Override
+ public boolean getTcpNoDelay() throws SocketException {
+ return delegate.getTcpNoDelay();
+ }
+
+ @Override
+ public void setSoLinger(final boolean on, final int linger) throws SocketException {
+ delegate.setSoLinger(on, linger);
+ }
+
+ @Override
+ public int getSoLinger() throws SocketException {
+ return delegate.getSoLinger();
+ }
+
+ @Override
+ public void sendUrgentData(final int data) throws IOException {
+ delegate.sendUrgentData(data);
+ }
+
+ @Override
+ public void setOOBInline(final boolean on) throws SocketException {
+ delegate.setOOBInline(on);
+ }
+
+ @Override
+ public boolean getOOBInline() throws SocketException {
+ return delegate.getOOBInline();
+ }
+
+ @Override
+ public synchronized void setSoTimeout(final int timeout) throws SocketException {
+ delegate.setSoTimeout(timeout);
+ }
+
+ @Override
+ public synchronized int getSoTimeout() throws SocketException {
+ return delegate.getSoTimeout();
+ }
+
+ @Override
+ public synchronized void setSendBufferSize(final int size) throws SocketException {
+ delegate.setSendBufferSize(size);
+ }
+
+ @Override
+ public synchronized int getSendBufferSize() throws SocketException {
+ return delegate.getSendBufferSize();
+ }
+
+ @Override
+ public synchronized void setReceiveBufferSize(final int size) throws SocketException {
+ delegate.setReceiveBufferSize(size);
+ }
+
+ @Override
+ public synchronized int getReceiveBufferSize() throws SocketException {
+ return delegate.getReceiveBufferSize();
+ }
+
+ @Override
+ public void setKeepAlive(final boolean on) throws SocketException {
+ delegate.setKeepAlive(on);
+ }
+
+ @Override
+ public boolean getKeepAlive() throws SocketException {
+ return delegate.getKeepAlive();
+ }
+
+ @Override
+ public void setTrafficClass(final int tc) throws SocketException {
+ delegate.setTrafficClass(tc);
+ }
+
+ @Override
+ public int getTrafficClass() throws SocketException {
+ return delegate.getTrafficClass();
+ }
+
+ @Override
+ public void setReuseAddress(final boolean on) throws SocketException {
+ delegate.setReuseAddress(on);
+ }
+
+ @Override
+ public boolean getReuseAddress() throws SocketException {
+ return delegate.getReuseAddress();
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ delegate.close();
+ }
+
+ @Override
+ public void shutdownInput() throws IOException {
+ delegate.shutdownInput();
+ }
+
+ @Override
+ public void shutdownOutput() throws IOException {
+ delegate.shutdownOutput();
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+
+ @Override
+ public boolean isConnected() {
+ return delegate.isConnected();
+ }
+
+ @Override
+ public boolean isBound() {
+ return delegate.isBound();
+ }
+
+ @Override
+ public boolean isClosed() {
+ return delegate.isClosed();
+ }
+
+ @Override
+ public boolean isInputShutdown() {
+ return delegate.isInputShutdown();
+ }
+
+ @Override
+ public boolean isOutputShutdown() {
+ return delegate.isOutputShutdown();
+ }
+
+ @Override
+ public void setPerformancePreferences(final int connectionTime, final int latency, final int bandwidth) {
+ delegate.setPerformancePreferences(connectionTime, latency, bandwidth);
+ }
+}
diff --git a/src/test/java/org/apache/commons/net/ftp/FTPClientModeZTest.java b/src/test/java/org/apache/commons/net/ftp/FTPClientModeZTest.java
new file mode 100644
index 000000000..a47d12495
--- /dev/null
+++ b/src/test/java/org/apache/commons/net/ftp/FTPClientModeZTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.commons.net.ftp;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.readAllBytes;
+import static java.nio.file.Files.write;
+import static org.apache.commons.io.FileUtils.deleteDirectory;
+import static org.apache.commons.net.ftp.FTP.COMPRESSED_MODE_Z_TRANSFER_MODE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.ftpserver.FtpServer;
+import org.apache.ftpserver.FtpServerFactory;
+import org.apache.ftpserver.ftplet.Authority;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.ftplet.UserManager;
+import org.apache.ftpserver.listener.Listener;
+import org.apache.ftpserver.listener.ListenerFactory;
+import org.apache.ftpserver.usermanager.PropertiesUserManagerFactory;
+import org.apache.ftpserver.usermanager.impl.BaseUser;
+import org.apache.ftpserver.usermanager.impl.WritePermission;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class FTPClientModeZTest {
+
+ private static final String DEFAULT_HOME = "ftp_root/";
+ private final boolean useLocalPassiveFTP;
+
+ @Parameters(name = "useLocalPassiveFTP={0}")
+ public static Boolean[] testParameters() {
+ return new Boolean[] { true, false };
+ }
+
+ public FTPClientModeZTest(boolean useLocalPassiveFTP) {
+ this.useLocalPassiveFTP = useLocalPassiveFTP;
+ }
+
+ @Test
+ public void testRetrievingFiles() throws Exception {
+
+ new File(DEFAULT_HOME).mkdirs();
+ String filename = "test_download.txt";
+ String fileContent = "Created at " + Instant.now();
+ write(Paths.get(DEFAULT_HOME).resolve(filename), fileContent.getBytes(UTF_8));
+
+ runWithFTPserver((port, user, password) -> {
+ FTPClient client = new FTPClient();
+ try {
+ client.connect("localhost", port);
+ client.login(user, password);
+ if (useLocalPassiveFTP) {
+ client.enterLocalPassiveMode();
+ }
+ assertTrue("Mode Z successfully activated",
+ client.setFileTransferMode(COMPRESSED_MODE_Z_TRANSFER_MODE));
+
+ FTPFile[] files = client.listFiles();
+ assertEquals("Only single file in home directory", 1, files.length);
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ assertTrue("File successfully transferred", client.retrieveFile(files[0].getName(), bos));
+ assertEquals("File content is not corrupted", fileContent, new String(bos.toByteArray(), UTF_8));
+ } finally {
+ client.logout();
+ }
+ });
+ }
+
+ @Test
+ public void testStoringFiles() throws Exception {
+
+ runWithFTPserver((port, user, password) -> {
+ FTPClient client = new FTPClient();
+ try {
+ client.connect("localhost", port);
+ client.login(user, password);
+ if (useLocalPassiveFTP) {
+ client.enterLocalPassiveMode();
+ }
+ assertTrue("Mode Z successfully activated",
+ client.setFileTransferMode(COMPRESSED_MODE_Z_TRANSFER_MODE));
+
+ FTPFile[] filesBeforeUpload = client.listFiles();
+ assertEquals("No files in home directory before upload", 0, filesBeforeUpload.length);
+
+ String filename = "test_upload.txt";
+ String fileContent = "Created at " + Instant.now();
+ assertTrue("File successfully transferred",
+ client.storeFile(filename, new ByteArrayInputStream(fileContent.getBytes(UTF_8))));
+
+ FTPFile[] filesAfterUpload = client.listFiles();
+ assertEquals("Single file in home directory after upload", 1, filesAfterUpload.length);
+
+ Path p = Paths.get(DEFAULT_HOME, filename);
+ assertEquals("File content is not corrupted", fileContent, new String(readAllBytes(p), UTF_8));
+ } finally {
+ client.logout();
+ }
+ });
+ }
+
+ @Before
+ @After
+ public void cleanup() throws IOException {
+ deleteDirectory(new File(DEFAULT_HOME));
+ }
+
+ private static final class FtpServerAndPort {
+ private final int port;
+ private final FtpServer ftpServer;
+
+ FtpServerAndPort(FtpServer ftpServer, int port) {
+ this.port = port;
+ this.ftpServer = ftpServer;
+ }
+ }
+
+ @FunctionalInterface
+ interface Runner {
+ void run(int port, String user, String password) throws Exception;
+ }
+
+ private static void runWithFTPserver(Runner runner) throws Exception {
+
+ String username = "test";
+ String password = "test";
+ FtpServerAndPort ftpServerAndPort = setupPlainFTPserver(username, password);
+ try {
+ runner.run(ftpServerAndPort.port, username, password);
+ } finally {
+ ftpServerAndPort.ftpServer.stop();
+ }
+ }
+
+ private static FtpServerAndPort setupPlainFTPserver(final String username, final String password)
+ throws FtpException {
+
+ final FtpServerFactory serverFactory = new FtpServerFactory();
+
+ // Init user
+ serverFactory.setUserManager(initUserManager(username, password));
+
+ final ListenerFactory factory = new ListenerFactory();
+ // Automatically assign port.
+ factory.setPort(0);
+
+ // replace the default listener
+ Listener listener = factory.createListener();
+ serverFactory.addListener("default", listener);
+
+ // start the server
+ FtpServer server = serverFactory.createServer();
+ server.start();
+
+ return new FtpServerAndPort(server, listener.getPort());
+ }
+
+ private static UserManager initUserManager(final String username, final String password) throws FtpException {
+
+ final PropertiesUserManagerFactory propertiesUserManagerFactory = new PropertiesUserManagerFactory();
+ final UserManager userManager = propertiesUserManagerFactory.createUserManager();
+ BaseUser user = new BaseUser();
+ user.setName(username);
+ user.setPassword(password);
+
+ List authorities = new ArrayList();
+ authorities.add(new WritePermission());
+ user.setAuthorities(authorities);
+
+ new File(DEFAULT_HOME).mkdirs();
+ user.setHomeDirectory(DEFAULT_HOME);
+ userManager.save(user);
+ return userManager;
+ }
+
+}