You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2015/07/05 16:48:49 UTC

[1/2] mina-sshd git commit: [SSHD-522] Expose ability to do SFTP client version re-negotiation

Repository: mina-sshd
Updated Branches:
  refs/heads/master f8a0e8129 -> 8e4476cc6


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
index 9523514..af4b0d2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
@@ -18,89 +18,25 @@
  */
 package org.apache.sshd.client.subsystem.sftp;
 
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_APPEND_DATA;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_READ_ATTRIBUTES;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_READ_DATA;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_WRITE_ATTRIBUTES;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_WRITE_DATA;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V3;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V4;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V5;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V6;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_ALL;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_CREATETIME;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_SIZE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_SUBSECOND_TIMES;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_REGULAR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_SYMLINK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_APPEND;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_CREAT;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_CREATE_NEW;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_CREATE_TRUNCATE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_EXCL;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_OPEN_EXISTING;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_OPEN_OR_CREATE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_READ;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_TRUNC;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_TRUNCATE_EXISTING;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_WRITE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_ATTRS;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_BLOCK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_CLOSE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_DATA;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_FSETSTAT;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_FSTAT;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_HANDLE;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_INIT;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_LINK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_LSTAT;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_MKDIR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_NAME;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_OPEN;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_OPENDIR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_READ;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_READDIR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_READLINK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_REALPATH;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_REMOVE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RENAME;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RENAME_ATOMIC;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RENAME_OVERWRITE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RMDIR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_SETSTAT;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_STAT;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_STATUS;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_SYMLINK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_UNBLOCK;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_VERSION;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_WRITE;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FX_EOF;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FX_OK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IFDIR;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IFLNK;
-import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IFREG;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.attribute.FileTime;
+import java.io.StreamCorruptedException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.client.SftpException;
@@ -109,13 +45,12 @@ import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.FactoryManagerUtils;
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+import org.apache.sshd.common.subsystem.sftp.extensions.VersionsParser.Versions;
 import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-import org.apache.sshd.common.util.io.InputStreamWithChannel;
-import org.apache.sshd.common.util.io.OutputStreamWithChannel;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -366,828 +301,48 @@ public class DefaultSftpClient extends AbstractSftpClient {
         }
     }
 
-    protected void checkStatus(Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkStatus(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
-            }
-
-            if (substatus != SSH_FX_OK) {
-                throw new SftpException(substatus, msg);
-            }
-        } else {
-            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
-        }
-    }
-
-    protected String checkHandle(Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkHandle(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
-            }
-            throw new SftpException(substatus, msg);
-        } else if (type == SSH_FXP_HANDLE) {
-            String handle = ValidateUtils.checkNotNullAndNotEmpty(buffer.getString(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY);
-            return handle;
-        } else {
-            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
-        }
-    }
-
-    protected Attributes checkAttributes(Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkAttributes(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
-            }
-            throw new SftpException(substatus, msg);
-        } else if (type == SSH_FXP_ATTRS) {
-            return readAttributes(buffer);
-        } else {
-            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
-        }
-    }
-
-    protected String checkOneName(Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkOneName(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
-            }
-            throw new SftpException(substatus, msg);
-        } else if (type == SSH_FXP_NAME) {
-            int len = buffer.getInt();
-            if (len != 1) {
-                throw new SshException("SFTP error: received " + len + " names instead of 1");
-            }
-            String name = buffer.getString(), longName = null;
-            if (version == SFTP_V3) {
-                longName = buffer.getString();
-            }
-            Attributes attrs = readAttributes(buffer);
-            if (log.isTraceEnabled()) {
-                log.trace("checkOneName(id={}) ({})[{}]: {}", Integer.valueOf(id), name, longName, attrs);
-            }
-            return name;
-        } else {
-            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
-        }
-    }
-
-    protected Attributes readAttributes(Buffer buffer) throws IOException {
-        Attributes attrs = new Attributes();
-        int flags = buffer.getInt();
-        if (version == SFTP_V3) {
-            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                attrs.flags.add(Attribute.Size);
-                attrs.size = buffer.getLong();
-            }
-            if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                attrs.flags.add(Attribute.UidGid);
-                attrs.uid = buffer.getInt();
-                attrs.gid = buffer.getInt();
-            }
-            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                attrs.flags.add(Attribute.Perms);
-                attrs.perms = buffer.getInt();
-            }
-            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                attrs.flags.add(Attribute.AcModTime);
-                attrs.atime = buffer.getInt();
-                attrs.mtime = buffer.getInt();
-            }
-        } else if (version >= SFTP_V4) {
-            attrs.type = buffer.getUByte();
-            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                attrs.flags.add(Attribute.Size);
-                attrs.size = buffer.getLong();
-            }
-            if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                attrs.flags.add(Attribute.OwnerGroup);
-                attrs.owner = buffer.getString();
-                attrs.group = buffer.getString();
-            }
-            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                attrs.flags.add(Attribute.Perms);
-                attrs.perms = buffer.getInt();
-            }
-            
-            // update the permissions according to the type
-            switch (attrs.type) {
-                case SSH_FILEXFER_TYPE_REGULAR:
-                    attrs.perms |= S_IFREG;
-                    break;
-                case SSH_FILEXFER_TYPE_DIRECTORY:
-                    attrs.perms |= S_IFDIR;
-                    break;
-                case SSH_FILEXFER_TYPE_SYMLINK:
-                    attrs.perms |= S_IFLNK;
-                    break;
-                default:    // do nothing
-            }
-
-            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                attrs.flags.add(Attribute.AccessTime);
-                attrs.accessTime = readTime(buffer, flags);
-                attrs.atime = (int) attrs.accessTime.to(TimeUnit.SECONDS);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                attrs.flags.add(Attribute.CreateTime);
-                attrs.createTime = readTime(buffer, flags);
-                attrs.ctime = (int) attrs.createTime.to(TimeUnit.SECONDS);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                attrs.flags.add(Attribute.ModifyTime);
-                attrs.modifyTime = readTime(buffer, flags);
-                attrs.mtime = (int) attrs.modifyTime.to(TimeUnit.SECONDS);
-            }
-            // TODO: acl
-        } else {
-            throw new IllegalStateException("readAttributes - unsupported version: " + version);
-        }
-        return attrs;
-    }
-
-    private FileTime readTime(Buffer buffer, int flags) {
-        long secs = buffer.getLong();
-        long millis = secs * 1000;
-        if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-            millis += buffer.getInt() / 1000000l;
-        }
-        return FileTime.from(millis, TimeUnit.MILLISECONDS);
-    }
-
-    protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
-        if (version == SFTP_V3) {
-            int flags = 0;
-            for (Attribute a : attributes.flags) {
-                switch (a) {
-                    case Size:
-                        flags |= SSH_FILEXFER_ATTR_SIZE;
-                        break;
-                    case UidGid:
-                        flags |= SSH_FILEXFER_ATTR_UIDGID;
-                        break;
-                    case Perms:
-                        flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
-                        break;
-                    case AcModTime:
-                        flags |= SSH_FILEXFER_ATTR_ACMODTIME;
-                        break;
-                    default:    // do nothing
-                }
-            }
-            buffer.putInt(flags);
-            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(attributes.size);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
-                buffer.putInt(attributes.uid);
-                buffer.putInt(attributes.gid);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributes.perms);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
-                buffer.putInt(attributes.atime);
-                buffer.putInt(attributes.mtime);
-            }
-        } else if (version >= SFTP_V4) {
-            int flags = 0;
-            for (Attribute a : attributes.flags) {
-                switch (a) {
-                    case Size:
-                        flags |= SSH_FILEXFER_ATTR_SIZE;
-                        break;
-                    case OwnerGroup:
-                        flags |= SSH_FILEXFER_ATTR_OWNERGROUP;
-                        break;
-                    case Perms:
-                        flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
-                        break;
-                    case AccessTime:
-                        flags |= SSH_FILEXFER_ATTR_ACCESSTIME;
-                        break;
-                    case ModifyTime:
-                        flags |= SSH_FILEXFER_ATTR_MODIFYTIME;
-                        break;
-                    case CreateTime:
-                        flags |= SSH_FILEXFER_ATTR_CREATETIME;
-                        break;
-                    default:    // do nothing
-                }
-            }
-            buffer.putInt(flags);
-            buffer.putByte((byte) attributes.type);
-            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
-                buffer.putLong(attributes.size);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
-                buffer.putString(attributes.owner != null ? attributes.owner : "OWNER@", StandardCharsets.UTF_8);
-                buffer.putString(attributes.group != null ? attributes.group : "GROUP@", StandardCharsets.UTF_8);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
-                buffer.putInt(attributes.perms);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
-                buffer.putLong(attributes.accessTime.to(TimeUnit.SECONDS));
-                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-                    long nanos = attributes.accessTime.to(TimeUnit.NANOSECONDS);
-                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-                    buffer.putInt((int) nanos);
-                }
-                buffer.putInt(attributes.atime);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
-                buffer.putLong(attributes.createTime.to(TimeUnit.SECONDS));
-                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-                    long nanos = attributes.createTime.to(TimeUnit.NANOSECONDS);
-                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-                    buffer.putInt((int) nanos);
-                }
-                buffer.putInt(attributes.atime);
-            }
-            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
-                buffer.putLong(attributes.modifyTime.to(TimeUnit.SECONDS));
-                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
-                    long nanos = attributes.modifyTime.to(TimeUnit.NANOSECONDS);
-                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
-                    buffer.putInt((int) nanos);
-                }
-                buffer.putInt(attributes.atime);
-            }
-            // TODO: acl
-        } else {
-            throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version);
-        }
-    }
-
-    @Override
-    public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(path);
-        if (version == SFTP_V3) {
-            int mode = 0;
-            for (OpenMode m : options) {
-                switch (m) {
-                    case Read:
-                        mode |= SSH_FXF_READ;
-                        break;
-                    case Write:
-                        mode |= SSH_FXF_WRITE;
-                        break;
-                    case Append:
-                        mode |= SSH_FXF_APPEND;
-                        break;
-                    case Create:
-                        mode |= SSH_FXF_CREAT;
-                        break;
-                    case Truncate:
-                        mode |= SSH_FXF_TRUNC;
-                        break;
-                    case Exclusive:
-                        mode |= SSH_FXF_EXCL;
-                        break;
-                    default:    // do nothing
-                }
-            }
-            buffer.putInt(mode);
-        } else {
-            int mode = 0;
-            int access = 0;
-            if (options.contains(OpenMode.Read)) {
-                access |= ACE4_READ_DATA | ACE4_READ_ATTRIBUTES;
-            }
-            if (options.contains(OpenMode.Write)) {
-                access |= ACE4_WRITE_DATA | ACE4_WRITE_ATTRIBUTES;
-            }
-            if (options.contains(OpenMode.Append)) {
-                access |= ACE4_APPEND_DATA;
-            }
-            if (options.contains(OpenMode.Create) && options.contains(OpenMode.Exclusive)) {
-                mode |= SSH_FXF_CREATE_NEW;
-            } else if (options.contains(OpenMode.Create) && options.contains(OpenMode.Truncate)) {
-                mode |= SSH_FXF_CREATE_TRUNCATE;
-            } else if (options.contains(OpenMode.Create)) {
-                mode |= SSH_FXF_OPEN_OR_CREATE;
-            } else if (options.contains(OpenMode.Truncate)) {
-                mode |= SSH_FXF_TRUNCATE_EXISTING;
-            } else {
-                mode |= SSH_FXF_OPEN_EXISTING;
-            }
-            if (version >= SFTP_V5) {
-                buffer.putInt(access);
-            }
-            buffer.putInt(mode);
-        }
-        writeAttributes(buffer, new Attributes());
-        return new DefaultCloseableHandle(this, checkHandle(receive(send(SSH_FXP_OPEN, buffer))));
-    }
-
-    @Override
-    public void close(Handle handle) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(handle.id);
-        checkStatus(receive(send(SSH_FXP_CLOSE, buffer)));
-    }
-
-    @Override
-    public void remove(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(path);
-        checkStatus(receive(send(SSH_FXP_REMOVE, buffer)));
-    }
-
-    @Override
-    public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(oldPath);
-        buffer.putString(newPath);
-        
-        int numOptions = GenericUtils.size(options);
-        if (version >= SFTP_V5) {
-            int opts = 0;
-            if (numOptions > 0) {
-                for (CopyMode opt : options) {
-                    switch (opt) {
-                        case Atomic:
-                            opts |= SSH_FXP_RENAME_ATOMIC;
-                            break;
-                        case Overwrite:
-                            opts |= SSH_FXP_RENAME_OVERWRITE;
-                            break;
-                        default:    // do nothing
+    /**
+     * @param selector The {@link SftpVersionSelector} to use
+     * @return The selected version (may be same as current)
+     * @throws IOException If failed to negotiate
+     */
+    public int negotiateVersion(SftpVersionSelector selector) throws IOException {
+        int current = getVersion();
+        Set<Integer> available = GenericUtils.asSortedSet(Collections.singleton(Integer.valueOf(current)));
+        Map<String,?> parsed = getParsedServerExtensions();
+        Collection<String> extensions = ParserUtils.supportedExtensions(parsed);
+        if ((GenericUtils.size(extensions) > 0) && extensions.contains(SftpConstants.EXT_VERSELECT)) {
+            Versions vers = GenericUtils.isEmpty(parsed) ? null : (Versions) parsed.get(SftpConstants.EXT_VERSIONS);
+            Collection<String> reported = (vers == null) ? null : vers.versions;
+            if (GenericUtils.size(reported) > 0) {
+                for (String v : reported) {
+                    if (!available.add(Integer.valueOf(v))) {
+                        continue;
                     }
                 }
             }
-            buffer.putInt(opts);
-        } else if (numOptions > 0) {
-            throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")"
-                                                  + " - copy options can not be used with this SFTP version: " + options);
         }
-        checkStatus(receive(send(SSH_FXP_RENAME, buffer)));
-    }
 
-    @Override
-    public int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(handle.id);
-        buffer.putLong(fileOffset);
-        buffer.putInt(len);
-        return checkData(receive(send(SSH_FXP_READ, buffer)), dstoff, dst);
-    }
-
-    protected int checkData(Buffer buffer, int dstoff, byte[] dst) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkData(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
-            }
-
-            if (substatus == SSH_FX_EOF) {
-                return -1;
-            }
-
-            throw new SftpException(substatus, msg);
-        } else if (type == SSH_FXP_DATA) {
-            int len = buffer.getInt();
-            buffer.getRawBytes(dst, dstoff, len);
-            return len;
-        } else {
-            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        int selected = selector.selectVersion(current, new ArrayList<Integer>(available));
+        if (log.isDebugEnabled()) {
+            log.debug("negotiateVersion({}) {} -> {}", Integer.valueOf(current), available, Integer.valueOf(selected));
         }
-    }
 
-    @Override
-    public void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException {
-        // do some bounds checking first
-        if ((fileOffset < 0) || (srcoff < 0) || (len < 0)) {
-            throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters "
-                                             + " are non-negative values: file-offset=" + fileOffset
-                                             + ", src-offset=" + srcoff + ", len=" + len);
-        }
-        if ((srcoff + len) > src.length) {
-            throw new IllegalArgumentException("write(" + handle + ")"
-                                             + " cannot read bytes " + srcoff + " to " + (srcoff + len)
-                                             + " when array is only of length " + src.length);
-        }
-
-        Buffer buffer = new ByteArrayBuffer(handle.id.length() + len + Long.SIZE /* some extra fields */);
-        buffer.putString(handle.id);
-        buffer.putLong(fileOffset);
-        buffer.putBytes(src, srcoff, len);
-        checkStatus(receive(send(SSH_FXP_WRITE, buffer)));
-    }
-
-    @Override
-    public void mkdir(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(path.length() +  Long.SIZE /* some extra fields */);
-        buffer.putString(path, StandardCharsets.UTF_8);
-        buffer.putInt(0);
-        if (version != SFTP_V3) {
-            buffer.putByte((byte) 0);
-        }
-        checkStatus(receive(send(SSH_FXP_MKDIR, buffer)));
-    }
-
-    @Override
-    public void rmdir(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(path.length() +  Long.SIZE /* some extra fields */);
-        buffer.putString(path);
-        checkStatus(receive(send(SSH_FXP_RMDIR, buffer)));
-    }
-
-    @Override
-    public CloseableHandle openDir(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(path);
-        return new DefaultCloseableHandle(this, checkHandle(receive(send(SSH_FXP_OPENDIR, buffer))));
-    }
-
-    @Override
-    public DirEntry[] readDir(Handle handle) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(handle.id);
-        return checkDir(receive(send(SSH_FXP_READDIR, buffer)));
-    }
-
-    protected DirEntry[] checkDir(Buffer buffer) throws IOException {
-        int length = buffer.getInt();
-        int type = buffer.getUByte();
-        int id = buffer.getInt();
-        if (type == SSH_FXP_STATUS) {
-            int substatus = buffer.getInt();
-            String msg = buffer.getString();
-            String lang = buffer.getString();
-            if (log.isTraceEnabled()) {
-                log.trace("checkDir(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
-            }
-            if (substatus == SSH_FX_EOF) {
-                return null;
-            }
-            throw new SftpException(substatus, msg);
-        } else if (type == SSH_FXP_NAME) {
-            int len = buffer.getInt();
-            DirEntry[] entries = new DirEntry[len];
-            for (int i = 0; i < len; i++) {
-                String name = buffer.getString();
-                String longName = (version == SFTP_V3) ? buffer.getString() : null;
-                Attributes attrs = readAttributes(buffer);
-                if (log.isTraceEnabled()) {
-                    log.trace("checkDir(id={})[{}] ({})[{}]: {}", Integer.valueOf(id), Integer.valueOf(i), name, longName, attrs);
-                }
-
-                entries[i] = new DirEntry(name, longName, attrs);
-            }
-            return entries;
-        } else {
-            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
-        }
-    }
-
-    @Override
-    public String canonicalPath(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(path);
-        return checkOneName(receive(send(SSH_FXP_REALPATH, buffer)));
-    }
-
-    @Override
-    public Attributes stat(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(path);
-        if (version >= SFTP_V4) {
-            buffer.putInt(SSH_FILEXFER_ATTR_ALL);
-        }
-        return checkAttributes(receive(send(SSH_FXP_STAT, buffer)));
-    }
-
-    @Override
-    public Attributes lstat(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(path);
-        if (version >= SFTP_V4) {
-            buffer.putInt(SSH_FILEXFER_ATTR_ALL);
-        }
-        return checkAttributes(receive(send(SSH_FXP_LSTAT, buffer)));
-    }
-
-    @Override
-    public Attributes stat(Handle handle) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(handle.id);
-        if (version >= SFTP_V4) {
-            buffer.putInt(SSH_FILEXFER_ATTR_ALL);
-        }
-        return checkAttributes(receive(send(SSH_FXP_FSTAT, buffer)));
-    }
-
-    @Override
-    public void setStat(String path, Attributes attributes) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(path);
-        writeAttributes(buffer, attributes);
-        checkStatus(receive(send(SSH_FXP_SETSTAT, buffer)));
-    }
-
-    @Override
-    public void setStat(Handle handle, Attributes attributes) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(handle.id);
-        writeAttributes(buffer, attributes);
-        checkStatus(receive(send(SSH_FXP_FSETSTAT, buffer)));
-    }
-
-    @Override
-    public String readLink(String path) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
-        buffer.putString(path);
-        return checkOneName(receive(send(SSH_FXP_READLINK, buffer)));
-    }
-
-    @Override
-    public void link(String linkPath, String targetPath, boolean symbolic) throws IOException {
-        Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */);
-        if (version < SFTP_V6) {
-            if (!symbolic) {
-                throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version);
-            }
-            buffer.putString(targetPath);
-            buffer.putString(linkPath);
-            checkStatus(receive(send(SSH_FXP_SYMLINK, buffer)));
-        } else {
-            buffer.putString(targetPath);
-            buffer.putString(linkPath);
-            buffer.putBoolean(symbolic);
-            checkStatus(receive(send(SSH_FXP_LINK, buffer)));
-        }
-    }
-
-    @Override
-    public void lock(Handle handle, long offset, long length, int mask) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(handle.id);
-        buffer.putLong(offset);
-        buffer.putLong(length);
-        buffer.putInt(mask);
-        checkStatus(receive(send(SSH_FXP_BLOCK, buffer)));
-    }
-
-    @Override
-    public void unlock(Handle handle, long offset, long length) throws IOException {
-        Buffer buffer = new ByteArrayBuffer();
-        buffer.putString(handle.id);
-        buffer.putLong(offset);
-        buffer.putLong(length);
-        checkStatus(receive(send(SSH_FXP_UNBLOCK, buffer)));
-    }
-
-    @Override
-    public Iterable<DirEntry> readDir(final String path) throws IOException {
-        return new Iterable<DirEntry>() {
-            @Override
-            public Iterator<DirEntry> iterator() {
-                return new Iterator<DirEntry>() {
-                    private CloseableHandle handle;
-                    private DirEntry[] entries;
-                    private int index;
-
-                    {
-                        open();
-                        load();
-                    }
-
-                    @Override
-                    public boolean hasNext() {
-                        return (entries != null) && (index < entries.length);
-                    }
-
-                    @Override
-                    public DirEntry next() {
-                        DirEntry entry = entries[index++];
-                        if (index >= entries.length) {
-                            load();
-                        }
-                        return entry;
-                    }
-
-                    @SuppressWarnings("synthetic-access")
-                    private void open() {
-                        try {
-                            handle = openDir(path);
-                            if (log.isDebugEnabled()) {
-                                log.debug("readDir(" + path + ") handle=" + handle);
-                            }
-                        } catch (IOException e) {
-                            if (log.isDebugEnabled()) {
-                                log.debug("readDir(" + path + ") failed (" + e.getClass().getSimpleName() + ") to open dir: " + e.getMessage());
-                            }
-                            throw new RuntimeException(e);
-                        }
-                    }
-
-                    @SuppressWarnings("synthetic-access")
-                    private void load() {
-                        try {
-                            entries = readDir(handle);
-                            index = 0;
-                            if (entries == null) {
-                                handle.close();
-                            }
-                        } catch (IOException e) {
-                            entries = null;
-                            try {
-                                handle.close();
-                            } catch (IOException t) {
-                                if (log.isTraceEnabled()) {
-                                    log.trace(t.getClass().getSimpleName() + " while close handle=" + handle
-                                            + " due to " + e.getClass().getSimpleName() + " [" + e.getMessage() + "]"
-                                            + ": " + t.getMessage());
-                                }
-                            }
-                            throw new RuntimeException(e);
-                        }
-                    }
-
-                    @Override
-                    public void remove() {
-                        throw new UnsupportedOperationException("readDir(" + path + ") Iterator#remove() N/A");
-                    }
-                };
-            }
-        };
-    }
-
-    @Override
-    public InputStream read(final String path, final int bufferSize, final Collection<OpenMode> mode) throws IOException {
-        if (bufferSize < MIN_READ_BUFFER_SIZE) {
-            throw new IllegalArgumentException("Insufficient read buffer size: " + bufferSize + ", min.=" + MIN_READ_BUFFER_SIZE);
+        if (selected == current) {
+            return current;
         }
 
-        return new InputStreamWithChannel() {
-            private byte[] bb = new byte[1];
-            private byte[] buffer = new byte[bufferSize];
-            private int index;
-            private int available;
-            private CloseableHandle handle = DefaultSftpClient.this.open(path, mode);
-            private long offset;
-
-            @Override
-            public boolean isOpen() {
-                return (handle != null) && handle.isOpen();
-            }
-
-            @Override
-            public int read() throws IOException {
-                int read = read(bb, 0, 1);
-                if (read > 0) {
-                    return bb[0];
-                }
-
-                return read;
-            }
-
-            @Override
-            public int read(byte[] b, int off, int len) throws IOException {
-                if (!isOpen()) {
-                    throw new IOException("read(" + path + ") stream closed");
-                }
-
-                int idx = off;
-                while (len > 0) {
-                    if (index >= available) {
-                        available = DefaultSftpClient.this.read(handle, offset, buffer, 0, buffer.length);
-                        if (available < 0) {
-                            if (idx == off) {
-                                return -1;
-                            } else {
-                                break;
-                            }
-                        }
-                        offset += available;
-                        index = 0;
-                    }
-                    if (index >= available) {
-                        break;
-                    }
-                    int nb = Math.min(len, available - index);
-                    System.arraycopy(buffer, index, b, idx, nb);
-                    index += nb;
-                    idx += nb;
-                    len -= nb;
-                }
-                return idx - off;
-            }
-
-            @Override
-            public void close() throws IOException {
-                if (isOpen()) {
-                    try {
-                        handle.close();
-                    } finally {
-                        handle = null;
-                    }
-                }
-            }
-        };
-    }
-
-    @Override
-    public OutputStream write(final String path, final int bufferSize, final Collection<OpenMode> mode) throws IOException {
-        if (bufferSize < MIN_WRITE_BUFFER_SIZE) {
-            throw new IllegalArgumentException("Insufficient write buffer size: " + bufferSize + ", min.=" + MIN_WRITE_BUFFER_SIZE);
+        if (!available.contains(Integer.valueOf(selected))) {
+            throw new StreamCorruptedException("Selected version (" + selected + ") not part of available: " + available);
         }
 
-        return new OutputStreamWithChannel() {
-            private byte[] bb = new byte[1];
-            private byte[] buffer = new byte[bufferSize];
-            private int index;
-            private CloseableHandle handle = DefaultSftpClient.this.open(path, mode);
-            private long offset;
-
-            @Override
-            public boolean isOpen() {
-                return (handle != null) && handle.isOpen();
-            }
-
-            @Override
-            public void write(int b) throws IOException {
-                bb[0] = (byte) b;
-                write(bb, 0, 1);
-            }
-
-            @Override
-            public void write(byte[] b, int off, int len) throws IOException {
-                if (!isOpen()) {
-                    throw new IOException("write(" + path + ")[len=" + len + "] stream is closed");
-                }
-
-                do {
-                    int nb = Math.min(len, buffer.length - index);
-                    System.arraycopy(b, off, buffer, index, nb);
-                    index += nb;
-                    if (index == buffer.length) {
-                        flush();
-                    }
-                    off += nb;
-                    len -= nb;
-                } while (len > 0);
-            }
-
-            @Override
-            public void flush() throws IOException {
-                if (!isOpen()) {
-                    throw new IOException("flush(" + path + ") stream is closed");
-                }
-
-                DefaultSftpClient.this.write(handle, offset, buffer, 0, index);
-                offset += index;
-                index = 0;
-            }
-
-            @Override
-            public void close() throws IOException {
-                if (isOpen()) {
-                    try {
-                        try {
-                            if (index > 0) {
-                                flush();
-                            }
-                        } finally {
-                            handle.close();
-                        }
-                    } finally {
-                        handle = null;
-                    }
-                }
-            }
-        };
+        String verVal = String.valueOf(selected);
+        Buffer buffer = new ByteArrayBuffer((Integer.SIZE / Byte.SIZE) + SftpConstants.EXT_VERSELECT.length()     // extension name
+                                         + (Integer.SIZE / Byte.SIZE) + verVal.length());
+        buffer.putString(SftpConstants.EXT_VERSELECT);
+        buffer.putString(verVal);
+        checkStatus(receive(send(SftpConstants.SSH_FXP_EXTENDED, buffer)));
+        version = selected;
+        return selected;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
index 050716f..a2af81f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
@@ -42,6 +42,7 @@ import org.apache.sshd.common.FactoryManagerUtils;
 import org.apache.sshd.common.file.util.BaseFileSystem;
 import org.apache.sshd.common.file.util.ImmutableList;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 
 public class SftpFileSystem extends BaseFileSystem<SftpPath> {
@@ -57,6 +58,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
 
     private final String id;
     private final ClientSession session;
+    private final SftpVersionSelector selector;
     private final Queue<SftpClient> pool;
     private final ThreadLocal<Wrapper> wrappers = new ThreadLocal<>();
     private SftpPath defaultDir;
@@ -64,10 +66,11 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
     private int writeBufferSize = SftpClient.DEFAULT_WRITE_BUFFER_SIZE;
     private final List<FileStore> stores;
 
-    public SftpFileSystem(SftpFileSystemProvider provider, String id, ClientSession session) throws IOException {
+    public SftpFileSystem(SftpFileSystemProvider provider, String id, ClientSession session, SftpVersionSelector selector) throws IOException {
         super(provider);
         this.id = id;
         this.session = session;
+        this.selector = ValidateUtils.checkNotNull(selector, "No SFTP version selector provided", GenericUtils.EMPTY_OBJECT_ARRAY);
         this.stores = Collections.unmodifiableList(Collections.<FileStore>singletonList(new SftpFileStore(id, this)));
         this.pool = new LinkedBlockingQueue<>(FactoryManagerUtils.getIntProperty(session, POOL_SIZE_PROP, DEFAULT_POOL_SIZE));
         try (SftpClient client = getClient()) {
@@ -75,6 +78,10 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         }
     }
 
+    public final SftpVersionSelector getSftpVersionSelector() {
+        return selector;
+    }
+
     public final String getId() {
         return id;
     }
@@ -129,7 +136,7 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
             while (wrapper == null) {
                 SftpClient client = pool.poll();
                 if (client == null) {
-                    client = session.createSftpClient();
+                    client = session.createSftpClient(getSftpVersionSelector());
                 }
                 if (!client.isClosing()) {
                     wrapper = new Wrapper(client, getReadBufferSize(), getWriteBufferSize());

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
index df0c1e8..23ae4f2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
@@ -110,11 +110,16 @@ public class SftpFileSystemProvider extends FileSystemProvider {
                             )));
 
     private final SshClient client;
+    private final SftpVersionSelector selector;
     private final Map<String, SftpFileSystem> fileSystems = new HashMap<String, SftpFileSystem>();
     protected final Logger log;
 
     public SftpFileSystemProvider() {
-        this(null);
+        this((SshClient) null);
+    }
+
+    public SftpFileSystemProvider(SftpVersionSelector selector) {
+        this(null, selector);
     }
 
     /**
@@ -124,7 +129,12 @@ public class SftpFileSystemProvider extends FileSystemProvider {
      * @see SshClient#setUpDefaultClient()
      */
     public SftpFileSystemProvider(SshClient client) {
+        this(client, SftpVersionSelector.CURRENT);
+    }
+
+    public SftpFileSystemProvider(SshClient client, SftpVersionSelector selector) {
         this.log = LoggerFactory.getLogger(getClass());
+        this.selector = ValidateUtils.checkNotNull(selector, "No SFTP version selector provided", GenericUtils.EMPTY_OBJECT_ARRAY);
         if (client == null) {
             // TODO: make this configurable using system properties
             client = SshClient.setUpDefaultClient();
@@ -138,6 +148,10 @@ public class SftpFileSystemProvider extends FileSystemProvider {
         return SftpConstants.SFTP_SUBSYSTEM_NAME;
     }
 
+    public final SftpVersionSelector getSftpVersionSelector() {
+        return selector;
+    }
+
     @Override // NOTE: co-variant return
     public SftpFileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
         String host = ValidateUtils.checkNotNullAndNotEmpty(uri.getHost(), "Host not provided", GenericUtils.EMPTY_OBJECT_ARRAY);
@@ -168,7 +182,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
                 session.addPasswordIdentity(password);
                 session.auth().verify(FactoryManagerUtils.getLongProperty(env, AUTH_TIME_PROP_NAME, DEFAULT_AUTH_TIME));
 
-                fileSystem = new SftpFileSystem(this, id, session);
+                fileSystem = new SftpFileSystem(this, id, session, getSftpVersionSelector());
                 fileSystems.put(id, fileSystem);
             } catch(Exception e) {
                 if (session != null) {
@@ -207,7 +221,7 @@ public class SftpFileSystemProvider extends FileSystemProvider {
                 throw new FileSystemAlreadyExistsException(id);
             }
 
-            fileSystem = new SftpFileSystem(this, id, session);
+            fileSystem = new SftpFileSystem(this, id, session, getSftpVersionSelector());
             fileSystems.put(id, fileSystem);
         }
         

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
new file mode 100644
index 0000000..40d9ad7
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpVersionSelector.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.client.subsystem.sftp;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpVersionSelector {
+    /**
+     * @param current The current version negotiated with the server
+     * @param available Extra versions available - may be empty and/or contain
+     * only the current one
+     * @return The new requested version - if same as current, then nothing is done
+     */
+    int selectVersion(int current, List<Integer> available);
+    
+    /**
+     * An {@link SftpVersionSelector} that returns the current version
+     */
+    SftpVersionSelector CURRENT = new SftpVersionSelector() {
+            @Override
+            public int selectVersion(int current, List<Integer> available) {
+                return current;
+            }
+        };
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
index 3371904..0aa0572 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -178,7 +178,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
 	private final byte[] workBuf = new byte[Integer.SIZE / Byte.SIZE]; // TODO in JDK-8 use Integer.BYTES
     private FileSystem fileSystem = FileSystems.getDefault();
     private Path defaultDir = fileSystem.getPath(System.getProperty("user.dir"));
-
+    private long requestsCount;
     private int version;
     private final Map<String, byte[]> extensions = new HashMap<>();
     private final Map<String, Handle> handles = new HashMap<>();
@@ -591,6 +591,10 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                 log.warn("Unknown command type received: {}", Integer.valueOf(type));
                 sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command " + type + " is unsupported or not implemented");
         }
+
+        if (type != SSH_FXP_INIT) {
+            requestsCount++;
+        }
     }
 
     protected void doExtended(Buffer buffer, int id) throws IOException {
@@ -774,17 +778,33 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             sendStatus(id, e);
         }
     }
-    
+
     protected void doVersionSelect(Buffer buffer, int id) throws IOException {
+        /*
+         * The 'version-select' MUST be the first request from the client to the
+         * server; if it is not, the server MUST fail the request and close the
+         * channel.
+         */
+        if (requestsCount > 0L) {
+           sendStatus(id, SSH_FX_FAILURE, "Version selection not the 1st request");
+           session.close(true);
+           return;
+        }
+
         String proposed = buffer.getString();
         Boolean result = validateProposedVersion(id, proposed);
+        /*
+         * "MUST then close the channel without processing any further requests"
+         */
         if (result == null) {   // response sent internally
+            session.close(true);
             return;
         } if (result.booleanValue()) {
             version = Integer.parseInt(proposed);
             sendStatus(id, SSH_FX_OK, "");
         } else {
             sendStatus(id, SSH_FX_FAILURE, "Unsupported version " + proposed);
+            session.close(true);
         }
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
index aa7054f..3997c5b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java
@@ -47,8 +47,11 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.root.RootedFileSystemProvider;
@@ -111,11 +114,7 @@ public class SftpFileSystemTest extends BaseTestSupport {
     }
 
     @Test
-    public void testFileSystem() throws IOException {
-        Path targetPath = detectTargetFolder().toPath();
-        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
-        Utils.deleteRecursive(lclSftp);
-
+    public void testFileSystem() throws Exception {
         try(FileSystem fs = FileSystems.newFileSystem(createDefaultFileSystemURI(),
                 new TreeMap<String,Object>() {
                     private static final long serialVersionUID = 1L;    // we're not serializing it
@@ -125,106 +124,7 @@ public class SftpFileSystemTest extends BaseTestSupport {
                         put(SftpFileSystemProvider.WRITE_BUFFER_PROP_NAME, Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE));
                     }
             })) {
-
-            Iterable<Path> rootDirs = fs.getRootDirectories();
-            for (Path root : rootDirs) {
-                String  rootName = root.toString();
-                try (DirectoryStream<Path> ds = Files.newDirectoryStream(root)) {
-                    for (Path child : ds) {
-                        System.out.append('\t').append('[').append(rootName).append("] ").println(child);
-                    }
-                } catch(IOException | RuntimeException e) {
-                    // TODO on Windows one might get share problems for *.sys files
-                    // e.g. "C:\hiberfil.sys: The process cannot access the file because it is being used by another process"
-                    // for now, Windows is less of a target so we are lenient with it
-                    if (OsUtils.isWin32()) {
-                        System.err.println(e.getClass().getSimpleName() + " while accessing children of root=" + root + ": " + e.getMessage());
-                    } else {
-                        throw e;
-                    }
-                }
-            }
-
-            Path current = fs.getPath(".").toRealPath().normalize();
-            System.out.append("CWD: ").println(current);
-
-            Path parentPath = targetPath.getParent();
-            Path clientFolder = lclSftp.resolve("client");
-            String remFile1Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve(getCurrentTestName() + "-1.txt"));
-            Path file1 = fs.getPath(remFile1Path);
-            Files.createDirectories(file1.getParent());
-
-            String  expected="Hello world: " + getCurrentTestName();
-            {
-                Files.write(file1, expected.getBytes());
-                String buf = new String(Files.readAllBytes(file1));
-                assertEquals("Mismatched read test data", expected, buf);
-            }
-    
-            String remFile2Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve(getCurrentTestName() + "-2.txt"));
-            Path file2 = fs.getPath(remFile2Path);
-            String remFile3Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve(getCurrentTestName() + "-3.txt"));
-            Path file3 = fs.getPath(remFile3Path);
-            try {
-                Files.move(file2, file3);
-                fail("Unexpected success in moving " + file2 + " => " + file3);
-            } catch (NoSuchFileException e) {
-                // expected
-            }
-
-            Files.write(file2, "h".getBytes());
-            try {
-                Files.move(file1, file2);
-                fail("Unexpected success in moving " + file1 + " => " + file2);
-            } catch (FileAlreadyExistsException e) {
-                // expected
-            }
-            Files.move(file1, file2, StandardCopyOption.REPLACE_EXISTING);
-            Files.move(file2, file1);
-    
-            Map<String, Object> attrs = Files.readAttributes(file1, "*");
-            System.out.append(file1.toString()).append(" attributes: ").println(attrs);
-    
-            // TODO: symbolic links only work for absolute files
-    //        Path link = fs.getPath("target/sftp/client/test2.txt");
-    //        Files.createSymbolicLink(link, link.relativize(file));
-    //        assertTrue(Files.isSymbolicLink(link));
-    //        assertEquals("test.txt", Files.readSymbolicLink(link).toString());
-    
-            // TODO there are many issues with Windows and symbolic links - for now they are of a lesser interest
-            if (OsUtils.isUNIX()) {
-                Path link = fs.getPath(remFile2Path);
-                Path linkParent = link.getParent();
-                Path relPath = linkParent.relativize(file1);
-                Files.createSymbolicLink(link, relPath);
-                assertTrue("Not a symbolic link: " + link, Files.isSymbolicLink(link));
-
-                Path symLink = Files.readSymbolicLink(link);
-                assertEquals("mismatched symbolic link name", relPath.toString(), symLink.toString());
-                Files.delete(link);
-            }
-    
-            attrs = Files.readAttributes(file1, "*", LinkOption.NOFOLLOW_LINKS);
-            System.out.append(file1.toString()).append(" no-follow attributes: ").println(attrs);
-    
-            assertEquals("Mismatched symlink data", expected, new String(Files.readAllBytes(file1)));
-    
-            try (FileChannel channel = FileChannel.open(file1)) {
-                try (FileLock lock = channel.lock()) {
-                    System.out.println("Locked " + lock.toString());
-    
-                    try (FileChannel channel2 = FileChannel.open(file1)) {
-                        try (FileLock lock2 = channel2.lock()) {
-                            System.out.println("Locked " + lock2.toString());
-                            fail("Unexpected success in re-locking " + file1);
-                        } catch (OverlappingFileLockException e) {
-                            // expected
-                        }
-                    }
-                }
-            }
-    
-            Files.delete(file1);
+            testFileSystem(fs);
         }
     }
 
@@ -359,6 +259,161 @@ public class SftpFileSystemTest extends BaseTestSupport {
         }        
     }
 
+    @Test
+    public void testSftpVersionSelector() throws Exception {
+        final AtomicInteger selected = new AtomicInteger(-1);
+        SftpVersionSelector selector = new SftpVersionSelector() {
+                @Override
+                public int selectVersion(int current, List<Integer> available) {
+                    int numAvailable = GenericUtils.size(available);
+                    Integer maxValue = null;
+                    if (numAvailable == 1) {
+                        maxValue = available.get(0);
+                    } else {
+                        for (Integer v : available) {
+                            if (v.intValue() == current) {
+                                continue;
+                            }
+                            
+                            if ((maxValue == null) || (maxValue.intValue() < v.intValue())) {
+                                maxValue = v;
+                            }
+                        }
+                    }
+
+                    selected.set(maxValue.intValue());
+                    return selected.get();
+                }
+            };
+        try(SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+            
+            try (ClientSession session = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try(FileSystem fs = session.createSftpFileSystem(selector)) {
+                    assertTrue("Not an SftpFileSystem", fs instanceof SftpFileSystem);
+                    
+                    try(SftpClient sftp = ((SftpFileSystem) fs).getClient()) {
+                        assertEquals("Mismatched negotiated version", selected.get(), sftp.getVersion());
+                    }
+
+                    testFileSystem(fs);
+                }
+            } finally {
+                client.stop();
+            }
+        }
+
+    }
+    
+    private void testFileSystem(FileSystem fs) throws Exception {
+        Iterable<Path> rootDirs = fs.getRootDirectories();
+        for (Path root : rootDirs) {
+            String  rootName = root.toString();
+            try (DirectoryStream<Path> ds = Files.newDirectoryStream(root)) {
+                for (Path child : ds) {
+                    System.out.append('\t').append('[').append(rootName).append("] ").println(child);
+                }
+            } catch(IOException | RuntimeException e) {
+                // TODO on Windows one might get share problems for *.sys files
+                // e.g. "C:\hiberfil.sys: The process cannot access the file because it is being used by another process"
+                // for now, Windows is less of a target so we are lenient with it
+                if (OsUtils.isWin32()) {
+                    System.err.println(e.getClass().getSimpleName() + " while accessing children of root=" + root + ": " + e.getMessage());
+                } else {
+                    throw e;
+                }
+            }
+        }
+
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(lclSftp);
+
+        Path current = fs.getPath(".").toRealPath().normalize();
+        System.out.append("CWD: ").println(current);
+
+        Path parentPath = targetPath.getParent();
+        Path clientFolder = lclSftp.resolve("client");
+        String remFile1Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve(getCurrentTestName() + "-1.txt"));
+        Path file1 = fs.getPath(remFile1Path);
+        Files.createDirectories(file1.getParent());
+
+        String  expected="Hello world: " + getCurrentTestName();
+        {
+            Files.write(file1, expected.getBytes());
+            String buf = new String(Files.readAllBytes(file1));
+            assertEquals("Mismatched read test data", expected, buf);
+        }
+
+        String remFile2Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve(getCurrentTestName() + "-2.txt"));
+        Path file2 = fs.getPath(remFile2Path);
+        String remFile3Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve(getCurrentTestName() + "-3.txt"));
+        Path file3 = fs.getPath(remFile3Path);
+        try {
+            Files.move(file2, file3);
+            fail("Unexpected success in moving " + file2 + " => " + file3);
+        } catch (NoSuchFileException e) {
+            // expected
+        }
+
+        Files.write(file2, "h".getBytes());
+        try {
+            Files.move(file1, file2);
+            fail("Unexpected success in moving " + file1 + " => " + file2);
+        } catch (FileAlreadyExistsException e) {
+            // expected
+        }
+        Files.move(file1, file2, StandardCopyOption.REPLACE_EXISTING);
+        Files.move(file2, file1);
+
+        Map<String, Object> attrs = Files.readAttributes(file1, "*");
+        System.out.append(file1.toString()).append(" attributes: ").println(attrs);
+
+        // TODO: symbolic links only work for absolute files
+//        Path link = fs.getPath("target/sftp/client/test2.txt");
+//        Files.createSymbolicLink(link, link.relativize(file));
+//        assertTrue(Files.isSymbolicLink(link));
+//        assertEquals("test.txt", Files.readSymbolicLink(link).toString());
+
+        // TODO there are many issues with Windows and symbolic links - for now they are of a lesser interest
+        if (OsUtils.isUNIX()) {
+            Path link = fs.getPath(remFile2Path);
+            Path linkParent = link.getParent();
+            Path relPath = linkParent.relativize(file1);
+            Files.createSymbolicLink(link, relPath);
+            assertTrue("Not a symbolic link: " + link, Files.isSymbolicLink(link));
+
+            Path symLink = Files.readSymbolicLink(link);
+            assertEquals("mismatched symbolic link name", relPath.toString(), symLink.toString());
+            Files.delete(link);
+        }
+
+        attrs = Files.readAttributes(file1, "*", LinkOption.NOFOLLOW_LINKS);
+        System.out.append(file1.toString()).append(" no-follow attributes: ").println(attrs);
+
+        assertEquals("Mismatched symlink data", expected, new String(Files.readAllBytes(file1)));
+
+        try (FileChannel channel = FileChannel.open(file1)) {
+            try (FileLock lock = channel.lock()) {
+                System.out.println("Locked " + lock.toString());
+
+                try (FileChannel channel2 = FileChannel.open(file1)) {
+                    try (FileLock lock2 = channel2.lock()) {
+                        System.out.println("Locked " + lock2.toString());
+                        fail("Unexpected success in re-locking " + file1);
+                    } catch (OverlappingFileLockException e) {
+                        // expected
+                    }
+                }
+            }
+        }
+
+        Files.delete(file1);
+    }
+
     private URI createDefaultFileSystemURI() {
         return createFileSystemURI(getCurrentTestName());
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
index b1e12db..9d90231 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
@@ -39,11 +39,13 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.Set;
 import java.util.Vector;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.sshd.client.SftpException;
 import org.apache.sshd.client.SshClient;
@@ -254,67 +256,8 @@ public class SftpTest extends BaseTestSupport {
                 session.addPasswordIdentity(getCurrentTestName());
                 session.auth().verify(5L, TimeUnit.SECONDS);
 
-                Path targetPath = detectTargetFolder().toPath();
-                Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
-                Utils.deleteRecursive(lclSftp);
-                Files.createDirectories(lclSftp);
-
-                Path parentPath = targetPath.getParent();
-                Path clientFolder = lclSftp.resolve("client");
-                String dir = Utils.resolveRelativeRemotePath(parentPath, clientFolder);
-                String file = dir + "/" + getCurrentTestName() + ".txt";
-
                 try (SftpClient sftp = session.createSftpClient()) {
-                    sftp.mkdir(dir);
-            
-                    try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
-                        byte[] d = "0123456789\n".getBytes();
-                        sftp.write(h, 0, d, 0, d.length);
-                        sftp.write(h, d.length, d, 0, d.length);
-                
-                        SftpClient.Attributes attrs = sftp.stat(h);
-                        assertNotNull("No handle attributes", attrs);
-                    }            
-            
-                    try(SftpClient.CloseableHandle h = sftp.openDir(dir)) {
-                        SftpClient.DirEntry[] dirEntries = sftp.readDir(h);
-                        assertNotNull("No dir entries", dirEntries);
-                        assertEquals("Mismatced number of dir entries", 1, dirEntries.length);
-                        assertNull("Unexpected entry read", sftp.readDir(h));
-                    }
-            
-                    sftp.remove(file);
-    
-                    byte[] workBuf = new byte[IoUtils.DEFAULT_COPY_SIZE * Short.SIZE];
-                    new Random(System.currentTimeMillis()).nextBytes(workBuf);
-                    try (OutputStream os = sftp.write(file)) {
-                        os.write(workBuf);
-                    }
-            
-                    try (InputStream is = sftp.read(file, IoUtils.DEFAULT_COPY_SIZE)) {
-                        int readLen = is.read(workBuf);
-                        assertEquals("Mismatched read data length", workBuf.length, readLen);
-        
-                        int i = is.read();
-                        assertEquals("Unexpected read past EOF", -1, i);
-                    }
-        
-                    SftpClient.Attributes attributes = sftp.stat(file);
-                    assertTrue("Test file not detected as regular", attributes.isRegularFile());
-            
-                    attributes = sftp.stat(dir);
-                    assertTrue("Test directory not reported as such", attributes.isDirectory());
-            
-                    int nb = 0;
-                    for (SftpClient.DirEntry entry : sftp.readDir(dir)) {
-                        assertNotNull("Unexpected null entry", entry);
-                        nb++;
-                    }
-                    assertEquals("Mismatched read dir entries", 1, nb);
-            
-                    sftp.remove(file);
-            
-                    sftp.rmdir(dir);
+                    testClient(sftp);
                 }
             } finally {
                 client.stop();
@@ -794,6 +737,113 @@ public class SftpTest extends BaseTestSupport {
     }
 
     @Test
+    public void testSftpVersionSelector() throws Exception {
+        final AtomicInteger selected = new AtomicInteger(-1);
+        SftpVersionSelector selector = new SftpVersionSelector() {
+                @Override
+                public int selectVersion(int current, List<Integer> available) {
+                    int numAvailable = GenericUtils.size(available);
+                    Integer maxValue = null;
+                    if (numAvailable == 1) {
+                        maxValue = available.get(0);
+                    } else {
+                        for (Integer v : available) {
+                            if (v.intValue() == current) {
+                                continue;
+                            }
+                            
+                            if ((maxValue == null) || (maxValue.intValue() < v.intValue())) {
+                                maxValue = v;
+                            }
+                        }
+                    }
+
+                    selected.set(maxValue.intValue());
+                    return selected.get();
+                }
+            };
+
+        try(SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+            
+            try (ClientSession session = client.connect(getCurrentTestName(), "localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try(SftpClient sftp = session.createSftpClient(selector)) {
+                    assertEquals("Mismatched negotiated version", selected.get(), sftp.getVersion());
+                    testClient(sftp);
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    private void testClient(SftpClient sftp) throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(lclSftp);
+        Files.createDirectories(lclSftp);
+
+        Path parentPath = targetPath.getParent();
+        Path clientFolder = lclSftp.resolve("client");
+        String dir = Utils.resolveRelativeRemotePath(parentPath, clientFolder);
+        String file = dir + "/" + getCurrentTestName() + ".txt";
+
+        sftp.mkdir(dir);
+        
+        try(SftpClient.CloseableHandle h = sftp.open(file, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) {
+            byte[] d = "0123456789\n".getBytes();
+            sftp.write(h, 0, d, 0, d.length);
+            sftp.write(h, d.length, d, 0, d.length);
+    
+            SftpClient.Attributes attrs = sftp.stat(h);
+            assertNotNull("No handle attributes", attrs);
+        }            
+
+        try(SftpClient.CloseableHandle h = sftp.openDir(dir)) {
+            SftpClient.DirEntry[] dirEntries = sftp.readDir(h);
+            assertNotNull("No dir entries", dirEntries);
+            assertEquals("Mismatced number of dir entries", 1, dirEntries.length);
+            assertNull("Unexpected entry read", sftp.readDir(h));
+        }
+
+        sftp.remove(file);
+
+        byte[] workBuf = new byte[IoUtils.DEFAULT_COPY_SIZE * Short.SIZE];
+        new Random(System.currentTimeMillis()).nextBytes(workBuf);
+        try (OutputStream os = sftp.write(file)) {
+            os.write(workBuf);
+        }
+
+        try (InputStream is = sftp.read(file, IoUtils.DEFAULT_COPY_SIZE)) {
+            int readLen = is.read(workBuf);
+            assertEquals("Mismatched read data length", workBuf.length, readLen);
+
+            int i = is.read();
+            assertEquals("Unexpected read past EOF", -1, i);
+        }
+
+        SftpClient.Attributes attributes = sftp.stat(file);
+        assertTrue("Test file not detected as regular", attributes.isRegularFile());
+
+        attributes = sftp.stat(dir);
+        assertTrue("Test directory not reported as such", attributes.isDirectory());
+
+        int nb = 0;
+        for (SftpClient.DirEntry entry : sftp.readDir(dir)) {
+            assertNotNull("Unexpected null entry", entry);
+            nb++;
+        }
+        assertEquals("Mismatched read dir entries", 1, nb);
+
+        sftp.remove(file);
+
+        sftp.rmdir(dir);
+    }
+
+    @Test
     public void testCreateSymbolicLink() throws Exception {
         // Do not execute on windows as the file system does not support symlinks
         Assume.assumeTrue("Skip non-Unix O/S", OsUtils.isUNIX());


[2/2] mina-sshd git commit: [SSHD-522] Expose ability to do SFTP client version re-negotiation

Posted by lg...@apache.org.
[SSHD-522] Expose ability to do SFTP client version re-negotiation


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

Branch: refs/heads/master
Commit: 8e4476cc6030ff147e87aed0d358f99799b58208
Parents: f8a0e81
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Sun Jul 5 17:48:40 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Sun Jul 5 17:48:40 2015 +0300

----------------------------------------------------------------------
 .../sshd/client/session/ClientSession.java      |  14 +
 .../sshd/client/session/ClientSessionImpl.java  |  24 +-
 .../subsystem/sftp/AbstractSftpClient.java      | 931 ++++++++++++++++++-
 .../subsystem/sftp/DefaultSftpClient.java       | 919 +-----------------
 .../client/subsystem/sftp/SftpFileSystem.java   |  11 +-
 .../subsystem/sftp/SftpFileSystemProvider.java  |  20 +-
 .../subsystem/sftp/SftpVersionSelector.java     |  45 +
 .../server/subsystem/sftp/SftpSubsystem.java    |  24 +-
 .../subsystem/sftp/SftpFileSystemTest.java      | 265 +++---
 .../sshd/client/subsystem/sftp/SftpTest.java    | 170 ++--
 10 files changed, 1365 insertions(+), 1058 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index f74f7c3..9dc9ee2 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -34,6 +34,7 @@ import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.scp.ScpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
 import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.future.SshFuture;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
@@ -174,11 +175,24 @@ public interface ClientSession extends Session {
 
     /**
      * Create an SFTP client from this session.
+     * @return The created {@link SftpClient}
+     * @throws IOException if failed to create the client
      */
     SftpClient createSftpClient() throws IOException;
+    /**
+     * @param selector The {@link SftpVersionSelector} to use - <B>Note:</B>
+     * if the server does not support versions re-negotiation then the
+     * selector will be presented with only one &quot;choice&quot; - the
+     * current version
+     * @return The created {@link SftpClient}
+     * @throws IOException If failed to create the client or re-negotiate
+     */
+    SftpClient createSftpClient(SftpVersionSelector selector) throws IOException;
 
     FileSystem createSftpFileSystem() throws IOException;
+    FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException;
     FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException;
+    FileSystem createSftpFileSystem(SftpVersionSelector selector, int readBufferSize, int writeBufferSize) throws IOException;
 
     /**
      * Start forwarding the given local address on the client to the given address on the server.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index ef1a156..f2ee39b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -45,6 +45,7 @@ import org.apache.sshd.client.subsystem.sftp.DefaultSftpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpFileSystem;
 import org.apache.sshd.client.subsystem.sftp.SftpFileSystemProvider;
+import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedResource;
 import org.apache.sshd.common.Service;
@@ -397,17 +398,34 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession
 
     @Override
     public SftpClient createSftpClient() throws IOException {
-        return new DefaultSftpClient(this);
+        return createSftpClient(SftpVersionSelector.CURRENT);
+    }
+
+    @Override
+    public SftpClient createSftpClient(SftpVersionSelector selector) throws IOException {
+        DefaultSftpClient client = new DefaultSftpClient(this);
+        client.negotiateVersion(selector);
+        return client;
     }
 
     @Override
     public FileSystem createSftpFileSystem() throws IOException {
-        return createSftpFileSystem(SftpClient.DEFAULT_READ_BUFFER_SIZE, SftpClient.DEFAULT_WRITE_BUFFER_SIZE);
+        return createSftpFileSystem(SftpVersionSelector.CURRENT);
+    }
+
+    @Override
+    public FileSystem createSftpFileSystem(SftpVersionSelector selector) throws IOException {
+        return createSftpFileSystem(selector, SftpClient.DEFAULT_READ_BUFFER_SIZE, SftpClient.DEFAULT_WRITE_BUFFER_SIZE);
     }
 
     @Override
     public FileSystem createSftpFileSystem(int readBufferSize, int writeBufferSize) throws IOException {
-        SftpFileSystemProvider provider = new SftpFileSystemProvider((org.apache.sshd.client.SshClient) factoryManager);
+        return createSftpFileSystem(SftpVersionSelector.CURRENT, readBufferSize, writeBufferSize);
+    }
+
+    @Override
+    public FileSystem createSftpFileSystem(SftpVersionSelector selector, int readBufferSize, int writeBufferSize) throws IOException {
+        SftpFileSystemProvider provider = new SftpFileSystemProvider((org.apache.sshd.client.SshClient) factoryManager, selector);
         SftpFileSystem  fs = provider.newFileSystem(this);
         fs.setReadBufferSize(readBufferSize);
         fs.setWriteBufferSize(writeBufferSize);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8e4476cc/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
index 1807cb4..e6a7ccf 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
@@ -19,21 +19,99 @@
 
 package org.apache.sshd.client.subsystem.sftp;
 
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_APPEND_DATA;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_READ_ATTRIBUTES;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_READ_DATA;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_WRITE_ATTRIBUTES;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.ACE4_WRITE_DATA;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V3;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V4;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V5;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SFTP_V6;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_ALL;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_CREATETIME;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_SIZE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_SUBSECOND_TIMES;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_DIRECTORY;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_REGULAR;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FILEXFER_TYPE_SYMLINK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_APPEND;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_CREAT;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_CREATE_NEW;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_CREATE_TRUNCATE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_EXCL;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_OPEN_EXISTING;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_OPEN_OR_CREATE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_READ;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_TRUNC;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_TRUNCATE_EXISTING;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXF_WRITE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_ATTRS;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_BLOCK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_CLOSE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_DATA;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_FSETSTAT;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_FSTAT;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_HANDLE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_LINK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_LSTAT;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_MKDIR;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_NAME;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_OPEN;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_OPENDIR;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_READ;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_READDIR;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_READLINK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_REALPATH;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_REMOVE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RENAME;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RENAME_ATOMIC;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RENAME_OVERWRITE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_RMDIR;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_SETSTAT;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_STAT;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_STATUS;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_SYMLINK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_UNBLOCK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FXP_WRITE;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FX_EOF;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.SSH_FX_OK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IFDIR;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IFLNK;
+import static org.apache.sshd.common.subsystem.sftp.SftpConstants.S_IFREG;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.attribute.FileTime;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.apache.sshd.client.SftpException;
 import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtensionFactory;
+import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.io.InputStreamWithChannel;
+import org.apache.sshd.common.util.io.OutputStreamWithChannel;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
 
 /**
@@ -157,6 +235,15 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
         }
 
         Map<String,byte[]> extensions = getServerExtensions();
+        Map<String,Object> parsed = getParsedServerExtensions(extensions);
+        return factory.create(this, this, extensions, parsed);
+    }
+
+    protected Map<String,Object> getParsedServerExtensions() {
+        return getParsedServerExtensions(getServerExtensions());
+    }
+
+    protected Map<String,Object> getParsedServerExtensions(Map<String,byte[]> extensions) {
         Map<String,Object> parsed = parsedExtensionsHolder.get();
         if (parsed == null) {
             if ((parsed=ParserUtils.parse(extensions)) == null) {
@@ -164,7 +251,849 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
             }
             parsedExtensionsHolder.set(parsed);
         }
+        
+        return parsed;
+    }
 
-        return factory.create(this, this, extensions, parsed);
+    protected void checkStatus(Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkStatus(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+
+            if (substatus != SSH_FX_OK) {
+                throw new SftpException(substatus, msg);
+            }
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+
+    protected String checkHandle(Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkHandle(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+            throw new SftpException(substatus, msg);
+        } else if (type == SSH_FXP_HANDLE) {
+            String handle = ValidateUtils.checkNotNullAndNotEmpty(buffer.getString(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY);
+            return handle;
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+
+    protected Attributes checkAttributes(Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkAttributes(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+            throw new SftpException(substatus, msg);
+        } else if (type == SSH_FXP_ATTRS) {
+            return readAttributes(buffer);
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+
+    protected String checkOneName(Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkOneName(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+            throw new SftpException(substatus, msg);
+        } else if (type == SSH_FXP_NAME) {
+            int len = buffer.getInt();
+            if (len != 1) {
+                throw new SshException("SFTP error: received " + len + " names instead of 1");
+            }
+            String name = buffer.getString(), longName = null;
+            int version = getVersion();
+            if (version == SFTP_V3) {
+                longName = buffer.getString();
+            }
+            Attributes attrs = readAttributes(buffer);
+            if (log.isTraceEnabled()) {
+                log.trace("checkOneName(id={}) ({})[{}]: {}", Integer.valueOf(id), name, longName, attrs);
+            }
+            return name;
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+
+    protected Attributes readAttributes(Buffer buffer) throws IOException {
+        Attributes attrs = new Attributes();
+        int flags = buffer.getInt();
+        int version = getVersion();
+        if (version == SFTP_V3) {
+            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+                attrs.flags.add(Attribute.Size);
+                attrs.size = buffer.getLong();
+            }
+            if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
+                attrs.flags.add(Attribute.UidGid);
+                attrs.uid = buffer.getInt();
+                attrs.gid = buffer.getInt();
+            }
+            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                attrs.flags.add(Attribute.Perms);
+                attrs.perms = buffer.getInt();
+            }
+            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+                attrs.flags.add(Attribute.AcModTime);
+                attrs.atime = buffer.getInt();
+                attrs.mtime = buffer.getInt();
+            }
+        } else if (version >= SFTP_V4) {
+            attrs.type = buffer.getUByte();
+            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+                attrs.flags.add(Attribute.Size);
+                attrs.size = buffer.getLong();
+            }
+            if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+                attrs.flags.add(Attribute.OwnerGroup);
+                attrs.owner = buffer.getString();
+                attrs.group = buffer.getString();
+            }
+            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                attrs.flags.add(Attribute.Perms);
+                attrs.perms = buffer.getInt();
+            }
+            
+            // update the permissions according to the type
+            switch (attrs.type) {
+                case SSH_FILEXFER_TYPE_REGULAR:
+                    attrs.perms |= S_IFREG;
+                    break;
+                case SSH_FILEXFER_TYPE_DIRECTORY:
+                    attrs.perms |= S_IFDIR;
+                    break;
+                case SSH_FILEXFER_TYPE_SYMLINK:
+                    attrs.perms |= S_IFLNK;
+                    break;
+                default:    // do nothing
+            }
+
+            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+                attrs.flags.add(Attribute.AccessTime);
+                attrs.accessTime = readTime(buffer, flags);
+                attrs.atime = (int) attrs.accessTime.to(TimeUnit.SECONDS);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+                attrs.flags.add(Attribute.CreateTime);
+                attrs.createTime = readTime(buffer, flags);
+                attrs.ctime = (int) attrs.createTime.to(TimeUnit.SECONDS);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+                attrs.flags.add(Attribute.ModifyTime);
+                attrs.modifyTime = readTime(buffer, flags);
+                attrs.mtime = (int) attrs.modifyTime.to(TimeUnit.SECONDS);
+            }
+            // TODO: acl
+        } else {
+            throw new IllegalStateException("readAttributes - unsupported version: " + version);
+        }
+        return attrs;
+    }
+
+    protected FileTime readTime(Buffer buffer, int flags) {
+        long secs = buffer.getLong();
+        long millis = secs * 1000;
+        if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+            millis += buffer.getInt() / 1000000l;
+        }
+        return FileTime.from(millis, TimeUnit.MILLISECONDS);
+    }
+
+    protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
+        int version = getVersion();
+        if (version == SFTP_V3) {
+            int flags = 0;
+            for (Attribute a : attributes.flags) {
+                switch (a) {
+                    case Size:
+                        flags |= SSH_FILEXFER_ATTR_SIZE;
+                        break;
+                    case UidGid:
+                        flags |= SSH_FILEXFER_ATTR_UIDGID;
+                        break;
+                    case Perms:
+                        flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
+                        break;
+                    case AcModTime:
+                        flags |= SSH_FILEXFER_ATTR_ACMODTIME;
+                        break;
+                    default:    // do nothing
+                }
+            }
+            buffer.putInt(flags);
+            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+                buffer.putLong(attributes.size);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_UIDGID) != 0) {
+                buffer.putInt(attributes.uid);
+                buffer.putInt(attributes.gid);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                buffer.putInt(attributes.perms);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
+                buffer.putInt(attributes.atime);
+                buffer.putInt(attributes.mtime);
+            }
+        } else if (version >= SFTP_V4) {
+            int flags = 0;
+            for (Attribute a : attributes.flags) {
+                switch (a) {
+                    case Size:
+                        flags |= SSH_FILEXFER_ATTR_SIZE;
+                        break;
+                    case OwnerGroup:
+                        flags |= SSH_FILEXFER_ATTR_OWNERGROUP;
+                        break;
+                    case Perms:
+                        flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
+                        break;
+                    case AccessTime:
+                        flags |= SSH_FILEXFER_ATTR_ACCESSTIME;
+                        break;
+                    case ModifyTime:
+                        flags |= SSH_FILEXFER_ATTR_MODIFYTIME;
+                        break;
+                    case CreateTime:
+                        flags |= SSH_FILEXFER_ATTR_CREATETIME;
+                        break;
+                    default:    // do nothing
+                }
+            }
+            buffer.putInt(flags);
+            buffer.putByte((byte) attributes.type);
+            if ((flags & SSH_FILEXFER_ATTR_SIZE) != 0) {
+                buffer.putLong(attributes.size);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
+                buffer.putString(attributes.owner != null ? attributes.owner : "OWNER@", StandardCharsets.UTF_8);
+                buffer.putString(attributes.group != null ? attributes.group : "GROUP@", StandardCharsets.UTF_8);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
+                buffer.putInt(attributes.perms);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
+                buffer.putLong(attributes.accessTime.to(TimeUnit.SECONDS));
+                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                    long nanos = attributes.accessTime.to(TimeUnit.NANOSECONDS);
+                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                    buffer.putInt((int) nanos);
+                }
+                buffer.putInt(attributes.atime);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_CREATETIME) != 0) {
+                buffer.putLong(attributes.createTime.to(TimeUnit.SECONDS));
+                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                    long nanos = attributes.createTime.to(TimeUnit.NANOSECONDS);
+                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                    buffer.putInt((int) nanos);
+                }
+                buffer.putInt(attributes.atime);
+            }
+            if ((flags & SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
+                buffer.putLong(attributes.modifyTime.to(TimeUnit.SECONDS));
+                if ((flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES) != 0) {
+                    long nanos = attributes.modifyTime.to(TimeUnit.NANOSECONDS);
+                    nanos = nanos % TimeUnit.SECONDS.toNanos(1);
+                    buffer.putInt((int) nanos);
+                }
+                buffer.putInt(attributes.atime);
+            }
+            // TODO: acl
+        } else {
+            throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version);
+        }
+    }
+
+    @Override
+    public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(path);
+        int version = getVersion(), mode = 0;
+        if (version == SFTP_V3) {
+            for (OpenMode m : options) {
+                switch (m) {
+                    case Read:
+                        mode |= SSH_FXF_READ;
+                        break;
+                    case Write:
+                        mode |= SSH_FXF_WRITE;
+                        break;
+                    case Append:
+                        mode |= SSH_FXF_APPEND;
+                        break;
+                    case Create:
+                        mode |= SSH_FXF_CREAT;
+                        break;
+                    case Truncate:
+                        mode |= SSH_FXF_TRUNC;
+                        break;
+                    case Exclusive:
+                        mode |= SSH_FXF_EXCL;
+                        break;
+                    default:    // do nothing
+                }
+            }
+        } else {
+            if (version >= SFTP_V5) {
+                int access = 0;
+                if (options.contains(OpenMode.Read)) {
+                    access |= ACE4_READ_DATA | ACE4_READ_ATTRIBUTES;
+                }
+                if (options.contains(OpenMode.Write)) {
+                    access |= ACE4_WRITE_DATA | ACE4_WRITE_ATTRIBUTES;
+                }
+                if (options.contains(OpenMode.Append)) {
+                    access |= ACE4_APPEND_DATA;
+                }
+                buffer.putInt(access);
+            }
+
+            if (options.contains(OpenMode.Create) && options.contains(OpenMode.Exclusive)) {
+                mode |= SSH_FXF_CREATE_NEW;
+            } else if (options.contains(OpenMode.Create) && options.contains(OpenMode.Truncate)) {
+                mode |= SSH_FXF_CREATE_TRUNCATE;
+            } else if (options.contains(OpenMode.Create)) {
+                mode |= SSH_FXF_OPEN_OR_CREATE;
+            } else if (options.contains(OpenMode.Truncate)) {
+                mode |= SSH_FXF_TRUNCATE_EXISTING;
+            } else {
+                mode |= SSH_FXF_OPEN_EXISTING;
+            }
+        }
+        buffer.putInt(mode);
+        writeAttributes(buffer, new Attributes());
+        return new DefaultCloseableHandle(this, checkHandle(receive(send(SSH_FXP_OPEN, buffer))));
+    }
+
+    @Override
+    public void close(Handle handle) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(handle.id);
+        checkStatus(receive(send(SSH_FXP_CLOSE, buffer)));
+    }
+
+    @Override
+    public void remove(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(path);
+        checkStatus(receive(send(SSH_FXP_REMOVE, buffer)));
+    }
+
+    @Override
+    public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(oldPath);
+        buffer.putString(newPath);
+        
+        int numOptions = GenericUtils.size(options);
+        int version = getVersion();
+        if (version >= SFTP_V5) {
+            int opts = 0;
+            if (numOptions > 0) {
+                for (CopyMode opt : options) {
+                    switch (opt) {
+                        case Atomic:
+                            opts |= SSH_FXP_RENAME_ATOMIC;
+                            break;
+                        case Overwrite:
+                            opts |= SSH_FXP_RENAME_OVERWRITE;
+                            break;
+                        default:    // do nothing
+                    }
+                }
+            }
+            buffer.putInt(opts);
+        } else if (numOptions > 0) {
+            throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")"
+                                                  + " - copy options can not be used with this SFTP version: " + options);
+        }
+        checkStatus(receive(send(SSH_FXP_RENAME, buffer)));
+    }
+
+    @Override
+    public int read(Handle handle, long fileOffset, byte[] dst, int dstoff, int len) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(handle.id);
+        buffer.putLong(fileOffset);
+        buffer.putInt(len);
+        return checkData(receive(send(SSH_FXP_READ, buffer)), dstoff, dst);
+    }
+
+    protected int checkData(Buffer buffer, int dstoff, byte[] dst) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkData(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+
+            if (substatus == SSH_FX_EOF) {
+                return -1;
+            }
+
+            throw new SftpException(substatus, msg);
+        } else if (type == SSH_FXP_DATA) {
+            int len = buffer.getInt();
+            buffer.getRawBytes(dst, dstoff, len);
+            return len;
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+
+    @Override
+    public void write(Handle handle, long fileOffset, byte[] src, int srcoff, int len) throws IOException {
+        // do some bounds checking first
+        if ((fileOffset < 0) || (srcoff < 0) || (len < 0)) {
+            throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters "
+                                             + " are non-negative values: file-offset=" + fileOffset
+                                             + ", src-offset=" + srcoff + ", len=" + len);
+        }
+        if ((srcoff + len) > src.length) {
+            throw new IllegalArgumentException("write(" + handle + ")"
+                                             + " cannot read bytes " + srcoff + " to " + (srcoff + len)
+                                             + " when array is only of length " + src.length);
+        }
+
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + len + Long.SIZE /* some extra fields */);
+        buffer.putString(handle.id);
+        buffer.putLong(fileOffset);
+        buffer.putBytes(src, srcoff, len);
+        checkStatus(receive(send(SSH_FXP_WRITE, buffer)));
+    }
+
+    @Override
+    public void mkdir(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() +  Long.SIZE /* some extra fields */);
+        buffer.putString(path, StandardCharsets.UTF_8);
+        buffer.putInt(0);
+
+        int version = getVersion();
+        if (version != SFTP_V3) {
+            buffer.putByte((byte) 0);
+        }
+
+        checkStatus(receive(send(SSH_FXP_MKDIR, buffer)));
+    }
+
+    @Override
+    public void rmdir(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() +  Long.SIZE /* some extra fields */);
+        buffer.putString(path);
+        checkStatus(receive(send(SSH_FXP_RMDIR, buffer)));
+    }
+
+    @Override
+    public CloseableHandle openDir(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(path);
+        return new DefaultCloseableHandle(this, checkHandle(receive(send(SSH_FXP_OPENDIR, buffer))));
+    }
+
+    @Override
+    public DirEntry[] readDir(Handle handle) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(handle.id.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(handle.id);
+        return checkDir(receive(send(SSH_FXP_READDIR, buffer)));
+    }
+
+    protected DirEntry[] checkDir(Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isTraceEnabled()) {
+                log.trace("checkDir(id={}) - status: {} [{}] {}", Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+            if (substatus == SSH_FX_EOF) {
+                return null;
+            }
+            throw new SftpException(substatus, msg);
+        } else if (type == SSH_FXP_NAME) {
+            int len = buffer.getInt();
+            DirEntry[] entries = new DirEntry[len];
+            for (int i = 0; i < len; i++) {
+                String name = buffer.getString();
+                int version = getVersion();
+                String longName = (version == SFTP_V3) ? buffer.getString() : null;
+                Attributes attrs = readAttributes(buffer);
+                if (log.isTraceEnabled()) {
+                    log.trace("checkDir(id={})[{}] ({})[{}]: {}", Integer.valueOf(id), Integer.valueOf(i), name, longName, attrs);
+                }
+
+                entries[i] = new DirEntry(name, longName, attrs);
+            }
+            return entries;
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+
+    @Override
+    public String canonicalPath(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(path);
+        return checkOneName(receive(send(SSH_FXP_REALPATH, buffer)));
+    }
+
+    @Override
+    public Attributes stat(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(path);
+
+        int version = getVersion();
+        if (version >= SFTP_V4) {
+            buffer.putInt(SSH_FILEXFER_ATTR_ALL);
+        }
+
+        return checkAttributes(receive(send(SSH_FXP_STAT, buffer)));
+    }
+
+    @Override
+    public Attributes lstat(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(path);
+
+        int version = getVersion();
+        if (version >= SFTP_V4) {
+            buffer.putInt(SSH_FILEXFER_ATTR_ALL);
+        }
+
+        return checkAttributes(receive(send(SSH_FXP_LSTAT, buffer)));
+    }
+
+    @Override
+    public Attributes stat(Handle handle) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(handle.id);
+
+        int version = getVersion();
+        if (version >= SFTP_V4) {
+            buffer.putInt(SSH_FILEXFER_ATTR_ALL);
+        }
+
+        return checkAttributes(receive(send(SSH_FXP_FSTAT, buffer)));
+    }
+
+    @Override
+    public void setStat(String path, Attributes attributes) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(path);
+        writeAttributes(buffer, attributes);
+        checkStatus(receive(send(SSH_FXP_SETSTAT, buffer)));
+    }
+
+    @Override
+    public void setStat(Handle handle, Attributes attributes) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(handle.id);
+        writeAttributes(buffer, attributes);
+        checkStatus(receive(send(SSH_FXP_FSETSTAT, buffer)));
+    }
+
+    @Override
+    public String readLink(String path) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */);
+        buffer.putString(path);
+        return checkOneName(receive(send(SSH_FXP_READLINK, buffer)));
+    }
+
+    @Override
+    public void link(String linkPath, String targetPath, boolean symbolic) throws IOException {
+        Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */);
+        int version = getVersion();
+        if (version < SFTP_V6) {
+            if (!symbolic) {
+                throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version);
+            }
+            buffer.putString(targetPath);
+            buffer.putString(linkPath);
+            checkStatus(receive(send(SSH_FXP_SYMLINK, buffer)));
+        } else {
+            buffer.putString(targetPath);
+            buffer.putString(linkPath);
+            buffer.putBoolean(symbolic);
+            checkStatus(receive(send(SSH_FXP_LINK, buffer)));
+        }
+    }
+
+    @Override
+    public void lock(Handle handle, long offset, long length, int mask) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(handle.id);
+        buffer.putLong(offset);
+        buffer.putLong(length);
+        buffer.putInt(mask);
+        checkStatus(receive(send(SSH_FXP_BLOCK, buffer)));
+    }
+
+    @Override
+    public void unlock(Handle handle, long offset, long length) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        buffer.putString(handle.id);
+        buffer.putLong(offset);
+        buffer.putLong(length);
+        checkStatus(receive(send(SSH_FXP_UNBLOCK, buffer)));
+    }
+
+    @Override
+    public Iterable<DirEntry> readDir(final String path) throws IOException {
+        return new Iterable<DirEntry>() {
+            @Override
+            public Iterator<DirEntry> iterator() {
+                return new Iterator<DirEntry>() {
+                    private CloseableHandle handle;
+                    private DirEntry[] entries;
+                    private int index;
+
+                    {
+                        open();
+                        load();
+                    }
+
+                    @Override
+                    public boolean hasNext() {
+                        return (entries != null) && (index < entries.length);
+                    }
+
+                    @Override
+                    public DirEntry next() {
+                        DirEntry entry = entries[index++];
+                        if (index >= entries.length) {
+                            load();
+                        }
+                        return entry;
+                    }
+
+                    @SuppressWarnings("synthetic-access")
+                    private void open() {
+                        try {
+                            handle = openDir(path);
+                            if (log.isDebugEnabled()) {
+                                log.debug("readDir(" + path + ") handle=" + handle);
+                            }
+                        } catch (IOException e) {
+                            if (log.isDebugEnabled()) {
+                                log.debug("readDir(" + path + ") failed (" + e.getClass().getSimpleName() + ") to open dir: " + e.getMessage());
+                            }
+                            throw new RuntimeException(e);
+                        }
+                    }
+
+                    @SuppressWarnings("synthetic-access")
+                    private void load() {
+                        try {
+                            entries = readDir(handle);
+                            index = 0;
+                            if (entries == null) {
+                                handle.close();
+                            }
+                        } catch (IOException e) {
+                            entries = null;
+                            try {
+                                handle.close();
+                            } catch (IOException t) {
+                                if (log.isTraceEnabled()) {
+                                    log.trace(t.getClass().getSimpleName() + " while close handle=" + handle
+                                            + " due to " + e.getClass().getSimpleName() + " [" + e.getMessage() + "]"
+                                            + ": " + t.getMessage());
+                                }
+                            }
+                            throw new RuntimeException(e);
+                        }
+                    }
+
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException("readDir(" + path + ") Iterator#remove() N/A");
+                    }
+                };
+            }
+        };
+    }
+
+    @Override
+    public InputStream read(final String path, final int bufferSize, final Collection<OpenMode> mode) throws IOException {
+        if (bufferSize < MIN_READ_BUFFER_SIZE) {
+            throw new IllegalArgumentException("Insufficient read buffer size: " + bufferSize + ", min.=" + MIN_READ_BUFFER_SIZE);
+        }
+
+        return new InputStreamWithChannel() {
+            private byte[] bb = new byte[1];
+            private byte[] buffer = new byte[bufferSize];
+            private int index;
+            private int available;
+            private CloseableHandle handle = AbstractSftpClient.this.open(path, mode);
+            private long offset;
+
+            @Override
+            public boolean isOpen() {
+                return (handle != null) && handle.isOpen();
+            }
+
+            @Override
+            public int read() throws IOException {
+                int read = read(bb, 0, 1);
+                if (read > 0) {
+                    return bb[0];
+                }
+
+                return read;
+            }
+
+            @Override
+            public int read(byte[] b, int off, int len) throws IOException {
+                if (!isOpen()) {
+                    throw new IOException("read(" + path + ") stream closed");
+                }
+
+                int idx = off;
+                while (len > 0) {
+                    if (index >= available) {
+                        available = AbstractSftpClient.this.read(handle, offset, buffer, 0, buffer.length);
+                        if (available < 0) {
+                            if (idx == off) {
+                                return -1;
+                            } else {
+                                break;
+                            }
+                        }
+                        offset += available;
+                        index = 0;
+                    }
+                    if (index >= available) {
+                        break;
+                    }
+                    int nb = Math.min(len, available - index);
+                    System.arraycopy(buffer, index, b, idx, nb);
+                    index += nb;
+                    idx += nb;
+                    len -= nb;
+                }
+                return idx - off;
+            }
+
+            @Override
+            public void close() throws IOException {
+                if (isOpen()) {
+                    try {
+                        handle.close();
+                    } finally {
+                        handle = null;
+                    }
+                }
+            }
+        };
+    }
+
+    @Override
+    public OutputStream write(final String path, final int bufferSize, final Collection<OpenMode> mode) throws IOException {
+        if (bufferSize < MIN_WRITE_BUFFER_SIZE) {
+            throw new IllegalArgumentException("Insufficient write buffer size: " + bufferSize + ", min.=" + MIN_WRITE_BUFFER_SIZE);
+        }
+
+        return new OutputStreamWithChannel() {
+            private byte[] bb = new byte[1];
+            private byte[] buffer = new byte[bufferSize];
+            private int index;
+            private CloseableHandle handle = AbstractSftpClient.this.open(path, mode);
+            private long offset;
+
+            @Override
+            public boolean isOpen() {
+                return (handle != null) && handle.isOpen();
+            }
+
+            @Override
+            public void write(int b) throws IOException {
+                bb[0] = (byte) b;
+                write(bb, 0, 1);
+            }
+
+            @Override
+            public void write(byte[] b, int off, int len) throws IOException {
+                if (!isOpen()) {
+                    throw new IOException("write(" + path + ")[len=" + len + "] stream is closed");
+                }
+
+                do {
+                    int nb = Math.min(len, buffer.length - index);
+                    System.arraycopy(b, off, buffer, index, nb);
+                    index += nb;
+                    if (index == buffer.length) {
+                        flush();
+                    }
+                    off += nb;
+                    len -= nb;
+                } while (len > 0);
+            }
+
+            @Override
+            public void flush() throws IOException {
+                if (!isOpen()) {
+                    throw new IOException("flush(" + path + ") stream is closed");
+                }
+
+                AbstractSftpClient.this.write(handle, offset, buffer, 0, index);
+                offset += index;
+                index = 0;
+            }
+
+            @Override
+            public void close() throws IOException {
+                if (isOpen()) {
+                    try {
+                        try {
+                            if (index > 0) {
+                                flush();
+                            }
+                        } finally {
+                            handle.close();
+                        }
+                    } finally {
+                        handle = null;
+                    }
+                }
+            }
+        };
     }
 }