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));