You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by gn...@apache.org on 2013/07/22 20:37:05 UTC

[8/9] git commit: Fix SFTP subsystem support for links

Fix SFTP subsystem support for links

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

Branch: refs/heads/master
Commit: ca990bdbcdf7894b95f5bbc3e65b5239f34e2a6b
Parents: 21c1cee
Author: Guillaume Nodet <gn...@apache.org>
Authored: Mon Jul 22 18:22:52 2013 +0200
Committer: Guillaume Nodet <gn...@apache.org>
Committed: Mon Jul 22 18:22:52 2013 +0200

----------------------------------------------------------------------
 .../java/org/apache/sshd/client/SftpClient.java |  18 +-
 .../sshd/client/sftp/DefaultSftpClient.java     |   1 +
 .../org/apache/sshd/common/file/SshFile.java    |  27 ++-
 .../file/nativefs/NativeFileSystemView.java     |   2 +-
 .../common/file/nativefs/NativeSshFile.java     |  88 ++++----
 .../apache/sshd/server/sftp/SftpSubsystem.java  | 203 +++++++++++++++----
 6 files changed, 232 insertions(+), 107 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ca990bdb/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
index 5150a81..99a7cd2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SftpClient.java
@@ -28,6 +28,9 @@ import java.util.EnumSet;
  */
 public interface SftpClient {
 
+    //
+    // Permission flags
+    //
     int S_IFMT =   0170000;  // bitmask for the file type bitfields
     int S_IFSOCK = 0140000;  // socket
     int S_IFLNK =  0120000;  // symbolic link
@@ -102,21 +105,12 @@ public interface SftpClient {
             this.mtime = mtime;
             return this;
         }
-        public boolean isDirectory() {
-            return (perms & S_IFMT) == S_IFDIR;
-        }
-        public boolean isRegularFile() {
-            return (perms & S_IFMT) == S_IFREG;
-        }
-        public boolean isSymlink() {
-            return (perms & S_IFMT) == S_IFLNK;
-        }
     }
 
     public static class DirEntry {
-        public final String filename;
-        public final String longFilename;
-        public final Attributes attributes;
+        public String filename;
+        public String longFilename;
+        public Attributes attributes;
         public DirEntry(String filename, String longFilename, Attributes attributes) {
             this.filename = filename;
             this.longFilename = longFilename;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ca990bdb/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
index 1dd6351..da7645e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/sftp/DefaultSftpClient.java
@@ -405,6 +405,7 @@ public class DefaultSftpClient implements SftpClient {
             }
         }
         buffer.putInt(mode);
+        buffer.putInt(0);
         return checkHandle(receive(send(SSH_FXP_OPEN, buffer)));
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ca990bdb/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java b/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
index 1da1748..0b204c8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/SshFile.java
@@ -33,6 +33,9 @@ import java.util.Set;
  */
 public interface SshFile {
 
+    //
+    // File attributes
+    //
     enum Attribute {
         Size,               // long
         Uid,                // int
@@ -42,12 +45,28 @@ public interface SshFile {
         IsDirectory,        // boolean
         IsRegularFile,      // boolean
         IsSymbolicLink,     // boolean
-        Permissions,        // int
+        Permissions,        // EnumSet<Permission>
         CreationTime,       // long
         LastModifiedTime,   // long
         LastAccessTime      // long
     }
 
+    //
+    // File permissions
+    //
+    enum Permission {
+        UserRead,
+        UserWrite,
+        UserExecute,
+        GroupRead,
+        GroupWrite,
+        GroupExecute,
+        OthersRead,
+        OthersWrite,
+        OthersExecute
+    }
+
+
     /**
      * Get the full path from the base directory of the FileSystemView.
      * @return a path where the path separator is '/' (even if the operating system
@@ -61,14 +80,16 @@ public interface SshFile {
      */
     String getName();
 
-    Map<Attribute,Object> getAttributes() throws IOException;
+    Map<Attribute,Object> getAttributes(boolean followLinks) throws IOException;
 
     void setAttributes(Map<Attribute, Object> attributes) throws IOException;
 
-    Object getAttribute(Attribute attribute) throws IOException;
+    Object getAttribute(Attribute attribute, boolean followLinks) throws IOException;
 
     void setAttribute(Attribute attribute, Object value) throws IOException;
 
+    String readSymbolicLink() throws IOException;
+
 
     /**
      * Get the owner name of the file

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ca990bdb/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemView.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemView.java b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemView.java
index 9ba8994..dbf07bc 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemView.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeFileSystemView.java
@@ -113,7 +113,7 @@ public class NativeFileSystemView implements FileSystemView {
 		return System.getProperty("user.dir");
 	}
 
-    public NativeSshFile createNativeSshFile(final String fileName2, final File fileObj,	final String userName2) {
+    public NativeSshFile createNativeSshFile(final String fileName2, final File fileObj, final String userName2) {
 		return new NativeSshFile(this, fileName2, fileObj, userName2);
 	}
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ca990bdb/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
index ee7cd6e..2366706 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/file/nativefs/NativeSshFile.java
@@ -27,9 +27,9 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.RandomAccessFile;
 import java.lang.reflect.Method;
-import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
+import java.nio.file.Path;
 import java.nio.file.attribute.FileTime;
 import java.nio.file.attribute.GroupPrincipal;
 import java.nio.file.attribute.PosixFilePermission;
@@ -38,6 +38,7 @@ import java.nio.file.attribute.UserPrincipalLookupService;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -604,8 +605,11 @@ public class NativeSshFile implements SshFile {
         return fileName;
     }
 
-    public Map<Attribute, Object> getAttributes() throws IOException {
-        Map<String, Object> a = Files.readAttributes(file.toPath(), "unix:size,uid,owner,gid,group,isDirectory,isRegularFile,isSymbolicLink,permissions,creationTime,lastModifiedTime,lastAccessTime", LinkOption.NOFOLLOW_LINKS);
+    public Map<Attribute, Object> getAttributes(boolean followLinks) throws IOException {
+        Map<String, Object> a = Files.readAttributes(
+                file.toPath(),
+                "unix:size,uid,owner,gid,group,isDirectory,isRegularFile,isSymbolicLink,permissions,creationTime,lastModifiedTime,lastAccessTime",
+                followLinks ? new LinkOption[0] : new LinkOption[] { LinkOption.NOFOLLOW_LINKS });
         Map<Attribute, Object> map = new HashMap<Attribute, Object>();
         map.put(Attribute.Size, a.get("size"));
         map.put(Attribute.Uid, a.get("uid"));
@@ -622,19 +626,19 @@ public class NativeSshFile implements SshFile {
         return map;
     }
 
-    private int fromPerms(Set<PosixFilePermission> perms) {
-        int p = 0;
+    private EnumSet<Permission> fromPerms(Set<PosixFilePermission> perms) {
+        EnumSet<Permission> p = EnumSet.noneOf(Permission.class);
         for (PosixFilePermission perm : perms) {
             switch (perm) {
-                case OWNER_READ:     p |= 0000400; break;
-                case OWNER_WRITE:    p |= 0000200; break;
-                case OWNER_EXECUTE:  p |= 0000100; break;
-                case GROUP_READ:     p |= 0000040; break;
-                case GROUP_WRITE:    p |= 0000020; break;
-                case GROUP_EXECUTE:  p |= 0000010; break;
-                case OTHERS_READ:    p |= 0000004; break;
-                case OTHERS_WRITE:   p |= 0000002; break;
-                case OTHERS_EXECUTE: p |= 0000001; break;
+                case OWNER_READ:     p.add(Permission.UserRead); break;
+                case OWNER_WRITE:    p.add(Permission.UserWrite); break;
+                case OWNER_EXECUTE:  p.add(Permission.UserExecute); break;
+                case GROUP_READ:     p.add(Permission.GroupRead); break;
+                case GROUP_WRITE:    p.add(Permission.GroupWrite); break;
+                case GROUP_EXECUTE:  p.add(Permission.GroupExecute); break;
+                case OTHERS_READ:    p.add(Permission.OthersRead); break;
+                case OTHERS_WRITE:   p.add(Permission.OthersWrite); break;
+                case OTHERS_EXECUTE: p.add(Permission.OthersExecute); break;
             }
         }
         return p;
@@ -652,7 +656,7 @@ public class NativeSshFile implements SshFile {
                 case CreationTime:     name = "unix:creationTime"; value = FileTime.fromMillis((Long) value); break;
                 case LastModifiedTime: name = "unix:lastModifiedTime"; value = FileTime.fromMillis((Long) value); break;
                 case LastAccessTime:   name = "unix:lastAccessTime"; value = FileTime.fromMillis((Long) value); break;
-                case Permissions:      name = "unix:permissions"; value = toPerms((Integer) value); break;
+                case Permissions:      name = "unix:permissions"; value = toPerms((EnumSet<Permission>) value); break;
             }
             if (name != null && value != null) {
                 Files.setAttribute(file.toPath(), name, value, LinkOption.NOFOLLOW_LINKS);
@@ -670,40 +674,26 @@ public class NativeSshFile implements SshFile {
         return lookupService.lookupPrincipalByName(name);
     }
 
-    private Set<PosixFilePermission> toPerms(int perms) {
-        Set<PosixFilePermission> p = new HashSet<PosixFilePermission>();
-        if ((perms & 0000400) != 0) {
-            p.add(PosixFilePermission.OWNER_READ);
-        }
-        if ((perms & 0000200) != 0) {
-            p.add(PosixFilePermission.OWNER_WRITE);
-        }
-        if ((perms & 0000100) != 0) {
-            p.add(PosixFilePermission.OWNER_EXECUTE);
-        }
-        if ((perms & 0000040) != 0) {
-            p.add(PosixFilePermission.GROUP_READ);
-        }
-        if ((perms & 0000020) != 0) {
-            p.add(PosixFilePermission.GROUP_WRITE);
-        }
-        if ((perms & 0000010) != 0) {
-            p.add(PosixFilePermission.GROUP_EXECUTE);
-        }
-        if ((perms & 0000004) != 0) {
-            p.add(PosixFilePermission.OTHERS_READ);
-        }
-        if ((perms & 0000002) != 0) {
-            p.add(PosixFilePermission.OTHERS_WRITE);
-        }
-        if ((perms & 0000001) != 0) {
-            p.add(PosixFilePermission.OTHERS_EXECUTE);
+    private Set<PosixFilePermission> toPerms(EnumSet<Permission> perms) {
+        Set<PosixFilePermission> set = new HashSet<PosixFilePermission>();
+        for (Permission p : perms) {
+            switch (p) {
+                case UserRead:      set.add(PosixFilePermission.OWNER_READ); break;
+                case UserWrite:     set.add(PosixFilePermission.OWNER_WRITE); break;
+                case UserExecute:   set.add(PosixFilePermission.OWNER_EXECUTE); break;
+                case GroupRead:     set.add(PosixFilePermission.GROUP_READ); break;
+                case GroupWrite:    set.add(PosixFilePermission.GROUP_WRITE); break;
+                case GroupExecute:  set.add(PosixFilePermission.GROUP_EXECUTE); break;
+                case OthersRead:    set.add(PosixFilePermission.OTHERS_READ); break;
+                case OthersWrite:   set.add(PosixFilePermission.OTHERS_WRITE); break;
+                case OthersExecute: set.add(PosixFilePermission.OTHERS_EXECUTE); break;
+            }
         }
-        return p;
+        return set;
     }
 
-    public Object getAttribute(Attribute attribute) throws IOException {
-        return getAttributes().get(attribute);
+    public Object getAttribute(Attribute attribute, boolean followLinks) throws IOException {
+        return getAttributes(followLinks).get(attribute);
     }
 
     public void setAttribute(Attribute attribute, Object value) throws IOException {
@@ -711,4 +701,10 @@ public class NativeSshFile implements SshFile {
         map.put(attribute, value);
         setAttributes(map);
     }
+
+    public String readSymbolicLink() throws IOException {
+        Path path = file.toPath();
+        Path link = Files.readSymbolicLink(path);
+        return link.toString();
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/ca990bdb/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
index 39f80f9..c2c5b92 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/sftp/SftpSubsystem.java
@@ -25,9 +25,9 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Date;
+import java.util.EnumSet;
 import java.util.GregorianCalendar;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -228,22 +228,16 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     }
 
     protected static class FileHandle extends Handle {
-        int flags;
         OutputStream output;
         long outputPos;
         InputStream input;
         long inputPos;
         long length;
 
-        public FileHandle(SshFile sshFile, int flags) {
+        public FileHandle(SshFile sshFile) {
             super(sshFile);
-            this.flags = flags;
         }
 
-        public int getFlags() {
-            return flags;
-        }
-        
         public int read(byte[] data, long offset) throws IOException {
             if (input != null && offset >= length) {
                 return -1;
@@ -374,6 +368,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         int id = buffer.getInt();
         switch (type) {
             case SSH_FXP_INIT: {
+                log.debug("Received SSH_FXP_INIT (version={})", version);
                 if (length != 5) {
                     throw new IllegalArgumentException();
                 }
@@ -405,7 +400,8 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
 
                 String path = buffer.getString();
                 int pflags = buffer.getInt();
-                // attrs
+                Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
+                log.debug("Received SSH_FXP_OPEN (path={}, pflags={}, attrs={})", new Object[] { path, pflags, attrs });
                 try {
                     SshFile file = resolveFile(path);
                     if (file.doesExist()) {
@@ -427,8 +423,9 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                     if ((pflags & SSH_FXF_TRUNC) != 0) {
                         file.truncate();
                     }
+                    file.setAttributes(attrs);
                     String handle = UUID.randomUUID().toString();
-                    handles.put(handle, new FileHandle(file, pflags)); // handle flags conversion
+                    handles.put(handle, new FileHandle(file));
                     sendHandle(id, handle);
                 } catch (IOException e) {
                     sendStatus(id, SSH_FX_FAILURE, e.getMessage() == null ? "" : e.getMessage());
@@ -437,6 +434,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_CLOSE: {
                 String handle = buffer.getString();
+                log.debug("Received SSH_FXP_CLOSE (handle={})", handle);
                 try {
                     Handle h = handles.get(handle);
                     if (h == null) {
@@ -455,6 +453,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 String handle = buffer.getString();
                 long offset = buffer.getLong();
                 int len = buffer.getInt();
+                log.debug("Received SSH_FXP_READ (handle={}, offset={}, length={})", new Object[] { handle, offset, len });
                 try {
                     Handle p = handles.get(handle);
                     if (!(p instanceof FileHandle)) {
@@ -482,6 +481,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 String handle = buffer.getString();
                 long offset = buffer.getLong();
                 byte[] data = buffer.getBytes();
+                log.debug("Received SSH_FXP_WRITE (handle={}, offset={}, data=...)", handle, offset);
                 try {
                     Handle p = handles.get(handle);
                     if (!(p instanceof FileHandle)) {
@@ -500,12 +500,12 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 }
                 break;
             }
-            case SSH_FXP_LSTAT:
-            case SSH_FXP_STAT: {
+            case SSH_FXP_LSTAT: {
                 String path = buffer.getString();
+                log.debug("Received SSH_FXP_LSTAT (path={})", path);
                 try {
                     SshFile p = resolveFile(path);
-                    sendAttrs(id, p);
+                    sendAttrs(id, p, false);
                 } catch (FileNotFoundException e) {
                     sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
                 } catch (IOException e) {
@@ -515,12 +515,47 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_FSTAT: {
                 String handle = buffer.getString();
+                log.debug("Received SSH_FXP_FSTAT (handle={})", handle);
                 try {
                     Handle p = handles.get(handle);
                     if (p == null) {
                         sendStatus(id, SSH_FX_FAILURE, handle);
                     } else {
-                        sendAttrs(id, p.getFile());
+                        sendAttrs(id, p.getFile(), true);
+                    }
+                } catch (FileNotFoundException e) {
+                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
+                } catch (IOException e) {
+                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
+                }
+                break;
+            }
+            case SSH_FXP_SETSTAT: {
+                String path = buffer.getString();
+                Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
+                log.debug("Received SSH_FXP_SETSTAT (path={}, attrs={})", path, attrs);
+                try {
+                    SshFile p = resolveFile(path);
+                    p.setAttributes(attrs);
+                    sendStatus(id, SSH_FX_OK, "");
+                } catch (FileNotFoundException e) {
+                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
+                } catch (IOException e) {
+                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
+                }
+                break;
+            }
+            case SSH_FXP_FSETSTAT: {
+                String handle = buffer.getString();
+                Map<SshFile.Attribute, Object> attrs = readAttrs(buffer);
+                log.debug("Received SSH_FXP_FSETSTAT (handle={}, attrs={})", handle, attrs);
+                try {
+                    Handle p = handles.get(handle);
+                    if (p == null) {
+                        sendStatus(id, SSH_FX_FAILURE, handle);
+                    } else {
+                        p.getFile().setAttributes(attrs);
+                        sendStatus(id, SSH_FX_OK, "");
                     }
                 } catch (FileNotFoundException e) {
                     sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
@@ -531,6 +566,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_OPENDIR: {
                 String path = buffer.getString();
+                log.debug("Received SSH_FXP_OPENDIR (path={})", path);
                 try {
                     SshFile p = resolveFile(path);
                     if (!p.doesExist()) {
@@ -551,6 +587,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_READDIR: {
                 String handle = buffer.getString();
+                log.debug("Received SSH_FXP_READDIR (handle={})", handle);
                 try {
                     Handle p = handles.get(handle);
                     if (!(p instanceof DirectoryHandle)) {
@@ -589,6 +626,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_REMOVE: {
                 String path = buffer.getString();
+                log.debug("Received SSH_FXP_REMOVE (path={})", path);
                 try {
                     SshFile p = resolveFile(path);
                     if (!p.doesExist()) {
@@ -607,6 +645,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_MKDIR: {
                 String path = buffer.getString();
+                log.debug("Received SSH_FXP_MKDIR (path={})", path);
                 // attrs
                 try {
                     SshFile p = resolveFile(path);
@@ -630,6 +669,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_RMDIR: {
                 String path = buffer.getString();
+                log.debug("Received SSH_FXP_RMDIR (path={})", path);
                 // attrs
                 try {
                     SshFile p = resolveFile(path);
@@ -657,6 +697,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
             case SSH_FXP_REALPATH: {
                 String path = buffer.getString();
+                log.debug("Received SSH_FXP_REALPATH (path={})", path);
                 if (path.trim().length() == 0) {
                     path = ".";
                 }
@@ -672,9 +713,23 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 }
                 break;
             }
+            case SSH_FXP_STAT: {
+                String path = buffer.getString();
+                log.debug("Received SSH_FXP_STAT (path={})", path);
+                try {
+                    SshFile p = resolveFile(path);
+                    sendAttrs(id, p, true);
+                } catch (FileNotFoundException e) {
+                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
+                } catch (IOException e) {
+                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
+                }
+                break;
+            }
             case SSH_FXP_RENAME: {
                 String oldPath = buffer.getString();
                 String newPath = buffer.getString();
+                log.debug("Received SSH_FXP_RENAME (oldPath={}, newPath={})", oldPath, newPath);
                 try {
                     SshFile o = resolveFile(oldPath);
                     SshFile n = resolveFile(newPath);
@@ -692,15 +747,18 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 }
                 break;
             }
-            case SSH_FXP_SETSTAT:
-            case SSH_FXP_FSETSTAT: {
-                // This is required for WinSCP / Cyberduck to upload properly
-                // Blindly reply "OK"
-                // TODO implement it
-                sendStatus(id, SSH_FX_OK, "");
+            case SSH_FXP_READLINK: {
+                String path = buffer.getString();
+                log.debug("Received SSH_FXP_READLINK (path={})", path);
+                try {
+                    SshFile f = resolveFile(path);
+                    String l = f.readSymbolicLink();
+                    sendLink(id, l);
+                } catch (IOException e) {
+                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
+                }
                 break;
             }
-
             default: {
                 log.error("Received: {}", type);
                 sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
@@ -717,11 +775,11 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         send(buffer);
     }
 
-    protected void sendAttrs(int id, SshFile file) throws IOException {
+    protected void sendAttrs(int id, SshFile file, boolean followLinks) throws IOException {
         Buffer buffer = new Buffer();
         buffer.putByte((byte) SSH_FXP_ATTRS);
         buffer.putInt(id);
-        writeAttrs(buffer, file);
+        writeAttrs(buffer, file, followLinks);
         send(buffer);
     }
 
@@ -745,6 +803,18 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         send(buffer);
     }
 
+    protected void sendLink(int id, String link) throws IOException {
+        Buffer buffer = new Buffer();
+        buffer.putByte((byte) SSH_FXP_NAME);
+        buffer.putInt(id);
+        buffer.putInt(1);
+        //normalize the given path, use *nix style separator
+        buffer.putString(link);
+        buffer.putString(link);
+        buffer.putInt(0);
+        send(buffer);
+    }
+
     protected void sendName(int id, Iterator<SshFile> files) throws IOException {
         Buffer buffer = new Buffer();
         buffer.putByte((byte) SSH_FXP_NAME);
@@ -756,7 +826,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             SshFile f = files.next();
             buffer.putString(f.getName());
             buffer.putString(getLongName(f)); // Format specified in the specs
-            writeAttrs(buffer, f);
+            writeAttrs(buffer, f, false);
             nb++;
         }
         int oldpos = buffer.wpos();
@@ -767,7 +837,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     }
 
     private String getLongName(SshFile f) throws IOException {
-        Map<SshFile.Attribute, Object> attributes = f.getAttributes();
+        Map<SshFile.Attribute, Object> attributes = f.getAttributes(true);
         String username = (String) attributes.get(SshFile.Attribute.Owner);
         if (username.length() > 8) {
             username = username.substring(0, 8);
@@ -790,19 +860,19 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
 
         boolean isDirectory = (Boolean) attributes.get(SshFile.Attribute.IsDirectory);
         boolean isLink = (Boolean) attributes.get(SshFile.Attribute.IsSymbolicLink);
-        int perms = (Integer) attributes.get(SshFile.Attribute.Permissions);
+        int perms = getPermissions(attributes);
 
         StringBuilder sb = new StringBuilder();
         sb.append(isDirectory ? "d" : isLink ? "l" : "-");
-        sb.append((perms & 0000400) != 0 ? "r" : "-");
-        sb.append((perms & 0000200) != 0 ? "w" : "-");
-        sb.append((perms & 0000100) != 0 ? "x" : "-");
-        sb.append((perms & 0000040) != 0 ? "r" : "-");
-        sb.append((perms & 0000020) != 0 ? "w" : "-");
-        sb.append((perms & 0000010) != 0 ? "x" : "-");
-        sb.append((perms & 0000004) != 0 ? "r" : "-");
-        sb.append((perms & 0000002) != 0 ? "w" : "-");
-        sb.append((perms & 0000001) != 0 ? "x" : "-");
+        sb.append((perms & S_IRUSR) != 0 ? "r" : "-");
+        sb.append((perms & S_IWUSR) != 0 ? "w" : "-");
+        sb.append((perms & S_IXUSR) != 0 ? "x" : "-");
+        sb.append((perms & S_IRGRP) != 0 ? "r" : "-");
+        sb.append((perms & S_IWGRP) != 0 ? "w" : "-");
+        sb.append((perms & S_IXGRP) != 0 ? "x" : "-");
+        sb.append((perms & S_IROTH) != 0 ? "r" : "-");
+        sb.append((perms & S_IWOTH) != 0 ? "w" : "-");
+        sb.append((perms & S_IXOTH) != 0 ? "x" : "-");
         sb.append(" ");
         sb.append("  1");
         sb.append(" ");
@@ -819,31 +889,53 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         return sb.toString();
     }
 
-    protected void writeAttrs(Buffer buffer, SshFile file) throws IOException {
+    protected int getPermissions(Map<SshFile.Attribute, Object> attributes) {
+        boolean isReg = (Boolean) attributes.get(SshFile.Attribute.IsRegularFile);
+        boolean isDir = (Boolean) attributes.get(SshFile.Attribute.IsDirectory);
+        boolean isLnk = (Boolean) attributes.get(SshFile.Attribute.IsSymbolicLink);
+        int pf = 0;
+        EnumSet<SshFile.Permission> perms = (EnumSet<SshFile.Permission>) attributes.get(SshFile.Attribute.Permissions);
+        for (SshFile.Permission p : perms) {
+            switch (p) {
+                case UserRead:      pf |= S_IRUSR; break;
+                case UserWrite:     pf |= S_IWUSR; break;
+                case UserExecute:   pf |= S_IXUSR; break;
+                case GroupRead:     pf |= S_IRGRP; break;
+                case GroupWrite:    pf |= S_IWGRP; break;
+                case GroupExecute:  pf |= S_IXGRP; break;
+                case OthersRead:    pf |= S_IROTH; break;
+                case OthersWrite:   pf |= S_IWOTH; break;
+                case OthersExecute: pf |= S_IXOTH; break;
+            }
+        }
+        pf |= isReg ? S_IFREG : 0;
+        pf |= isDir ? S_IFDIR : 0;
+        pf |= isLnk ? S_IFLNK : 0;
+        return pf;
+    }
+
+    protected void writeAttrs(Buffer buffer, SshFile file, boolean followLinks) throws IOException {
         if (!file.doesExist()) {
             throw new FileNotFoundException(file.getAbsolutePath());
         }
-        Map<SshFile.Attribute, Object> attributes = file.getAttributes();
+        Map<SshFile.Attribute, Object> attributes = file.getAttributes(followLinks);
         boolean isReg = (Boolean) attributes.get(SshFile.Attribute.IsRegularFile);
         boolean isDir = (Boolean) attributes.get(SshFile.Attribute.IsDirectory);
         boolean isLnk = (Boolean) attributes.get(SshFile.Attribute.IsSymbolicLink);
-        int p = (Integer) attributes.get(SshFile.Attribute.Permissions);
-        p |= isReg ? S_IFREG : 0;
-        p |= isDir ? S_IFDIR : 0;
-        p |= isLnk ? S_IFLNK : 0;
-        if (isReg) {
+        int pf = getPermissions(attributes);
+        if (isReg || isLnk) {
             buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_UIDGID | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME);
             buffer.putLong((Long) attributes.get(SshFile.Attribute.Size));
             buffer.putInt((Integer) attributes.get(SshFile.Attribute.Uid));
             buffer.putInt((Integer) attributes.get(SshFile.Attribute.Gid));
-            buffer.putInt(p);
+            buffer.putInt(pf);
             buffer.putInt(((Long) attributes.get(SshFile.Attribute.LastAccessTime)) / 1000);
             buffer.putInt(((Long) attributes.get(SshFile.Attribute.LastModifiedTime)) / 1000);
-        } else if (isDir || isLnk) {
+        } else if (isDir) {
             buffer.putInt(SSH_FILEXFER_ATTR_UIDGID | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME);
             buffer.putInt((Integer) attributes.get(SshFile.Attribute.Uid));
             buffer.putInt((Integer) attributes.get(SshFile.Attribute.Gid));
-            buffer.putInt(p);
+            buffer.putInt(pf);
             buffer.putInt(((Long) attributes.get(SshFile.Attribute.LastAccessTime)) / 1000);
             buffer.putInt(((Long) attributes.get(SshFile.Attribute.LastModifiedTime)) / 1000);
         } else {
@@ -851,11 +943,32 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         }
     }
 
+    protected Map<SshFile.Attribute, Object> readAttrs(Buffer buffer) throws IOException {
+        Map<SshFile.Attribute, Object> attrs = new HashMap<SshFile.Attribute, Object>();
+        int flags = buffer.getInt();
+        if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+            attrs.put(SshFile.Attribute.Size, buffer.getLong());
+        }
+        if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
+            attrs.put(SshFile.Attribute.Uid, buffer.getInt());
+            attrs.put(SshFile.Attribute.Gid, buffer.getInt());
+        }
+        if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+            attrs.put(SshFile.Attribute.Permissions, buffer.getInt());
+        }
+        if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+            attrs.put(SshFile.Attribute.LastAccessTime, ((long) buffer.getInt()) * 1000);
+            attrs.put(SshFile.Attribute.LastModifiedTime, ((long) buffer.getInt()) * 1000);
+        }
+        return attrs;
+    }
+
     protected void sendStatus(int id, int substatus, String msg) throws IOException {
         sendStatus(id, substatus, msg, "");
     }
 
     protected void sendStatus(int id, int substatus, String msg, String lang) throws IOException {
+        log.debug("Send SSH_FXP_STATUS (substatus={}, msg={})", substatus, msg);
         Buffer buffer = new Buffer();
         buffer.putByte((byte) SSH_FXP_STATUS);
         buffer.putInt(id);