You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2016/01/11 07:28:55 UTC

mina-sshd git commit: [SSHD-623] Add 'end-of-list' indicator for SFTP SSH_FXP_NAME and SSH_FXP_READDIR responses

Repository: mina-sshd
Updated Branches:
  refs/heads/master 2bd4f5709 -> 66d53ac5a


[SSHD-623] Add 'end-of-list' indicator for SFTP SSH_FXP_NAME and SSH_FXP_READDIR responses


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/66d53ac5
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/66d53ac5
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/66d53ac5

Branch: refs/heads/master
Commit: 66d53ac5aec0e6c7e6a37923e48c06daf99e61b1
Parents: 2bd4f57
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Jan 11 08:28:45 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Jan 11 08:28:45 2016 +0200

----------------------------------------------------------------------
 .../subsystem/sftp/AbstractSftpClient.java      |  92 +++++++++---
 .../sshd/client/subsystem/sftp/SftpClient.java  |  57 ++++++++
 .../subsystem/sftp/SftpDirEntryIterator.java    |  18 ++-
 .../sshd/common/subsystem/sftp/SftpHelper.java  |  80 +++++++++++
 .../server/subsystem/sftp/SftpSubsystem.java    | 143 +++++++++++--------
 .../client/subsystem/sftp/SftpVersionsTest.java |  50 +++++++
 6 files changed, 361 insertions(+), 79 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/66d53ac5/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
index 53e29b2..ac37b31 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.subsystem.AbstractSubsystemClient;
 import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
@@ -107,11 +108,6 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme
     }
 
     @Override
-    public int read(Handle handle, long fileOffset, byte[] dst) throws IOException {
-        return read(handle, fileOffset, dst, 0, dst.length);
-    }
-
-    @Override
     public OutputStream write(String path) throws IOException {
         return write(path, DEFAULT_WRITE_BUFFER_SIZE);
     }
@@ -367,10 +363,13 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme
             if (version == SftpConstants.SFTP_V3) {
                 longName = buffer.getString();
             }
+
             Attributes attrs = readAttributes(buffer);
+            Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
+            // TODO decide what to do if not-null and not TRUE
             if (log.isTraceEnabled()) {
-                log.trace("checkOneName({})[id={}] ({})[{}]: {}",
-                          getClientChannel(), id, name, longName, attrs);
+                log.trace("checkOneName({})[id={}] ({})[{}] eol={}: {}",
+                          getClientChannel(), id, name, longName, indicator, attrs);
             }
             return name;
         }
@@ -766,8 +765,27 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme
         checkStatus(SftpConstants.SSH_FXP_RENAME, buffer);
     }
 
-    @Override
+    @Override   // TODO make this a default method in Java 8
+    public int read(Handle handle, long fileOffset, byte[] dst) throws IOException {
+        return read(handle, fileOffset, dst, null);
+    }
+
+    @Override   // TODO make this a default method in Java 8
+    public int read(Handle handle, long fileOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
+        return read(handle, fileOffset, dst, 0, dst.length, eofSignalled);
+    }
+
+    @Override   // TODO make this a default method in Java 8
     public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len) throws IOException {
+        return read(handle, fileOffset, dst, dstOffset, len, null);
+    }
+
+    @Override
+    public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException {
+        if (eofSignalled != null) {
+            eofSignalled.set(null);
+        }
+
         if (!isOpen()) {
             throw new IOException("read(" + handle + "/" + fileOffset + ")[" + dstOffset + "/" + len + "] client is closed");
         }
@@ -777,22 +795,38 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme
         buffer.putBytes(id);
         buffer.putLong(fileOffset);
         buffer.putInt(len);
-        return checkData(SftpConstants.SSH_FXP_READ, buffer, dstOffset, dst);
+        return checkData(SftpConstants.SSH_FXP_READ, buffer, dstOffset, dst, eofSignalled);
     }
 
-    protected int checkData(int cmd, Buffer request, int dstOffset, byte[] dst) throws IOException {
+    protected int checkData(int cmd, Buffer request, int dstOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
+        if (eofSignalled != null) {
+            eofSignalled.set(null);
+        }
         int reqId = send(cmd, request);
         Buffer response = receive(reqId);
-        return checkData(response, dstOffset, dst);
+        return checkData(response, dstOffset, dst, eofSignalled);
     }
 
-    protected int checkData(Buffer buffer, int dstoff, byte[] dst) throws IOException {
+    protected int checkData(Buffer buffer, int dstoff, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
+        if (eofSignalled != null) {
+            eofSignalled.set(null);
+        }
+
         int length = buffer.getInt();
         int type = buffer.getUByte();
         int id = buffer.getInt();
         if (type == SftpConstants.SSH_FXP_DATA) {
             int len = buffer.getInt();
             buffer.getRawBytes(dst, dstoff, len);
+            Boolean indicator = SftpHelper.getEndOfFileIndicatorValue(buffer, getVersion());
+            if (log.isTraceEnabled()) {
+                log.trace("checkData({}][id={}] offset={}, len={}, EOF={}",
+                          getClientChannel(), id, dstoff, len, indicator);
+            }
+            if (eofSignalled != null) {
+                eofSignalled.set(indicator);
+            }
+
             return len;
         }
 
@@ -903,8 +937,16 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme
         return handle;
     }
 
-    @Override
+    @Override   // TODO in JDK-8 make this a default method
     public List<DirEntry> readDir(Handle handle) throws IOException {
+        return readDir(handle, null);
+    }
+
+    @Override
+    public List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException {
+        if (eolIndicator != null) {
+            eolIndicator.set(null);    // assume unknown information
+        }
         if (!isOpen()) {
             throw new IOException("readDir(" + handle + ") client is closed");
         }
@@ -912,28 +954,44 @@ public abstract class AbstractSftpClient extends AbstractSubsystemClient impleme
         byte[] id = handle.getIdentifier();
         Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* some extra fields */, false);
         buffer.putBytes(id);
-        return checkDir(receive(send(SftpConstants.SSH_FXP_READDIR, buffer)));
+        return checkDir(receive(send(SftpConstants.SSH_FXP_READDIR, buffer)), eolIndicator);
     }
 
-    protected List<DirEntry> checkDir(Buffer buffer) throws IOException {
+    protected List<DirEntry> checkDir(Buffer buffer, AtomicReference<Boolean> eolIndicator) throws IOException {
+        if (eolIndicator != null) {
+            eolIndicator.set(null);    // assume unknown
+        }
         int length = buffer.getInt();
         int type = buffer.getUByte();
         int id = buffer.getInt();
         if (type == SftpConstants.SSH_FXP_NAME) {
             int len = buffer.getInt();
+            int version = getVersion();
+            ClientChannel channel = getClientChannel();
+            if (log.isDebugEnabled()) {
+                log.debug("checkDir({}}[id={}] reading {} entries", channel, id, len);
+            }
             List<DirEntry> entries = new ArrayList<DirEntry>(len);
             for (int i = 0; i < len; i++) {
                 String name = buffer.getString();
-                int version = getVersion();
                 String longName = (version == SftpConstants.SFTP_V3) ? buffer.getString() : null;
                 Attributes attrs = readAttributes(buffer);
                 if (log.isTraceEnabled()) {
                     log.trace("checkDir({})[id={}][{}] ({})[{}]: {}",
-                              getClientChannel(), id, i, name, longName, attrs);
+                              channel, id, i, name, longName, attrs);
                 }
 
                 entries.add(new DirEntry(name, longName, attrs));
             }
+
+            Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
+            if (eolIndicator != null) {
+                eolIndicator.set(indicator);
+            }
+
+            if (log.isDebugEnabled()) {
+                log.debug("checkDir({}}[id={}] read count={}, eol={}", channel, entries.size(), indicator);
+            }
             return entries;
         }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/66d53ac5/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index c63c95a..fa439f7 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -32,6 +32,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.subsystem.SubsystemClient;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
@@ -485,10 +486,53 @@ public interface SftpClient extends SubsystemClient {
 
     void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException;
 
+    /**
+     * Reads data from the open (file) handle
+     *
+     * @param handle     The file {@link Handle} to read from
+     * @param fileOffset The file offset to read from
+     * @param dst        The destination buffer
+     * @return Number of read bytes - {@code -1} if EOF reached
+     * @throws IOException If failed to read the data
+     * @see #read(Handle, long, byte[], int, int)
+     */
     int read(Handle handle, long fileOffset, byte[] dst) throws IOException;
 
+    /**
+     * Reads data from the open (file) handle
+     *
+     * @param handle     The file {@link Handle} to read from
+     * @param fileOffset The file offset to read from
+     * @param dst        The destination buffer
+     * @param eofSignalled If not {@code null} then upon return holds a value indicating
+     *                   whether EOF was reached due to the read. If {@code null} indicator
+     *                   value then this indication is not available
+     * @return Number of read bytes - {@code -1} if EOF reached
+     * @throws IOException If failed to read the data
+     * @see #read(Handle, long, byte[], int, int, AtomicReference)
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A>
+     */
+    int read(Handle handle, long fileOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException;
+
     int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len) throws IOException;
 
+    /**
+     * Reads data from the open (file) handle
+     *
+     * @param handle     The file {@link Handle} to read from
+     * @param fileOffset The file offset to read from
+     * @param dst        The destination buffer
+     * @param dstOffset  Offset in destination buffer to place the read data
+     * @param len        Available destination buffer size to read
+     * @param eofSignalled If not {@code null} then upon return holds a value indicating
+     *                   whether EOF was reached due to the read. If {@code null} indicator
+     *                   value then this indication is not available
+     * @return Number of read bytes - {@code -1} if EOF reached
+     * @throws IOException If failed to read the data
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A>
+     */
+    int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException;
+
     void write(Handle handle, long fileOffset, byte[] src) throws IOException;
 
     void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException;
@@ -511,6 +555,19 @@ public interface SftpClient extends SubsystemClient {
      */
     List<DirEntry> readDir(Handle handle) throws IOException;
 
+    /**
+     * @param handle Directory {@link Handle} to read from
+     * @return A {@link List} of entries - {@code null} to indicate no more entries
+     * @param eolIndicator An indicator that can be used to get information
+     * whether end of list has been reached - ignored if {@code null}. Upon
+     * return, set value indicates whether all entries have been exhausted - a {@code null}
+     * value means that this information cannot be provided and another call to
+     * {@code readDir} is necessary in order to verify that no more entries are pending
+     * @throws IOException If failed to access the remote site
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     */
+    List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException;
+
     String canonicalPath(String path) throws IOException;
 
     Attributes stat(String path) throws IOException;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/66d53ac5/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java
index 72cdaca..c828015 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpDirEntryIterator.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.nio.channels.Channel;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
 import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
@@ -37,6 +38,7 @@ import org.apache.sshd.common.util.logging.AbstractLoggingBean;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class SftpDirEntryIterator extends AbstractLoggingBean implements Iterator<DirEntry>, Channel {
+    private final AtomicReference<Boolean> eolIndicator = new AtomicReference<>();
     private final SftpClient client;
     private final String dirPath;
     private CloseableHandle dirHandle;
@@ -127,10 +129,20 @@ public class SftpDirEntryIterator extends AbstractLoggingBean implements Iterato
 
     protected List<DirEntry> load(CloseableHandle handle) {
         try {
-            List<DirEntry> entries = client.readDir(handle);
-            if (entries == null) {
+            // check if previous call yielded an end-of-list indication
+            Boolean eolReached = eolIndicator.getAndSet(null);
+            if ((eolReached != null) && eolReached.booleanValue()) {
                 if (log.isTraceEnabled()) {
-                    log.trace("load(" + getPath() + ") exhausted all entries");
+                    log.trace("load({}) exhausted all entries on previous call", getPath());
+                }
+                return null;
+            }
+
+            List<DirEntry> entries = client.readDir(handle, eolIndicator);
+            eolReached = eolIndicator.get();
+            if ((entries == null) || ((eolReached != null) && eolReached.booleanValue())) {
+                if (log.isTraceEnabled()) {
+                    log.trace("load({}) exhausted all entries - EOL={}", getPath(), eolReached);
                 }
                 close();
             }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/66d53ac5/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
index d03cac0..3f97a6f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
@@ -48,6 +48,8 @@ import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.sshd.common.PropertyResolver;
+import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
@@ -62,12 +64,90 @@ import org.apache.sshd.server.subsystem.sftp.UnixDateFormat;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public final class SftpHelper {
+    /**
+     * Used to control whether to append the end-of-list indicator for
+     * SSH_FXP_NAME responses via {@link #indicateEndOfNamesList(Buffer, int, PropertyResolver, Boolean)}
+     * call, as indicated by <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     */
+    public static final String APPEND_END_OF_LIST_INDICATOR = "sftp-append-eol-indicator";
+
+    /**
+     * Default value for {@link #APPEND_END_OF_LIST_INDICATOR} if none configured
+     */
+    public static final boolean DEFAULT_APPEND_END_OF_LIST_INDICATOR = true;
 
     private SftpHelper() {
         throw new UnsupportedOperationException("No instance allowed");
     }
 
     /**
+     * Retrieves the end-of-file indicator for {@code SSH_FXP_DATA} responses, provided
+     * the version is at least 6, and the buffer has enough available data
+     *
+     * @param buffer  The {@link Buffer} to retrieve the data from
+     * @param version The SFTP version being used
+     * @return The indicator value - {@code null} if none retrieved
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A>
+     */
+    public static Boolean getEndOfFileIndicatorValue(Buffer buffer, int version) {
+        return (version <  SftpConstants.SFTP_V6) || (buffer.available() < 1) ? null : Boolean.valueOf(buffer.getBoolean());
+    }
+
+    /**
+     * Retrieves the end-of-list indicator for {@code SSH_FXP_NAME} responses, provided
+     * the version is at least 6, and the buffer has enough available data
+     *
+     * @param buffer  The {@link Buffer} to retrieve the data from
+     * @param version The SFTP version being used
+     * @return The indicator value - {@code null} if none retrieved
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     * @see #indicateEndOfNamesList(Buffer, int, PropertyResolver, Boolean)
+     */
+    public static Boolean getEndOfListIndicatorValue(Buffer buffer, int version) {
+        return (version <  SftpConstants.SFTP_V6) || (buffer.available() < 1) ? null : Boolean.valueOf(buffer.getBoolean());
+    }
+
+    /**
+     * Appends the end-of-list={@code TRUE} indicator for {@code SSH_FXP_NAME} responses, provided
+     * the version is at least 6 and the feature is enabled
+     *
+     * @param buffer   The {@link Buffer} to append the indicator
+     * @param version  The SFTP version being used
+     * @param resolver The {@link PropertyResolver} to query whether to enable the feature
+     * @return The actual indicator value used - {@code null} if none appended
+     * @see #indicateEndOfNamesList(Buffer, int, PropertyResolver, Boolean)
+     */
+    public static Boolean indicateEndOfNamesList(Buffer buffer, int version, PropertyResolver resolver) {
+        return indicateEndOfNamesList(buffer, version, resolver, Boolean.TRUE);
+    }
+
+    /**
+     * Appends the end-of-list indicator for {@code SSH_FXP_NAME} responses, provided the version
+     * is at least 6, the feature is enabled and the indicator value is not {@code null}
+     *
+     * @param buffer         The {@link Buffer} to append the indicator
+     * @param version        The SFTP version being used
+     * @param resolver       The {@link PropertyResolver} to query whether to enable the feature
+     * @param indicatorValue The indicator value - {@code null} means don't append the indicator
+     * @return The actual indicator value used - {@code null} if none appended
+     * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A>
+     * @see #APPEND_END_OF_LIST_INDICATOR
+     * @see #DEFAULT_APPEND_END_OF_LIST_INDICATOR
+     */
+    public static Boolean indicateEndOfNamesList(Buffer buffer, int version, PropertyResolver resolver, Boolean indicatorValue) {
+        if ((version < SftpConstants.SFTP_V6) || (indicatorValue == null)) {
+            return null;
+        }
+
+        if (!PropertyResolverUtils.getBooleanProperty(resolver, APPEND_END_OF_LIST_INDICATOR, DEFAULT_APPEND_END_OF_LIST_INDICATOR)) {
+            return null;
+        }
+
+        buffer.putBoolean(indicatorValue.booleanValue());
+        return indicatorValue;
+    }
+
+    /**
      * Writes a file / folder's attributes to a buffer
      *
      * @param buffer The target {@link Buffer}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/66d53ac5/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
index 59c5098..a92a64a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -1536,10 +1536,19 @@ public class SftpSubsystem
                  */
                 result = doRealPathV345(id, path, options);
             } else {
-                // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13 section 8.9
-                int control = 0;
+                /*
+                 * See https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9
+                 *
+                 *      This field is optional, and if it is not present in the packet, it
+                 *      is assumed to be SSH_FXP_REALPATH_NO_CHECK.
+                 */
+                int control = SftpConstants.SSH_FXP_REALPATH_NO_CHECK;
                 if (buffer.available() > 0) {
                     control = buffer.getUByte();
+                    if (log.isDebugEnabled()) {
+                        log.debug("doRealPath({}) - control=0x{} for path={}",
+                                  getServerSession(), Integer.toHexString(control), path);
+                    }
                 }
 
                 Collection<String> extraPaths = new LinkedList<>();
@@ -1551,35 +1560,42 @@ public class SftpSubsystem
 
                 Path p = result.getFirst();
                 Boolean status = result.getSecond();
-                if (control == SftpConstants.SSH_FXP_REALPATH_STAT_IF) {
-                    if (status == null) {
-                        attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
-                    } else if (status) {
-                        try {
-                            attrs = getAttributes(p, IoUtils.getLinkOptions(false));
-                        } catch (IOException e) {
-                            if (log.isDebugEnabled()) {
-                                log.debug("doRealPath({}) - failed ({}) to retrieve attributes of {}: {}",
-                                          getServerSession(), e.getClass().getSimpleName(), p, e.getMessage());
+                switch (control) {
+                    case SftpConstants.SSH_FXP_REALPATH_STAT_IF:
+                        if (status == null) {
+                            attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
+                        } else if (status) {
+                            try {
+                                attrs = getAttributes(p, IoUtils.getLinkOptions(false));
+                            } catch (IOException e) {
+                                if (log.isDebugEnabled()) {
+                                    log.debug("doRealPath({}) - failed ({}) to retrieve attributes of {}: {}",
+                                              getServerSession(), e.getClass().getSimpleName(), p, e.getMessage());
+                                }
+                                if (log.isTraceEnabled()) {
+                                    log.trace("doRealPath(" + getServerSession() + ")[" + p + "] attributes retrieval failure details", e);
+                                }
                             }
-                            if (log.isTraceEnabled()) {
-                                log.trace("doRealPath(" + getServerSession() + ")[" + p + "] attributes retrieval failure details", e);
+                        } else {
+                            if (log.isDebugEnabled()) {
+                                log.debug("doRealPath({}) - dummy attributes for non-existing file: {}", getServerSession(), p);
                             }
                         }
-                    } else {
-                        if (log.isDebugEnabled()) {
-                            log.debug("doRealPath({}) - dummy attributes for non-existing file: {}",
-                                      getServerSession(), p);
+                        break;
+                    case SftpConstants.SSH_FXP_REALPATH_STAT_ALWAYS:
+                        if (status == null) {
+                            attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
+                        } else if (status) {
+                            attrs = getAttributes(p, options);
+                        } else {
+                            throw new FileNotFoundException(p.toString());
                         }
-                    }
-                } else if (control == SftpConstants.SSH_FXP_REALPATH_STAT_ALWAYS) {
-                    if (status == null) {
-                        attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
-                    } else if (status) {
-                        attrs = getAttributes(p, options);
-                    } else {
-                        throw new FileNotFoundException(p.toString());
-                    }
+                        break;
+                    case SftpConstants.SSH_FXP_REALPATH_NO_CHECK:
+                        break;
+                    default:
+                        log.warn("doRealPath({}) unknown control value 0x{} for path={}",
+                                 getServerSession(), Integer.toHexString(control), p);
                 }
             }
         } catch (IOException | RuntimeException e) {
@@ -1594,6 +1610,10 @@ public class SftpSubsystem
         Path p = resolveFile(path);
         int numExtra = GenericUtils.size(extraPaths);
         if (numExtra > 0) {
+            if (log.isDebugEnabled()) {
+                log.debug("doRealPathV6({})[id={}] path={}, extra={}",
+                          getServerSession(), id, path, extraPaths);
+            }
             StringBuilder sb = new StringBuilder(GenericUtils.length(path) + numExtra * 8);
             sb.append(path);
 
@@ -1795,13 +1815,15 @@ public class SftpSubsystem
 
                 int count = doReadDir(id, handle, dh, reply, PropertyResolverUtils.getIntProperty(getServerSession(), MAX_PACKET_LENGTH_PROP, DEFAULT_MAX_PACKET_LENGTH));
                 BufferUtils.updateLengthPlaceholder(reply, lenPos, count);
-                if (log.isDebugEnabled()) {
-                    log.debug("doReadDir({})({})[{}] - sent {} entries", getServerSession(), handle, h, count);
-                }
+                ServerSession session = getServerSession();
                 if ((!dh.isSendDot()) && (!dh.isSendDotDot()) && (!dh.hasNext())) {
-                    // if no more files to send
                     dh.markDone();
                 }
+
+                Boolean indicator = SftpHelper.indicateEndOfNamesList(reply, getVersion(), session, Boolean.valueOf(dh.isDone()));
+                if (log.isDebugEnabled()) {
+                    log.debug("doReadDir({})({})[{}] - seding {} entries - eol={}", session, handle, h, count, indicator);
+                }
             } else {
                 // empty directory
                 dh.markDone();
@@ -2556,6 +2578,33 @@ public class SftpSubsystem
         send(buffer);
     }
 
+    protected void sendLink(Buffer buffer, int id, String link) throws IOException {
+        //in case we are running on Windows
+        String unixPath = link.replace(File.separatorChar, '/');
+        //normalize the given path, use *nix style separator
+        String normalizedPath = SelectorUtils.normalizePath(unixPath, "/");
+
+        buffer.putByte((byte) SftpConstants.SSH_FXP_NAME);
+        buffer.putInt(id);
+        buffer.putInt(1);   // one response
+        buffer.putString(normalizedPath);
+
+        /*
+         * As per the spec (https://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.10):
+         *
+         *      The server will respond with a SSH_FXP_NAME packet containing only
+         *      one name and a dummy attributes value.
+         */
+        Map<String, Object> attrs = Collections.<String, Object>emptyMap();
+        if (version == SftpConstants.SFTP_V3) {
+            buffer.putString(SftpHelper.getLongName(normalizedPath, attrs));
+        }
+
+        writeAttrs(buffer, attrs);
+        SftpHelper.indicateEndOfNamesList(buffer, getVersion(), getServerSession());
+        send(buffer);
+    }
+
     protected void sendPath(Buffer buffer, int id, Path f, Map<String, ?> attrs) throws IOException {
         buffer.putByte((byte) SftpConstants.SSH_FXP_NAME);
         buffer.putInt(id);
@@ -2574,34 +2623,10 @@ public class SftpSubsystem
         if (version == SftpConstants.SFTP_V3) {
             f = resolveFile(normalizedPath);
             buffer.putString(getLongName(f, getShortName(f), attrs));
-            buffer.putInt(0);   // no flags
-        } else if (version >= SftpConstants.SFTP_V4) {
-            writeAttrs(buffer, attrs);
-        } else {
-            throw new IllegalStateException("sendPath(" + f + ") unsupported version: " + version);
-        }
-        send(buffer);
-    }
-
-    protected void sendLink(Buffer buffer, int id, String link) throws IOException {
-        //in case we are running on Windows
-        String unixPath = link.replace(File.separatorChar, '/');
-        buffer.putByte((byte) SftpConstants.SSH_FXP_NAME);
-        buffer.putInt(id);
-        buffer.putInt(1);   // one response
-
-        buffer.putString(unixPath);
-        if (version == SftpConstants.SFTP_V3) {
-            buffer.putString(unixPath);
         }
 
-        /*
-         * As per the spec:
-         *
-         *      The server will respond with a SSH_FXP_NAME packet containing only
-         *      one name and a dummy attributes value.
-         */
-        SftpHelper.writeAttrs(buffer, version, Collections.<String, Object>emptyMap());
+        writeAttrs(buffer, attrs);
+        SftpHelper.indicateEndOfNamesList(buffer, getVersion(), getServerSession());
         send(buffer);
     }
 
@@ -2740,7 +2765,7 @@ public class SftpSubsystem
     }
 
     protected void writeAttrs(Buffer buffer, Map<String, ?> attributes) throws IOException {
-        SftpHelper.writeAttrs(buffer, version, attributes);
+        SftpHelper.writeAttrs(buffer, getVersion(), attributes);
     }
 
     protected Map<String, Object> getAttributes(Path file, LinkOption... options) throws IOException {
@@ -3225,7 +3250,7 @@ public class SftpSubsystem
     }
 
     protected Map<String, Object> readAttrs(Buffer buffer) throws IOException {
-        return SftpHelper.readAttrs(buffer, version);
+        return SftpHelper.readAttrs(buffer, getVersion());
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/66d53ac5/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
index 2af1423..bd1e61e 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpVersionsTest.java
@@ -38,10 +38,12 @@ import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
 import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
@@ -447,6 +449,54 @@ public class SftpVersionsTest extends AbstractSftpClientTestSupport {
         assertEquals("Mismatched invocations count", numInvoked, numInvocations.get());
     }
 
+    @Test   // see SSHD-623
+    public void testEndOfListIndicator() throws Exception {
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try (SftpClient sftp = session.createSftpClient(getTestedVersion())) {
+                    AtomicReference<Boolean> eolIndicator = new AtomicReference<>();
+                    int version = sftp.getVersion();
+                    Path targetPath = detectTargetFolder();
+                    Path parentPath = targetPath.getParent();
+                    String remotePath = Utils.resolveRelativeRemotePath(parentPath, targetPath);
+
+                    try (CloseableHandle handle = sftp.openDir(remotePath)) {
+                        List<DirEntry> entries = sftp.readDir(handle, eolIndicator);
+                        for (int index = 1; entries != null; entries = sftp.readDir(handle, eolIndicator), index++) {
+                            Boolean value = eolIndicator.get();
+                            if (version < SftpConstants.SFTP_V6) {
+                                assertNull("Unexpected indicator value at iteration #" + index, value);
+                            } else {
+                                assertNotNull("No indicator returned at iteration #" + index, value);
+                                if (value.booleanValue()) {
+                                    break;
+                                }
+                            }
+                            eolIndicator.set(null);    // make sure starting fresh
+                        }
+
+                        Boolean value = eolIndicator.get();
+                        if (version < SftpConstants.SFTP_V6) {
+                            assertNull("Unexpected end-of-list indication received at end of entries", value);
+                            assertNull("Unexpected no last entries indication", entries);
+                        } else {
+                            assertNotNull("No end-of-list indication received at end of entries", value);
+                            assertNotNull("No last received entries", entries);
+                            assertTrue("Bad end-of-list value", value.booleanValue());
+                        }
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
     @Override
     public String toString() {
         return getClass().getSimpleName() + "[" + getTestedVersion() + "]";