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 11:12:56 UTC

mina-sshd git commit: [SSHD-520] Add support for 'md5-hash' SFTP extension(s)

Repository: mina-sshd
Updated Branches:
  refs/heads/master 9d0662215 -> d5032f99d


[SSHD-520] Add support for 'md5-hash' SFTP extension(s)


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

Branch: refs/heads/master
Commit: d5032f99dbedaf439b4734e39b1bb4a1faa49c1f
Parents: 9d06622
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Sun Jul 5 12:12:47 2015 +0300
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Sun Jul 5 12:12:47 2015 +0300

----------------------------------------------------------------------
 .../common/subsystem/sftp/SftpConstants.java    |   3 +
 .../server/subsystem/sftp/SftpSubsystem.java    | 144 ++++++++++++++++++-
 .../sshd/client/subsystem/sftp/SftpTest.java    |   9 +-
 3 files changed, 143 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d5032f99/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 431085a..4aa555b 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
@@ -229,6 +229,9 @@ public final class SftpConstants {
     public static final String EXT_SUPPORTED2 = "supported2";
     public static final String EXT_VERSELECT = "version-select";
     public static final String EXT_COPYFILE = "copy-file";
+    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;
 
     private SftpConstants() {
         throw new UnsupportedOperationException("No instance");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d5032f99/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 0ee0421..740ceb9 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
@@ -80,6 +80,8 @@ import java.util.concurrent.TimeUnit;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.FactoryManagerUtils;
 import org.apache.sshd.common.config.VersionProperties;
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.file.FileSystemAware;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.util.GenericUtils;
@@ -142,11 +144,14 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
                 // TODO text-seek - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt
                 // TODO space-available - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
                 // TODO home-directory - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
+                // TODO check-file-handle/check-file-name - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.2
                 Collections.unmodifiableSet(
                         GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER,
                                 Arrays.asList(
                                         SftpConstants.EXT_VERSELECT,
-                                        SftpConstants.EXT_COPYFILE
+                                        SftpConstants.EXT_COPYFILE,
+                                        SftpConstants.EXT_MD5HASH,
+                                        SftpConstants.EXT_MD5HASH_HANDLE
                                 )));
 
     static {
@@ -249,17 +254,20 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
     }
 
     protected class FileHandle extends Handle {
+        private final int access;
         private final FileChannel channel;
         private long pos;
         private final List<FileLock> locks = new ArrayList<>();
 
         public FileHandle(Path file, int flags, int access, Map<String, Object> attrs) throws IOException {
             super(file);
+            this.access = access;
+
             Set<OpenOption> options = new HashSet<>();
-            if ((access & ACE4_READ_DATA) != 0 || (access & ACE4_READ_ATTRIBUTES) != 0) {
+            if (((access & ACE4_READ_DATA) != 0) || ((access & ACE4_READ_ATTRIBUTES) != 0)) {
                 options.add(StandardOpenOption.READ);
             }
-            if ((access & ACE4_WRITE_DATA) != 0 || (access & ACE4_WRITE_ATTRIBUTES) != 0) {
+            if (((access & ACE4_WRITE_DATA) != 0) || ((access & ACE4_WRITE_ATTRIBUTES) != 0)) {
                 options.add(StandardOpenOption.WRITE);
             }
             switch (flags & SSH_FXF_ACCESS_DISPOSITION) {
@@ -302,7 +310,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             }
             FileChannel channel;
             try {
-                  channel = FileChannel.open(file, options, attributes);
+                channel = FileChannel.open(file, options, attributes);
             } catch (UnsupportedOperationException e) {
                 channel = FileChannel.open(file, options);
                 setAttributes(file, attrs);
@@ -311,6 +319,10 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             this.pos = 0;
         }
 
+        public int getAccessMask() {
+            return access;
+        }
+
         public int read(byte[] data, long offset) throws IOException {
             return read(data, 0, data.length, offset);
         }
@@ -602,6 +614,10 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             case SftpConstants.EXT_COPYFILE:
                 doCopyFile(buffer, id);
                 break;
+            case SftpConstants.EXT_MD5HASH:
+            case SftpConstants.EXT_MD5HASH_HANDLE:
+                doMD5Hash(buffer, id, extension);
+                break;
             default:
                 log.info("Received unsupported SSH_FXP_EXTENDED({})", extension);
                 sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(" + extension + ") is unsupported or not implemented");
@@ -621,6 +637,120 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
         sendStatus(id, SSH_FX_OP_UNSUPPORTED, "Command SSH_FXP_EXTENDED(text-seek) is unsupported or not implemented");
     }
 
+    protected void doMD5Hash(Buffer buffer, int id, String targetType) throws IOException {
+        String target = buffer.getString();
+        long startOffset = buffer.getLong();
+        long length = buffer.getLong();
+        byte[] quickCheckHash = buffer.getBytes();
+        if (log.isDebugEnabled()) {
+            log.debug("doMD5Hash({})[{}] offset={}, length={}, quick-hash={}",
+                      targetType, target, Long.valueOf(startOffset), Long.valueOf(length), BufferUtils.printHex(':', quickCheckHash));
+        }
+        
+        try {
+            Path path;
+            if (SftpConstants.EXT_MD5HASH_HANDLE.equalsIgnoreCase(targetType)) {
+                Handle p = handles.get(target);
+                if (p == null) {
+                    throw new FileNotFoundException("Unknown handle: " + target);
+                }
+                
+                if (!(p instanceof FileHandle)) {
+                    throw new IOException("Not a file: " + p);
+                }
+                
+                path = p.getFile();
+
+                /*
+                 * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1:
+                 * 
+                 *      The handle MUST be a file handle, and ACE4_READ_DATA MUST
+                 *      have been included in the desired-access when the file
+                 *      was opened
+                 */
+                int access = ((FileHandle) p).getAccessMask();
+                if ((access & ACE4_READ_DATA) == 0) {
+                    throw new AccessDeniedException(path.toString(), path.toString(), "File not opened for read");
+                }
+            } else {
+                path = resolveFile(target);
+                if (Files.isDirectory(path, IoUtils.getLinkOptions(false))) {
+                    throw new IOException("Not a file: " + path);
+                }
+            }
+
+            /*
+             * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1:
+             *
+             *      If both start-offset and length are zero, the entire file should be included
+             */
+            long effectiveLength = length;
+            if ((startOffset == 0L) && (length == 0L)) {
+                effectiveLength = Files.size(path);
+            }
+            
+            Digest digest = BuiltinDigests.md5.create();
+            byte[] workBuf = new byte[(int) Math.min(effectiveLength, SftpConstants.MD5_QUICK_HASH_SIZE)];
+            ByteBuffer bb = ByteBuffer.wrap(workBuf);
+            boolean hashMatches = false;
+
+            try(FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
+                channel.position(startOffset);
+
+                /*
+                 * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1:
+                 * 
+                 *      If this is a zero length string, the client does not have the
+                 *      data, and is requesting the hash for reasons other than comparing
+                 *      with a local file.  The server MAY return SSH_FX_OP_UNSUPPORTED in
+                 *      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");
+                }
+                
+                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); 
+                        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));
+                    }
+                }
+            }
+
+            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),
+                          Boolean.valueOf(hashMatches), BufferUtils.printHex(':', hashValue));
+            }
+
+            buffer.clear();
+
+            buffer.putByte((byte) SSH_FXP_EXTENDED_REPLY);
+            buffer.putInt(id);
+            buffer.putString(targetType);
+            buffer.putBytes(hashValue);
+            send(buffer);
+        } catch(Exception e) {
+            sendStatus(id, e);
+        }
+    }
+    
     protected void doVersionSelect(Buffer buffer, int id) throws IOException {
         String proposed = buffer.getString();
         Boolean result = validateProposedVersion(id, proposed);
@@ -2278,7 +2408,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
 
     protected void sendStatus(int id, Exception e) throws IOException {
         int substatus;
-        if (e instanceof NoSuchFileException || e instanceof FileNotFoundException) {
+        if ((e instanceof NoSuchFileException) || (e instanceof FileNotFoundException)) {
             substatus = SSH_FX_NO_SUCH_FILE;
         } else if (e instanceof FileAlreadyExistsException) {
             substatus = SSH_FX_FILE_ALREADY_EXISTS;
@@ -2288,6 +2418,8 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
             substatus = SSH_FX_PERMISSION_DENIED;
         } else if (e instanceof OverlappingFileLockException) {
             substatus = SSH_FX_LOCK_CONFLICT;
+        } else if (e instanceof UnsupportedOperationException) {
+            substatus = SSH_FX_OP_UNSUPPORTED;
         } else {
             substatus = SSH_FX_FAILURE;
         }
@@ -2295,7 +2427,7 @@ public class SftpSubsystem extends AbstractLoggingBean implements Command, Runna
     }
 
     protected void sendStatus(int id, int substatus, String msg) throws IOException {
-        sendStatus(id, substatus, msg != null ? msg : "", "");
+        sendStatus(id, substatus, (msg != null) ? msg : "", "");
     }
 
     protected void sendStatus(int id, int substatus, String msg, String lang) throws IOException {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/d5032f99/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 62a142b..ad56705 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
@@ -60,6 +60,7 @@ import org.apache.sshd.common.util.io.IoUtils;
 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.SftpSubsystem;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.BaseTestSupport;
 import org.apache.sshd.util.BogusPasswordAuthenticator;
@@ -618,13 +619,7 @@ public class SftpTest extends BaseTestSupport {
         }
     }
 
-    private static Set<String> EXPECTED_EXTENSIONS = 
-            Collections.unmodifiableSet(
-                    GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER,
-                            Arrays.asList(
-                                    SftpConstants.EXT_VERSELECT,
-                                    SftpConstants.EXT_COPYFILE
-                            )));
+    private static Set<String> EXPECTED_EXTENSIONS = SftpSubsystem.DEFAULT_SUPPORTED_CLIENT_EXTENSIONS; 
     private static void assertSupportedExtensions(String extName, Collection<String> extensionNames) {
         assertEquals(extName + "[count]", EXPECTED_EXTENSIONS.size(), GenericUtils.size(extensionNames));