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 15:08:07 UTC

[2/2] mina-sshd git commit: [SSHD-521] Expose SFTP extensions access via interfaces for the client

[SSHD-521] Expose SFTP extensions access via interfaces for the client


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

Branch: refs/heads/master
Commit: f8a0e81292c14aec501be1d6fdabcc0919f0fd17
Parents: ec3f287
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Sun Jul 5 16:07:48 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Sun Jul 5 16:07:48 2015 +0300

----------------------------------------------------------------------
 .../sshd/client/subsystem/SubsystemClient.java  |   6 +-
 .../subsystem/sftp/AbstractSftpClient.java      |  42 ++++-
 .../subsystem/sftp/DefaultSftpClient.java       |  13 +-
 .../client/subsystem/sftp/RawSftpClient.java    |  44 +++++
 .../sshd/client/subsystem/sftp/SftpClient.java  |  19 ++
 .../client/subsystem/sftp/SftpFileSystem.java   |  33 ++++
 .../extensions/BuiltinSftpClientExtensions.java | 112 +++++++++++
 .../sftp/extensions/CopyFileExtension.java      |  36 ++++
 .../sftp/extensions/MD5FileExtension.java       |  39 ++++
 .../sftp/extensions/MD5HandleExtension.java     |  42 +++++
 .../sftp/extensions/SftpClientExtension.java    |  34 ++++
 .../extensions/SftpClientExtensionFactory.java  |  35 ++++
 .../impl/AbstractMD5HashExtension.java          |  74 ++++++++
 .../impl/AbstractSftpClientExtension.java       | 132 +++++++++++++
 .../extensions/impl/CopyFileExtensionImpl.java  |  53 ++++++
 .../extensions/impl/MD5FileExtensionImpl.java   |  42 +++++
 .../extensions/impl/MD5HandleExtensionImpl.java |  43 +++++
 .../subsystem/sftp/extensions/ParserUtils.java  |  24 +++
 .../server/subsystem/sftp/SftpSubsystem.java    |  54 ++++--
 .../sshd/client/subsystem/sftp/SftpTest.java    | 185 ++++++++++++++++++-
 .../BuiltinSftpClientExtensionsTest.java        |  84 +++++++++
 21 files changed, 1116 insertions(+), 30 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/SubsystemClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/SubsystemClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/SubsystemClient.java
index 7f86bd7..05d0a2e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/SubsystemClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/SubsystemClient.java
@@ -21,11 +21,15 @@ package org.apache.sshd.client.subsystem;
 
 import java.nio.channels.Channel;
 
+import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedResource;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public interface SubsystemClient extends NamedResource, Channel {
-    // marker interface for subsystems
+    /**
+     * @return The underlying {@link ClientSession} used
+     */
+    ClientSession getClientSession();
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/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 fc7a02b..1807cb4 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
@@ -25,15 +25,23 @@ import java.io.OutputStream;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
+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.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.logging.AbstractLoggingBean;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class AbstractSftpClient extends AbstractLoggingBean implements SftpClient {
+public abstract class AbstractSftpClient extends AbstractLoggingBean implements SftpClient, RawSftpClient {
+    private final AtomicReference<Map<String,Object>> parsedExtensionsHolder = new AtomicReference<Map<String,Object>>(null);
+
     protected AbstractSftpClient() {
         super();
     }
@@ -127,4 +135,36 @@ public abstract class AbstractSftpClient extends AbstractLoggingBean implements
     public void symLink(String linkPath, String targetPath) throws IOException {
         link(linkPath, targetPath, true);
     }
+
+    @Override
+    public <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType) {
+        Object  instance = getExtension(BuiltinSftpClientExtensions.fromType(extensionType));
+        if (instance == null) {
+            return null;
+        } else {
+            return extensionType.cast(instance);
+        }
+    }
+
+    @Override
+    public SftpClientExtension getExtension(String extensionName) {
+        return getExtension(BuiltinSftpClientExtensions.fromName(extensionName));
+    }
+    
+    protected SftpClientExtension getExtension(SftpClientExtensionFactory factory) {
+        if (factory == null) {
+            return null;
+        }
+
+        Map<String,byte[]> extensions = getServerExtensions();
+        Map<String,Object> parsed = parsedExtensionsHolder.get();
+        if (parsed == null) {
+            if ((parsed=ParserUtils.parse(extensions)) == null) {
+                parsed = Collections.<String,Object>emptyMap();
+            }
+            parsedExtensionsHolder.set(parsed);
+        }
+
+        return factory.create(this, this, extensions, parsed);
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/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 440ba88..9523514 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
@@ -167,6 +167,11 @@ public class DefaultSftpClient extends AbstractSftpClient {
     }
 
     @Override
+    public ClientSession getClientSession() {
+        return clientSession;
+    }
+
+    @Override
     public Map<String, byte[]> getServerExtensions() {
         return exposedExtensions;
     }
@@ -255,18 +260,20 @@ public class DefaultSftpClient extends AbstractSftpClient {
         }
     }
 
-    protected int send(int cmd, Buffer buffer) throws IOException {
+    @Override
+    public int send(int cmd, Buffer buffer) throws IOException {
         int id = cmdId.incrementAndGet();
         OutputStream dos = channel.getInvertedIn();
         BufferUtils.writeInt(dos, 1 /* cmd */ + (Integer.SIZE / Byte.SIZE) /* id */ + buffer.available(), workBuf);
-        dos.write(cmd);
+        dos.write(cmd & 0xFF);
         BufferUtils.writeInt(dos, id, workBuf);
         dos.write(buffer.array(), buffer.rpos(), buffer.available());
         dos.flush();
         return id;
     }
 
-    protected Buffer receive(int id) throws IOException {
+    @Override
+    public Buffer receive(int id) throws IOException {
         synchronized (messages) {
             while (true) {
                 if (closing) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/RawSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/RawSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/RawSftpClient.java
new file mode 100644
index 0000000..ab80812
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/RawSftpClient.java
@@ -0,0 +1,44 @@
+/*
+ * 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.io.IOException;
+
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface RawSftpClient {
+    /**
+     * @param cmd Command to send - <B>Note:</B> only lower 8-bits are used
+     * @param buffer The {@link Buffer} containing the command data
+     * @return The assigned request id
+     * @throws IOException if failed to send command
+     */
+    int send(int cmd, Buffer buffer) throws IOException;
+    
+    /**
+     * @param id The expected request id
+     * @return The received response {@link Buffer} containing the request id
+     * @throws IOException If connection closed or interrupted
+     */
+    Buffer receive(int id) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index d7ded06..481ef3a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -36,6 +36,7 @@ import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.client.subsystem.SubsystemClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 
@@ -313,4 +314,22 @@ public interface SftpClient extends SubsystemClient {
     OutputStream write(String path, Collection<OpenMode> mode) throws IOException;
     OutputStream write(String path, int bufferSize, Collection<OpenMode> mode) throws IOException;
 
+    /**
+     * @param extensionType The extension type
+     * @return The extension instance - <B>Note:</B> it is up to the caller
+     * to invoke {@link SftpClientExtension#isSupported()} - {@code null} if
+     * this extension type is not implemented by the client
+     * @see #getServerExtensions()
+     */
+    <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType);
+
+    /**
+     * @param extensionName The extension name
+     * @return The {@link SftpClientExtension} name - ignored if {@code null}/empty
+     * @return The extension instance - <B>Note:</B> it is up to the caller
+     * to invoke {@link SftpClientExtension#isSupported()} - {@code null} if
+     * this extension type is not implemented by the client
+     * @see #getServerExtensions()
+     */
+    SftpClientExtension getExtension(String extensionName);
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/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 f2abfa7..050716f 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
@@ -21,6 +21,7 @@ package org.apache.sshd.client.subsystem.sftp;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.StreamCorruptedException;
 import java.nio.file.FileStore;
 import java.nio.file.FileSystemException;
 import java.nio.file.attribute.GroupPrincipal;
@@ -41,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.buffer.Buffer;
 
 public class SftpFileSystem extends BaseFileSystem<SftpPath> {
     public static final String POOL_SIZE_PROP = "sftp-fs-pool-size";
@@ -192,6 +194,11 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
         }
 
         @Override
+        public ClientSession getClientSession() {
+            return delegate.getClientSession();
+        }
+
+        @Override
         public Map<String, byte[]> getServerExtensions() {
             return delegate.getServerExtensions();
         }
@@ -446,6 +453,32 @@ public class SftpFileSystem extends BaseFileSystem<SftpPath> {
             }
             delegate.unlock(handle, offset, length);
         }
+
+        @Override
+        public int send(int cmd, Buffer buffer) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("send(cmd=" + cmd + ") client is closed");
+            }
+            
+            if (delegate instanceof RawSftpClient) {
+                return ((RawSftpClient) delegate).send(cmd, buffer);
+            } else {
+                throw new StreamCorruptedException("send(cmd=" + cmd + ") delegate is not a " + RawSftpClient.class.getSimpleName());
+            }
+        }
+
+        @Override
+        public Buffer receive(int id) throws IOException {
+            if (!isOpen()) {
+                throw new IOException("receive(id=" + id + ") client is closed");
+            }
+            
+            if (delegate instanceof RawSftpClient) {
+                return ((RawSftpClient) delegate).receive(id);
+            } else {
+                throw new StreamCorruptedException("receive(id=" + id + ") delegate is not a " + RawSftpClient.class.getSimpleName());
+            }
+        }
     }
 
     protected static class DefaultUserPrincipalLookupService extends UserPrincipalLookupService {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
new file mode 100644
index 0000000..059966e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensions.java
@@ -0,0 +1,112 @@
+/*
+ * 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.extensions;
+
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.impl.CopyFileExtensionImpl;
+import org.apache.sshd.client.subsystem.sftp.extensions.impl.MD5FileExtensionImpl;
+import org.apache.sshd.client.subsystem.sftp.extensions.impl.MD5HandleExtensionImpl;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public enum BuiltinSftpClientExtensions implements SftpClientExtensionFactory {
+    COPY_FILE(SftpConstants.EXT_COPYFILE, CopyFileExtension.class) {
+            @Override   // co-variant return
+            public CopyFileExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new CopyFileExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
+            }
+        },
+    MD5_FILE(SftpConstants.EXT_MD5HASH, MD5FileExtension.class) {
+            @Override   // co-variant return
+            public MD5FileExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new MD5FileExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
+            }
+        },
+    MD5_HANDLE(SftpConstants.EXT_MD5HASH_HANDLE, MD5HandleExtension.class) {
+            @Override   // co-variant return
+            public MD5HandleExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new MD5HandleExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
+            }
+        };
+
+    private final String name;
+
+    @Override
+    public final String getName() {
+        return name;
+    }
+
+    private final Class<? extends SftpClientExtension> type;
+    public final Class<? extends SftpClientExtension> getType() {
+        return type;
+    }
+
+    private BuiltinSftpClientExtensions(String name, Class<? extends SftpClientExtension> type) {
+        this.name = name;
+        this.type = type;
+    }
+    
+    @Override
+    public SftpClientExtension create(SftpClient client, RawSftpClient raw) {
+        Map<String,byte[]> extensions = client.getServerExtensions();
+        return create(client, raw, extensions, ParserUtils.parse(extensions));
+    }
+
+    public static final Set<BuiltinSftpClientExtensions> VALUES =
+            Collections.unmodifiableSet(EnumSet.allOf(BuiltinSftpClientExtensions.class));
+
+    public static final BuiltinSftpClientExtensions fromName(String n) {
+        return NamedResource.Utils.findByName(n, String.CASE_INSENSITIVE_ORDER, VALUES);
+    }
+    
+    public static final BuiltinSftpClientExtensions fromInstance(Object o) {
+        return fromType((o == null) ? null : o.getClass());
+    }
+
+    public static final BuiltinSftpClientExtensions fromType(Class<?> type) {
+        if ((type == null) || (!SftpClientExtension.class.isAssignableFrom(type))) {
+            return null;
+        }
+
+        // the base class is assignable to everybody so we cannot distinguish between the enum(s)
+        if (SftpClientExtension.class == type) {
+            return null;
+        }
+
+        for (BuiltinSftpClientExtensions v : VALUES) {
+            Class<?> vt = v.getType();
+            if (vt.isAssignableFrom(type)) {
+                return v;
+            }
+        }
+        
+        return null;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyFileExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyFileExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyFileExtension.java
new file mode 100644
index 0000000..b78228f
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CopyFileExtension.java
@@ -0,0 +1,36 @@
+/*
+ * 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.extensions;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-6">copy-file</A> extension
+ */
+public interface CopyFileExtension extends SftpClientExtension {
+    /**
+     * @param src The (<U>remote</U>) file source path
+     * @param dst The (<U>remote</U>) file destination path
+     * @param overwriteDestination If {@code true} then OK to override destination if exists
+     * @throws IOException If failed to execute the command or extension not supported
+     */
+    void copyFile(String src, String dst, boolean overwriteDestination) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5FileExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5FileExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5FileExtension.java
new file mode 100644
index 0000000..c22b003
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5FileExtension.java
@@ -0,0 +1,39 @@
+/*
+ * 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.extensions;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface MD5FileExtension extends SftpClientExtension {
+    /**
+     * @param path The (remote) path
+     * @param offset The offset to start calculating the hash
+     * @param length The number of data bytes to calculate the hash on - if
+     * greater than available, then up to whatever is available
+     * @param quickHash A quick-hash of the 1st 2048 bytes - ignored if {@code null}/empty
+     * @return The hash value if the quick hash matches (or {@code null}/empty), or
+     * {@code null}/empty if the quick hash is provided and it does not match
+     * @throws IOException If failed to calculate the hash
+     */
+    byte[] getHash(String path, long offset, long length, byte[] quickHash) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5HandleExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5HandleExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5HandleExtension.java
new file mode 100644
index 0000000..8e5fcf9
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/MD5HandleExtension.java
@@ -0,0 +1,42 @@
+/*
+ * 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.extensions;
+
+import java.io.IOException;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface MD5HandleExtension extends SftpClientExtension {
+    /**
+     * @param handle The (remote) file {@link SftpClient.Handle}
+     * @param offset The offset to start calculating the hash
+     * @param length The number of data bytes to calculate the hash on - if
+     * greater than available, then up to whatever is available
+     * @param quickHash A quick-hash of the 1st 2048 bytes - ignored if {@code null}/empty
+     * @return The hash value if the quick hash matches (or {@code null}/empty), or
+     * {@code null}/empty if the quick hash is provided and it does not match
+     * @throws IOException If failed to calculate the hash
+     */
+    byte[] getHash(SftpClient.Handle handle, long offset, long length, byte[] quickHash) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtension.java
new file mode 100644
index 0000000..b9eb5d7
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtension.java
@@ -0,0 +1,34 @@
+/*
+ * 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.extensions;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.OptionalFeature;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpClientExtension extends NamedResource, OptionalFeature {
+    /**
+     * @return The {@link SftpClient} used to issue the extended command
+     */
+    SftpClient getClient();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtensionFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtensionFactory.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtensionFactory.java
new file mode 100644
index 0000000..3d07aa0
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/SftpClientExtensionFactory.java
@@ -0,0 +1,35 @@
+/*
+ * 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.extensions;
+
+import java.util.Map;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.common.NamedResource;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SftpClientExtensionFactory extends NamedResource {
+    // TODO make this a default method for JDK-8
+    SftpClientExtension create(SftpClient client, RawSftpClient raw);
+    SftpClientExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
new file mode 100644
index 0000000..12c51ca
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractMD5HashExtension.java
@@ -0,0 +1,74 @@
+/*
+ * 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.extensions.impl;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractMD5HashExtension extends AbstractSftpClientExtension {
+    protected AbstractMD5HashExtension(String name, SftpClient client, RawSftpClient raw, Collection<String> extras) {
+        super(name, client, raw, extras);
+    }
+
+    protected byte[] doGetHash(String target, long offset, long length, byte[] quickHash) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        String opcode = getName();
+        buffer.putString(opcode);
+        buffer.putString(target);
+        buffer.putLong(offset);
+        buffer.putLong(length);
+        buffer.putBytes((quickHash == null) ? GenericUtils.EMPTY_BYTE_ARRAY : quickHash);
+        
+        if (log.isDebugEnabled()) {
+            log.debug("doGetHash({})[{}] - offset={}, length={}, quick-hash={}",
+                      opcode, target, Long.valueOf(offset), Long.valueOf(length), BufferUtils.printHex(':', quickHash));
+        }
+
+        buffer = checkExtendedReplyBuffer(receive(sendExtendedCommand(buffer)));
+        if (buffer == null) {
+            throw new StreamCorruptedException("Missing extended reply data");
+        }
+        
+        String targetType = buffer.getString();
+        if (String.CASE_INSENSITIVE_ORDER.compare(targetType, opcode) != 0) {
+            throw new StreamCorruptedException("Mismatched reply target type: expected=" + opcode + ", actual=" + targetType);
+        }
+
+        byte[] hashValue = buffer.getBytes();
+        if (log.isDebugEnabled()) {
+            log.debug("doGetHash({})[{}] - offset={}, length={}, quick-hash={} - result={}",
+                    opcode, target, Long.valueOf(offset), Long.valueOf(length),
+                    BufferUtils.printHex(':', quickHash), BufferUtils.printHex(':', hashValue));
+        }
+        
+        return hashValue;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
new file mode 100644
index 0000000..14eef9c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractSftpClientExtension.java
@@ -0,0 +1,132 @@
+/*
+ * 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.extensions.impl;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+
+import org.apache.sshd.client.SftpException;
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+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.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractSftpClientExtension extends AbstractLoggingBean implements SftpClientExtension, RawSftpClient {
+    private final String name;
+    private final SftpClient client;
+    private final RawSftpClient raw;
+    private final boolean supported;
+    
+    protected AbstractSftpClientExtension(String name, SftpClient client, RawSftpClient raw, Collection<String> extras) {
+        this(name, client, raw, GenericUtils.isEmpty(extras) ? false : extras.contains(name));
+    }
+
+    protected AbstractSftpClientExtension(String name, SftpClient client, RawSftpClient raw, boolean supported) {
+        this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No extension name", GenericUtils.EMPTY_OBJECT_ARRAY);
+        this.client = ValidateUtils.checkNotNull(client, "No client instance", GenericUtils.EMPTY_OBJECT_ARRAY);
+        this.raw = ValidateUtils.checkNotNull(raw, "No raw access", GenericUtils.EMPTY_OBJECT_ARRAY);
+        this.supported = supported;
+    }
+
+    @Override
+    public final String getName() {
+        return name;
+    }
+
+    @Override
+    public final SftpClient getClient() {
+        return client;
+    }
+
+    protected void sendAndCheckExtendedCommandStatus(Buffer buffer) throws IOException {
+        int reqId = sendExtendedCommand(buffer);
+        if (log.isDebugEnabled()) {
+            log.debug("sendAndCheckExtendedCommandStatus(" + getName() + ") id=" + reqId);
+        }
+        checkStatus(receive(reqId));
+    }
+
+    protected int sendExtendedCommand(Buffer buffer) throws IOException {
+        return send(SftpConstants.SSH_FXP_EXTENDED, buffer);
+    }
+
+    @Override
+    public int send(int cmd, Buffer buffer) throws IOException {
+        return raw.send(cmd, buffer);
+    }
+
+    @Override
+    public Buffer receive(int id) throws IOException {
+        return raw.receive(id);
+    }
+
+    @Override
+    public final boolean isSupported() {
+        return supported;
+    }
+
+    protected void checkStatus(Buffer buffer) throws IOException {
+        if (checkExtendedReplyBuffer(buffer) != null) {
+            throw new StreamCorruptedException("Unexpected extended reply received");
+        }
+    }
+
+    /**
+     * @param buffer The {@link Buffer} to check
+     * @return The {@link Buffer} if this is an {@link SftpConstants#SSH_FXP_EXTENDED_REPLY},
+     * or {@code null} if this is a {@link SftpConstants#SSH_FXP_STATUS} carrying
+     * an {@link SftpConstants#SSH_FX_OK} result
+     * @throws IOException If a non-{@link SftpConstants#SSH_FX_OK} result or
+     * not a {@link SftpConstants#SSH_FXP_EXTENDED_REPLY} buffer
+     */
+    protected Buffer checkExtendedReplyBuffer(Buffer buffer) throws IOException {
+        int length = buffer.getInt();
+        int type = buffer.getUByte();
+        int id = buffer.getInt();
+        if (type == SftpConstants.SSH_FXP_STATUS) {
+            int substatus = buffer.getInt();
+            String msg = buffer.getString();
+            String lang = buffer.getString();
+            if (log.isDebugEnabled()) {
+                log.debug("checkStatus({}}[id={}] - status: {} [{}] {}",
+                          getName(), Integer.valueOf(id), Integer.valueOf(substatus), lang, msg);
+            }
+
+            if (substatus != SftpConstants.SSH_FX_OK) {
+                throw new SftpException(substatus, msg);
+            }
+            
+            return null;
+        } else if (type == SftpConstants.SSH_FXP_EXTENDED_REPLY) {
+            return buffer;
+        } else {
+            throw new SshException("Unexpected SFTP packet received: type=" + type + ", id=" + id + ", length=" + length);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImpl.java
new file mode 100644
index 0000000..7186df6
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CopyFileExtensionImpl.java
@@ -0,0 +1,53 @@
+/*
+ * 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.extensions.impl;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.CopyFileExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class CopyFileExtensionImpl extends AbstractSftpClientExtension implements CopyFileExtension {
+    public CopyFileExtensionImpl(SftpClient client, RawSftpClient raw, Collection<String> extra) {
+        super(SftpConstants.EXT_COPYFILE, client, raw, extra);
+    }
+
+    @Override
+    public void copyFile(String src, String dst, boolean overwriteDestination) throws IOException {
+        Buffer buffer = new ByteArrayBuffer((Integer.SIZE / Byte.SIZE) + GenericUtils.length(getName())
+                                          + (Integer.SIZE / Byte.SIZE) + GenericUtils.length(src)
+                                          + (Integer.SIZE / Byte.SIZE) + GenericUtils.length(dst)
+                                          + 1 /* override destination */);
+        buffer.putString(getName());
+        buffer.putString(src);
+        buffer.putString(dst);
+        buffer.putBoolean(overwriteDestination);
+        sendAndCheckExtendedCommandStatus(buffer);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5FileExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5FileExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5FileExtensionImpl.java
new file mode 100644
index 0000000..b0340be
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5FileExtensionImpl.java
@@ -0,0 +1,42 @@
+/*
+ * 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.extensions.impl;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.MD5FileExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class MD5FileExtensionImpl extends AbstractMD5HashExtension implements MD5FileExtension {
+    public MD5FileExtensionImpl(SftpClient client, RawSftpClient raw, Collection<String> extra) {
+        super(SftpConstants.EXT_MD5HASH, client, raw, extra);
+    }
+
+    @Override
+    public byte[] getHash(String path, long offset, long length, byte[] quickHash) throws IOException {
+        return doGetHash(path, offset, length, quickHash);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5HandleExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5HandleExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5HandleExtensionImpl.java
new file mode 100644
index 0000000..e6ab476
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/MD5HandleExtensionImpl.java
@@ -0,0 +1,43 @@
+/*
+ * 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.extensions.impl;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.extensions.MD5HandleExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class MD5HandleExtensionImpl extends AbstractMD5HashExtension implements MD5HandleExtension {
+    public MD5HandleExtensionImpl(SftpClient client, RawSftpClient raw, Collection<String> extra) {
+        super(SftpConstants.EXT_MD5HASH_HANDLE, client, raw, extra);
+    }
+
+    @Override
+    public byte[] getHash(SftpClient.Handle handle, long offset, long length, byte[] quickHash) throws IOException {
+        return doGetHash(handle.id, offset, length, quickHash);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
index d64bebc..f883c89 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
@@ -27,7 +27,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
+import org.apache.sshd.common.subsystem.sftp.extensions.Supported2Parser.Supported2;
+import org.apache.sshd.common.subsystem.sftp.extensions.SupportedParser.Supported;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 
@@ -118,6 +121,27 @@ public final class ParserUtils {
         }
     }
 
+    public static final Set<String> supportedExtensions(Map<String,?> parsed) {
+        if (GenericUtils.isEmpty(parsed)) {
+            return Collections.emptySet();
+        }
+        
+        Supported sup = (Supported) parsed.get(SupportedParser.INSTANCE.getName());
+        Collection<String> extra = (sup == null) ? null : sup.extensionNames;
+        Supported2 sup2 = (Supported2) parsed.get(Supported2Parser.INSTANCE.getName());
+        Collection<String> extra2 = (sup2 == null) ? null : sup2.extensionNames;
+        if (GenericUtils.isEmpty(extra)) {
+            return GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, extra2);
+        } else if (GenericUtils.isEmpty(extra2)) {
+            return GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, extra);
+        }
+        
+        Set<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+        result.addAll(extra);
+        result.addAll(extra2);
+        return result;
+    }
+
     /**
      * @param extensions The received extensions in encoded form
      * @return A {@link Map} of all the successfully decoded extensions

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/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 740ceb9..3371904 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
@@ -690,9 +690,12 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             }
             
             Digest digest = BuiltinDigests.md5.create();
+            digest.init();
+
             byte[] workBuf = new byte[(int) Math.min(effectiveLength, SftpConstants.MD5_QUICK_HASH_SIZE)];
             ByteBuffer bb = ByteBuffer.wrap(workBuf);
             boolean hashMatches = false;
+            byte[] hashValue = null;
 
             try(FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
                 channel.position(startOffset);
@@ -706,33 +709,54 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                  *      this case.
                  */
                 if (GenericUtils.length(quickCheckHash) <= 0) {
-                    // TODO consider allowing it - e.g., if the requested effective length is <= than some (configurable) threshold
-                    throw new UnsupportedOperationException(targetType + " w/o q quick check hash is not supported");
+                    // TODO consider limiting it - e.g., if the requested effective length is <= than some (configurable) threshold
+                    hashMatches = true;
+                } else {
+                    int readLen = channel.read(bb);
+                    effectiveLength -= readLen;
+                    digest.update(workBuf, 0, readLen);
+    
+                    hashValue = digest.digest();
+                    hashMatches = Arrays.equals(quickCheckHash, hashValue);
+                    if (hashMatches) {
+                        /*
+                         * Need to re-initialize the digester due to the Javadoc:
+                         * 
+                         *      "The digest method can be called once for a given number
+                         *       of updates. After digest has been called, the MessageDigest
+                         *       object is reset to its initialized state." 
+                         */
+                        if (effectiveLength > 0L) {
+                            digest = BuiltinDigests.md5.create();
+                            digest.init();
+                            digest.update(workBuf, 0, readLen);
+                            hashValue = null;   // start again
+                        }
+                    } else {
+                        if (log.isTraceEnabled()) {
+                            log.trace("doMD5Hash({})[{}] offset={}, length={} - quick-hash mismatched expected={}, actual={}",
+                                      targetType, target, Long.valueOf(startOffset), Long.valueOf(length),
+                                      BufferUtils.printHex(':', quickCheckHash), BufferUtils.printHex(':', hashValue));
+                        }
+                    }
                 }
-                
-                int readLen = channel.read(bb);
-                effectiveLength -= readLen;
-                digest.update(workBuf, 0, readLen);
 
-                byte[] hashValue = digest.digest();
-                hashMatches = Arrays.equals(quickCheckHash, hashValue);
                 if (hashMatches) {
                     while(effectiveLength > 0L) {
                         bb.clear();
-                        readLen = channel.read(bb); 
+                        int readLen = channel.read(bb); 
                         effectiveLength -= readLen;
                         digest.update(workBuf, 0, readLen);
                     }
-                } else {
-                    if (log.isTraceEnabled()) {
-                        log.trace("doMD5Hash({})[{}] offset={}, length={} - quick-hash mismatched expected={}, actual={}",
-                                  targetType, target, Long.valueOf(startOffset), Long.valueOf(length),
-                                  BufferUtils.printHex(':', quickCheckHash), BufferUtils.printHex(':', hashValue));
+                    
+                    if (hashValue == null) {    // check if did any more iterations after the quick hash
+                        hashValue = digest.digest();
                     }
+                } else {
+                    hashValue = GenericUtils.EMPTY_BYTE_ARRAY;
                 }
             }
 
-            byte[] hashValue = hashMatches ? digest.digest() : GenericUtils.EMPTY_BYTE_ARRAY;
             if (log.isDebugEnabled()) {
                 log.debug("doMD5Hash({})[{}] offset={}, length={}, quick-hash={} - match={}, hash={}",
                           targetType, target, Long.valueOf(startOffset), Long.valueOf(length), BufferUtils.printHex(':', quickCheckHash),

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/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 ad56705..b1e12db 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
@@ -30,8 +30,10 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
@@ -43,9 +45,18 @@ import java.util.Set;
 import java.util.Vector;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.sshd.client.SftpException;
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
+import org.apache.sshd.client.subsystem.sftp.extensions.CopyFileExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.MD5FileExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.MD5HandleExtension;
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
 import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.root.RootedFileSystemProvider;
 import org.apache.sshd.common.session.Session;
@@ -55,6 +66,7 @@ import org.apache.sshd.common.subsystem.sftp.extensions.Supported2Parser.Support
 import org.apache.sshd.common.subsystem.sftp.extensions.SupportedParser.Supported;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.server.Command;
@@ -78,7 +90,6 @@ import org.junit.runners.MethodSorters;
 
 import com.jcraft.jsch.ChannelSftp;
 import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.SftpException;
 
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class SftpTest extends BaseTestSupport {
@@ -520,7 +531,7 @@ public class SftpTest extends BaseTestSupport {
                 real = c.realpath(path + "/foobar");
                 System.out.println(real);
                 fail("Expected SftpException");
-            } catch (SftpException e) {
+            } catch (com.jcraft.jsch.SftpException e) {
                 // ok
             }
         } finally {
@@ -530,6 +541,14 @@ public class SftpTest extends BaseTestSupport {
 
     @Test
     public void testRename() 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 = assertHierarchyTargetFolderExists(lclSftp.resolve("client"));
+
         try(SshClient client = SshClient.setUpDefaultClient()) {
             client.start();
             
@@ -537,14 +556,6 @@ 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 = assertHierarchyTargetFolderExists(lclSftp.resolve("client"));
-        
                 try(SftpClient sftp = session.createSftpClient()) {
                     Path file1 = clientFolder.resolve(getCurrentTestName() + "-1.txt");
                     String file1Path = Utils.resolveRelativeRemotePath(parentPath, file1);
@@ -583,6 +594,152 @@ public class SftpTest extends BaseTestSupport {
     }
 
     @Test
+    public void testCopyFileExtension() throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(lclSftp);
+        Files.createDirectories(lclSftp);
+
+        byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+        Path srcFile = lclSftp.resolve("src.txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+        Path dstFile = lclSftp.resolve("dst.txt");
+        String dstPath = Utils.resolveRelativeRemotePath(parentPath, dstFile);
+        
+        LinkOption[] options = IoUtils.getLinkOptions(false);
+        assertFalse("Destination file unexpectedly exists", Files.exists(dstFile, options));
+
+        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()) {
+                    CopyFileExtension ext = assertExtensionCreated(sftp, CopyFileExtension.class);
+                    ext.copyFile(srcPath, dstPath, false);
+                    assertTrue("Source file not preserved", Files.exists(srcFile, options));
+                    assertTrue("Destination file not created", Files.exists(dstFile, options));
+                    
+                    byte[] actual = Files.readAllBytes(dstFile);
+                    assertArrayEquals("Mismatched copied data", data, actual);
+                    
+                    try {
+                        ext.copyFile(srcPath, dstPath, false);
+                        fail("Unexpected success to overwrite existing destination: " + dstFile);
+                    } catch(IOException e) {
+                        assertTrue("Not an SftpException", e instanceof SftpException);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testMD5HashExtensionOnSmallFile() throws Exception {
+        testMD5HashExtension((getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testMD5HashExtensionOnLargeFile() throws Exception {
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName() + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8);
+        final int TEST_SIZE = Byte.SIZE * SftpConstants.MD5_QUICK_HASH_SIZE; 
+        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(TEST_SIZE + seed.length)) {
+            while (baos.size() < TEST_SIZE) {
+                baos.write(seed);
+            }
+
+            testMD5HashExtension(baos.toByteArray());
+        }
+    }
+
+    private void testMD5HashExtension(byte[] data) throws Exception {
+        Path targetPath = detectTargetFolder().toPath();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Utils.deleteRecursive(lclSftp);
+        Files.createDirectories(lclSftp);
+
+        Digest digest = BuiltinDigests.md5.create();
+        digest.init();
+        digest.update(data);
+
+        byte[] expectedHash = digest.digest();
+        byte[] quickHash = expectedHash;
+        if (data.length > SftpConstants.MD5_QUICK_HASH_SIZE) {
+            byte[] quickData = new byte[SftpConstants.MD5_QUICK_HASH_SIZE];
+            System.arraycopy(data, 0, quickData, 0, quickData.length);
+            digest = BuiltinDigests.md5.create();
+            digest.init();
+            digest.update(quickData);
+            quickHash = digest.digest();
+        }
+
+        Path srcFile = lclSftp.resolve("src.txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+
+        Path parentPath = targetPath.getParent();
+        String srcPath = Utils.resolveRelativeRemotePath(parentPath, srcFile);
+        String srcFolder = Utils.resolveRelativeRemotePath(parentPath, srcFile.getParent());
+
+        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()) {
+                    MD5FileExtension file = assertExtensionCreated(sftp, MD5FileExtension.class);
+                    try {
+                        byte[] actual = file.getHash(srcFolder, 0L, 0L, quickHash);
+                        fail("Unexpected file success on folder=" + srcFolder + ": " + BufferUtils.printHex(':', actual));
+                    } catch(IOException e) {    // expected - not allowed to hash a folder
+                        assertTrue("Not an SftpException", e instanceof SftpException);
+                    }
+
+                    MD5HandleExtension hndl = assertExtensionCreated(sftp, MD5HandleExtension.class);
+                    try(CloseableHandle dirHandle = sftp.openDir(srcFolder)) {
+                        try {
+                            byte[] actual = hndl.getHash(dirHandle, 0L, 0L, quickHash);
+                            fail("Unexpected handle success on folder=" + srcFolder + ": " + BufferUtils.printHex(':', actual));
+                        } catch(IOException e) {    // expected - not allowed to hash a folder
+                            assertTrue("Not an SftpException", e instanceof SftpException);
+                        }
+                    }
+
+                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Read)) {
+                        for (byte[] qh : new byte[][] { GenericUtils.EMPTY_BYTE_ARRAY, quickHash }) {
+                            for (boolean useFile : new boolean[] { true, false }) {
+                                byte[] actualHash = useFile ? file.getHash(srcPath, 0L, 0L, qh) : hndl.getHash(fileHandle, 0L, 0L, qh);
+                                String type = useFile ? file.getClass().getSimpleName() : hndl.getClass().getSimpleName();
+                                if (!Arrays.equals(expectedHash, actualHash)) {
+                                    fail("Mismatched hash for quick=" + BufferUtils.printHex(':', qh) + " using " + type
+                                       + ": expected=" + BufferUtils.printHex(':', expectedHash)
+                                       + ", actual=" + BufferUtils.printHex(':', actualHash));
+                                }
+                            }
+                        }
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+    private static <E extends SftpClientExtension> E assertExtensionCreated(SftpClient sftp, Class<E> type) {
+        E instance = sftp.getExtension(type);
+        assertNotNull("Extension not created: " + type.getSimpleName(), instance);
+        assertTrue("Extension not supported: " + instance.getName(), instance.isSupported());
+        return instance;
+    }
+
+    @Test
     public void testServerExtensionsDeclarations() throws Exception {
         try(SshClient client = SshClient.setUpDefaultClient()) {
             client.start();
@@ -612,6 +769,14 @@ public class SftpTest extends BaseTestSupport {
                             assertSupportedExtensions(extName, ((Supported2) extValue).extensionNames);
                         }
                     }
+                    
+                    for (BuiltinSftpClientExtensions type : BuiltinSftpClientExtensions.VALUES) {
+                        String extensionName = type.getName();
+                        SftpClientExtension instance = sftp.getExtension(extensionName);
+                        assertNotNull("Extension not implemented:" + extensionName, instance);
+                        assertEquals("Mismatched instance name", extensionName, instance.getName());
+                        assertTrue("Extension not supported: " + extensionName, instance.isSupported());
+                    }
                 }
             } finally {
                 client.stop();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f8a0e812/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java
new file mode 100644
index 0000000..11be24f
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/BuiltinSftpClientExtensionsTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.extensions;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.util.BaseTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class BuiltinSftpClientExtensionsTest extends BaseTestSupport {
+    public BuiltinSftpClientExtensionsTest() {
+        super();
+    }
+
+    @Test
+    public void testFromName() {
+        for (String name : new String[] { null, "", getCurrentTestName() }) {
+            assertNull("Unexpected result for name='" + name + "'", BuiltinSftpClientExtensions.fromName(name));
+        }
+
+        for (BuiltinSftpClientExtensions expected : BuiltinSftpClientExtensions.VALUES) {
+            String name = expected.getName();
+            for (int index = 0; index < name.length(); index++) {
+                BuiltinSftpClientExtensions actual = BuiltinSftpClientExtensions.fromName(name);
+                assertSame(name, expected, actual);
+                name = shuffleCase(name);
+            }
+        }
+    }
+
+    @Test
+    public void testFromType() {
+        for (Class<?> clazz : new Class<?>[] { null, getClass(), SftpClientExtension.class }) {
+            assertNull("Unexpected value for class=" + clazz, BuiltinSftpClientExtensions.fromType(clazz));
+        }
+
+        for (BuiltinSftpClientExtensions expected : BuiltinSftpClientExtensions.VALUES) {
+            Class<?> type = expected.getType();
+            BuiltinSftpClientExtensions actual = BuiltinSftpClientExtensions.fromType(type);
+            assertSame(type.getSimpleName(), expected, actual);
+        }
+    }
+
+    @Test
+    public void testFromInstance() {
+        for (Object instance : new Object[] { null, this }) {
+            assertNull("Unexpected value for " + instance, BuiltinSftpClientExtensions.fromInstance(instance));
+        }
+
+        SftpClient mockClient = Mockito.mock(SftpClient.class);
+        RawSftpClient mockRaw = Mockito.mock(RawSftpClient.class);
+
+        for (BuiltinSftpClientExtensions expected : BuiltinSftpClientExtensions.VALUES) {
+            SftpClientExtension e = expected.create(mockClient, mockRaw);
+            BuiltinSftpClientExtensions actual = BuiltinSftpClientExtensions.fromInstance(e);
+            assertSame(expected.getName(), expected, actual);
+            assertEquals("Mismatched extension name", expected.getName(), actual.getName());
+        }        
+    }
+}