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 2015/02/23 16:30:10 UTC

[09/15] mina-sshd git commit: [SSHD-408] Implement sftp v4, v5 and v6

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/22581fb8/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 5313f6e..700e61d 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,35 +25,52 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
-import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.AccessDeniedException;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryNotEmptyException;
 import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.AclEntryFlag;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.FileTime;
 import java.nio.file.attribute.GroupPrincipal;
 import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.nio.file.attribute.UserPrincipal;
 import java.nio.file.attribute.UserPrincipalLookupService;
+import java.nio.file.attribute.AclEntry;
+import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.Collection;
-import java.util.EnumSet;
+import java.util.Collections;
 import java.util.GregorianCalendar;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.file.FileSystemAware;
@@ -131,40 +148,6 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         }
     }
 
-    //
-    // File attributes
-    //
-    enum Attribute {
-        Size,               // long
-        Uid,                // int
-        Owner,              // String
-        Gid,                // int
-        Group,              // String
-        IsDirectory,        // boolean
-        IsRegularFile,      // boolean
-        IsSymbolicLink,     // boolean
-        Permissions,        // EnumSet<Permission>
-        CreationTime,       // long
-        LastModifiedTime,   // long
-        LastAccessTime,     // long
-        NLink               // int
-    }
-
-    //
-    // File permissions
-    //
-    enum Permission {
-        UserRead,
-        UserWrite,
-        UserExecute,
-        GroupRead,
-        GroupWrite,
-        GroupExecute,
-        OthersRead,
-        OthersWrite,
-        OthersExecute
-    }
-
     public enum UnsupportedAttributePolicy {
         Ignore,
         Warn,
@@ -176,9 +159,14 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
      */
     public static final String MAX_OPEN_HANDLES_PER_SESSION = "max-open-handles-per-session";
 
+    /**
+     * Force the use of a given sftp version
+     */
+    public static final String SFTP_VERSION = "sftp-version";
+
     public static final int LOWER_SFTP_IMPL = 3; // Working implementation from v3
-    public static final int HIGHER_SFTP_IMPL = 3; //  .. up to
-    public static final String ALL_SFTP_IMPL = "3";
+    public static final int HIGHER_SFTP_IMPL = 6; //  .. up to
+    public static final String ALL_SFTP_IMPL = "3,4,5,6";
     public static final int  MAX_PACKET_LENGTH = 1024 * 16;
 
     public static final int SSH_FXP_INIT =             1;
@@ -200,7 +188,10 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     public static final int SSH_FXP_STAT =            17;
     public static final int SSH_FXP_RENAME =          18;
     public static final int SSH_FXP_READLINK =        19;
-    public static final int SSH_FXP_SYMLINK =         20;
+    public static final int SSH_FXP_SYMLINK =         20; // v3 -> v5
+    public static final int SSH_FXP_LINK =            21; // v6
+    public static final int SSH_FXP_BLOCK =           22; // v6
+    public static final int SSH_FXP_UNBLOCK =         23; // v6
     public static final int SSH_FXP_STATUS =         101;
     public static final int SSH_FXP_HANDLE =         102;
     public static final int SSH_FXP_DATA =           103;
@@ -209,23 +200,81 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     public static final int SSH_FXP_EXTENDED =       200;
     public static final int SSH_FXP_EXTENDED_REPLY = 201;
 
-    public static final int SSH_FX_OK =                0;
-    public static final int SSH_FX_EOF =               1;
-    public static final int SSH_FX_NO_SUCH_FILE =      2;
-    public static final int SSH_FX_PERMISSION_DENIED = 3;
-    public static final int SSH_FX_FAILURE =           4;
-    public static final int SSH_FX_BAD_MESSAGE =       5;
-    public static final int SSH_FX_NO_CONNECTION =     6;
-    public static final int SSH_FX_CONNECTION_LOST =   7;
-    public static final int SSH_FX_OP_UNSUPPORTED =    8;
-
-    public static final int SSH_FX_FILE_ALREADY_EXISTS = 11; // Not in v3, but we need it
-
-    public static final int SSH_FILEXFER_ATTR_SIZE =        0x00000001;
-    public static final int SSH_FILEXFER_ATTR_UIDGID =      0x00000002;
-    public static final int SSH_FILEXFER_ATTR_PERMISSIONS = 0x00000004;
-    public static final int SSH_FILEXFER_ATTR_ACMODTIME =   0x00000008; //v3 naming convention
-    public static final int SSH_FILEXFER_ATTR_EXTENDED =    0x80000000;
+    public static final int SSH_FX_OK =                           0;
+    public static final int SSH_FX_EOF =                          1;
+    public static final int SSH_FX_NO_SUCH_FILE =                 2;
+    public static final int SSH_FX_PERMISSION_DENIED =            3;
+    public static final int SSH_FX_FAILURE =                      4;
+    public static final int SSH_FX_BAD_MESSAGE =                  5;
+    public static final int SSH_FX_NO_CONNECTION =                6;
+    public static final int SSH_FX_CONNECTION_LOST =              7;
+    public static final int SSH_FX_OP_UNSUPPORTED =               8;
+    public static final int SSH_FX_INVALID_HANDLE =               9;
+    public static final int SSH_FX_NO_SUCH_PATH =                10;
+    public static final int SSH_FX_FILE_ALREADY_EXISTS =         11;
+    public static final int SSH_FX_WRITE_PROTECT =               12;
+    public static final int SSH_FX_NO_MEDIA =                    13;
+    public static final int SSH_FX_NO_SPACE_ON_FILESYSTEM =      14;
+    public static final int SSH_FX_QUOTA_EXCEEDED =              15;
+    public static final int SSH_FX_UNKNOWN_PRINCIPLE =           16;
+    public static final int SSH_FX_LOCK_CONFLICT =               17;
+    public static final int SSH_FX_DIR_NOT_EMPTY =               18;
+    public static final int SSH_FX_NOT_A_DIRECTORY =             19;
+    public static final int SSH_FX_INVALID_FILENAME =            20;
+    public static final int SSH_FX_LINK_LOOP =                   21;
+    public static final int SSH_FX_CANNOT_DELETE =               22;
+    public static final int SSH_FX_INVALID_PARAMETER =           23;
+    public static final int SSH_FX_FILE_IS_A_DIRECTORY =         24;
+    public static final int SSH_FX_BYTE_RANGE_LOCK_CONFLICT =    25;
+    public static final int SSH_FX_BYTE_RANGE_LOCK_REFUSED =     26;
+    public static final int SSH_FX_DELETE_PENDING =              27;
+    public static final int SSH_FX_FILE_CORRUPT =                28;
+    public static final int SSH_FX_OWNER_INVALID =               29;
+    public static final int SSH_FX_GROUP_INVALID =               30;
+    public static final int SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK = 31;
+
+    public static final int SSH_FILEXFER_ATTR_SIZE =              0x00000001;
+    public static final int SSH_FILEXFER_ATTR_UIDGID =            0x00000002;
+    public static final int SSH_FILEXFER_ATTR_PERMISSIONS =       0x00000004;
+    public static final int SSH_FILEXFER_ATTR_ACMODTIME =         0x00000008; // v3 naming convention
+    public static final int SSH_FILEXFER_ATTR_ACCESSTIME =        0x00000008; // v4
+    public static final int SSH_FILEXFER_ATTR_CREATETIME =        0x00000010; // v4
+    public static final int SSH_FILEXFER_ATTR_MODIFYTIME =        0x00000020; // v4
+    public static final int SSH_FILEXFER_ATTR_ACL =               0x00000040; // v4
+    public static final int SSH_FILEXFER_ATTR_OWNERGROUP =        0x00000080; // v4
+    public static final int SSH_FILEXFER_ATTR_SUBSECOND_TIMES =   0x00000100; // v5
+    public static final int SSH_FILEXFER_ATTR_BITS =              0x00000200; // v5
+    public static final int SSH_FILEXFER_ATTR_ALLOCATION_SIZE =   0x00000400; // v6
+    public static final int SSH_FILEXFER_ATTR_TEXT_HINT =         0x00000800; // v6
+    public static final int SSH_FILEXFER_ATTR_MIME_TYPE =         0x00001000; // v6
+    public static final int SSH_FILEXFER_ATTR_LINK_COUNT =        0x00002000; // v6
+    public static final int SSH_FILEXFER_ATTR_UNTRANSLATED_NAME = 0x00004000; // v6
+    public static final int SSH_FILEXFER_ATTR_CTIME =             0x00008000; // v6
+    public static final int SSH_FILEXFER_ATTR_EXTENDED =          0x80000000;
+
+    public static final int SSH_FILEXFER_ATTR_ALL =               0x0000FFFF; // All attributes
+
+    public static final int SSH_FILEXFER_ATTR_FLAGS_READONLY =         0x00000001;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_SYSTEM =           0x00000002;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_HIDDEN =           0x00000004;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE = 0x00000008;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_ARCHIVE =          0x00000010;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED =        0x00000020;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_COMPRESSED =       0x00000040;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_SPARSE =           0x00000080;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY =      0x00000100;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE =        0x00000200;
+    public static final int SSH_FILEXFER_ATTR_FLAGS_SYNC =             0x00000400;
+
+    public static final int SSH_FILEXFER_TYPE_REGULAR =      1;
+    public static final int SSH_FILEXFER_TYPE_DIRECTORY =    2;
+    public static final int SSH_FILEXFER_TYPE_SYMLINK =      3;
+    public static final int SSH_FILEXFER_TYPE_SPECIAL =      4;
+    public static final int SSH_FILEXFER_TYPE_UNKNOWN =      5;
+    public static final int SSH_FILEXFER_TYPE_SOCKET =       6; // v5
+    public static final int SSH_FILEXFER_TYPE_CHAR_DEVICE =  7; // v5
+    public static final int SSH_FILEXFER_TYPE_BLOCK_DEVICE = 8; // v5
+    public static final int SSH_FILEXFER_TYPE_FIFO         = 9; // v5
 
     public static final int SSH_FXF_READ =   0x00000001;
     public static final int SSH_FXF_WRITE =  0x00000002;
@@ -233,6 +282,63 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     public static final int SSH_FXF_CREAT =  0x00000008;
     public static final int SSH_FXF_TRUNC =  0x00000010;
     public static final int SSH_FXF_EXCL =   0x00000020;
+    public static final int SSH_FXF_TEXT =   0x00000040;
+
+    public static final int SSH_FXF_ACCESS_DISPOSITION = 0x00000007;
+    public static final int SSH_FXF_CREATE_NEW =         0x00000000;
+    public static final int SSH_FXF_CREATE_TRUNCATE =    0x00000001;
+    public static final int SSH_FXF_OPEN_EXISTING =      0x00000002;
+    public static final int SSH_FXF_OPEN_OR_CREATE =     0x00000003;
+    public static final int SSH_FXF_TRUNCATE_EXISTING =  0x00000004;
+    public static final int SSH_FXF_APPEND_DATA =        0x00000008;
+    public static final int SSH_FXF_APPEND_DATA_ATOMIC = 0x00000010;
+    public static final int SSH_FXF_TEXT_MODE =          0x00000020;
+    public static final int SSH_FXF_READ_LOCK =          0x00000040;
+    public static final int SSH_FXF_WRITE_LOCK =         0x00000080;
+    public static final int SSH_FXF_DELETE_LOCK =        0x00000100;
+
+    public static final int SSH_FXP_RENAME_OVERWRITE = 0x00000001;
+    public static final int SSH_FXP_RENAME_ATOMIC =    0x00000002;
+    public static final int SSH_FXP_RENAME_NATIVE =    0x00000004;
+
+    public static final int SSH_FXP_REALPATH_NO_CHECK    = 0x00000001;
+    public static final int SSH_FXP_REALPATH_STAT_IF     = 0x00000002;
+    public static final int SSH_FXP_REALPATH_STAT_ALWAYS = 0x00000003;
+
+    public static final int SSH_FXF_RENAME_OVERWRITE =  0x00000001;
+    public static final int SSH_FXF_RENAME_ATOMIC =     0x00000002;
+    public static final int SSH_FXF_RENAME_NATIVE =     0x00000004;
+
+    public static final int ACE4_ACCESS_ALLOWED_ACE_TYPE      = 0x00000000;
+    public static final int ACE4_ACCESS_DENIED_ACE_TYPE       = 0x00000001;
+    public static final int ACE4_SYSTEM_AUDIT_ACE_TYPE        = 0x00000002;
+    public static final int ACE4_SYSTEM_ALARM_ACE_TYPE        = 0x00000003;
+
+    public static final int ACE4_FILE_INHERIT_ACE             = 0x00000001;
+    public static final int ACE4_DIRECTORY_INHERIT_ACE        = 0x00000002;
+    public static final int ACE4_NO_PROPAGATE_INHERIT_ACE     = 0x00000004;
+    public static final int ACE4_INHERIT_ONLY_ACE             = 0x00000008;
+    public static final int ACE4_SUCCESSFUL_ACCESS_ACE_FLAG   = 0x00000010;
+    public static final int ACE4_FAILED_ACCESS_ACE_FLAG       = 0x00000020;
+    public static final int ACE4_IDENTIFIER_GROUP             = 0x00000040;
+
+    public static final int ACE4_READ_DATA            = 0x00000001;
+    public static final int ACE4_LIST_DIRECTORY       = 0x00000001;
+    public static final int ACE4_WRITE_DATA           = 0x00000002;
+    public static final int ACE4_ADD_FILE             = 0x00000002;
+    public static final int ACE4_APPEND_DATA          = 0x00000004;
+    public static final int ACE4_ADD_SUBDIRECTORY     = 0x00000004;
+    public static final int ACE4_READ_NAMED_ATTRS     = 0x00000008;
+    public static final int ACE4_WRITE_NAMED_ATTRS    = 0x00000010;
+    public static final int ACE4_EXECUTE              = 0x00000020;
+    public static final int ACE4_DELETE_CHILD         = 0x00000040;
+    public static final int ACE4_READ_ATTRIBUTES      = 0x00000080;
+    public static final int ACE4_WRITE_ATTRIBUTES     = 0x00000100;
+    public static final int ACE4_DELETE               = 0x00010000;
+    public static final int ACE4_READ_ACL             = 0x00020000;
+    public static final int ACE4_WRITE_ACL            = 0x00040000;
+    public static final int ACE4_WRITE_OWNER          = 0x00080000;
+    public static final int ACE4_SYNCHRONIZE          = 0x00100000;
 
     public static final int S_IFMT =   0170000;  // bitmask for the file type bitfields
     public static final int S_IFSOCK = 0140000;  // socket
@@ -255,6 +361,10 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     public static final int S_IWOTH =  0000002;
     public static final int S_IXOTH =  0000001;
 
+    public static int SFTP_V3 = 3;
+    public static int SFTP_V4 = 4;
+    public static int SFTP_V5 = 5;
+    public static int SFTP_V6 = 6;
 
     private ExitCallback callback;
     private InputStream in;
@@ -271,7 +381,8 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     private Path defaultDir = fileSystem.getPath(System.getProperty("user.dir"));
 
     private int version;
-    private Map<String, Handle> handles = new HashMap<>();
+    private final Map<String, byte[]> extensions = new HashMap<>();
+    private final Map<String, Handle> handles = new HashMap<>();
 
     private UnsupportedAttributePolicy unsupportedAttributePolicy = UnsupportedAttributePolicy.Warn;
 
@@ -335,25 +446,64 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         }
     }
 
-    protected static class FileHandle extends Handle {
-        SeekableByteChannel channel;
+    protected class FileHandle extends Handle {
+        final FileChannel channel;
+        final List<FileLock> locks = new ArrayList<>();
 
-        public FileHandle(Path file, int flags) throws IOException {
+        public FileHandle(Path file, int flags, int access, Map<String, Object> attrs) throws IOException {
             super(file);
             Set<OpenOption> options = new HashSet<>();
-            if ((flags & SSH_FXF_READ) != 0) {
+            if ((access & ACE4_READ_DATA) != 0 || (access & ACE4_READ_ATTRIBUTES) != 0) {
                 options.add(StandardOpenOption.READ);
             }
-            if ((flags & SSH_FXF_WRITE) != 0) {
+            if ((access & ACE4_WRITE_DATA) != 0 || (access & ACE4_WRITE_ATTRIBUTES) != 0) {
                 options.add(StandardOpenOption.WRITE);
             }
-            if ((flags & SSH_FXF_APPEND) != 0) {
+            switch (flags & SSH_FXF_ACCESS_DISPOSITION) {
+            case SSH_FXF_CREATE_NEW:
+                options.add(StandardOpenOption.CREATE_NEW);
+                break;
+            case SSH_FXF_CREATE_TRUNCATE:
+                options.add(StandardOpenOption.CREATE);
+                options.add(StandardOpenOption.TRUNCATE_EXISTING);
+                break;
+            case SSH_FXF_OPEN_EXISTING:
+                break;
+            case SSH_FXF_OPEN_OR_CREATE:
+                options.add(StandardOpenOption.CREATE);
+                break;
+            case SSH_FXF_TRUNCATE_EXISTING:
+                options.add(StandardOpenOption.TRUNCATE_EXISTING);
+                break;
+            }
+            if ((flags & SSH_FXF_APPEND_DATA) != 0) {
                 options.add(StandardOpenOption.APPEND);
             }
-            if ((flags & SSH_FXF_TRUNC) != 0) {
-                options.add(StandardOpenOption.TRUNCATE_EXISTING);
+            FileAttribute<?>[] attributes = new FileAttribute<?>[attrs.size()];
+            int index = 0;
+            for (Map.Entry<String, Object> attr : attrs.entrySet()) {
+                final String key = attr.getKey();
+                final Object val = attr.getValue();
+                attributes[index++] = new FileAttribute<Object>() {
+                    @Override
+                    public String name() {
+                        return key;
+                    }
+
+                    @Override
+                    public Object value() {
+                        return val;
+                    }
+                };
+            }
+            FileChannel channel;
+            try {
+                  channel = FileChannel.open(file, options, attributes);
+            } catch (UnsupportedOperationException e) {
+                channel = FileChannel.open(file, options);
+                setAttributes(file, attrs);
             }
-            channel = Files.newByteChannel(file, options);
+            this.channel = channel;
         }
 
         public int read(byte[] data, long offset) throws IOException {
@@ -370,6 +520,32 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         public void close() throws IOException {
             channel.close();
         }
+
+        public void lock(long offset, long length, int mask) throws IOException {
+            long size = length == 0 ? channel.size() - offset : length;
+            FileLock lock = channel.tryLock(offset, size, false);
+            synchronized (locks) {
+                locks.add(lock);
+            }
+        }
+
+        public boolean unlock(long offset, long length) throws IOException {
+            long size = length == 0 ? channel.size() - offset : length;
+            FileLock lock = null;
+            for (Iterator<FileLock> iterator = locks.iterator(); iterator.hasNext();) {
+                FileLock l = iterator.next();
+                if (l.position() == offset && l.size() == size) {
+                    iterator.remove();
+                    lock = l;
+                    break;
+                }
+            }
+            if (lock != null) {
+                lock.release();
+                return true;
+            }
+            return false;
+        }
     }
 
     public SftpSubsystem() {
@@ -411,7 +587,10 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     }
 
     public void setFileSystem(FileSystem fileSystem) {
-        this.fileSystem = fileSystem;
+        if (fileSystem != this.fileSystem) {
+            this.fileSystem = fileSystem;
+            this.defaultDir = fileSystem.getRootDirectories().iterator().next();
+        }
     }
 
     public void setExitCallback(ExitCallback callback) {
@@ -435,7 +614,7 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         try {
             pendingFuture = executors.submit(this);
         } catch (RuntimeException e) {    // e.g., RejectedExecutionException
-            log.error("Failed (" + e.getClass().getSimpleName() + ") to start command: " + e.getMessage(), e);
+            log.error("Failed (" + e.getClass().getSimpleName() + ") to start command: " + e.toString(), e);
             throw new IOException(e);
         }
     }
@@ -495,405 +674,95 @@ 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={})", id);
-                // see https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt - section 4 - Protocol Initialization
-                if (length < 5) { // we don't care about extensions
-                    throw new IllegalArgumentException("Incomplete SSH_FXP_INIT data: length=" + length);
-                }
-                version = id;
-                if (version >= LOWER_SFTP_IMPL) {
-                    version = Math.min(version, HIGHER_SFTP_IMPL);
-                    buffer.clear();
-                    buffer.putByte((byte) SSH_FXP_VERSION);
-                    buffer.putInt(version);
-                    send(buffer);
-                } else {
-                    // We only support version 3 (Version 1 and 2 are not common)
-                    sendStatus(id, SSH_FX_OP_UNSUPPORTED, "SFTP server only support versions " + ALL_SFTP_IMPL);
-                }
-
+                doInit(buffer, id);
                 break;
             }
             case SSH_FXP_OPEN: {
-                if (session.getFactoryManager().getProperties() != null) {
-                    String maxHandlesString = session.getFactoryManager().getProperties().get(MAX_OPEN_HANDLES_PER_SESSION);
-                    if (maxHandlesString != null) {
-                        int maxHandleCount = Integer.parseInt(maxHandlesString);
-                        if (handles.size() > maxHandleCount) {
-                            sendStatus(id, SSH_FX_FAILURE, "Too many open handles");
-                            break;
-                        }
-                    }
-                }
-
-                String path = buffer.getString();
-                int pflags = buffer.getInt();
-                Map<Attribute, Object> attrs = readAttrs(buffer);
-                log.debug("Received SSH_FXP_OPEN (path={}, pflags={}, attrs={})", new Object[] { path, pflags, attrs });
-                try {
-                    Path file = resolveFile(path);
-                    if (Files.exists(file)) {
-                        if ((pflags & SSH_FXF_READ) != 0 && !Files.isReadable(file)) {
-                            sendStatus(id, SSH_FX_PERMISSION_DENIED, "Can not read " + path);
-                            return;
-                        }
-                        if ((pflags & SSH_FXF_WRITE) != 0 && !Files.isWritable(file)) {
-                            sendStatus(id, SSH_FX_PERMISSION_DENIED, "Can not write " + path);
-                            return;
-                        }
-                        if (((pflags & SSH_FXF_CREAT) != 0) && ((pflags & SSH_FXF_EXCL) != 0)) {
-                            sendStatus(id, SSH_FX_FAILURE, path);
-                            return;
-                        }
-                    } else {
-                        if (((pflags & SSH_FXF_CREAT) != 0)) {
-                            Files.createFile(file);
-                        } else {
-                            sendStatus(id, SSH_FX_NO_SUCH_FILE, "No such file " + path);
-                            return;
-                        }
-                    }
-                    if (((pflags & SSH_FXF_CREAT) != 0)) {
-                        setAttributes(file, attrs);
-                    }
-                    String handle = UUID.randomUUID().toString();
-                    handles.put(handle, new FileHandle(file, pflags));
-                    sendHandle(id, handle);
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage() == null ? "" : e.getMessage());
-                }
+                doOpen(buffer, id);
                 break;
             }
             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) {
-                        sendStatus(id, SSH_FX_FAILURE, handle, "");
-                    } else {
-                        handles.remove(handle);
-                        h.close();
-                        sendStatus(id, SSH_FX_OK, "", "");
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doClose(buffer, id);
                 break;
             }
             case SSH_FXP_READ: {
-                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)) {
-                        sendStatus(id, SSH_FX_FAILURE, handle);
-                    } else {
-                        FileHandle fh = (FileHandle) p;
-                        byte[] b = new byte[Math.min(len, Buffer.MAX_LEN)];
-                        len = fh.read(b, offset);
-                        if (len >= 0) {
-                            Buffer buf = new Buffer(len + 5);
-                            buf.putByte((byte) SSH_FXP_DATA);
-                            buf.putInt(id);
-                            buf.putBytes(b, 0, len);
-                            send(buf);
-                        } else {
-                            sendStatus(id, SSH_FX_EOF, "");
-                        }
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doRead(buffer, id);
                 break;
             }
             case SSH_FXP_WRITE: {
-                String handle = buffer.getString();
-                long offset = buffer.getLong();
-                byte[] data = buffer.getBytes();
-                log.debug("Received SSH_FXP_WRITE (handle={}, offset={}, data=byte[{}])", new Object[] { handle, offset, data.length });
-                try {
-                    Handle p = handles.get(handle);
-                    if (!(p instanceof FileHandle)) {
-                        sendStatus(id, SSH_FX_FAILURE, handle);
-                    } else {
-                        FileHandle fh = (FileHandle) p;
-                        fh.write(data, offset);
-                        sendStatus(id, SSH_FX_OK, "");
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doWrite(buffer, id);
                 break;
             }
             case SSH_FXP_LSTAT: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_LSTAT (path={})", path);
-                try {
-                    Path p = resolveFile(path);
-                    sendAttrs(id, p, false);
-                } catch (FileNotFoundException e) {
-                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doLStat(buffer, id);
                 break;
             }
             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(), true);
-                    }
-                } catch (FileNotFoundException e) {
-                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doFStat(buffer, id);
                 break;
             }
             case SSH_FXP_SETSTAT: {
-                String path = buffer.getString();
-                Map<Attribute, Object> attrs = readAttrs(buffer);
-                log.debug("Received SSH_FXP_SETSTAT (path={}, attrs={})", path, attrs);
-                try {
-                    Path p = resolveFile(path);
-                    setAttributes(p, 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());
-                } catch (UnsupportedOperationException e) {
-                    sendStatus(id, SSH_FX_FAILURE, "");
-                }
+                doSetStat(buffer, id);
                 break;
             }
             case SSH_FXP_FSETSTAT: {
-                String handle = buffer.getString();
-                Map<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 {
-                        setAttributes(p.getFile(), attrs);
-                        sendStatus(id, SSH_FX_OK, "");
-                    }
-                } catch (FileNotFoundException e) {
-                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
-                } catch (IOException | UnsupportedOperationException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doFSetStat(buffer, id);
                 break;
             }
             case SSH_FXP_OPENDIR: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_OPENDIR (path={})", path);
-                try {
-                    Path p = resolveFile(path);
-                    if (!Files.exists(p)) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
-                    } else if (!Files.isDirectory(p)) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
-                    } else if (!Files.isReadable(p)) {
-                        sendStatus(id, SSH_FX_PERMISSION_DENIED, path);
-                    } else {
-                        String handle = UUID.randomUUID().toString();
-                        handles.put(handle, new DirectoryHandle(p));
-                        sendHandle(id, handle);
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doOpenDir(buffer, id);
                 break;
             }
             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)) {
-                        sendStatus(id, SSH_FX_FAILURE, handle);
-                    } else if (((DirectoryHandle) p).isDone()) {
-                        sendStatus(id, SSH_FX_EOF, "", "");
-                    } else if (!Files.exists(p.getFile())) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getFile().toString());
-                    } else if (!Files.isDirectory(p.getFile())) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getFile().toString());
-                    } else if (!Files.isReadable(p.getFile())) {
-                        sendStatus(id, SSH_FX_PERMISSION_DENIED, p.getFile().toString());
-                    } else {
-                        DirectoryHandle dh = (DirectoryHandle) p;
-                        if (dh.hasNext()) {
-                            // There is at least one file in the directory.
-                            // 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()) {
-                                // if no more files to send
-                                dh.setDone(true);
-                                dh.clearFileList();
-                            }
-                        } else {
-                            // empty directory
-                            dh.setDone(true);
-                            dh.clearFileList();
-                            sendStatus(id, SSH_FX_EOF, "", "");
-                        }
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doReadDir(buffer, id);
                 break;
             }
             case SSH_FXP_REMOVE: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_REMOVE (path={})", path);
-                try {
-                    Path p = resolveFile(path);
-                    if (!Files.exists(p)) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-                    } else if (Files.isDirectory(p)) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-                    } else {
-                        Files.delete(p);
-                        sendStatus(id, SSH_FX_OK, "");
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doRemove(buffer, id);
                 break;
             }
             case SSH_FXP_MKDIR: {
-                String path = buffer.getString();
-                Map<Attribute, Object> attrs = readAttrs(buffer);
-
-                log.debug("Received SSH_FXP_MKDIR (path={})", path);
-                // attrs
-                try {
-                    Path p = resolveFile(path);
-                    if (Files.exists(p)) {
-                        if (Files.isDirectory(p)) {
-                            sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, p.toString());
-                        } else {
-                            sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-                        }
-                    } else {
-                        Files.createDirectory(p);
-                        setAttributes(p, attrs);
-                        sendStatus(id, SSH_FX_OK, "");
-                    }
-                } catch (AccessDeniedException e) {
-                    sendStatus(id, SSH_FX_PERMISSION_DENIED, e.getMessage());
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doMakeDirectory(buffer, id);
                 break;
             }
             case SSH_FXP_RMDIR: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_RMDIR (path={})", path);
-                // attrs
-                try {
-                    Path p = resolveFile(path);
-                    if (Files.isDirectory(p)) {
-                        Files.delete(p);
-                        sendStatus(id, SSH_FX_OK, "");
-                    } else {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doRemoveDirectory(buffer, id);
                 break;
             }
             case SSH_FXP_REALPATH: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_REALPATH (path={})", path);
-                if (path.trim().length() == 0) {
-                    path = ".";
-                }
-                try {
-                    Path p = resolveFile(path).toAbsolutePath().normalize();
-                    sendPath(id, p, false);
-                } catch (FileNotFoundException e) {
-                    e.printStackTrace();
-                    sendStatus(id, SSH_FX_NO_SUCH_FILE, e.getMessage());
-                } catch (IOException e) {
-                    e.printStackTrace();
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doRealPath(buffer, id);
                 break;
             }
             case SSH_FXP_STAT: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_STAT (path={})", path);
-                try {
-                    Path 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());
-                }
+                doStat(buffer, id);
                 break;
             }
             case SSH_FXP_RENAME: {
-                String oldPath = buffer.getString();
-                String newPath = buffer.getString();
-                log.debug("Received SSH_FXP_RENAME (oldPath={}, newPath={})", oldPath, newPath);
-                try {
-                    Path o = resolveFile(oldPath);
-                    Path n = resolveFile(newPath);
-                    if (!Files.exists(o)) {
-                        sendStatus(id, SSH_FX_NO_SUCH_FILE, o.toString());
-                    } else if (Files.exists(n)) {
-                        sendStatus(id, SSH_FX_FAILURE, n.toString());
-                    } else {
-                        Files.move(o, n);
-                        sendStatus(id, SSH_FX_OK, "");
-                    }
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doRename(buffer, id);
                 break;
             }
             case SSH_FXP_READLINK: {
-                String path = buffer.getString();
-                log.debug("Received SSH_FXP_READLINK (path={})", path);
-                try {
-                    Path f = resolveFile(path);
-                    String l = Files.readSymbolicLink(f).toString();
-                    sendLink(id, l);
-                } catch (UnsupportedOperationException e) {
-                    sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doReadLink(buffer, id);
                 break;
             }
             case SSH_FXP_SYMLINK: {
-                String linkpath = buffer.getString();
-                String targetpath = buffer.getString();
-                log.debug("Received SSH_FXP_SYMLINK (linkpath={}, targetpath={})", linkpath, targetpath);
-                try {
-                    Path link = resolveFile(linkpath);
-                    Path target = fileSystem.getPath(targetpath);
-                    Files.createSymbolicLink(link, target);
-                    sendStatus(id, SSH_FX_OK, "");
-                } catch (UnsupportedOperationException e) {
-                    sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
-                } catch (IOException e) {
-                    sendStatus(id, SSH_FX_FAILURE, e.getMessage());
-                }
+                doSymLink(buffer, id);
+                break;
+            }
+            case SSH_FXP_LINK: {
+                doLink(buffer, id);
+                break;
+            }
+            case SSH_FXP_BLOCK: {
+                doBlock(buffer, id);
+                break;
+            }
+            case SSH_FXP_UNBLOCK: {
+                doUnblock(buffer, id);
+                break;
+            }
+            case SSH_FXP_EXTENDED: {
+                doExtended(buffer, id);
                 break;
             }
             default: {
@@ -904,6 +773,627 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         }
     }
 
+    protected void doExtended(Buffer buffer, int id) throws IOException {
+        String extension = buffer.getString();
+        switch (extension) {
+        case "text-seek":
+            doTextSeek(buffer, id);
+            break;
+        case "version-select":
+            doVersionSelect(buffer, id);
+            break;
+        default:
+            log.error("Received unsupported SSH_FXP_EXTENDED({})", extension);
+            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented");
+            break;
+        }
+    }
+
+    protected void doTextSeek(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        long line = buffer.getLong();
+        log.debug("Received SSH_FXP_EXTENDED(text-seek) (handle={}, line={})", handle, line);
+        // TODO : implement text-seek
+        sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(text-seek) is unsupported or not implemented");
+    }
+
+    protected void doVersionSelect(Buffer buffer, int id) throws IOException {
+        String ver = buffer.getString();
+        log.debug("Received SSH_FXP_EXTENDED(version-select) (version={})", version);
+        if (Integer.toString(SFTP_V3).equals(ver)) {
+            version = SFTP_V3;
+        } else if (Integer.toString(SFTP_V4).equals(ver)) {
+            version = SFTP_V4;
+        } else if (Integer.toString(SFTP_V5).equals(ver)) {
+            version = SFTP_V5;
+        } else if (Integer.toString(SFTP_V6).equals(ver)) {
+            version = SFTP_V6;
+        } else {
+            sendStatus(id, SSH_FX_FAILURE, "Unsupported version " + ver);
+            return;
+        }
+        sendStatus(id, SSH_FX_OK, "");
+    }
+
+    protected void doBlock(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        long offset = buffer.getLong();
+        long length = buffer.getLong();
+        int mask = buffer.getInt();
+        log.debug("Received SSH_FXP_BLOCK (handle={}, offset={}, length={}, mask={})", new Object[] { handle, offset, length, mask });
+        try {
+            Handle p = handles.get(handle);
+            if (!(p instanceof FileHandle)) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
+                return;
+            }
+            FileHandle fileHandle = (FileHandle) p;
+            fileHandle.lock(offset, length, mask);
+            sendStatus(id, SSH_FX_OK, "");
+        } catch (IOException | OverlappingFileLockException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doUnblock(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        long offset = buffer.getLong();
+        long length = buffer.getLong();
+        log.debug("Received SSH_FXP_UNBLOCK (handle={}, offset={}, length={})", new Object[] { handle, offset, length });
+        try {
+            Handle p = handles.get(handle);
+            if (!(p instanceof FileHandle)) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
+                return;
+            }
+            FileHandle fileHandle = (FileHandle) p;
+            boolean found = fileHandle.unlock(offset, length);
+            sendStatus(id, found ? SSH_FX_OK : SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK, "");
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doLink(Buffer buffer, int id) throws IOException {
+        String targetpath = buffer.getString();
+        String linkpath = buffer.getString();
+        boolean symLink = buffer.getBoolean();
+        log.debug("Received SSH_FXP_LINK (linkpath={}, targetpath={}, symlink={})", new Object[] { linkpath, targetpath, symLink });
+        try {
+            Path link = resolveFile(linkpath);
+            Path target = fileSystem.getPath(targetpath);
+            if (symLink) {
+                Files.createSymbolicLink(link, target);
+            } else {
+                Files.createLink(link, target);
+            }
+            sendStatus(id, SSH_FX_OK, "");
+        } catch (UnsupportedOperationException e) {
+            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_SYMLINK is unsupported or not implemented");
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doSymLink(Buffer buffer, int id) throws IOException {
+        String targetpath = buffer.getString();
+        String linkpath = buffer.getString();
+        log.debug("Received SSH_FXP_SYMLINK (linkpath={}, targetpath={})", linkpath, targetpath);
+        try {
+            Path link = resolveFile(linkpath);
+            Path target = fileSystem.getPath(targetpath);
+            Files.createSymbolicLink(link, target);
+            sendStatus(id, SSH_FX_OK, "");
+        } catch (UnsupportedOperationException e) {
+            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_SYMLINK is unsupported or not implemented");
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doReadLink(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        log.debug("Received SSH_FXP_READLINK (path={})", path);
+        try {
+            Path f = resolveFile(path);
+            String l = Files.readSymbolicLink(f).toString();
+            sendLink(id, l);
+        } catch (UnsupportedOperationException e) {
+            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_READLINK is unsupported or not implemented");
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doRename(Buffer buffer, int id) throws IOException {
+        String oldPath = buffer.getString();
+        String newPath = buffer.getString();
+        int flags = 0;
+        if (version >= SFTP_V5) {
+            flags = buffer.getInt();
+        }
+        log.debug("Received SSH_FXP_RENAME (oldPath={}, newPath={}, flags={})", new Object[] { oldPath, newPath, flags });
+        try {
+            List<CopyOption> opts = new ArrayList<>();
+            if ((flags & SSH_FXP_RENAME_ATOMIC) != 0) {
+                opts.add(StandardCopyOption.ATOMIC_MOVE);
+            }
+            if ((flags & SSH_FXP_RENAME_OVERWRITE) != 0) {
+                opts.add(StandardCopyOption.REPLACE_EXISTING);
+            }
+            Path o = resolveFile(oldPath);
+            Path n = resolveFile(newPath);
+            Files.move(o, n, opts.toArray(new CopyOption[opts.size()]));
+            sendStatus(id, SSH_FX_OK, "");
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doStat(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        int flags = SSH_FILEXFER_ATTR_ALL;
+        if (version >= SFTP_V4) {
+            flags = buffer.getInt();
+        }
+        log.debug("Received SSH_FXP_STAT (path={}, flags={})", path, flags);
+        try {
+            Path p = resolveFile(path);
+            sendAttrs(id, p, flags, true);
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doRealPath(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        log.debug("Received SSH_FXP_REALPATH (path={})", path);
+        if (path.trim().length() == 0) {
+            path = ".";
+        }
+        try {
+            if (version < SFTP_V6) {
+                Path p = resolveFile(path).toAbsolutePath().normalize();
+                if (!Files.exists(p)) {
+                    throw new FileNotFoundException(p.toString());
+                }
+                sendPath(id, p, Collections.<String, Object>emptyMap());
+            } else {
+                // Read control byte
+                int control = 0;
+                if (buffer.available() > 0) {
+                    control = buffer.getByte();
+                }
+                List<String> paths = new ArrayList<>();
+                while (buffer.available() > 0) {
+                    paths.add(buffer.getString());
+                }
+                // Resolve path
+                Path p = resolveFile(path);
+                for (String p2 : paths) {
+                    p = p.resolve(p2);
+                }
+                p = p.toAbsolutePath().normalize();
+                Map<String, Object> attrs = Collections.emptyMap();
+                if (control == SSH_FXP_REALPATH_STAT_IF) {
+                    try {
+                        attrs = getAttributes(p, false);
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                } else if (control == SSH_FXP_REALPATH_STAT_ALWAYS) {
+                    attrs = getAttributes(p, false);
+                }
+                sendPath(id, p, attrs);
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        log.debug("Received SSH_FXP_RMDIR (path={})", path);
+        // attrs
+        try {
+            Path p = resolveFile(path);
+            if (Files.isDirectory(p)) {
+                Files.delete(p);
+                sendStatus(id, SSH_FX_OK, "");
+            } else {
+                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        Map<String, Object> attrs = readAttrs(buffer);
+
+        log.debug("Received SSH_FXP_MKDIR (path={})", path);
+        // attrs
+        try {
+            Path p = resolveFile(path);
+            if (Files.exists(p)) {
+                if (Files.isDirectory(p)) {
+                    sendStatus(id, SSH_FX_FILE_ALREADY_EXISTS, p.toString());
+                } else {
+                    sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
+                }
+            } else {
+                Files.createDirectory(p);
+                setAttributes(p, attrs);
+                sendStatus(id, SSH_FX_OK, "");
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doRemove(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        log.debug("Received SSH_FXP_REMOVE (path={})", path);
+        try {
+            Path p = resolveFile(path);
+            if (!Files.exists(p)) {
+                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
+            } else if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) {
+                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.toString());
+            } else {
+                Files.delete(p);
+                sendStatus(id, SSH_FX_OK, "");
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doReadDir(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        log.debug("Received SSH_FXP_READDIR (handle={})", handle);
+        try {
+            Handle p = handles.get(handle);
+            if (!(p instanceof DirectoryHandle)) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
+            } else if (((DirectoryHandle) p).isDone()) {
+                sendStatus(id, SSH_FX_EOF, "", "");
+            } else if (!Files.exists(p.getFile())) {
+                sendStatus(id, SSH_FX_NO_SUCH_FILE, p.getFile().toString());
+            } else if (!Files.isDirectory(p.getFile())) {
+                sendStatus(id, SSH_FX_NOT_A_DIRECTORY, p.getFile().toString());
+            } else if (!Files.isReadable(p.getFile())) {
+                sendStatus(id, SSH_FX_PERMISSION_DENIED, p.getFile().toString());
+            } else {
+                DirectoryHandle dh = (DirectoryHandle) p;
+                if (dh.hasNext()) {
+                    // There is at least one file in the directory.
+                    // 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()) {
+                        // if no more files to send
+                        dh.setDone(true);
+                        dh.clearFileList();
+                    }
+                } else {
+                    // empty directory
+                    dh.setDone(true);
+                    dh.clearFileList();
+                    sendStatus(id, SSH_FX_EOF, "", "");
+                }
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doOpenDir(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        log.debug("Received SSH_FXP_OPENDIR (path={})", path);
+        try {
+            Path p = resolveFile(path);
+            if (!Files.exists(p)) {
+                sendStatus(id, SSH_FX_NO_SUCH_FILE, path);
+            } else if (!Files.isDirectory(p)) {
+                sendStatus(id, SSH_FX_NOT_A_DIRECTORY, path);
+            } else if (!Files.isReadable(p)) {
+                sendStatus(id, SSH_FX_PERMISSION_DENIED, path);
+            } else {
+                String handle = UUID.randomUUID().toString();
+                handles.put(handle, new DirectoryHandle(p));
+                sendHandle(id, handle);
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doFSetStat(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        Map<String, 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_INVALID_HANDLE, handle);
+            } else {
+                setAttributes(p.getFile(), attrs);
+                sendStatus(id, SSH_FX_OK, "");
+            }
+        } catch (IOException | UnsupportedOperationException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doSetStat(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        Map<String, Object> attrs = readAttrs(buffer);
+        log.debug("Received SSH_FXP_SETSTAT (path={}, attrs={})", path, attrs);
+        try {
+            Path p = resolveFile(path);
+            setAttributes(p, attrs);
+            sendStatus(id, SSH_FX_OK, "");
+        } catch (IOException | UnsupportedOperationException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doFStat(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        int flags = SSH_FILEXFER_ATTR_ALL;
+        if (version >= SFTP_V4) {
+            flags = buffer.getInt();
+        }
+        log.debug("Received SSH_FXP_FSTAT (handle={}, flags={})", handle, flags);
+        try {
+            Handle p = handles.get(handle);
+            if (p == null) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
+            } else {
+                sendAttrs(id, p.getFile(), flags, true);
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doLStat(Buffer buffer, int id) throws IOException {
+        String path = buffer.getString();
+        int flags = SSH_FILEXFER_ATTR_ALL;
+        if (version >= SFTP_V4) {
+            flags = buffer.getInt();
+        }
+        log.debug("Received SSH_FXP_LSTAT (path={}, flags={})", path, flags);
+        try {
+            Path p = resolveFile(path);
+            sendAttrs(id, p, flags, false);
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doWrite(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        long offset = buffer.getLong();
+        byte[] data = buffer.getBytes();
+        log.debug("Received SSH_FXP_WRITE (handle={}, offset={}, data=byte[{}])", new Object[] { handle, offset, data.length });
+        try {
+            Handle p = handles.get(handle);
+            if (!(p instanceof FileHandle)) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
+            } else {
+                FileHandle fh = (FileHandle) p;
+                fh.write(data, offset);
+                sendStatus(id, SSH_FX_OK, "");
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doRead(Buffer buffer, int id) throws IOException {
+        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)) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle);
+            } else {
+                FileHandle fh = (FileHandle) p;
+                byte[] b = new byte[Math.min(len, Buffer.MAX_LEN)];
+                len = fh.read(b, offset);
+                if (len >= 0) {
+                    Buffer buf = new Buffer(len + 5);
+                    buf.putByte((byte) SSH_FXP_DATA);
+                    buf.putInt(id);
+                    buf.putBytes(b, 0, len);
+                    send(buf);
+                } else {
+                    sendStatus(id, SSH_FX_EOF, "");
+                }
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doClose(Buffer buffer, int id) throws IOException {
+        String handle = buffer.getString();
+        log.debug("Received SSH_FXP_CLOSE (handle={})", handle);
+        try {
+            Handle h = handles.get(handle);
+            if (h == null) {
+                sendStatus(id, SSH_FX_INVALID_HANDLE, handle, "");
+            } else {
+                handles.remove(handle);
+                h.close();
+                sendStatus(id, SSH_FX_OK, "", "");
+            }
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doOpen(Buffer buffer, int id) throws IOException {
+        if (session.getFactoryManager().getProperties() != null) {
+            String maxHandlesString = session.getFactoryManager().getProperties().get(MAX_OPEN_HANDLES_PER_SESSION);
+            if (maxHandlesString != null) {
+                int maxHandleCount = Integer.parseInt(maxHandlesString);
+                if (handles.size() > maxHandleCount) {
+                    sendStatus(id, SSH_FX_FAILURE, "Too many open handles");
+                    return;
+                }
+            }
+        }
+
+        String path = buffer.getString();
+        int access = 0;
+        if (version >= SFTP_V5) {
+            access = buffer.getInt();
+        }
+        int pflags = buffer.getInt();
+        if (version < SFTP_V5) {
+            int flags = pflags;
+            pflags = 0;
+            switch (flags & (SSH_FXF_READ | SSH_FXF_WRITE)) {
+            case SSH_FXF_READ:
+                access |= ACE4_READ_DATA | ACE4_READ_ATTRIBUTES;
+                break;
+            case SSH_FXF_WRITE:
+                access |= ACE4_WRITE_DATA | ACE4_WRITE_ATTRIBUTES;
+                break;
+            default:
+                access |= ACE4_READ_DATA | ACE4_READ_ATTRIBUTES;
+                access |= ACE4_WRITE_DATA | ACE4_WRITE_ATTRIBUTES;
+                break;
+            }
+            if ((flags & SSH_FXF_APPEND) != 0) {
+                access |= ACE4_APPEND_DATA;
+                pflags |= SSH_FXF_APPEND_DATA | SSH_FXF_APPEND_DATA_ATOMIC;
+            }
+            if ((flags & SSH_FXF_CREAT) != 0) {
+                if ((flags & SSH_FXF_EXCL) != 0) {
+                    pflags |= SSH_FXF_CREATE_NEW;
+                } else if ((flags & SSH_FXF_TRUNC) != 0) {
+                    pflags |= SSH_FXF_CREATE_TRUNCATE;
+                } else {
+                    pflags |= SSH_FXF_OPEN_OR_CREATE;
+                }
+            } else {
+                if ((flags & SSH_FXF_TRUNC) != 0) {
+                    pflags |= SSH_FXF_TRUNCATE_EXISTING;
+                } else {
+                    pflags |= SSH_FXF_OPEN_EXISTING;
+                }
+            }
+        }
+        Map<String, Object> attrs = readAttrs(buffer);
+        log.debug("Received SSH_FXP_OPEN (path={}, access={}, pflags={}, attrs={})", new Object[]{path, access, pflags, attrs});
+        try {
+            Path file = resolveFile(path);
+            String handle = UUID.randomUUID().toString();
+            handles.put(handle, new FileHandle(file, pflags, access, attrs));
+            sendHandle(id, handle);
+        } catch (IOException e) {
+            sendStatus(id, e);
+        }
+    }
+
+    protected void doInit(Buffer buffer, int id) throws IOException {
+        log.debug("Received SSH_FXP_INIT (version={})", id);
+        version = id;
+        while (buffer.available() > 0) {
+            String name = buffer.getString();
+            byte[] data = buffer.getBytes();
+            extensions.put(name, data);
+        }
+
+        int low = LOWER_SFTP_IMPL;
+        int hig = HIGHER_SFTP_IMPL;
+        String all = ALL_SFTP_IMPL;
+
+        if (session.getFactoryManager().getProperties() != null) {
+            String sftpVersion = session.getFactoryManager().getProperties().get(SFTP_VERSION);
+            if (sftpVersion != null) {
+                low = hig = Integer.parseInt(sftpVersion);
+                all = sftpVersion;
+            }
+        }
+        if (version >= low) {
+            version = Math.min(version, hig);
+            buffer.clear();
+            buffer.putByte((byte) SSH_FXP_VERSION);
+            buffer.putInt(version);
+
+            // newline
+            buffer.putString("newline");
+            buffer.putString(System.getProperty("line.separator"));
+
+            // versions
+            buffer.putString("versions");
+            buffer.putString(all);
+
+            // supported
+            buffer.putString("supported");
+            buffer.putInt(5 * 4); // length of 5 integers
+            // supported-attribute-mask
+            buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS
+                    | SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_CREATETIME
+                    | SSH_FILEXFER_ATTR_MODIFYTIME | SSH_FILEXFER_ATTR_OWNERGROUP
+                    | SSH_FILEXFER_ATTR_BITS);
+            // TODO: supported-attribute-bits
+            buffer.putInt(0);
+            // supported-open-flags
+            buffer.putInt(SSH_FXF_READ | SSH_FXF_WRITE | SSH_FXF_APPEND
+                    | SSH_FXF_CREAT | SSH_FXF_TRUNC | SSH_FXF_EXCL);
+            // TODO: supported-access-mask
+            buffer.putInt(0);
+            // max-read-size
+            buffer.putInt(0);
+
+            // supported2
+            buffer.putString("supported2");
+            buffer.putInt(8 * 4); // length of 7 integers + 2 shorts
+            // supported-attribute-mask
+            buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS
+                    | SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_CREATETIME
+                    | SSH_FILEXFER_ATTR_MODIFYTIME | SSH_FILEXFER_ATTR_OWNERGROUP
+                    | SSH_FILEXFER_ATTR_BITS);
+            // TODO: supported-attribute-bits
+            buffer.putInt(0);
+            // supported-open-flags
+            buffer.putInt(SSH_FXF_ACCESS_DISPOSITION | SSH_FXF_APPEND_DATA);
+            // TODO: supported-access-mask
+            buffer.putInt(0);
+            // max-read-size
+            buffer.putInt(0);
+            // supported-open-block-vector
+            buffer.putShort(0);
+            // supported-block-vector
+            buffer.putShort(0);
+            // attrib-extension-count
+            buffer.putInt(0);
+            // extension-count
+            buffer.putInt(0);
+
+                /*
+                buffer.putString("acl-supported");
+                buffer.putInt(4);
+                // capabilities
+                buffer.putInt(0);
+                */
+
+            send(buffer);
+        } else {
+            // We only support version >= 3 (Version 1 and 2 are not common)
+            sendStatus(id, SSH_FX_OP_UNSUPPORTED, "SFTP server only support versions " + all);
+        }
+    }
+
     protected void sendHandle(int id, String handle) throws IOException {
         Buffer buffer = new Buffer();
         buffer.putByte((byte) SSH_FXP_HANDLE);
@@ -912,19 +1402,15 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         send(buffer);
     }
 
-    protected void sendAttrs(int id, Path file, boolean followLinks) throws IOException {
+    protected void sendAttrs(int id, Path file, int flags, boolean followLinks) throws IOException {
         Buffer buffer = new Buffer();
         buffer.putByte((byte) SSH_FXP_ATTRS);
         buffer.putInt(id);
-        writeAttrs(buffer, file, followLinks);
+        writeAttrs(buffer, file, flags, followLinks);
         send(buffer);
     }
 
-    protected void sendPath(int id, Path f) throws IOException {
-        sendPath(id, f, true);
-    }
-
-    protected void sendPath(int id, Path f, boolean sendAttrs) throws IOException {
+    protected void sendPath(int id, Path f, Map<String, Object> attrs) throws IOException {
         Buffer buffer = new Buffer();
         buffer.putByte((byte) SSH_FXP_NAME);
         buffer.putInt(id);
@@ -934,13 +1420,19 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         if (normalizedPath.length() == 0) {
             normalizedPath = "/";
         }
-        buffer.putString(normalizedPath);
+        buffer.putString(normalizedPath, StandardCharsets.UTF_8);
         f = resolveFile(normalizedPath);
         if (f.getFileName() == null) {
             f = resolveFile(".");
         }
-        buffer.putString(getLongName(f, sendAttrs)); // Format specified in the specs
-        buffer.putInt(0);
+        if (version == SFTP_V3) {
+            buffer.putString(getLongName(f, attrs), StandardCharsets.UTF_8); // Format specified in the specs
+            buffer.putInt(0);
+        } else if (version >= SFTP_V4) {
+            writeAttrs(buffer, attrs);
+        } else {
+            throw new IllegalStateException();
+        }
         send(buffer);
     }
 
@@ -965,9 +1457,11 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         int nb = 0;
         while (files.hasNext() && buffer.wpos() < MAX_PACKET_LENGTH) {
             Path f = files.next();
-            buffer.putString(f.getFileName().toString());
-            buffer.putString(getLongName(f)); // Format specified in the specs
-            writeAttrs(buffer, f, false);
+            buffer.putString(f.getFileName().toString(), StandardCharsets.UTF_8);
+            if (version == SFTP_V3) {
+                buffer.putString(getLongName(f), StandardCharsets.UTF_8); // Format specified in the specs
+            }
+            writeAttrs(buffer, f, SSH_FILEXFER_ATTR_ALL, false);
             nb++;
         }
         int oldpos = buffer.wpos();
@@ -982,21 +1476,22 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
     }
 
     private String getLongName(Path f, boolean sendAttrs) throws IOException {
-        Map<Attribute, Object> attributes;
+        Map<String, Object> attributes;
         if (sendAttrs) {
             attributes = getAttributes(f, false);
         } else {
-            attributes = new HashMap<>();
-            attributes.put(Attribute.Owner, "owner");
-            attributes.put(Attribute.Group, "group");
-            attributes.put(Attribute.Size, (long) 0);
-            attributes.put(Attribute.IsDirectory, false);
-            attributes.put(Attribute.IsSymbolicLink, false);
-            attributes.put(Attribute.IsRegularFile, false);
-            attributes.put(Attribute.Permissions, EnumSet.noneOf(Permission.class));
-            attributes.put(Attribute.LastModifiedTime, (long) 0);
-        }
-        String username = (String) attributes.get(Attribute.Owner);
+            attributes = Collections.emptyMap();
+        }
+        return getLongName(f, attributes);
+    }
+
+    private String getLongName(Path f, Map<String, Object> attributes) throws IOException {
+        String username;
+        if (attributes.containsKey("owner")) {
+            username = attributes.get("owner").toString();
+        } else {
+            username = "owner";
+        }
         if (username.length() > 8) {
             username = username.substring(0, 8);
         } else {
@@ -1004,7 +1499,12 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
                 username = username + " ";
             }
         }
-        String group = (String) attributes.get(Attribute.Group);
+        String group;
+        if (attributes.containsKey("group")) {
+            group = attributes.get("group").toString();
+        } else {
+            group = "group";
+        }
         if (group.length() > 8) {
             group = group.substring(0, 8);
         } else {
@@ -1013,27 +1513,25 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
             }
         }
 
-        long length = (Long) attributes.get(Attribute.Size);
+        Long length = (Long) attributes.get("size");
+        if (length == null) {
+            length = 0l;
+        }
         String lengthString = String.format("%1$8s", length);
 
-        boolean isDirectory = (Boolean) attributes.get(Attribute.IsDirectory);
-        boolean isLink = (Boolean) attributes.get(Attribute.IsSymbolicLink);
-        int perms = getPermissions(attributes);
+        Boolean isDirectory = (Boolean) attributes.get("isDirectory");
+        Boolean isLink = (Boolean) attributes.get("isSymbolicLink");
+        Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
+        if (perms == null) {
+            perms = new HashSet<>();
+        }
 
         StringBuilder sb = new StringBuilder();
-        sb.append(isDirectory ? "d" : isLink ? "l" : "-");
-        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((isDirectory != null && isDirectory) ? "d" : (isLink != null && isLink) ? "l" : "-");
+        sb.append(PosixFilePermissions.toString(perms));
         sb.append("  ");
-        sb.append(attributes.containsKey(Attribute.NLink)
-                ? attributes.get(Attribute.NLink) : "1");
+        sb.append(attributes.containsKey("nlink")
+                ? attributes.get("nlink") : "1");
         sb.append(" ");
         sb.append(username);
         sb.append(" ");
@@ -1041,73 +1539,50 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         sb.append(" ");
         sb.append(lengthString);
         sb.append(" ");
-        sb.append(getUnixDate((Long) attributes.get(Attribute.LastModifiedTime)));
+        sb.append(getUnixDate((FileTime) attributes.get("lastModifiedTime")));
         sb.append(" ");
         sb.append(f.getFileName().toString());
 
         return sb.toString();
     }
 
-    protected Map<Attribute, Object> getPermissions(int perms) {
-        Map<Attribute, Object> attrs = new HashMap<>();
-        if ((perms & S_IFMT) == S_IFREG) {
-            attrs.put(Attribute.IsRegularFile, Boolean.TRUE);
-        }
-        if ((perms & S_IFMT) == S_IFDIR) {
-            attrs.put(Attribute.IsDirectory, Boolean.TRUE);
-        }
-        if ((perms & S_IFMT) == S_IFLNK) {
-            attrs.put(Attribute.IsSymbolicLink, Boolean.TRUE);
-        }
-        EnumSet<Permission> p = EnumSet.noneOf(Permission.class);
-        if ((perms & S_IRUSR) != 0) {
-            p.add(Permission.UserRead);
-        }
-        if ((perms & S_IWUSR) != 0) {
-            p.add(Permission.UserWrite);
-        }
-        if ((perms & S_IXUSR) != 0) {
-            p.add(Permission.UserExecute);
-        }
-        if ((perms & S_IRGRP) != 0) {
-            p.add(Permission.GroupRead);
-        }
-        if ((perms & S_IWGRP) != 0) {
-            p.add(Permission.GroupWrite);
-        }
-        if ((perms & S_IXGRP) != 0) {
-            p.add(Permission.GroupExecute);
-        }
-        if ((perms & S_IROTH) != 0) {
-            p.add(Permission.OthersRead);
-        }
-        if ((perms & S_IWOTH) != 0) {
-            p.add(Permission.OthersWrite);
-        }
-        if ((perms & S_IXOTH) != 0) {
-            p.add(Permission.OthersExecute);
-        }
-        attrs.put(Attribute.Permissions, p);
-        return attrs;
-    }
-
-    protected int getPermissions(Map<Attribute, Object> attributes) {
-        boolean isReg = (Boolean) attributes.get(Attribute.IsRegularFile);
-        boolean isDir = (Boolean) attributes.get(Attribute.IsDirectory);
-        boolean isLnk = (Boolean) attributes.get(Attribute.IsSymbolicLink);
+    protected int attributesToPermissions(Map<String, Object> attributes) {
+        boolean isReg = getBool((Boolean) attributes.get("isRegularFile"));
+        boolean isDir = getBool((Boolean) attributes.get("isDirectory"));
+        boolean isLnk = getBool((Boolean) attributes.get("isSymbolicLink"));
         int pf = 0;
-        EnumSet<Permission> perms = (EnumSet<Permission>) attributes.get(Attribute.Permissions);
-        for (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;
+        Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
+        if (perms != null) {
+            for (PosixFilePermission p : perms) {
+                switch (p) {
+                case OWNER_READ:
+                    pf |= S_IRUSR;
+                    break;
+                case OWNER_WRITE:
+                    pf |= S_IWUSR;
+                    break;
+                case OWNER_EXECUTE:
+                    pf |= S_IXUSR;
+                    break;
+                case GROUP_READ:
+                    pf |= S_IRGRP;
+                    break;
+                case GROUP_WRITE:
+                    pf |= S_IWGRP;
+                    break;
+                case GROUP_EXECUTE:
+                    pf |= S_IXGRP;
+                    break;
+                case OTHERS_READ:
+                    pf |= S_IROTH;
+                    break;
+                case OTHERS_WRITE:
+                    pf |= S_IWOTH;
+                    break;
+                case OTHERS_EXECUTE:
+                    pf |= S_IXOTH;
+                    break;
+                }
             }
         }
         pf |= isReg ? S_IFREG : 0;
@@ -1116,41 +1591,93 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         return pf;
     }
 
-    protected void writeAttrs(Buffer buffer, Path file, boolean followLinks) throws IOException {
-        if (!Files.exists(file)) {
+    protected void writeAttrs(Buffer buffer, Path file, int flags, boolean followLinks) throws IOException {
+        LinkOption[] options = followLinks ? new LinkOption[0] : new LinkOption[] { LinkOption.NOFOLLOW_LINKS };
+        if (!Files.exists(file, options)) {
             throw new FileNotFoundException(file.toString());
         }
-        Map<Attribute, Object> attributes = getAttributes(file, followLinks);
-        boolean isReg = getBool((Boolean) attributes.get(Attribute.IsRegularFile));
-        boolean isDir = getBool((Boolean) attributes.get(Attribute.IsDirectory));
-        boolean isLnk = getBool((Boolean) attributes.get(Attribute.IsSymbolicLink));
-        int flags = 0;
-        if ((isReg || isLnk) && attributes.containsKey(Attribute.Size)) {
-            flags |= SSH_FILEXFER_ATTR_SIZE;
-        }
-        if (attributes.containsKey(Attribute.Uid) && attributes.containsKey(Attribute.Gid)) {
-            flags |= SSH_FILEXFER_ATTR_UIDGID;
-        }
-        if (attributes.containsKey(Attribute.Permissions)) {
-            flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
-        }
-        if (attributes.containsKey(Attribute.LastAccessTime) && attributes.containsKey(Attribute.LastModifiedTime)) {
-            flags |= SSH_FILEXFER_ATTR_ACMODTIME;
-        }
-        buffer.putInt(flags);
-        if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-            buffer.putLong((Long) attributes.get(Attribute.Size));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-            buffer.putInt((Integer) attributes.get(Attribute.Uid));
-            buffer.putInt((Integer) attributes.get(Attribute.Gid));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-            buffer.putInt(getPermissions(attributes));
-        }
-        if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-            buffer.putInt(((Long) attributes.get(Attribute.LastAccessTime)) / 1000);
-            buffer.putInt(((Long) attributes.get(Attribute.LastModifiedTime)) / 1000);
+        Map<String, Object> attributes = getAttributes(file, flags, followLinks);
+        writeAttrs(buffer, attributes);
+    }
+
+    protected void writeAttrs(Buffer buffer, Map<String, Object> attributes) throws IOException {
+        if (version == SFTP_V3) {
+            boolean isReg = getBool((Boolean) attributes.get("isRegularFile"));
+            boolean isDir = getBool((Boolean) attributes.get("isDirectory"));
+            boolean isLnk = getBool((Boolean) attributes.get("isSymbolicLink"));
+            int flags =
+                    ((isReg || isLnk) && attributes.containsKey("size") ? SSH_FILEXFER_ATTR_SIZE : 0) |
+                    (attributes.containsKey("uid") && attributes.containsKey("gid") ? SSH_FILEXFER_ATTR_UIDGID : 0) |
+                    (attributes.containsKey("permissions") ? SSH_FILEXFER_ATTR_PERMISSIONS : 0) |
+                    (attributes.containsKey("lastModifiedTime") && attributes.containsKey("lastAccessTime") ? SSH_FILEXFER_ATTR_ACMODTIME : 0);
+            buffer.putInt(flags);
+            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+                buffer.putLong((Long) attributes.get("size"));
+            }
+            if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
+                buffer.putInt((Integer) attributes.get("uid"));
+                buffer.putInt((Integer) attributes.get("gid"));
+            }
+            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                buffer.putInt(attributesToPermissions(attributes));
+            }
+            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+                buffer.putInt(((FileTime) attributes.get("lastAccessTime")).to(TimeUnit.SECONDS));
+                buffer.putInt(((FileTime) attributes.get("lastModifiedTime")).to(TimeUnit.SECONDS));
+            }
+        } else if (version >= SFTP_V4) {
+            boolean isReg = getBool((Boolean) attributes.get("isRegularFile"));
+            boolean isDir = getBool((Boolean) attributes.get("isDirectory"));
+            boolean isLnk = getBool((Boolean) attributes.get("isSymbolicLink"));
+            int flags =
+                    ((isReg || isLnk) && attributes.containsKey("size") ? SSH_FILEXFER_ATTR_SIZE : 0) |
+                            (attributes.containsKey("owner") && attributes.containsKey("group") ? SSH_FILEXFER_ATTR_OWNERGROUP : 0) |
+                            (attributes.containsKey("permissions") ? SSH_FILEXFER_ATTR_PERMISSIONS : 0) |
+                            (attributes.containsKey("lastModifiedTime") ? SSH_FILEXFER_ATTR_MODIFYTIME : 0) |
+                            (attributes.containsKey("creationTime") ? SSH_FILEXFER_ATTR_CREATETIME : 0) |
+                            (attributes.containsKey("lastAccessTime") ? SSH_FILEXFER_ATTR_ACCESSTIME : 0);
+            buffer.putInt(flags);
+            buffer.putByte((byte) (isReg ? SSH_FILEXFER_TYPE_REGULAR :
+                    isDir ? SSH_FILEXFER_TYPE_DIRECTORY :
+                            isLnk ? SSH_FILEXFER_TYPE_SYMLINK :
+                                    SSH_FILEXFER_TYPE_UNKNOWN));
+            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+                buffer.putLong((Long) attributes.get("size"));
+            }
+            if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+                buffer.putString(attributes.get("owner").toString(), StandardCharsets.UTF_8);
+                buffer.putString(attributes.get("group").toString(), StandardCharsets.UTF_8);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                buffer.putInt(attributesToPermissions(attributes));
+            }
+            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+                buffer.putLong(((FileTime) attributes.get("lastAccessTime")).to(TimeUnit.SECONDS));
+                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                    long nanos = ((FileTime) attributes.get("lastAccessTime")).to(TimeUnit.NANOSECONDS);
+                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                    buffer.putInt((int) nanos);
+                }
+            }
+            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+                buffer.putLong(((FileTime) attributes.get("creationTime")).to(TimeUnit.SECONDS));
+                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                    long nanos = ((FileTime) attributes.get("creationTime")).to(TimeUnit.NANOSECONDS);
+                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                    buffer.putInt((int) nanos);
+                }
+            }
+            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+                buffer.putLong(((FileTime) attributes.get("lastModifiedTime")).to(TimeUnit.SECONDS));
+                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                    long nanos = ((FileTime) attributes.get("lastModifiedTime")).to(TimeUnit.NANOSECONDS);
+                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                    buffer.putInt((int) nanos);
+                }
+            }
+            // TODO: acls
+            // TODO: bits
+            // TODO: extended
         }
     }
 
@@ -1158,98 +1685,51 @@ public class SftpSubsystem implements Command, Runnable, SessionAware, FileSyste
         return bool != null && bool;
     }
 
-    protected Map<Attribute, Object> getAttributes(Path file, boolean followLinks) throws IOException {
-        String[] attrs = new String[] { "unix:*", "posix:*", "*" };
-        Map<String, Object> a = null;
-        for (String attr : attrs) {
-            try {
-                a = Files.readAttributes(
-                        file, attr,
-                        followLinks ? new LinkOption[0] : new LinkOption[]{LinkOption.NOFOLLOW_LINKS});
-                break;
-            } catch (UnsupportedOperationException e) {
-                // Ignore
-            }
-        }
-        if (a == null) {
-            throw new IllegalStateException();
-        }
-        Map<Attribute, Object> map = new HashMap<>();
-        map.put(Attribute.Size, a.get("size"));
-        if (a.containsKey("uid")) {
-            map.put(Attribute.Uid, a.get("uid"));
-        }
-        if (a.containsKey("owner")) {
-            map.put(Attribute.Owner, ((UserPrincipal) a.get("owner")).getName());
-        } else {
-            map.put(Attribute.Owner, session.getUsername());
-        }
-        if (a.containsKey("gid")) {
-            map.put(Attribute.Gid, a.get("gid"));
-        }
-        if (a.containsKey("group")) {
-            map.put(Attribute.Group, ((GroupPrincipal) a.get("group")).getName());
-        } else {
-            map.put(Attribute.Group, session.getUsername());
-        }
-        if (a.containsKey("nlink")) {
-            map.put(Attribute.NLink, a.get("nlink"));
-        }
-        map.put(Attribute.IsDirectory, a.get("isDirectory"));
-        map.put(Attribute.IsRegularFile, a.get("isRegularFile"));
-        map.put(Attribute.IsSymbolicLink, a.get("isSymbolicLink"));
-        map.put(Attribute.CreationTime, ((FileTime) a.get("creationTime")).toMillis());
-        map.put(Attribute.LastModifiedTime, ((FileTime) a.get("lastModifiedTime")).toMillis());
-        map.put(Attribute.LastAccessTime, ((FileTime) a.get("lastAccessTime")).toMillis());
-        if (a.containsKey("permissions")) {
-            map.put(Attribute.Permissions, fromPerms((Set<PosixFilePermission>) a.get("permissions")));
+    protected Map<String, Object> getAttributes(Path file, boolean followLinks) throws IOException {
+        return getAttributes(file, SSH_FILEXFER_ATTR_ALL, followLinks);
+    }
+
+    protected Map<String, Object

<TRUNCATED>