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 2015/07/06 13:46:54 UTC

mina-sshd git commit: [SSHD-527] Generate a synthetic ".." folder in SFTP listing of folder contents

Repository: mina-sshd
Updated Branches:
  refs/heads/master 7d79f7029 -> f7f21bc85


[SSHD-527] Generate a synthetic ".." folder in SFTP listing of folder contents


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

Branch: refs/heads/master
Commit: f7f21bc857aa021b787b581a13489855105e6e66
Parents: 7d79f70
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Jul 6 14:46:40 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Jul 6 14:46:40 2015 +0300

----------------------------------------------------------------------
 .../java/org/apache/sshd/client/SshClient.java  | 386 ++++++++++++-------
 .../subsystem/sftp/AbstractSftpClient.java      |  18 +-
 .../sshd/client/subsystem/sftp/SftpClient.java  |  20 +-
 .../client/subsystem/sftp/SftpFileSystem.java   |   2 +-
 .../subsystem/sftp/SftpFileSystemProvider.java  |  29 +-
 .../server/subsystem/sftp/SftpSubsystem.java    |  53 ++-
 .../org/apache/sshd/client/SshClientMain.java   |  31 ++
 .../subsystem/sftp/SftpFileSystemTest.java      |   3 +
 .../sshd/client/subsystem/sftp/SftpTest.java    |  29 +-
 9 files changed, 399 insertions(+), 172 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index fca2412..ae0b758 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -19,8 +19,10 @@
 package org.apache.sshd.client;
 
 import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.net.InetSocketAddress;
@@ -65,6 +67,7 @@ import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.IoConnectFuture;
 import org.apache.sshd.common.io.IoConnector;
+import org.apache.sshd.common.keyprovider.AbstractFileKeyPairProvider;
 import org.apache.sshd.common.session.AbstractSession;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.SecurityUtils;
@@ -317,6 +320,170 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
           Main class implementation
      *=================================*/
 
+    // NOTE: ClientSession#getFactoryManager is the SshClient
+    public static ClientSession setupClientSession(
+            String portOption, final BufferedReader stdin, final PrintStream stdout, final PrintStream stderr, String ... args)
+                    throws Exception {
+
+        int port = -1;
+        String host = null;
+        String login = null;
+        boolean error = false;
+        List<File> identities = new ArrayList<File>();
+        Map<String, String> options = new LinkedHashMap<String, String>();
+        int numArgs = GenericUtils.length(args);
+        for (int i = 0; i < numArgs; i++) {
+            String argName = args[i];
+            if (portOption.equals(argName)) {
+                if (i + 1 >= numArgs) {
+                    stderr.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+                
+                if (port > 0) {
+                    stderr.println(argName + " option value re-specified: " + port);
+                    error = true;
+                    break;
+                }
+                
+                if ((port=Integer.parseInt(args[++i])) <= 0) {
+                    stderr.println("Bad option value for " + argName + ": " + port);
+                    error = true;
+                    break;
+                }
+            } else if ("-i".equals(argName)) {
+                if (i + 1 >= numArgs) {
+                    stderr.println("option requires and argument: " + argName);
+                    error = true;
+                    break;
+                }
+                
+                File f = new File(args[++i]);
+                identities.add(f);
+            } else if ("-o".equals(argName)) {
+                if (i + 1 >= numArgs) {
+                    stderr.println("option requires and argument: " + argName);
+                    error = true;
+                    break;
+                }
+                String opt = args[++i];
+                int idx = opt.indexOf('=');
+                if (idx <= 0) {
+                    stderr.println("bad syntax for option: " + opt);
+                    error = true;
+                    break;
+                }
+                options.put(opt.substring(0, idx), opt.substring(idx + 1));
+            } else if ("-l".equals(argName)) {
+                if (i + 1 >= numArgs) {
+                    stderr.println("option requires an argument: " + argName);
+                    error = true;
+                    break;
+                }
+                
+                if (login != null) {
+                    stderr.println(argName + " option value re-specified: " + port);
+                    error = true;
+                    break;
+                }
+
+                login = args[++i];
+            } else if (argName.charAt(0) != '-') {
+                host = argName;
+                if (login == null) {
+                    int pos = host.indexOf('@');  // check if user@host
+                    if (pos > 0) {
+                        login = host.substring(0, pos);
+                        host = host.substring(pos + 1);
+                    }
+                }
+            }
+        }
+        
+        if ((!error) && GenericUtils.isEmpty(host)) {
+            stderr.println("Hostname not specified");
+            error = true;
+        }
+
+        if (login == null) {
+            login = System.getProperty("user.name");
+        }
+        
+        if (port <= 0) {
+            port = SshConfigFileReader.DEFAULT_PORT;
+        }
+        
+        if (error) {
+            return null;
+        }
+
+        SshClient client = SshClient.setUpDefaultClient();
+        try {
+            if (SecurityUtils.isBouncyCastleRegistered()) {
+                try {
+                    if (GenericUtils.isEmpty(identities)) {
+                        ClientIdentity.setKeyPairProvider(client,
+                                false,  // not strict - even though we should...
+                                true,   // supportedOnly
+                                new FilePasswordProvider() {
+                                    @Override
+                                    public String getPassword(String file) throws IOException {
+                                        stdout.print("Enter password for private key file=" + file + ": ");
+                                        return stdin.readLine();
+                                    }
+                                });
+                    } else {
+                        AbstractFileKeyPairProvider provider = SecurityUtils.createFileKeyPairProvider();
+                        provider.setFiles(identities);
+                        client.setKeyPairProvider(provider);
+                    }
+                } catch (Throwable t) {
+                    stderr.println("Error loading user keys: " + t.getMessage());
+                }
+            }
+
+            Map<String,Object> props = client.getProperties();
+            props.putAll(options);
+    
+            client.start();
+            client.setUserInteraction(new UserInteraction() {
+                    @Override
+                    public void welcome(String banner) {
+                        stdout.println(banner);
+                    }
+        
+                    @Override
+                    public String[] interactive(String destination, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
+                        int numPropmts = GenericUtils.length(prompt);
+                        String[] answers = new String[numPropmts];
+                        try {
+                            for (int i = 0; i < numPropmts; i++) {
+                                stdout.print(prompt[i] + " ");
+                                answers[i] = stdin.readLine();
+                            }
+                        } catch (IOException e) {
+                            // ignored
+                        }
+                        return answers;
+                    }
+                });
+            
+            // TODO use a configurable wait time
+            ClientSession session = client.connect(login, host, port).await().getSession();
+            try {
+                session.auth().verify();    // TODO use a configurable wait time
+                return session;
+            } catch(Exception e) {
+                session.close(true);
+                throw e;
+            }
+        } catch(Exception e) {
+            client.close();
+            throw e;
+        }
+    }
+
     public static void main(String[] args) throws Exception {
         Handler fh = new ConsoleHandler();
         fh.setLevel(Level.FINEST);
@@ -347,93 +514,55 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
         }
         root.addHandler(fh);
 
-        int port = SshConfigFileReader.DEFAULT_PORT;
-        String host = null;
-        String login = System.getProperty("user.name");
+        PrintStream stdout=System.out, stderr=System.err;
         boolean agentForward = false;
         List<String> command = null;
         int logLevel = 0;
         int socksPort = -1;
+        int numArgs = GenericUtils.length(args);
         boolean error = false;
-        List<String> identities = new ArrayList<String>();
-        Map<String, String> options = new LinkedHashMap<String, String>();
-
-        for (int i = 0; i < args.length; i++) {
-            if (command == null && "-p".equals(args[i])) {
-                if (i + 1 >= args.length) {
-                    System.err.println("option requires an argument: " + args[i]);
+        String target = null;
+        for (int i = 0; i < numArgs; i++) {
+            String argName = args[i];
+            if (command == null && "-D".equals(argName)) {
+                if (i + 1 >= numArgs) {
+                    System.err.println("option requires an argument: " + argName);
                     error = true;
                     break;
                 }
-                port = Integer.parseInt(args[++i]);
-            } else if (command == null && "-D".equals(args[i])) {
-                if (i + 1 >= args.length) {
-                    System.err.println("option requires an argument: " + args[i]);
+                if (socksPort > 0) {
+                    stderr.println(argName + " option value re-specified: " + socksPort);
                     error = true;
                     break;
                 }
-                socksPort = Integer.parseInt(args[++i]);
-            } else if (command == null && "-l".equals(args[i])) {
-                if (i + 1 >= args.length) {
-                    System.err.println("option requires an argument: " + args[i]);
+                
+                if ((socksPort=Integer.parseInt(args[++i])) <= 0) {
+                    stderr.println("Bad option value for " + argName + ": " + socksPort);
                     error = true;
                     break;
                 }
-                login = args[++i];
-            } else if (command == null && "-v".equals(args[i])) {
+            } else if (command == null && "-v".equals(argName)) {
                 logLevel += 1;
-            } else if (command == null && "-vv".equals(args[i])) {
+            } else if (command == null && "-vv".equals(argName)) {
                 logLevel += 2;
-            } else if (command == null && "-vvv".equals(args[i])) {
+            } else if (command == null && "-vvv".equals(argName)) {
                 logLevel += 3;
-            } else if (command == null && "-A".equals(args[i])) {
+            } else if (command == null && "-A".equals(argName)) {
                 agentForward = true;
-            } else if (command == null && "-a".equals(args[i])) {
+            } else if (command == null && "-a".equals(argName)) {
                 agentForward = false;
-            } else if (command == null && "-i".equals(args[i])) {
-                if (i + 1 >= args.length) {
-                    System.err.println("option requires and argument: " + args[i]);
-                    error = true;
-                    break;
-                }
-                identities.add(args[++i]);
-            } else if (command == null && "-o".equals(args[i])) {
-                if (i + 1 >= args.length) {
-                    System.err.println("option requires and argument: " + args[i]);
-                    error = true;
-                    break;
-                }
-                String opt = args[++i];
-                int idx = opt.indexOf('=');
-                if (idx <= 0) {
-                    System.err.println("bad syntax for option: " + opt);
-                    error = true;
-                    break;
-                }
-                options.put(opt.substring(0, idx), opt.substring(idx + 1));
-            } else if (command == null && args[i].startsWith("-")) {
-                System.err.println("illegal option: " + args[i]);
-                error = true;
-                break;
             } else {
-                if (command == null && host == null) {
-                    host = args[i];
+                if (command == null && target == null) {
+                    target = argName;
                 } else {
                     if (command == null) {
                         command = new ArrayList<String>();
                     }
-                    command.add(args[i]);
+                    command.add(argName);
                 }
             }
         }
-        if (host == null) {
-            System.err.println("hostname required");
-            error = true;
-        }
-        if (error) {
-            System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-D socksPort] [-l login] [-p port] [-o option=value] hostname [command]");
-            System.exit(-1);
-        }
+
         if (logLevel <= 0) {
             root.setLevel(Level.WARNING);
         } else if (logLevel == 1) {
@@ -444,98 +573,71 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
             root.setLevel(Level.FINEST);
         }
 
-        try(SshClient client = SshClient.setUpDefaultClient();
-            BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
-            if (SecurityUtils.isBouncyCastleRegistered()) {
-                try {
-                    ClientIdentity.setKeyPairProvider(client,
-                            false,  // not strict - even though we should...
-                            true,   // supportedOnly
-                            new FilePasswordProvider() {
-                                @Override
-                                public String getPassword(String file) throws IOException {
-                                    System.out.print("Enter password for private key file=" + file + ": ");
-                                    return stdin.readLine();
-                                }
-                            });
-                } catch (Throwable t) {
-                    System.out.println("Error loading user keys: " + t.getMessage());
+        ClientSession session=null;
+        try(BufferedReader stdin = new BufferedReader(new InputStreamReader(new NoCloseInputStream(System.in)))) {
+            if (!error) {
+                if ((session=setupClientSession("-p", stdin, stdout, stderr, args)) == null) {
+                    error = true;
                 }
             }
-    
-            Map<String,Object> props = client.getProperties();
-            props.putAll(options);
-    
-            client.start();
-            client.setUserInteraction(new UserInteraction() {
-                    @Override
-                    public void welcome(String banner) {
-                        System.out.println(banner);
-                    }
-        
-                    @Override
-                    public String[] interactive(String destination, String name, String instruction, String lang, String[] prompt, boolean[] echo) {
-                        String[] answers = new String[prompt.length];
-                        try {
-                            for (int i = 0; i < prompt.length; i++) {
-                                System.out.print(prompt[i] + " ");
-                                answers[i] = stdin.readLine();
-                            }
-                        } catch (IOException e) {
-                            // ignored
-                        }
-                        return answers;
-                    }
-                });
 
-            /*
-            String authSock = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME);
-            if (authSock == null && provider != null) {
-                Iterable<KeyPair> keys = provider.loadKeys();
-                AgentServer server = new AgentServer();
-                authSock = server.start();
-                SshAgent agent = new AgentClient(authSock);
-                for (KeyPair key : keys) {
-                    agent.addIdentity(key, "");
-                }
-                agent.close();
-                props.put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, authSock);
+            if (error) {
+                System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-D socksPort] [-l login] [-p port] [-o option=value] hostname/user@host [command]");
+                System.exit(-1);
             }
-            */
+
+            try(SshClient client = (SshClient) session.getFactoryManager()) {
+                /*
+                String authSock = System.getenv(SshAgent.SSH_AUTHSOCKET_ENV_NAME);
+                if (authSock == null && provider != null) {
+                    Iterable<KeyPair> keys = provider.loadKeys();
+                    AgentServer server = new AgentServer();
+                    authSock = server.start();
+                    SshAgent agent = new AgentClient(authSock);
+                    for (KeyPair key : keys) {
+                        agent.addIdentity(key, "");
+                    }
+                    agent.close();
+                    props.put(SshAgent.SSH_AUTHSOCKET_ENV_NAME, authSock);
+                }
+                */
     
-            try(ClientSession session = client.connect(login, host, port).await().getSession()) {
-                session.auth().verify();
-        
-                if (socksPort >= 0) {
-                    session.startDynamicPortForwarding(new SshdSocketAddress("localhost", socksPort));
-                    Thread.sleep(Long.MAX_VALUE);
-                } else {
-                    ClientChannel channel;
-                    if (command == null) {
-                        channel = session.createChannel(ClientChannel.CHANNEL_SHELL);
-                        ((ChannelShell) channel).setAgentForwarding(agentForward);
-                        channel.setIn(new NoCloseInputStream(System.in));
+                try {
+                    if (socksPort >= 0) {
+                        session.startDynamicPortForwarding(new SshdSocketAddress("localhost", socksPort));
+                        Thread.sleep(Long.MAX_VALUE);
                     } else {
-                        StringWriter w = new StringWriter();
-                        for (String cmd : command) {
-                            w.append(cmd).append(" ");
+                        ClientChannel channel;
+                        if (command == null) {
+                            channel = session.createChannel(ClientChannel.CHANNEL_SHELL);
+                            ((ChannelShell) channel).setAgentForwarding(agentForward);
+                            channel.setIn(new NoCloseInputStream(System.in));
+                        } else {
+                            StringWriter w = new StringWriter();
+                            for (String cmd : command) {
+                                w.append(cmd).append(" ");
+                            }
+                            w.close();
+                            channel = session.createChannel(ClientChannel.CHANNEL_EXEC, w.toString());
                         }
-                        w.close();
-                        channel = session.createChannel(ClientChannel.CHANNEL_EXEC, w.toString());
-                    }
-                    
-                    try {
-                        channel.setOut(new NoCloseOutputStream(System.out));
-                        channel.setErr(new NoCloseOutputStream(System.err));
-                        channel.open().await(); // TODO use verify and a configurable timeout
-                        channel.waitFor(ClientChannel.CLOSED, 0);
-                    } finally {
-                        channel.close();
+                        
+                        try {
+                            channel.setOut(new NoCloseOutputStream(System.out));
+                            channel.setErr(new NoCloseOutputStream(System.err));
+                            channel.open().await(); // TODO use verify and a configurable timeout
+                            channel.waitFor(ClientChannel.CLOSED, 0);
+                        } finally {
+                            channel.close();
+                        }
+                        session.close(false);
                     }
-                    session.close(false);
+                } finally {
+                    client.stop();
                 }
             } finally {
-                client.stop();
+                if (session != null) {
+                    session.close();
+                }
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/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 6f348da..bd1988c 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
@@ -91,10 +91,12 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -769,7 +771,7 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
     }
 
     @Override
-    public DirEntry[] readDir(Handle handle) throws IOException {
+    public List<DirEntry> readDir(Handle handle) throws IOException {
         if (!isOpen()) {
             throw new IOException("readDir(" + handle + ") client is closed");
         }
@@ -779,7 +781,7 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
         return checkDir(receive(send(SSH_FXP_READDIR, buffer)));
     }
 
-    protected DirEntry[] checkDir(Buffer buffer) throws IOException {
+    protected List<DirEntry> checkDir(Buffer buffer) throws IOException {
         int length = buffer.getInt();
         int type = buffer.getUByte();
         int id = buffer.getInt();
@@ -796,7 +798,7 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             throw new SftpException(substatus, msg);
         } else if (type == SSH_FXP_NAME) {
             int len = buffer.getInt();
-            DirEntry[] entries = new DirEntry[len];
+            List<DirEntry> entries = new ArrayList<DirEntry>(len);
             for (int i = 0; i < len; i++) {
                 String name = buffer.getString();
                 int version = getVersion();
@@ -806,7 +808,7 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
                     log.trace("checkDir(id={})[{}] ({})[{}]: {}", Integer.valueOf(id), Integer.valueOf(i), name, longName, attrs);
                 }
 
-                entries[i] = new DirEntry(name, longName, attrs);
+                entries.add(new DirEntry(name, longName, attrs));
             }
             return entries;
         } else {
@@ -972,7 +974,7 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             public Iterator<DirEntry> iterator() {
                 return new Iterator<DirEntry>() {
                     private CloseableHandle handle;
-                    private DirEntry[] entries;
+                    private List<DirEntry> entries;
                     private int index;
 
                     {
@@ -982,13 +984,13 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
 
                     @Override
                     public boolean hasNext() {
-                        return (entries != null) && (index < entries.length);
+                        return (entries != null) && (index < entries.size());
                     }
 
                     @Override
                     public DirEntry next() {
-                        DirEntry entry = entries[index++];
-                        if (index >= entries.length) {
+                        DirEntry entry = entries.get(index++);
+                        if (index >= entries.size()) {
                             load();
                         }
                         return entry;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/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 16295ba..bc3d5f5 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
@@ -31,6 +31,7 @@ import java.nio.channels.Channel;
 import java.nio.file.attribute.FileTime;
 import java.util.Collection;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -280,7 +281,17 @@ public interface SftpClient extends SubsystemClient {
 
     CloseableHandle openDir(String path) throws IOException;
 
-    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
+     * <B>Note:</B> the list may be <U>incomplete</U> since the client and
+     * server have some internal imposed limit on the number of entries they
+     * can process. Therefore several calls to this method may be required
+     * (until {@code null}). In order to iterate over all the entries use
+     * {@link #readDir(String)}
+     * @throws IOException If failed to access the remote site
+     */
+    List<DirEntry> readDir(Handle handle) throws IOException;
 
     String canonicalPath(String path) throws IOException;
 
@@ -308,6 +319,13 @@ public interface SftpClient extends SubsystemClient {
     // High level API
     //
 
+    /**
+     * @param path The remote directory path
+     * @return An {@link Iterable} that can be used to iterate over all the
+     * directory entries (unlike {@link #readDir(Handle)})
+     * @throws IOException If failed to access the remote site
+     * @see #readDir(Handle)
+     */
     Iterable<DirEntry> readDir(String path) throws IOException;
 
     // default values used if none specified

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
index a2af81f..2adb315 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
@@ -312,7 +312,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         }
 
         @Override
-        public DirEntry[] readDir(Handle handle) throws IOException {
+        public List<DirEntry> readDir(Handle handle) throws IOException {
             if (!isOpen()) {
                 throw new IOException("readDir(" + handle + ") client is closed");
             }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
index 23ae4f2..796f678 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
@@ -73,6 +73,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -328,20 +329,44 @@ public class SftpFileSystemProvider extends FileSystemProvider {
             @Override
             public Iterator<Path> iterator() {
                 return new Iterator<Path>() {
+                    private boolean dotIgnored, dotdotIgnored;
+                    private SftpClient.DirEntry curEntry = nextEntry();
+
                     @SuppressWarnings("synthetic-access")
                     private final Iterator<SftpClient.DirEntry> it = iter.iterator();
 
                     @Override
                     public boolean hasNext() {
-                        return it.hasNext();
+                        return (curEntry != null);
                     }
 
                     @Override
                     public Path next() {
-                        SftpClient.DirEntry entry = it.next();
+                        if (curEntry == null) {
+                            throw new NoSuchElementException("No next entry");
+                        }
+
+                        SftpClient.DirEntry entry = curEntry;
+                        curEntry = nextEntry();
                         return p.resolve(entry.filename);
                     }
 
+                    private SftpClient.DirEntry nextEntry() {
+                        while(it.hasNext()) {
+                            SftpClient.DirEntry entry = it.next();
+                            String name = entry.filename;
+                            if (".".equals(name) && (!dotIgnored)) {
+                                dotIgnored = true;
+                            } else if ("..".equals(name) && (!dotdotIgnored)) {
+                                dotdotIgnored = true;
+                            } else {
+                                return entry;
+                            }
+                        }
+                        
+                        return null;
+                    }
+
                     @Override
                     public void remove() {
                         throw new UnsupportedOperationException("newDirectoryStream(" + p + ") Iterator#remove() N/A");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/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 bb1f1e5..0ff4943 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
@@ -208,7 +208,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
     }
 
     protected static class DirectoryHandle extends Handle implements Iterator<Path> {
-        private boolean done;
+        private boolean done, sendDotDot;
         // the directory should be read once at "open directory"
         private DirectoryStream<Path> ds;
         private Iterator<Path> fileList;
@@ -216,6 +216,9 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         public DirectoryHandle(Path file) throws IOException {
             super(file);
             ds = Files.newDirectoryStream(file);
+            
+            Path parent = file.getParent();
+            sendDotDot = (parent != null);  // if no parent then no need to send ".."
             fileList = ds.iterator();
         }
 
@@ -227,6 +230,14 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             this.done = done;
         }
 
+        public boolean isSendDotDot() {
+            return sendDotDot;
+        }
+        
+        public void setSendDotDot(boolean sendIt) {
+            sendDotDot = sendIt;
+        }
+
         @Override
         public boolean hasNext() {
             return fileList.hasNext();
@@ -1228,12 +1239,12 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             } else if (!Files.isReadable(file)) {
                 sendStatus(id, SSH_FX_PERMISSION_DENIED, file.toString());
             } else {
-                if (dh.hasNext()) {
-                    // There is at least one file in the directory.
+                if (dh.isSendDotDot() || dh.hasNext()) {
+                    // There is at least one file in the directory or we need to send the "..".
                     // Send only a few files at a time to not create packets of a too
                     // large size or have a timeout to occur.
-                    sendName(id, dh);
-                    if (!dh.hasNext()) {
+                    sendDirEntries(id, dh);
+                    if ((!dh.isSendDotDot()) && (!dh.hasNext())) {
                         // if no more files to send
                         dh.setDone(true);
                         dh.clearFileList();
@@ -1252,11 +1263,14 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
 
     protected void doOpenDir(Buffer buffer, int id) throws IOException {
         String path = buffer.getString();
-        log.debug("Received SSH_FXP_OPENDIR (path={})", path);
+        Path f = resolveFile(path);
+        Path abs = f.toAbsolutePath();
+        Path p = abs.normalize();
+        log.debug("Received SSH_FXP_OPENDIR (path={})[{}]", path, p);
+
         try {
-            Path            p = resolveFile(path);
-            LinkOption[]    options = IoUtils.getLinkOptions(false);
-            Boolean         status = IoUtils.checkFileExists(p, options);
+            LinkOption[] options = IoUtils.getLinkOptions(false);
+            Boolean status = IoUtils.checkFileExists(p, options);
             if (status == null) {
                 throw new AccessDeniedException("Cannot determine open-dir existence of " + p);
             }
@@ -1738,7 +1752,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         send(buffer);
     }
 
-    protected void sendName(int id, Iterator<Path> files) throws IOException {
+    protected void sendDirEntries(int id, DirectoryHandle files) throws IOException {
         Buffer buffer = new ByteArrayBuffer();
         buffer.putByte((byte) SSH_FXP_NAME);
         buffer.putInt(id);
@@ -1746,13 +1760,22 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         buffer.putInt(0);
 
         int nb = 0, maxSize = FactoryManagerUtils.getIntProperty(session, MAX_PACKET_LENGTH_PROP, DEFAULT_MAX_PACKET_LENGTH);
-        while (files.hasNext() && (buffer.wpos() < maxSize)) {
-            Path    f = files.next();
-            String  shortName = getShortName(f);
-            buffer.putString(shortName, StandardCharsets.UTF_8);
+        while((files.isSendDotDot() || files.hasNext()) && (buffer.wpos() < maxSize)) {
+            Path f;
+            String  shortName;
+            if (files.isSendDotDot()) {
+                f = files.getFile().getParent();
+                shortName = "..";
+                files.setSendDotDot(false); // do not send it again
+            } else {
+                f = files.next();
+                shortName = getShortName(f);
+            }
+
+            buffer.putString(shortName);
             if (version == SFTP_V3) {
                 String  longName = getLongName(f);
-                buffer.putString(longName, StandardCharsets.UTF_8); // Format specified in the specs
+                buffer.putString(longName);
                 if (log.isTraceEnabled()) {
                     log.trace("sendName(id=" + id + ")[" + nb + "] - " + shortName + " [" + longName + "]");
                 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java b/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java
new file mode 100644
index 0000000..390960e
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/SshClientMain.java
@@ -0,0 +1,31 @@
+/*
+ * 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.sshd.client;
+
+/**
+ * Just a test class used to invoke {@link SshClient#main(String[])} in
+ * order to have logging - which is in {@code test} scope
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SshClientMain {
+    public static void main(String[] args) throws Exception {
+        SshClient.main(args);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
index 3997c5b..bfefc71 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
@@ -314,6 +314,9 @@ public class SftpFileSystemTest extends BaseTestSupport {
             String  rootName = root.toString();
             try (DirectoryStream<Path> ds = Files.newDirectoryStream(root)) {
                 for (Path child : ds) {
+                    String name = child.getFileName().toString();
+                    assertNotEquals("Unexpected dot name", ".", name);
+                    assertNotEquals("Unexpected dotdot name", "..", name);
                     System.out.append('\t').append('[').append(rootName).append("] ").println(child);
                 }
             } catch(IOException | RuntimeException e) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f7f21bc8/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
index 7aa686f..cf88921 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
@@ -39,6 +39,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -803,9 +804,23 @@ public class SftpTest extends BaseTestSupport {
         }            
 
         try(SftpClient.CloseableHandle h = sftp.openDir(dir)) {
-            SftpClient.DirEntry[] dirEntries = sftp.readDir(h);
+            List<SftpClient.DirEntry> dirEntries = sftp.readDir(h);
             assertNotNull("No dir entries", dirEntries);
-            assertEquals("Mismatced number of dir entries", 1, dirEntries.length);
+            
+            boolean dotFiltered = false, dotdotFiltered = false;
+            for (Iterator<SftpClient.DirEntry> it = dirEntries.iterator(); it.hasNext(); ) {
+                SftpClient.DirEntry entry = it.next();
+                String name = entry.filename;
+                if (".".equals(name) && (!dotFiltered)) {
+                    it.remove();
+                    dotFiltered = true;
+                } else if ("..".equals(name) && (!dotdotFiltered)) {
+                    it.remove();
+                    dotdotFiltered = true;
+                }
+            }
+
+            assertEquals("Mismatched number of dir entries", 1, dirEntries.size());
             assertNull("Unexpected entry read", sftp.readDir(h));
         }
 
@@ -832,9 +847,17 @@ public class SftpTest extends BaseTestSupport {
         assertTrue("Test directory not reported as such", attributes.isDirectory());
 
         int nb = 0;
+        boolean dotFiltered = false, dotdotFiltered = false;
         for (SftpClient.DirEntry entry : sftp.readDir(dir)) {
             assertNotNull("Unexpected null entry", entry);
-            nb++;
+            String name = entry.filename;
+            if (".".equals(name) && (!dotFiltered)) {
+                dotFiltered = true;
+            } else if ("..".equals(name) && (!dotdotFiltered)) {
+                dotdotFiltered = true;
+            } else {
+                nb++;
+            }
         }
         assertEquals("Mismatched read dir entries", 1, nb);