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 2016/01/04 13:46:03 UTC

[1/2] mina-sshd git commit: [SSHD-619] Send intermittent SSH_MSG_IGNORE messages in order to further strengthen cryptographic strength

Repository: mina-sshd
Updated Branches:
  refs/heads/master 00beb5a87 -> 926b16be1


[SSHD-619] Send intermittent SSH_MSG_IGNORE messages in order to further strengthen cryptographic strength


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

Branch: refs/heads/master
Commit: a535e45098100b4e0e0e1966d45eebce3e0af626
Parents: 00beb5a
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Jan 4 14:45:31 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Jan 4 14:45:31 2016 +0200

----------------------------------------------------------------------
 .../org/apache/sshd/common/FactoryManager.java  |  84 +++++++++++
 .../org/apache/sshd/common/io/IoSession.java    |   1 -
 .../sshd/common/session/AbstractSession.java    | 118 +++++++++++++--
 .../sshd/server/ServerFactoryManager.java       |  47 ------
 .../common/session/AbstractSessionTest.java     | 149 ++++++++++++++++++-
 5 files changed, 337 insertions(+), 62 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a535e450/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
index 0e2ff1f..eadc6cb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
@@ -198,6 +198,90 @@ public interface FactoryManager extends KexFactoryManager, SessionListenerManage
     String DEFAULT_VERSION = "SSHD-UNKNOWN";
 
     /**
+     * Key re-exchange will be automatically performed after the session
+     * has sent or received the given amount of bytes. If non-positive,
+     * then disabled. The default value is {@link #DEFAULT_REKEY_BYTES_LIMIT}
+     */
+    String REKEY_BYTES_LIMIT = "rekey-bytes-limit";
+
+    /**
+     * Default value for {@link #REKEY_BYTES_LIMIT} if no override
+     * @see <A HREF="https://tools.ietf.org/html/rfc4253#page-23">RFC4253 section 9</A>
+     */
+    long DEFAULT_REKEY_BYTES_LIMIT = 1024L * 1024L * 1024L; // 1GB
+
+    /**
+     * Key re-exchange will be automatically performed after the specified
+     * amount of time has elapsed since the last key exchange - in milliseconds.
+     * If non-positive then disabled. The default value is {@link #DEFAULT_REKEY_TIME_LIMIT}
+     */
+    String REKEY_TIME_LIMIT = "rekey-time-limit";
+
+    /**
+     * Default value for {@link #REKEY_TIME_LIMIT} if none specified
+     * @see <A HREF="https://tools.ietf.org/html/rfc4253#page-23">RFC4253 section 9</A>
+     */
+    long DEFAULT_REKEY_TIME_LIMIT = 60L * 60L * 1000L; // 1 hour
+
+    /**
+     * Key re-exchange will be automatically performed after the specified
+     * number of packets has been exchanged - positive 64-bit value. If
+     * non-positive then disabled. The default is {@link #DEFAULT_REKEY_PACKETS_LIMIT}
+     */
+    String REKEY_PACKETS_LIMIT = "rekey-packets-limit";
+
+    /**
+     * Default value for {@link #REKEY_PACKETS_LIMIT} if none specified
+     * @see <A HREF="https://tools.ietf.org/html/rfc4344#page-3">RFC4344 section 3.1</A>
+     */
+    long DEFAULT_REKEY_PACKETS_LIMIT = 1L << 31;
+
+    /**
+     * Key re-exchange will be automatically performed after the specified
+     * number of cipher blocks has been processed - positive 64-bit value. If
+     * non-positive then disabled. The default is calculated according to
+     * <A HREF="https://tools.ietf.org/html/rfc4344#page-3">RFC4344 section 3.2</A>
+     */
+    String REKEY_BLOCKS_LIMIT = "rekey-blocks-limit";
+
+    /**
+     * Average number of packets to be skipped before an {@code SSH_MSG_IGNORE}
+     * message is inserted in the stream. If non-positive, then feature is disabled
+     * @see #IGNORE_MESSAGE_VARIANCE
+     */
+    String IGNORE_MESSAGE_FREQUENCY = "ignore-message-frequency";
+
+    /**
+     * Default value of {@link #IGNORE_MESSAGE_FREQUENCY} if none set.
+     */
+    long DEFAULT_IGNORE_MESSAGE_FREQUENCY = 1024L;
+
+    /**
+     * The variance to be used around the configured {@link #IGNORE_MESSAGE_FREQUENCY}
+     * value in order to avoid insertion at a set frequency. If zero, then <U>exact</U>
+     * frequency is used. If negative, then the <U>absolute</U> value is used. If
+     * greater or equal to the frequency, then assumed to be zero - i.e., no variance
+     */
+    String IGNORE_MESSAGE_VARIANCE = "ignore-message-variance";
+
+    /**
+     * Default value for {@link #IGNORE_MESSAGE_VARIANCE} if none configured
+     */
+    int DEFAULT_IGNORE_MESSAGE_VARIANCE = 32;
+
+    /**
+     * Minimum size of {@code SSH_MSG_IGNORE} payload to send if feature enabled. If
+     * non-positive then no message is sent. Otherwise, the actual size is between this
+     * size and twice its value
+     */
+    String IGNORE_MESSAGE_SIZE = "ignore-message-size";
+
+    /**
+     * Value of {@link #IGNORE_MESSAGE_SIZE} if none configured
+     */
+    int DEFAULT_IGNORE_MESSAGE_SIZE = 16;
+
+    /**
      * An upper case string identifying the version of the software used on
      * client or server side. This version includes the name of the software
      * and usually looks like this: <code>SSHD-1.0</code>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a535e450/sshd-core/src/main/java/org/apache/sshd/common/io/IoSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/io/IoSession.java b/sshd-core/src/main/java/org/apache/sshd/common/io/IoSession.java
index 3885e31..2861b80 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/io/IoSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/io/IoSession.java
@@ -69,7 +69,6 @@ public interface IoSession extends Closeable {
      */
     IoWriteFuture write(Buffer buffer);
 
-
     /**
      * Closes this session immediately or after all queued write requests
      * are flushed.  This operation is asynchronous.  Wait for the returned

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a535e450/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
index 95edc26..92f5843 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
@@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.sshd.common.Closeable;
+import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
@@ -75,7 +76,6 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-import org.apache.sshd.server.ServerFactoryManager;
 
 /**
  * <P>
@@ -189,14 +189,21 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
     protected final AtomicLong inBlocksCount = new AtomicLong(0L);
     protected final AtomicLong outBlocksCount = new AtomicLong(0L);
     protected final AtomicLong lastKeyTimeValue = new AtomicLong(0L);
+    // we initialize them here in case super constructor calls some methods that use these values
+    protected long maxRekyPackets = FactoryManager.DEFAULT_REKEY_PACKETS_LIMIT;
+    protected long maxRekeyBytes = FactoryManager.DEFAULT_REKEY_BYTES_LIMIT;
+    protected long maxRekeyInterval = FactoryManager.DEFAULT_REKEY_TIME_LIMIT;
     protected final Queue<PendingWriteFuture> pendingPackets = new LinkedList<>();
 
     protected Service currentService;
-    // we initialize them here in case super constructor calls some methods that use these values
-    protected long maxRekyPackets = ServerFactoryManager.DEFAULT_REKEY_PACKETS_LIMIT;
-    protected long maxRekeyBytes = ServerFactoryManager.DEFAULT_REKEY_BYTES_LIMIT;
-    protected long maxRekeyInterval = ServerFactoryManager.DEFAULT_REKEY_TIME_LIMIT;
-    protected final AtomicLong maxRekeyBlocks = new AtomicLong(ServerFactoryManager.DEFAULT_REKEY_BYTES_LIMIT / 16);
+
+    // SSH_MSG_IGNORE stream padding
+    protected int ignorePacketDataLength = FactoryManager.DEFAULT_IGNORE_MESSAGE_SIZE;
+    protected long ignorePacketsFrequency = FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY;
+    protected int ignorePacketsVariance = FactoryManager.DEFAULT_IGNORE_MESSAGE_VARIANCE;
+
+    protected final AtomicLong maxRekeyBlocks = new AtomicLong(FactoryManager.DEFAULT_REKEY_BYTES_LIMIT / 16);
+    protected final AtomicLong ignorePacketsCount = new AtomicLong(FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY);
 
     /**
      * The factory manager used to retrieve factories of Ciphers, Macs and other objects
@@ -217,14 +224,14 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
         this.factoryManager = factoryManager;
         this.ioSession = ioSession;
 
-        maxRekeyBytes = PropertyResolverUtils.getLongProperty(this, ServerFactoryManager.REKEY_BYTES_LIMIT, maxRekeyBytes);
-        maxRekeyInterval = PropertyResolverUtils.getLongProperty(this, ServerFactoryManager.REKEY_TIME_LIMIT, maxRekeyInterval);
-        maxRekyPackets = PropertyResolverUtils.getLongProperty(this, ServerFactoryManager.REKEY_PACKETS_LIMIT, maxRekyPackets);
+        Factory<Random> factory = ValidateUtils.checkNotNull(factoryManager.getRandomFactory(), "No random factory for %s", ioSession);
+        random = ValidateUtils.checkNotNull(factory.create(), "No randomizer instance for %s", ioSession);
+
+        refreshConfiguration();
 
         ClassLoader loader = getClass().getClassLoader();
         sessionListenerProxy = EventListenerUtils.proxyWrapper(SessionListener.class, loader, sessionListeners);
         channelListenerProxy = EventListenerUtils.proxyWrapper(ChannelListener.class, loader, channelListeners);
-        random = ValidateUtils.checkNotNull(factoryManager.getRandomFactory(), "No random factory").create();
 
         // Delegate the task of further notifications to the session
         addSessionListener(factoryManager.getSessionListenerProxy());
@@ -378,6 +385,27 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
         }
     }
 
+    /**
+     * Refresh whatever internal configuration is not {@code final}
+     */
+    protected void refreshConfiguration() {
+        synchronized (lock) {
+            // re-keying configuration
+            maxRekeyBytes = PropertyResolverUtils.getLongProperty(this, FactoryManager.REKEY_BYTES_LIMIT, maxRekeyBytes);
+            maxRekeyInterval = PropertyResolverUtils.getLongProperty(this, FactoryManager.REKEY_TIME_LIMIT, maxRekeyInterval);
+            maxRekyPackets = PropertyResolverUtils.getLongProperty(this, FactoryManager.REKEY_PACKETS_LIMIT, maxRekyPackets);
+
+            // intermittent SSH_MSG_IGNORE stream padding
+            ignorePacketDataLength = PropertyResolverUtils.getIntProperty(this, FactoryManager.IGNORE_MESSAGE_SIZE, FactoryManager.DEFAULT_IGNORE_MESSAGE_SIZE);
+            ignorePacketsFrequency = PropertyResolverUtils.getLongProperty(this, FactoryManager.IGNORE_MESSAGE_FREQUENCY, FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY);
+            ignorePacketsVariance = PropertyResolverUtils.getIntProperty(this, FactoryManager.IGNORE_MESSAGE_VARIANCE, FactoryManager.DEFAULT_IGNORE_MESSAGE_VARIANCE);
+            if (ignorePacketsVariance >= ignorePacketsFrequency) {
+                ignorePacketsVariance = 0;
+            }
+
+            ignorePacketsCount.set(calculateNextIgnorePacketCount(random, ignorePacketsFrequency, ignorePacketsVariance));
+        }
+    }
 
     /**
      * Abstract method for processing incoming decoded packets.
@@ -518,9 +546,17 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
     }
 
     protected void handleIgnore(Buffer buffer) throws Exception {
+        handleIgnore(buffer.getBytes(), buffer);
+    }
+
+    protected void handleIgnore(byte[] data, Buffer buffer) throws Exception {
         if (log.isDebugEnabled()) {
             log.debug("handleIgnore({}) SSH_MSG_IGNORE", this);
         }
+
+        if (log.isTraceEnabled()) {
+            log.trace("handleIgnore({}) data: {}", this, BufferUtils.printHex(data));
+        }
     }
 
     protected void handleUnimplemented(Buffer buffer) throws Exception {
@@ -833,15 +869,71 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
     }
 
     protected IoWriteFuture doWritePacket(Buffer buffer) throws IOException {
+        Buffer ignoreBuf = null;
+        int ignoreDataLen = resolveIgnoreBufferDataLength();
+        if (ignoreDataLen > 0) {
+            ignoreBuf = createBuffer(SshConstants.SSH_MSG_IGNORE, ignoreDataLen + Byte.SIZE);
+            ignoreBuf.putInt(ignoreDataLen);
+
+            int wpos = ignoreBuf.wpos();
+            synchronized (lock) {
+                random.fill(ignoreBuf.array(), wpos, ignoreDataLen);
+            }
+            ignoreBuf.wpos(wpos + ignoreDataLen);
+
+            if (log.isDebugEnabled()) {
+                log.debug("doWritePacket({}) append SSH_MSG_IGNORE message", this);
+            }
+        }
+
         // Synchronize all write requests as needed by the encoding algorithm
         // and also queue the write request in this synchronized block to ensure
         // packets are sent in the correct order
         synchronized (encodeLock) {
+            if (ignoreBuf != null) {
+                encode(ignoreBuf);
+                ioSession.write(ignoreBuf);
+            }
+
             encode(buffer);
             return ioSession.write(buffer);
         }
     }
 
+    protected int resolveIgnoreBufferDataLength() {
+        if ((ignorePacketDataLength <= 0) || (ignorePacketsFrequency <= 0L) || (ignorePacketsVariance < 0)) {
+            return 0;
+        }
+
+        long count = ignorePacketsCount.decrementAndGet();
+        if (count > 0L) {
+            return 0;
+        }
+
+        synchronized (lock) {
+            ignorePacketsCount.set(calculateNextIgnorePacketCount(random, ignorePacketsFrequency, ignorePacketsVariance));
+            return ignorePacketDataLength + random.random(ignorePacketDataLength);
+        }
+    }
+
+    protected long calculateNextIgnorePacketCount(Random r, long freq, int variance) {
+        if ((freq <= 0L) || (variance < 0)) {
+            return -1L;
+        }
+
+        if (variance == 0) {
+            return freq;
+        }
+
+        int extra = r.random((variance < 0) ? (0 - variance) : variance);
+        long count = (variance < 0) ? (freq - extra) : (freq + extra);
+        if (log.isTraceEnabled()) {
+            log.trace("calculateNextIgnorePacketCount({}) count={}", this, count);
+        }
+
+        return count;
+    }
+
     /**
      * Send a global request and wait for the response.
      * This must only be used when sending a SSH_MSG_GLOBAL_REQUEST with a result expected,
@@ -892,7 +984,7 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
             len += outMac.getBlockSize();
         }
 
-        return prepareBuffer(cmd, new ByteArrayBuffer(new byte[Math.max(len, ByteArrayBuffer.DEFAULT_SIZE)], false));
+        return prepareBuffer(cmd, new ByteArrayBuffer(new byte[len + Byte.SIZE], false));
     }
 
     @Override
@@ -974,7 +1066,7 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
             outBytesCount.addAndGet(len);
             // Make buffer ready to be read
             buffer.rpos(off);
-        } catch (SshException e) {
+        } catch (IOException e) {
             throw e;
         } catch (Exception e) {
             throw new SshException(e);
@@ -1417,7 +1509,7 @@ public abstract class AbstractSession extends AbstractKexFactoryManager implemen
         // select the lowest cipher size
         int avgCipherBlockSize = Math.min(inBlockSize, outBlockSize);
         long recommendedByteRekeyBlocks = 1L << Math.min((avgCipherBlockSize * Byte.SIZE) / 4, 63);    // in case (block-size / 4) > 63
-        maxRekeyBlocks.set(PropertyResolverUtils.getLongProperty(this, ServerFactoryManager.REKEY_BLOCKS_LIMIT, recommendedByteRekeyBlocks));
+        maxRekeyBlocks.set(PropertyResolverUtils.getLongProperty(this, FactoryManager.REKEY_BLOCKS_LIMIT, recommendedByteRekeyBlocks));
         if (log.isDebugEnabled()) {
             log.debug("receiveNewKeys({}) inCipher={}, outCipher={}, recommended blocks limit={}, actual={}",
                       this, inCipher, outCipher, recommendedByteRekeyBlocks, maxRekeyBlocks);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a535e450/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
index 2e63121..2c2ff25 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/ServerFactoryManager.java
@@ -91,53 +91,6 @@ public interface ServerFactoryManager extends FactoryManager, ServerAuthenticati
     long DEFAULT_COMMAND_EXIT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
 
     /**
-     * Key re-exchange will be automatically performed after the session
-     * has sent or received the given amount of bytes. If non-positive,
-     * then disabled. The default value is {@link #DEFAULT_REKEY_BYTES_LIMIT}
-     */
-    String REKEY_BYTES_LIMIT = "rekey-bytes-limit";
-
-    /**
-     * Default value for {@link #REKEY_BYTES_LIMIT} if no override
-     * @see <A HREF="https://tools.ietf.org/html/rfc4253#page-23">RFC4253 section 9</A>
-     */
-    long DEFAULT_REKEY_BYTES_LIMIT = 1024L * 1024L * 1024L; // 1GB
-
-    /**
-     * Key re-exchange will be automatically performed after the specified
-     * amount of time has elapsed since the last key exchange - in milliseconds.
-     * If non-positive then disabled. The default value is {@link #DEFAULT_REKEY_TIME_LIMIT}
-     */
-    String REKEY_TIME_LIMIT = "rekey-time-limit";
-
-    /**
-     * Default value for {@link #REKEY_TIME_LIMIT} if none specified
-     * @see <A HREF="https://tools.ietf.org/html/rfc4253#page-23">RFC4253 section 9</A>
-     */
-    long DEFAULT_REKEY_TIME_LIMIT = 60L * 60L * 1000L; // 1 hour
-
-    /**
-     * Key re-exchange will be automatically performed after the specified
-     * number of packets has been exchanged - positive 64-bit value. If
-     * non-positive then disabled. The default is {@link #DEFAULT_REKEY_PACKETS_LIMIT}
-     */
-    String REKEY_PACKETS_LIMIT = "rekey-packets-limit";
-
-    /**
-     * Default value for {@link #REKEY_PACKETS_LIMIT} if none specified
-     * @see <A HREF="https://tools.ietf.org/html/rfc4344#page-3">RFC4344 section 3.1</A>
-     */
-    long DEFAULT_REKEY_PACKETS_LIMIT = 1L << 31;
-
-    /**
-     * Key re-exchange will be automatically performed after the specified
-     * number of cipher blocks has been processed - positive 64-bit value. If
-     * non-positive then disabled. The default is calculated according to
-     * <A HREF="https://tools.ietf.org/html/rfc4344#page-3">RFC4344 section 3.2</A>
-     */
-    String REKEY_BLOCKS_LIMIT = "rekey-blocks-limit";
-
-    /**
      * A URL pointing to the moduli file.
      * If not specified, the default internal file will be used.
      */

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/a535e450/sshd-core/src/test/java/org/apache/sshd/common/session/AbstractSessionTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/session/AbstractSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/common/session/AbstractSessionTest.java
index 210fde3..886f08f 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/session/AbstractSessionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/session/AbstractSessionTest.java
@@ -19,10 +19,21 @@
 package org.apache.sshd.common.session;
 
 import java.io.IOException;
+import java.net.SocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.channel.IoWriteFutureImpl;
+import org.apache.sshd.common.future.CloseFuture;
+import org.apache.sshd.common.io.IoService;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.io.IoWriteFuture;
 import org.apache.sshd.common.kex.KexProposalOption;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
@@ -43,11 +54,21 @@ public class AbstractSessionTest extends BaseTestSupport {
 
     private MySession session;
 
+    public AbstractSessionTest() {
+        super();
+    }
+
     @Before
     public void setUp() throws Exception {
         session = new MySession();
     }
 
+    public void tearDown() throws Exception {
+        if (session != null) {
+            session.close();
+        }
+    }
+
     @Test
     public void testReadIdentSimple() {
         Buffer buf = new ByteArrayBuffer("SSH-2.0-software\r\n".getBytes(StandardCharsets.UTF_8));
@@ -111,9 +132,130 @@ public class AbstractSessionTest extends BaseTestSupport {
         fail("Unexpected success: " + ident);
     }
 
+    @Test   // see SSHD-619
+    public void testMsgIgnorePadding() throws Exception {
+        final long frequency = Byte.SIZE;
+        PropertyResolverUtils.updateProperty(session, FactoryManager.IGNORE_MESSAGE_SIZE, Short.SIZE);
+        PropertyResolverUtils.updateProperty(session, FactoryManager.IGNORE_MESSAGE_FREQUENCY, frequency);
+        PropertyResolverUtils.updateProperty(session, FactoryManager.IGNORE_MESSAGE_VARIANCE, 0);
+        session.refreshConfiguration();
+
+        Buffer msg = session.createBuffer(SshConstants.SSH_MSG_DEBUG, Long.SIZE);
+        msg.putBoolean(true);   // display ?
+        msg.putString(getCurrentTestName());    // message
+        msg.putString("");  // language
+
+        MyIoSession ioSession = (MyIoSession) session.getIoSession();
+        Queue<Buffer> queue = ioSession.getOutgoingMessages();
+        int numIgnores = 0;
+        for (int cycle = 0; cycle < Byte.SIZE; cycle++) {
+            for (long index = 0; index <= frequency; index++) {
+                session.writePacket(msg);
+
+                Buffer data = queue.remove();
+                if (data != msg) {
+                    int cmd = data.getUByte();
+                    assertEquals("Mismatched buffer command at cycle " + cycle + "[" + index + "]", SshConstants.SSH_MSG_IGNORE, cmd);
+
+                    int len = data.getInt();
+                    assertTrue("Mismatched random padding data length at cycle " + cycle + "[" + index + "]: " + len, len >= Short.SIZE);
+                    numIgnores++;
+                }
+            }
+        }
+
+        assertEquals("Mismatched number of ignore messages", Byte.SIZE, numIgnores);
+    }
+
+    public static class MyIoSession implements IoSession {
+        private final Queue<Buffer> outgoing = new LinkedBlockingQueue<>();
+        private final AtomicBoolean open = new AtomicBoolean(true);
+
+        public MyIoSession() {
+            super();
+        }
+
+        public Queue<Buffer> getOutgoingMessages() {
+            return outgoing;
+        }
+
+        @Override
+        public boolean isClosed() {
+            return !isOpen();
+        }
+
+        @Override
+        public boolean isClosing() {
+            return !isOpen();
+        }
+
+        @Override
+        public boolean isOpen() {
+            return open.get();
+        }
+
+        @Override
+        public void close() throws IOException {
+            close(true);
+        }
+
+        @Override
+        public long getId() {
+            return 0;
+        }
+
+        @Override
+        public Object getAttribute(Object key) {
+            return null;
+        }
+
+        @Override
+        public Object setAttribute(Object key, Object value) {
+            return null;
+        }
+
+        @Override
+        public SocketAddress getRemoteAddress() {
+            return null;
+        }
+
+        @Override
+        public SocketAddress getLocalAddress() {
+            return null;
+        }
+
+        @Override
+        public IoWriteFuture write(Buffer buffer) {
+            if (!isOpen()) {
+                throw new IllegalStateException("Not open");
+            }
+            if (!outgoing.offer(buffer)) {
+                throw new IllegalStateException("Failed to offer outgoing buffer");
+            }
+
+            IoWriteFutureImpl future = new IoWriteFutureImpl(buffer);
+            future.setValue(Boolean.TRUE);
+            return future;
+        }
+
+        @Override
+        public CloseFuture close(boolean immediately) {
+            if (open.getAndSet(false)) {
+                outgoing.clear();
+            }
+
+            return null;
+        }
+
+        @Override
+        public IoService getService() {
+            return null;
+        }
+    }
+
     public static class MySession extends AbstractSession {
         public MySession() {
-            super(true, org.apache.sshd.util.test.Utils.setupTestServer(AbstractSessionTest.class), null);
+            super(true, org.apache.sshd.util.test.Utils.setupTestServer(AbstractSessionTest.class), new MyIoSession());
         }
 
         @Override
@@ -131,6 +273,11 @@ public class AbstractSessionTest extends BaseTestSupport {
         }
 
         @Override
+        protected void encode(Buffer buffer) throws IOException {
+            // ignored
+        }
+
+        @Override
         protected byte[] sendKexInit() throws IOException {
             return GenericUtils.EMPTY_BYTE_ARRAY;
         }


[2/2] mina-sshd git commit: Added '-r' flag support to SFTP 'rm' command

Posted by lg...@apache.org.
Added '-r' flag support to SFTP 'rm' command


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

Branch: refs/heads/master
Commit: 926b16be1cbf8e0cd68459817abc0417dd5ba87d
Parents: a535e45
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Mon Jan 4 14:45:53 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Mon Jan 4 14:45:53 2016 +0200

----------------------------------------------------------------------
 .../sshd/client/subsystem/sftp/SftpCommand.java | 76 +++++++++++++++++---
 1 file changed, 68 insertions(+), 8 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/926b16be/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
index 59b7622..4cc7d6b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpCommand.java
@@ -36,6 +36,7 @@ import java.util.logging.Level;
 import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.subsystem.sftp.SftpClient.Attributes;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.DirEntry;
 import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatExtensionInfo;
 import org.apache.sshd.client.subsystem.sftp.extensions.openssh.OpenSSHStatPathExtension;
 import org.apache.sshd.common.NamedResource;
@@ -297,7 +298,7 @@ public class SftpCommand implements Channel {
 
         @Override
         public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified", args);
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
 
             String newPath = resolveRemotePath(args);
             SftpClient sftp = getClient();
@@ -314,7 +315,7 @@ public class SftpCommand implements Channel {
 
         @Override
         public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified", args);
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
 
             String path = resolveRemotePath(args);
             SftpClient sftp = getClient();
@@ -359,13 +360,70 @@ public class SftpCommand implements Channel {
 
         @Override
         public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified", args);
+            String[] comps = GenericUtils.split(args, ' ');
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue(numArgs >= 1, "No arguments");
+            ValidateUtils.checkTrue(numArgs <= 2, "Too many arguments: %s", args);
+
+            String remotePath = comps[0];
+            boolean recursive = false;
+            boolean verbose = false;
+            if (remotePath.charAt(0) == '-') {
+                ValidateUtils.checkTrue(remotePath.length() > 1, "Missing flags specification: %s", args);
+                ValidateUtils.checkTrue(numArgs == 2, "Missing remote directory: %s", args);
+
+                for (int index = 1; index < remotePath.length(); index++) {
+                    char ch = remotePath.charAt(index);
+                    switch(ch) {
+                        case 'r' :
+                            recursive = true;
+                            break;
+                        case 'v':
+                            verbose = true;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Unknown flag (" + String.valueOf(ch) + ")");
+                    }
+                }
+                remotePath = comps[1];
+            }
 
-            String path = resolveRemotePath(args);
+            String path = resolveRemotePath(remotePath);
             SftpClient sftp = getClient();
-            sftp.remove(path);
+            if (recursive) {
+                Attributes attrs = sftp.stat(path);
+                ValidateUtils.checkTrue(attrs.isDirectory(), "Remote path not a directory: %s", args);
+                removeRecursive(sftp, path, attrs, stdout, verbose);
+            } else {
+                sftp.remove(path);
+                if (verbose) {
+                    stdout.append('\t').append("Removed ").println(path);
+                }
+            }
+
             return false;
         }
+
+        private void removeRecursive(SftpClient sftp, String path, Attributes attrs, PrintStream stdout, boolean verbose) throws IOException {
+            if (attrs.isDirectory()) {
+                for (DirEntry entry : sftp.readDir(path)) {
+                    String name = entry.getFilename();
+                    if (".".equals(name) || "..".equals(name)) {
+                        continue;
+                    }
+
+                    removeRecursive(sftp, path + "/" + name, entry.getAttributes(), stdout, verbose);
+                }
+
+                sftp.rmdir(path);
+            } else {
+                sftp.remove(path);
+            }
+
+            if (verbose) {
+                stdout.append('\t').append("Removed ").println(path);
+            }
+        }
     }
 
     private class RmdirCommandExecutor implements CommandExecutor {
@@ -376,7 +434,7 @@ public class SftpCommand implements Channel {
 
         @Override
         public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
-            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified", args);
+            ValidateUtils.checkNotNullAndNotEmpty(args, "No remote directory specified");
 
             String path = resolveRemotePath(args);
             SftpClient sftp = getClient();
@@ -413,13 +471,15 @@ public class SftpCommand implements Channel {
         @Override
         public boolean executeCommand(String args, BufferedReader stdin, PrintStream stdout, PrintStream stderr) throws Exception {
             String[] comps = GenericUtils.split(args, ' ');
-            ValidateUtils.checkTrue(GenericUtils.length(comps) == 1, "Invalid number of arguments: %s", args);
+            int numArgs = GenericUtils.length(comps);
+            ValidateUtils.checkTrue(numArgs <= 1, "Invalid number of arguments: %s", args);
 
             SftpClient sftp = getClient();
             OpenSSHStatPathExtension ext = sftp.getExtension(OpenSSHStatPathExtension.class);
             ValidateUtils.checkTrue(ext.isSupported(), "Extension not supported by server: %s", ext.getName());
 
-            OpenSSHStatExtensionInfo info = ext.stat(resolveRemotePath(GenericUtils.trimToEmpty(comps[0])));
+            String remPath = resolveRemotePath((numArgs >= 1) ? GenericUtils.trimToEmpty(comps[0]) :  GenericUtils.trimToEmpty(args));
+            OpenSSHStatExtensionInfo info = ext.stat(remPath);
             Field[] fields = info.getClass().getFields();
             for (Field f : fields) {
                 String name = f.getName();