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/07 13:44:06 UTC

mina-sshd git commit: [SSHD-519] Add support for SFTP 'check-file-handle'/'check-file-name' extensions

Repository: mina-sshd
Updated Branches:
  refs/heads/master 1806ac970 -> 4818061d8


[SSHD-519] Add support for SFTP 'check-file-handle'/'check-file-name' extensions


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

Branch: refs/heads/master
Commit: 4818061d8dec69b74da643e8ecd2633fa3be4b89
Parents: 1806ac9
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue Jul 7 14:43:55 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue Jul 7 14:43:55 2015 +0300

----------------------------------------------------------------------
 .../extensions/BuiltinSftpClientExtensions.java |  14 ++
 .../extensions/CheckFileHandleExtension.java    |  46 ++++
 .../sftp/extensions/CheckFileNameExtension.java |  44 ++++
 .../sftp/extensions/MD5FileExtension.java       |   1 +
 .../sftp/extensions/MD5HandleExtension.java     |   1 +
 .../impl/AbstractCheckFileExtension.java        |  84 +++++++
 .../impl/AbstractMD5HashExtension.java          |   3 +-
 .../impl/CheckFileHandleExtensionImpl.java      |  44 ++++
 .../impl/CheckFileNameExtensionImpl.java        |  43 ++++
 .../common/subsystem/sftp/SftpConstants.java    |   4 +
 .../server/subsystem/sftp/SftpSubsystem.java    | 191 ++++++++++++++-
 .../sftp/AbstractSftpClientTestSupport.java     |  90 +++++++
 .../sshd/client/subsystem/sftp/SftpTest.java    | 128 +---------
 .../AbstractCheckFileExtensionTest.java         | 234 +++++++++++++++++++
 .../AbstractMD5HashExtensionTest.java           | 181 ++++++++++++++
 15 files changed, 972 insertions(+), 136 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/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
index 059966e..7a23f13 100644
--- 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
@@ -26,6 +26,8 @@ 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.CheckFileHandleExtensionImpl;
+import org.apache.sshd.client.subsystem.sftp.extensions.impl.CheckFileNameExtensionImpl;
 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;
@@ -54,6 +56,18 @@ public enum BuiltinSftpClientExtensions implements SftpClientExtensionFactory {
             public MD5HandleExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
                 return new MD5HandleExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
             }
+        },
+    CHECK_FILE_NAME(SftpConstants.EXT_CHKFILE_NAME, CheckFileNameExtension.class) {
+            @Override   // co-variant return
+            public CheckFileNameExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new CheckFileNameExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
+            }
+        },
+    CHECK_FILE_HANDLE(SftpConstants.EXT_CHKFILE_HANDLE, CheckFileHandleExtension.class) {
+            @Override   // co-variant return
+            public CheckFileHandleExtension create(SftpClient client, RawSftpClient raw, Map<String,byte[]> extensions, Map<String,?> parsed) {
+                return new CheckFileHandleExtensionImpl(client, raw, ParserUtils.supportedExtensions(parsed));
+            }
         };
 
     private final String name;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileHandleExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileHandleExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileHandleExtension.java
new file mode 100644
index 0000000..40bdd4c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileHandleExtension.java
@@ -0,0 +1,46 @@
+/*
+ * 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 java.util.Collection;
+
+import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle;
+import org.apache.sshd.common.util.Pair;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 9.1.2</A>
+ */
+public interface CheckFileHandleExtension extends SftpClientExtension {
+    /**
+     * @param handle Remote file {@link Handle} - must be a file and opened for read
+     * @param algorithms Hash algorithms in preferred order
+     * @param startOffset Start offset of the hash
+     * @param length Length of data to hash - if zero then till EOF
+     * @param blockSize Input block size to calculate individual hashes - if
+     * zero the <U>one</U> hash of <U>all</U> the data
+     * @return A {@link Pair} where left=hash algorithm name, right=the calculated
+     * hashes.
+     * @throws IOException If failed to execute the command
+     */
+    Pair<String,Collection<byte[]>> checkFileHandle(Handle handle, Collection<String> algorithms, long startOffset, long length, int blockSize) throws IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileNameExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileNameExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileNameExtension.java
new file mode 100644
index 0000000..cb908f4
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/CheckFileNameExtension.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.extensions;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.apache.sshd.common.util.Pair;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 9.1.2</A>
+ */
+public interface CheckFileNameExtension extends SftpClientExtension {
+    /**
+     * @param name Remote file name/path
+     * @param algorithms Hash algorithms in preferred order
+     * @param startOffset Start offset of the hash
+     * @param length Length of data to hash - if zero then till EOF
+     * @param blockSize Input block size to calculate individual hashes - if
+     * zero the <U>one</U> hash of <U>all</U> the data
+     * @return A {@link Pair} where left=hash algorithm name, right=the calculated
+     * hashes.
+     * @throws IOException If failed to execute the command
+     */
+    Pair<String,Collection<byte[]>> checkFileName(String name, Collection<String> algorithms, long startOffset, long length, int blockSize) throws IOException;
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/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
index c22b003..4706e76 100644
--- 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
@@ -23,6 +23,7 @@ import java.io.IOException;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 9.1.1</A>
  */
 public interface MD5FileExtension extends SftpClientExtension {
     /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/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
index 8e5fcf9..1188b50 100644
--- 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
@@ -25,6 +25,7 @@ import org.apache.sshd.client.subsystem.sftp.SftpClient;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 9.1.1</A>
  */
 public interface MD5HandleExtension extends SftpClientExtension {
     /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.java
new file mode 100644
index 0000000..a31f8e5
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/AbstractCheckFileExtension.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.impl;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+import java.util.LinkedList;
+
+import org.apache.sshd.client.subsystem.sftp.RawSftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+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 AbstractCheckFileExtension extends AbstractSftpClientExtension {
+    protected AbstractCheckFileExtension(String name, SftpClient client, RawSftpClient raw, Collection<String> extras) {
+        super(name, client, raw, extras);
+    }
+
+    protected Pair<String,Collection<byte[]>> doGetHash(Object target, Collection<String> algorithms, long offset, long length, int blockSize) throws IOException {
+        Buffer buffer = new ByteArrayBuffer();
+        String opcode = getName();
+        buffer.putString(opcode);
+        if (target instanceof CharSequence) {
+            buffer.putString(target.toString());
+        } else {
+            buffer.putBytes((byte[]) target);
+        }
+        buffer.putString(GenericUtils.join(algorithms, ','));
+        buffer.putLong(offset);
+        buffer.putLong(length);
+        buffer.putInt(blockSize);
+        
+        if (log.isDebugEnabled()) {
+            log.debug("doGetHash({})[{}] - offset={}, length={}, block-size={}",
+                      opcode, (target instanceof CharSequence) ? target : BufferUtils.printHex(BufferUtils.EMPTY_HEX_SEPARATOR, (byte[]) target),
+                      Long.valueOf(offset), Long.valueOf(length), Integer.valueOf(blockSize));
+        }
+
+        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, SftpConstants.EXT_CHKFILE_RESPONSE) != 0) {
+            throw new StreamCorruptedException("Mismatched reply type: expected=" + SftpConstants.EXT_CHKFILE_RESPONSE + ", actual=" + targetType);
+        }
+
+        String algo = buffer.getString();
+        Collection<byte[]> hashes = new LinkedList<>();
+        while (buffer.available() > 0) {
+            byte[] hashValue = buffer.getBytes();
+            hashes.add(hashValue);
+        }
+
+        return new Pair<String, Collection<byte[]>>(algo, hashes);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/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
index 7aa7778..b7c138b 100644
--- 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
@@ -53,7 +53,8 @@ public abstract class AbstractMD5HashExtension extends AbstractSftpClientExtensi
         
         if (log.isDebugEnabled()) {
             log.debug("doGetHash({})[{}] - offset={}, length={}, quick-hash={}",
-                      opcode, target, Long.valueOf(offset), Long.valueOf(length), BufferUtils.printHex(':', quickHash));
+                      opcode, (target instanceof CharSequence) ? target : BufferUtils.printHex(BufferUtils.EMPTY_HEX_SEPARATOR, (byte[]) target),
+                      Long.valueOf(offset), Long.valueOf(length), BufferUtils.printHex(':', quickHash));
         }
 
         buffer = checkExtendedReplyBuffer(receive(sendExtendedCommand(buffer)));

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileHandleExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileHandleExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileHandleExtensionImpl.java
new file mode 100644
index 0000000..fabc3f3
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileHandleExtensionImpl.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.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.SftpClient.Handle;
+import org.apache.sshd.client.subsystem.sftp.extensions.CheckFileHandleExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.Pair;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class CheckFileHandleExtensionImpl extends AbstractCheckFileExtension implements CheckFileHandleExtension {
+    public CheckFileHandleExtensionImpl(SftpClient client, RawSftpClient raw, Collection<String> extras) {
+        super(SftpConstants.EXT_CHKFILE_HANDLE, client, raw, extras);
+    }
+
+    @Override
+    public Pair<String, Collection<byte[]>> checkFileHandle(Handle handle, Collection<String> algorithms, long startOffset, long length, int blockSize) throws IOException {
+        return doGetHash(handle.getIdentifier(), algorithms, startOffset, length, blockSize);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileNameExtensionImpl.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileNameExtensionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileNameExtensionImpl.java
new file mode 100644
index 0000000..b747cb0
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/extensions/impl/CheckFileNameExtensionImpl.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.CheckFileNameExtension;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.Pair;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class CheckFileNameExtensionImpl extends AbstractCheckFileExtension implements CheckFileNameExtension {
+    public CheckFileNameExtensionImpl(SftpClient client, RawSftpClient raw, Collection<String> extras) {
+        super(SftpConstants.EXT_CHKFILE_NAME, client, raw, extras);
+    }
+
+    @Override
+    public Pair<String, Collection<byte[]>> checkFileName(String name, Collection<String> algorithms, long startOffset, long length, int blockSize) throws IOException {
+        return doGetHash(name, algorithms, startOffset, length, blockSize);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
index 4aa555b..92be24e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
@@ -232,6 +232,10 @@ public final class SftpConstants {
     public static final String EXT_MD5HASH = "md5-hash";
     public static final String EXT_MD5HASH_HANDLE = "md5-hash-handle";
         public static final int MD5_QUICK_HASH_SIZE = 2048;
+    public static final String EXT_CHKFILE_HANDLE = "check-file-handle";
+    public static final String EXT_CHKFILE_NAME = "check-file-name";
+        public static final int MIN_CHKFILE_BLOCKSIZE = 256;
+        public static final String EXT_CHKFILE_RESPONSE = "check-file";
 
     private SftpConstants() {
         throw new UnsupportedOperationException("No instance");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/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 d2a8ce4..41b9cf3 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
@@ -37,6 +37,7 @@ import java.nio.file.DirectoryNotEmptyException;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileSystem;
+import java.nio.file.FileSystemLoopException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
@@ -80,6 +81,7 @@ import java.util.concurrent.TimeUnit;
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.FactoryManagerUtils;
+import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.config.VersionProperties;
 import org.apache.sshd.common.digest.BuiltinDigests;
 import org.apache.sshd.common.digest.Digest;
@@ -176,7 +178,9 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                                         SftpConstants.EXT_VERSELECT,
                                         SftpConstants.EXT_COPYFILE,
                                         SftpConstants.EXT_MD5HASH,
-                                        SftpConstants.EXT_MD5HASH_HANDLE
+                                        SftpConstants.EXT_MD5HASH_HANDLE,
+                                        SftpConstants.EXT_CHKFILE_HANDLE,
+                                        SftpConstants.EXT_CHKFILE_NAME
                                 )));
 
     static {
@@ -691,6 +695,10 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             case SftpConstants.EXT_MD5HASH_HANDLE:
                 doMD5Hash(buffer, id, extension);
                 break;
+            case SftpConstants.EXT_CHKFILE_HANDLE:
+            case SftpConstants.EXT_CHKFILE_NAME:
+                doCheckFileHash(buffer, id, extension);
+                break;
             default:
                 log.info("Received unsupported SSH_FXP_EXTENDED({})", extension);
                 sendStatus(BufferUtils.clear(buffer), id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented");
@@ -722,6 +730,149 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         throw new UnsupportedOperationException("doTextSeek(" + fileHandle + ")");
     }
 
+    protected void doCheckFileHash(Buffer buffer, int id, String targetType) throws IOException {
+        String target = buffer.getString();
+        String algList = buffer.getString();
+        String[] algos = GenericUtils.split(algList, ',');
+        long startOffset = buffer.getLong();
+        long length = buffer.getLong();
+        int blockSize = buffer.getInt();
+        try {
+            buffer.clear();
+            buffer.putByte((byte) SSH_FXP_EXTENDED_REPLY);
+            buffer.putInt(id);
+            buffer.putString(SftpConstants.EXT_CHKFILE_RESPONSE);
+            doCheckFileHash(id, targetType, target, Arrays.asList(algos), startOffset, length, blockSize, buffer);
+        } catch(Exception e) {
+            sendStatus(BufferUtils.clear(buffer), id, e);
+            return;
+        }
+        
+        send(buffer);
+    }
+
+    protected void doCheckFileHash(int id, String targetType, String target, Collection<String> algos,
+                                   long startOffset, long length, int blockSize, Buffer buffer)
+                    throws Exception {
+        Path path;
+        if (SftpConstants.EXT_CHKFILE_HANDLE.equalsIgnoreCase(targetType)) {
+            Handle h = handles.get(target);
+            FileHandle fileHandle = validateHandle(target, h, FileHandle.class); 
+            path = fileHandle.getFile();
+
+            /*
+             * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.2:
+             * 
+             *       If ACE4_READ_DATA was not included when the file was opened,
+             *       the server MUST return STATUS_PERMISSION_DENIED.
+             */
+            int access = fileHandle.getAccessMask();
+            if ((access & ACE4_READ_DATA) == 0) {
+                throw new AccessDeniedException("File not opened for read: " + path);
+            }
+        } else {
+            path = resolveFile(target);
+            
+            /*
+             * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.2:
+             * 
+             *      If 'check-file-name' refers to a SSH_FILEXFER_TYPE_SYMLINK, the
+             *      target should be opened.
+             */
+            for (int index=0; Files.isSymbolicLink(path) && (index < Byte.MAX_VALUE /* TODO make this configurable */); index++) {
+                path = Files.readSymbolicLink(path);
+            }
+            
+            if (Files.isSymbolicLink(path)) {
+                throw new FileSystemLoopException(target + " yields a circular or too long chain of symlinks");
+            }
+
+            if (Files.isDirectory(path, IoUtils.getLinkOptions(false))) {
+                throw new NotDirectoryException(path.toString());
+            }
+        }
+
+        ValidateUtils.checkNotNullAndNotEmpty(algos, "No hash algorithms specified", GenericUtils.EMPTY_OBJECT_ARRAY);
+        
+        NamedFactory<? extends Digest> factory = null;
+        for (String a : algos) {
+            if ((factory = BuiltinDigests.fromFactoryName(a)) != null) {
+                break;
+            }
+        }
+        ValidateUtils.checkNotNull(factory, "No matching digest factory found for %s", algos);
+
+        doCheckFileHash(id, path, factory, startOffset, length, blockSize, buffer);
+    }
+
+    protected void doCheckFileHash(int id, Path file, NamedFactory<? extends Digest> factory,
+                                   long startOffset, long length, int blockSize, Buffer buffer)
+                           throws Exception {
+        ValidateUtils.checkTrue((blockSize == 0) || (blockSize >= SftpConstants.MIN_CHKFILE_BLOCKSIZE), "Invalid block size: %d", blockSize);
+        ValidateUtils.checkNotNull(factory, "No digest factory provided", GenericUtils.EMPTY_OBJECT_ARRAY);
+        buffer.putString(factory.getName());
+        
+        long effectiveLength = length;
+        if (effectiveLength == 0L) {
+            long totalLength = Files.size(file);
+            effectiveLength = totalLength - startOffset;    
+        }
+
+        byte[] workBuf = (blockSize == 0)
+                       ? new byte[Math.min((int) effectiveLength, IoUtils.DEFAULT_COPY_SIZE)]
+                       : new byte[Math.min((int) effectiveLength, blockSize)]
+                       ;
+        ByteBuffer bb = ByteBuffer.wrap(workBuf);
+        try(FileChannel channel = FileChannel.open(file, IoUtils.EMPTY_OPEN_OPTIONS)) {
+            channel.position(startOffset);
+
+            Digest digest = factory.create();
+            digest.init();
+
+            if (blockSize == 0) {
+                while(effectiveLength > 0L) {
+                    bb.clear(); // prepare for next read
+
+                    int readLen = channel.read(bb);
+                    if (readLen < 0) {
+                        break;
+                    }
+
+                    effectiveLength -= readLen;
+                    digest.update(workBuf, 0, readLen);
+                }
+                
+                byte[] hashValue = digest.digest();
+                if (log.isTraceEnabled()) {
+                    log.trace("doCheckFileHash({}) offset={}, length={} - hash={}",
+                              file, Long.valueOf(startOffset), Long.valueOf(length),
+                              BufferUtils.printHex(':', hashValue));
+                }
+                buffer.putBytes(hashValue);
+            } else {
+                for (int count=0; effectiveLength > 0L; count++) {
+                    bb.clear(); // prepare for next read
+
+                    int readLen = channel.read(bb);
+                    if (readLen < 0) {
+                        break;
+                    }
+
+                    effectiveLength -= readLen;
+                    digest.update(workBuf, 0, readLen);
+
+                    byte[] hashValue = digest.digest(); // NOTE: this also resets the hash for the next read
+                    if (log.isTraceEnabled()) {
+                        log.trace("doCheckFileHash({})[{}] offset={}, length={} - hash={}",
+                                  file, Integer.valueOf(count), Long.valueOf(startOffset), Long.valueOf(length),
+                                  BufferUtils.printHex(':', hashValue));
+                    }
+                    buffer.putBytes(hashValue);
+                }
+            }
+        }
+    }
+
     protected void doMD5Hash(Buffer buffer, int id, String targetType) throws IOException {
         String target = buffer.getString();
         long startOffset = buffer.getLong();
@@ -730,6 +881,12 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         
         try {
             hashValue = doMD5Hash(id, targetType, target, startOffset, length, quickCheckHash);
+            if (log.isTraceEnabled()) {
+                log.debug("doMD5Hash({})[{}] offset={}, length={}, quick-hash={} - hash={}",
+                          targetType, target, Long.valueOf(startOffset), Long.valueOf(length), BufferUtils.printHex(':', quickCheckHash),
+                          BufferUtils.printHex(':', hashValue));
+            }
+
         } catch(Exception e) {
             sendStatus(BufferUtils.clear(buffer), id, e);
             return;
@@ -782,10 +939,15 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         if ((startOffset == 0L) && (length == 0L)) {
             effectiveLength = Files.size(path);
         }
-        
+
+        return doMD5Hash(id, path, startOffset, effectiveLength, quickCheckHash);
+    }
+
+    protected byte[] doMD5Hash(int id, Path path, long startOffset, long length, byte[] quickCheckHash) throws Exception {
         Digest digest = BuiltinDigests.md5.create();
         digest.init();
 
+        long effectiveLength = length;
         byte[] digestBuf = new byte[(int) Math.min(effectiveLength, SftpConstants.MD5_QUICK_HASH_SIZE)];
         ByteBuffer bb = ByteBuffer.wrap(digestBuf);
         boolean hashMatches = false;
@@ -807,6 +969,9 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                 hashMatches = true;
             } else {
                 int readLen = channel.read(bb);
+                if (readLen < 0) {
+                    throw new EOFException("EOF while read initial buffer from " + path);
+                }
                 effectiveLength -= readLen;
                 digest.update(digestBuf, 0, readLen);
 
@@ -828,8 +993,8 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                     }
                 } else {
                     if (log.isTraceEnabled()) {
-                        log.trace("doMD5Hash({})[{}] offset={}, length={} - quick-hash mismatched expected={}, actual={}",
-                                  targetType, target, Long.valueOf(startOffset), Long.valueOf(length),
+                        log.trace("doMD5Hash({}) offset={}, length={} - quick-hash mismatched expected={}, actual={}",
+                                  path, Long.valueOf(startOffset), Long.valueOf(length),
                                   BufferUtils.printHex(':', quickCheckHash), BufferUtils.printHex(':', hashValue));
                     }
                 }
@@ -837,8 +1002,12 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
 
             if (hashMatches) {
                 while(effectiveLength > 0L) {
-                    bb.clear();
-                    int readLen = channel.read(bb); 
+                    bb.clear(); // prepare for next read
+
+                    int readLen = channel.read(bb);
+                    if (readLen < 0) {
+                        break;  // user may have specified more than we have available
+                    }
                     effectiveLength -= readLen;
                     digest.update(digestBuf, 0, readLen);
                 }
@@ -851,12 +1020,12 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             }
         }
 
-        if (log.isDebugEnabled()) {
-            log.debug("doMD5Hash({})[{}] offset={}, length={}, quick-hash={} - match={}, hash={}",
-                      targetType, target, Long.valueOf(startOffset), Long.valueOf(length), BufferUtils.printHex(':', quickCheckHash),
-                      Boolean.valueOf(hashMatches), BufferUtils.printHex(':', hashValue));
+        if (log.isTraceEnabled()) {
+            log.trace("doMD5Hash({}) offset={}, length={} - matches={}, quick={} hash={}",
+                      path, Long.valueOf(startOffset), Long.valueOf(length), Boolean.valueOf(hashMatches),
+                      BufferUtils.printHex(':', quickCheckHash), BufferUtils.printHex(':', hashValue));
         }
-        
+
         return hashValue;
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
new file mode 100644
index 0000000..d4b7294
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java
@@ -0,0 +1,90 @@
+/*
+ * 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 java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.root.RootedFileSystemProvider;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.command.ScpCommandFactory;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.BaseTestSupport;
+import org.apache.sshd.util.BogusPasswordAuthenticator;
+import org.apache.sshd.util.EchoShellFactory;
+import org.apache.sshd.util.Utils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractSftpClientTestSupport extends BaseTestSupport {
+    protected SshServer sshd;
+    protected int port;
+    protected final FileSystemFactory fileSystemFactory;
+
+    protected AbstractSftpClientTestSupport() throws IOException {
+        Path targetPath = detectTargetFolder().toPath();
+        Path parentPath = targetPath.getParent();
+        final FileSystem fileSystem = new RootedFileSystemProvider().newFileSystem(parentPath, Collections.<String,Object>emptyMap());
+        fileSystemFactory = new FileSystemFactory() {
+            @Override
+            public FileSystem createFileSystem(Session session) throws IOException {
+                return fileSystem;
+            }
+        };
+    }
+
+    protected void setupServer() throws Exception {
+        sshd = SshServer.setUpDefaultServer();
+        sshd.setKeyPairProvider(Utils.createTestHostKeyProvider());
+        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory()));
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.setShellFactory(new EchoShellFactory());
+        sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE);
+        sshd.setFileSystemFactory(fileSystemFactory);
+        sshd.start();
+        port = sshd.getPort();
+    }
+    
+    protected void tearDownServer() throws Exception {
+        if (sshd != null) {
+            try {
+                sshd.stop(true);
+            } finally {
+                sshd = null;
+            }
+        }
+    }
+
+    protected static final <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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/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 f7ec553..d1e200a 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
@@ -97,37 +97,17 @@ import com.jcraft.jsch.ChannelSftp;
 import com.jcraft.jsch.JSch;
 
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class SftpTest extends BaseTestSupport {
+public class SftpTest extends AbstractSftpClientTestSupport {
 
-    private SshServer sshd;
-    private int port;
     private com.jcraft.jsch.Session session;
-    private final FileSystemFactory fileSystemFactory;
 
     public SftpTest() throws IOException {
-        Path targetPath = detectTargetFolder().toPath();
-        Path parentPath = targetPath.getParent();
-        final FileSystem fileSystem = new RootedFileSystemProvider().newFileSystem(parentPath, Collections.<String,Object>emptyMap());
-        fileSystemFactory = new FileSystemFactory() {
-            @Override
-            public FileSystem createFileSystem(Session session) throws IOException {
-                return fileSystem;
-            }
-        };
+        super();
     }
 
     @Before
     public void setUp() throws Exception {
-        sshd = SshServer.setUpDefaultServer();
-        sshd.setKeyPairProvider(Utils.createTestHostKeyProvider());
-        sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new SftpSubsystemFactory()));
-        sshd.setCommandFactory(new ScpCommandFactory());
-        sshd.setShellFactory(new EchoShellFactory());
-        sshd.setPasswordAuthenticator(BogusPasswordAuthenticator.INSTANCE);
-        sshd.setFileSystemFactory(fileSystemFactory);
-        sshd.start();
-        port = sshd.getPort();
-
+        setupServer();
         JSchLogger.init();
         JSch sch = new JSch();
         session = sch.getSession("sshd", "localhost", port);
@@ -141,9 +121,7 @@ public class SftpTest extends BaseTestSupport {
             session.disconnect();
         }
         
-        if (sshd != null) {
-            sshd.stop(true);
-        }
+        tearDownServer();
     }
 
     @Test
@@ -592,104 +570,6 @@ public class SftpTest extends BaseTestSupport {
     }
 
     @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();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java
new file mode 100644
index 0000000..1927d07
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractCheckFileExtensionTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+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.AbstractSftpClientTestSupport;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.util.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class AbstractCheckFileExtensionTest extends AbstractSftpClientTestSupport {
+    private static final Collection<Integer> DATA_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            Integer.valueOf(Byte.MAX_VALUE),
+                            Integer.valueOf(SftpConstants.MIN_CHKFILE_BLOCKSIZE),
+                            Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                            Integer.valueOf(Byte.SIZE *IoUtils.DEFAULT_COPY_SIZE)
+                    ));
+    private static final Collection<Integer> BLOCK_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                            Integer.valueOf(0),
+                            Integer.valueOf(SftpConstants.MIN_CHKFILE_BLOCKSIZE),
+                            Integer.valueOf(1024),
+                            Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE)
+                        ));
+    @SuppressWarnings("synthetic-access")
+    private static final Collection<Object[]> PARAMETERS =
+            Collections.unmodifiableCollection(new LinkedList<Object[]>() {
+                private static final long serialVersionUID = 1L;    // we're not serializing it
+                
+                {
+                    for (NamedFactory<?> factory : BuiltinDigests.VALUES) {
+                        String algorithm = factory.getName();
+                        for (Number dataSize : DATA_SIZES) {
+                            for (Number blockSize : BLOCK_SIZES) {
+                                add(new Object[] { algorithm, dataSize, blockSize });
+                            }
+                        }
+                    }
+                }
+            });
+
+    @Parameters(name = "{0} - dataSize={1}, blockSize={2}")
+    public static Collection<Object[]> parameters() {
+        return PARAMETERS;
+    }
+
+    private final String algorithm;
+    private final int dataSize, blockSize;
+
+    public AbstractCheckFileExtensionTest(String algorithm, int dataSize, int blockSize) throws IOException {
+        this.algorithm = algorithm;
+        this.dataSize = dataSize;
+        this.blockSize = blockSize;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testCheckFileExtension() throws Exception {
+        testCheckFileExtension(algorithm, dataSize, blockSize);
+    }
+
+    private void testCheckFileExtension(String expectedAlgorithm, int inputDataSize, int hashBlockSize) throws Exception {
+        NamedFactory<? extends Digest> factory = BuiltinDigests.fromFactoryName(expectedAlgorithm);
+        Digest digest = null;
+        if (blockSize == 0) {
+            digest = factory.create();
+            digest.init();
+        }
+
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName()
+                     + "-" + expectedAlgorithm
+                     + "-" + inputDataSize + "/" + hashBlockSize
+                     + System.getProperty("line.separator"))
+                .getBytes(StandardCharsets.UTF_8)
+                ;
+
+        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(inputDataSize + seed.length)) {
+            while (baos.size() < inputDataSize) {
+                baos.write(seed);
+                
+                if (digest != null) {
+                    digest.update(seed);
+                }
+            }
+
+            testCheckFileExtension(factory, baos.toByteArray(), hashBlockSize, (digest == null) ? null : digest.digest());
+        }
+    }
+
+    private void testCheckFileExtension(NamedFactory<? extends Digest> factory, byte[] data, int hashBlockSize, byte[] expectedHash) 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 srcFile = lclSftp.resolve(factory.getName() + "-data-" + data.length + "-" + hashBlockSize + ".txt");
+        Files.write(srcFile, data, IoUtils.EMPTY_OPEN_OPTIONS);
+
+        List<String> algorithms = new ArrayList<String>(BuiltinDigests.VALUES.size());
+        // put the selected algorithm 1st and then the rest
+        algorithms.add(factory.getName());
+        for (NamedFactory<? extends Digest> f : BuiltinDigests.VALUES) {
+            if (f == factory) {
+                continue;
+            }
+            
+            algorithms.add(f.getName());
+        }
+
+        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()) {
+                    CheckFileNameExtension file = assertExtensionCreated(sftp, CheckFileNameExtension.class);
+                    try {
+                        Pair<String,?> result = file.checkFileName(srcFolder, algorithms, 0L, 0L, hashBlockSize);
+                        fail("Unexpected success to hash folder=" + srcFolder + ": " + result.getFirst());
+                    } catch(IOException e) {    // expected - not allowed to hash a folder
+                        assertTrue("Not an SftpException", e instanceof SftpException);
+                    }
+
+                    CheckFileHandleExtension hndl = assertExtensionCreated(sftp, CheckFileHandleExtension.class);
+                    try(CloseableHandle dirHandle = sftp.openDir(srcFolder)) {
+                        try {
+                            Pair<String,?> result = hndl.checkFileHandle(dirHandle, algorithms, 0L, 0L, hashBlockSize);
+                            fail("Unexpected handle success on folder=" + srcFolder + ": " + result.getFirst());
+                        } catch(IOException e) {    // expected - not allowed to hash a folder
+                            assertTrue("Not an SftpException", e instanceof SftpException);
+                        }
+                    }
+                    
+                    validateHashResult(file, file.checkFileName(srcPath, algorithms, 0L, 0L, hashBlockSize), algorithms.get(0), expectedHash);
+                    try(CloseableHandle fileHandle = sftp.open(srcPath, SftpClient.OpenMode.Read)) {
+                        validateHashResult(hndl, hndl.checkFileHandle(fileHandle, algorithms, 0L, 0L, hashBlockSize), algorithms.get(0), expectedHash);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+    
+    private void validateHashResult(NamedResource hasher, Pair<String,Collection<byte[]>> result, String expectedAlgorithm, byte[] expectedHash) {
+        String name = hasher.getName();
+        assertNotNull("No result for hash=" + name, result);
+        assertEquals("Mismatched hash algorithms for " + name, expectedAlgorithm, result.getFirst());
+        
+        if (GenericUtils.length(expectedHash) > 0) {
+            Collection<byte[]> values = result.getSecond();
+            assertEquals("Mismatched hash values count for " + name, 1, GenericUtils.size(values));
+            
+            byte[] actualHash = values.iterator().next();
+            if (!Arrays.equals(expectedHash, actualHash)) {
+                fail("Mismatched hashes for " + name
+                   + ": expected=" + BufferUtils.printHex(':', expectedHash)
+                   + ", actual=" + BufferUtils.printHex(':', expectedHash));
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/4818061d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java
new file mode 100644
index 0000000..c5622b0
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/extensions/AbstractMD5HashExtensionTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+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.AbstractSftpClientTestSupport;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.util.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class AbstractMD5HashExtensionTest extends AbstractSftpClientTestSupport {
+    private static final List<Integer> DATA_SIZES =
+            Collections.unmodifiableList(
+                    Arrays.asList(
+                                Integer.valueOf(Byte.MAX_VALUE),
+                                Integer.valueOf(SftpConstants.MD5_QUICK_HASH_SIZE),
+                                Integer.valueOf(IoUtils.DEFAULT_COPY_SIZE),
+                                Integer.valueOf(Byte.SIZE * IoUtils.DEFAULT_COPY_SIZE)
+                            ));
+
+    @Parameters(name = "dataSize={0}")
+    public static Collection<Object[]> parameters() {
+        return parameterize(DATA_SIZES);
+    }
+
+    private final int size;
+
+    public AbstractMD5HashExtensionTest(int size) throws IOException {
+        this.size = size;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        setupServer();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        tearDownServer();
+    }
+
+    @Test
+    public void testMD5HashExtension() throws Exception {
+        testMD5HashExtension(size);
+    }
+
+    private void testMD5HashExtension(int dataSize) throws Exception {
+        byte[] seed = (getClass().getName() + "#" + getCurrentTestName() + "-" + dataSize + System.getProperty("line.separator")).getBytes(StandardCharsets.UTF_8);
+        try(ByteArrayOutputStream baos=new ByteArrayOutputStream(dataSize + seed.length)) {
+            while (baos.size() < dataSize) {
+                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 for file hash on " + srcFolder, 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 for handle hash on " + srcFolder, 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 + " on " + srcFile
+                                       + ": expected=" + BufferUtils.printHex(':', expectedHash)
+                                       + ", actual=" + BufferUtils.printHex(':', actualHash));
+                                }
+                            }
+                        }
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}