[SSHD-861] Handle correctly special characters in username/password when 
creating an SFTP file system URI


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

Branch: refs/heads/master
Commit: c24635d5cb86a106f5ca33f22e2a1b39d7aad512
Parents: 005ee40
Author: Lyor Goldstein <[email protected]>
Authored: Sun Nov 11 08:59:46 2018 +0200
Committer: Lyor Goldstein <[email protected]>
Committed: Sun Nov 11 18:56:28 2018 +0200

----------------------------------------------------------------------
 .../sshd/common/auth/BasicCredentialsImpl.java  |  92 ++++++++++++++
 .../common/auth/BasicCredentialsProvider.java   |  27 +++++
 .../common/auth/MutableBasicCredentials.java    |  27 +++++
 .../sshd/common/auth/MutablePassword.java       |  27 +++++
 .../apache/sshd/common/auth/PasswordHolder.java |  28 +++++
 .../loader/PrivateKeyEncryptionContext.java     |   5 +-
 .../apache/sshd/util/test/JUnitTestSupport.java |  22 +++-
 .../apache/sshd/util/test/SimpleUserInfo.java   |   3 +-
 .../subsystem/sftp/SftpFileSystemProvider.java  |  92 +++++++++++---
 .../subsystem/sftp/SftpFileSystemURITest.java   | 121 +++++++++++++++++++
 .../sftp/ApacheSshdSftpSessionFactory.java      |  15 ++-
 .../sftp/ApacheSshdSftpSessionFactoryTest.java  |   2 +-
 12 files changed, 432 insertions(+), 29 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsImpl.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsImpl.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsImpl.java
new file mode 100644
index 0000000..4e444f0
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsImpl.java
@@ -0,0 +1,92 @@
+/*
+ * 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.common.auth;
+
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public class BasicCredentialsImpl implements MutableBasicCredentials, 
Cloneable {
+    private String username;
+    private String password;
+
+    public BasicCredentialsImpl() {
+        super();
+    }
+
+    public BasicCredentialsImpl(String username, String password) {
+        this.username = username;
+        this.password = password;
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    @Override
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    @Override
+    public BasicCredentialsImpl clone() {
+        try {
+            return getClass().cast(super.clone());
+        } catch (CloneNotSupportedException e) {
+            throw new UnsupportedOperationException("Unexpected failure to 
clone: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getUsername(), getPassword());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        BasicCredentialsImpl other = (BasicCredentialsImpl) obj;
+        return Objects.equals(getUsername(), other.getUsername())
+            && Objects.equals(getPassword(), other.getPassword());
+    }
+
+    // NOTE: do not implement 'toString' on purpose to avoid inadvertent 
logging of contents
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsProvider.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsProvider.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsProvider.java
new file mode 100644
index 0000000..7ddde04
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/BasicCredentialsProvider.java
@@ -0,0 +1,27 @@
+/*
+ * 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.common.auth;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface BasicCredentialsProvider extends UsernameHolder, 
PasswordHolder {
+    // Nothing extra
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/main/java/org/apache/sshd/common/auth/MutableBasicCredentials.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/auth/MutableBasicCredentials.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/MutableBasicCredentials.java
new file mode 100644
index 0000000..a7bb1d5
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/MutableBasicCredentials.java
@@ -0,0 +1,27 @@
+/*
+ * 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.common.auth;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface MutableBasicCredentials extends BasicCredentialsProvider, 
MutableUserHolder, MutablePassword {
+    // Nothing extra
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/main/java/org/apache/sshd/common/auth/MutablePassword.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/auth/MutablePassword.java 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/MutablePassword.java
new file mode 100644
index 0000000..07e4843
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/auth/MutablePassword.java
@@ -0,0 +1,27 @@
+/*
+ * 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.common.auth;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface MutablePassword extends PasswordHolder {
+    void setPassword(String password);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/main/java/org/apache/sshd/common/auth/PasswordHolder.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/auth/PasswordHolder.java 
b/sshd-common/src/main/java/org/apache/sshd/common/auth/PasswordHolder.java
new file mode 100644
index 0000000..5f2eff7
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/auth/PasswordHolder.java
@@ -0,0 +1,28 @@
+/*
+ * 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.common.auth;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface PasswordHolder {
+    String getPassword();
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
index 5e03cdb..5303d57 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
@@ -30,13 +30,14 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.apache.sshd.common.auth.MutablePassword;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 
 /**
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
-public class PrivateKeyEncryptionContext implements Cloneable {
+public class PrivateKeyEncryptionContext implements MutablePassword, Cloneable 
{
     public static final String  DEFAULT_CIPHER_MODE = "CBC";
 
     private static final Map<String, PrivateKeyObfuscator> OBFUSCATORS =
@@ -82,10 +83,12 @@ public class PrivateKeyEncryptionContext implements 
Cloneable {
         cipherMode = value;
     }
 
+    @Override
     public String getPassword() {
         return password;
     }
 
+    @Override
     public void setPassword(String value) {
         password = value;
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
----------------------------------------------------------------------
diff --git 
a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java 
b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
index 01ba808..175cd93 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
@@ -51,6 +51,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BiPredicate;
 
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.util.GenericUtils;
@@ -360,6 +361,11 @@ public abstract class JUnitTestSupport extends Assert {
     }
 
     public static <E> void assertListEquals(String message, List<? extends E> 
expected, List<? extends E> actual) {
+        assertListEquals(message, expected, actual, Objects::equals);
+    }
+
+    public static <E> void assertListEquals(
+            String message, List<? extends E> expected, List<? extends E> 
actual, BiPredicate<? super E, ? super E> equator) {
         int expSize = GenericUtils.size(expected);
         int actSize = GenericUtils.size(actual);
         assertEquals(message + "[size]", expSize, actSize);
@@ -367,18 +373,28 @@ public abstract class JUnitTestSupport extends Assert {
         for (int index = 0; index < expSize; index++) {
             E expValue = expected.get(index);
             E actValue = actual.get(index);
-            assertEquals(message + "[" + index + "]", expValue, actValue);
+            if (!equator.test(expValue, actValue)) {
+                fail(message + "[" + index + "]: expected=" + expValue + ", 
actual=" + actValue);
+            }
         }
     }
 
-    public static <K, V> void assertMapEquals(String message, Map<? extends K, 
? extends V> expected, Map<? super K, ? extends V> actual) {
+    public static <K, V> void assertMapEquals(
+            String message, Map<? extends K, ? extends V> expected, Map<? 
super K, ? extends V> actual) {
+        assertMapEquals(message, expected, actual, Objects::equals);
+    }
+
+    public static <K, V> void assertMapEquals(
+            String message, Map<? extends K, ? extends V> expected, Map<? 
super K, ? extends V> actual, BiPredicate<? super V, ? super V> equator) {
         int numItems = GenericUtils.size(expected);
         assertEquals(message + "[size]", numItems, GenericUtils.size(actual));
 
         if (numItems > 0) {
             expected.forEach((key, expValue) -> {
                 V actValue = actual.get(key);
-                assertEquals(message + "[" + key + "]", expValue, actValue);
+                if (!equator.test(expValue, actValue)) {
+                    fail(message + "[" + key + "]: expected=" + expValue + ", 
actual=" + actValue);
+                }
             });
         }
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-core/src/test/java/org/apache/sshd/util/test/SimpleUserInfo.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/util/test/SimpleUserInfo.java 
b/sshd-core/src/test/java/org/apache/sshd/util/test/SimpleUserInfo.java
index 31f7c41..5062e8d 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/SimpleUserInfo.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/SimpleUserInfo.java
@@ -65,7 +65,8 @@ public class SimpleUserInfo implements UserInfo, 
UIKeyboardInteractive {
     }
 
     @Override
-    public String[] promptKeyboardInteractive(String destination, String name, 
String instruction, String[] prompt, boolean[] echo) {
+    public String[] promptKeyboardInteractive(
+            String destination, String name, String instruction, String[] 
prompt, boolean[] echo) {
         return new String[]{password};
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
----------------------------------------------------------------------
diff --git 
a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
 
b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
index 3c171d0..f8cd75b 100644
--- 
a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
+++ 
b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemProvider.java
@@ -24,6 +24,7 @@ import java.io.OutputStream;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.channels.FileChannel;
 import java.nio.charset.Charset;
 import java.nio.file.AccessDeniedException;
@@ -75,6 +76,9 @@ import org.apache.sshd.common.PropertyResolver;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.auth.BasicCredentialsImpl;
+import org.apache.sshd.common.auth.BasicCredentialsProvider;
+import org.apache.sshd.common.auth.MutableBasicCredentials;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.subsystem.sftp.SftpException;
@@ -184,12 +188,9 @@ public class SftpFileSystemProvider extends 
FileSystemProvider {
             port = SshConstants.DEFAULT_PORT;
         }
 
-        String userInfo = 
ValidateUtils.checkNotNullAndNotEmpty(uri.getUserInfo(), "UserInfo not 
provided");
-        String[] ui = GenericUtils.split(userInfo, ':');
-        ValidateUtils.checkTrue(GenericUtils.length(ui) == 2, "Invalid user 
info: %s", userInfo);
-
-        String username = ui[0];
-        String password = ui[1];
+        BasicCredentialsProvider credentials = parseCredentials(uri);
+        ValidateUtils.checkState(credentials != null, "No credentials 
provided");
+        String username = credentials.getUsername();
         String id = getFileSystemIdentifier(host, port, username);
         Map<String, Object> params = resolveFileSystemParameters(env, 
parseURIParameters(uri));
         PropertyResolver resolver = 
PropertyResolverUtils.toPropertyResolver(params);
@@ -198,6 +199,7 @@ public class SftpFileSystemProvider extends 
FileSystemProvider {
             PropertyResolverUtils.getCharset(resolver, 
NAME_DECORDER_CHARSET_PROP_NAME, DEFAULT_NAME_DECODER_CHARSET);
         long maxConnectTime = resolver.getLongProperty(CONNECT_TIME_PROP_NAME, 
DEFAULT_CONNECT_TIME);
         long maxAuthTime = resolver.getLongProperty(AUTH_TIME_PROP_NAME, 
DEFAULT_AUTH_TIME);
+        String password = credentials.getPassword();
 
         SftpFileSystem fileSystem;
         synchronized (fileSystems) {
@@ -308,6 +310,27 @@ public class SftpFileSystemProvider extends 
FileSystemProvider {
         return resolved;
     }
 
+    /**
+     * Attempts to parse the user information from the URI
+     *
+     * @param uri The {@link URI} value - ignored if {@code null} or does not
+     * contain any {@link URI#getUserInfo() user info}.
+     * @return The parsed credentials - {@code null} if none available
+     */
+    public static MutableBasicCredentials parseCredentials(URI uri) {
+        return parseCredentials((uri == null) ? "" : uri.getUserInfo());
+    }
+
+    public static MutableBasicCredentials parseCredentials(String userInfo) {
+        if (GenericUtils.isEmpty(userInfo)) {
+            return null;
+        }
+
+        String[] ui = GenericUtils.split(userInfo, ':');
+        ValidateUtils.checkTrue(GenericUtils.length(ui) == 2, "Invalid user 
info: %s", userInfo);
+        return new BasicCredentialsImpl(ui[0], ui[1]);
+    }
+
     public static Map<String, Object> parseURIParameters(URI uri) {
         return parseURIParameters((uri == null) ? "" : uri.getQuery());
     }
@@ -336,7 +359,9 @@ public class SftpFileSystemProvider extends 
FileSystemProvider {
             String key = p.substring(0, pos);
             String value = p.substring(pos + 1);
             if (NumberUtils.isIntegerNumber(value)) {
-                map.put(key, Long.parseLong(value));
+                map.put(key, Long.valueOf(value));
+            } else if ("true".equals(value) || "false".equals("value")) {
+                map.put(key, Boolean.valueOf(value));
             } else {
                 map.put(key, value);
             }
@@ -1235,22 +1260,53 @@ public class SftpFileSystemProvider extends 
FileSystemProvider {
     }
 
     public static URI createFileSystemURI(String host, int port, String 
username, String password, Map<String, ?> params) {
-        StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
-        sb.append(SftpConstants.SFTP_SUBSYSTEM_NAME)
-            .append("://").append(username).append(':').append(password)
-            .append('@').append(host).append(':').append(port)
-            .append('/');
-        if (GenericUtils.size(params) > 0) {
-            boolean firstParam = true;
-            // Cannot use forEach because firstParam is not effectively final
+        ValidateUtils.checkNotNullAndNotEmpty(host, "No host provided");
+
+        String queryPart = null;
+        int numParams = GenericUtils.size(params);
+        if (numParams > 0) {
+            StringBuilder sb = new StringBuilder(numParams * Short.SIZE);
             for (Map.Entry<String, ?> pe : params.entrySet()) {
                 String key = pe.getKey();
                 Object value = pe.getValue();
-                sb.append(firstParam ? '?' : 
'&').append(key).append('=').append(Objects.toString(value, null));
-                firstParam = false;
+                if (sb.length() > 0) {
+                    sb.append('&');
+                }
+                sb.append(key);
+                if (value != null) {
+                    sb.append('=').append(Objects.toString(value, null));
+                }
             }
+
+            queryPart = sb.toString();
         }
 
-        return URI.create(sb.toString());
+        try {
+            String userAuth = encodeCredentials(username, password);
+            return new URI(SftpConstants.SFTP_SUBSYSTEM_NAME, userAuth, host, 
port, "/", queryPart, null);
+        } catch (URISyntaxException e) {
+            throw new IllegalArgumentException("Failed (" + 
e.getClass().getSimpleName() + ")"
+                    + " to create access URI: " + e.getMessage(), e);
+        }
+    }
+
+    public static String encodeCredentials(String username, String password) {
+        ValidateUtils.checkNotNullAndNotEmpty(username, "No username 
provided");
+        ValidateUtils.checkNotNullAndNotEmpty(password, "No password 
provided");
+        /*
+         * There is no way to properly encode/decode credentials that already 
contain
+         * colon. See also https://tools.ietf.org/html/rfc3986#section-3.2.1:
+         *
+         *
+         *      Use of the format "user:password" in the userinfo field is
+         *      deprecated.  Applications should not render as clear text any 
data
+         *      after the first colon (":") character found within a userinfo
+         *      subcomponent unless the data after the colon is the empty 
string
+         *      (indicating no password).  Applications may choose to ignore or
+         *      reject such data when it is received as part of a reference and
+         *      should reject the storage of such data in unencrypted form.
+         */
+        ValidateUtils.checkTrue((username.indexOf(':') < 0) && 
(password.indexOf(':') < 0), "Reserved character used in credentials");
+        return username + ":" + password;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemURITest.java
----------------------------------------------------------------------
diff --git 
a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemURITest.java
 
b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemURITest.java
new file mode 100644
index 0000000..1e89e47
--- /dev/null
+++ 
b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemURITest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.auth.BasicCredentialsProvider;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class)   // see 
https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+@Category({ NoIoTestCase.class })
+public class SftpFileSystemURITest extends JUnitTestSupport {
+    private final String host;
+    private final int port;
+    private final String username;
+    private final String password;
+    private final Map<String, ?> params;
+
+    public SftpFileSystemURITest(String host, int port, String username, 
String password, Map<String, ?> params) {
+        this.host = host;
+        this.port = port;
+        this.username = username;
+        this.password = password;
+        this.params = params;
+    }
+
+    @Parameters(name = "host={0}, port={1}, user={2}, password={3}, 
params={4}")
+    public static List<Object[]> parameters() {
+        return new ArrayList<Object[]>() {
+            // Not serializing it
+            private static final long serialVersionUID = 1L;
+
+            {
+                add(new Object[] {SshdSocketAddress.LOCALHOST_NAME, 0, "user", 
"password", null});
+                add(new Object[] {"37.77.34.7", 2222, "user", "password", 
Collections.singletonMap("non-default-port", true)});
+                add(new Object[] {SshdSocketAddress.LOCALHOST_NAME, 
SshConstants.DEFAULT_PORT, "J@ck", "d@Ripper", new HashMap<String, Object>() {
+                        // not serializing it
+                        private static final long serialVersionUID = 1L;
+
+                        {
+                            put("param1", "1st");
+                            put("param2", 2);
+                            put("param3", false);
+                        }
+                    }
+                });
+                add(new Object[] {"19.65.7.3", 0, "J%ck", "d%Ripper", null});
+            }
+        };
+    }
+
+    @Test
+    public void testFullURIEncoding() {
+        URI uri = SftpFileSystemProvider.createFileSystemURI(host, port, 
username, password, params);
+        assertEquals("Mismatched scheme", SftpConstants.SFTP_SUBSYSTEM_NAME, 
uri.getScheme());
+        assertEquals("Mismatched host", host, uri.getHost());
+        assertEquals("Mismatched port", port, uri.getPort());
+
+        BasicCredentialsProvider credentials = 
SftpFileSystemProvider.parseCredentials(uri);
+        assertNotNull("No credentials provided", credentials);
+        assertEquals("Mismatched user", username, credentials.getUsername());
+        assertEquals("Mismatched password", password, 
credentials.getPassword());
+
+        Map<String, ?> uriParams = 
SftpFileSystemProvider.parseURIParameters(uri);
+        assertMapEquals(getCurrentTestName(), params, uriParams, (v1, v2) -> 
Objects.equals(v1.toString(), v2.toString()));
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName()
+            + "[host=" + host
+            + ", port=" + port
+            + ", username=" + username
+            + ", password=" + password
+            + ", params=" + params
+            + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
----------------------------------------------------------------------
diff --git 
a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
 
b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
index dfa5855..3c0a65c 100644
--- 
a/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
+++ 
b/sshd-spring-sftp/src/main/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactory.java
@@ -38,6 +38,7 @@ import 
org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
 import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.SshConstants;
+import org.apache.sshd.common.auth.MutableBasicCredentials;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.loader.pem.PEMResourceParserUtils;
@@ -62,7 +63,7 @@ import 
org.springframework.integration.file.remote.session.SharedSessionCapable;
 public class ApacheSshdSftpSessionFactory
         extends AbstractLoggingBean
         implements SessionFactory<DirEntry>, SharedSessionCapable,
-        SimpleClientConfigurator,
+        MutableBasicCredentials, SimpleClientConfigurator,
         InitializingBean, DisposableBean {
 
     // TODO add support for loading multiple private keys
@@ -119,19 +120,22 @@ public class ApacheSshdSftpSessionFactory
         this.portValue = port;
     }
 
-    public String getUser() {
+    @Override
+    public String getUsername() {
         return userValue;
     }
 
     /**
      * The remote user to use. This is a mandatory property.
      *
-     * @param user The username
+     * @param user The (never {@code null}/empty) username
      */
-    public void setUser(String user) {
+    @Override
+    public void setUsername(String user) {
         this.userValue = ValidateUtils.checkNotNullAndNotEmpty(user, "No user 
specified: %s", user);
     }
 
+    @Override
     public String getPassword() {
         return passwordValue;
     }
@@ -143,6 +147,7 @@ public class ApacheSshdSftpSessionFactory
      * @param password The password to use - if {@code null} then no password
      * is set - in which case the {@link #getPrivateKey()} resource is used
      */
+    @Override
     public void setPassword(String password) {
         this.passwordValue = password;
     }
@@ -396,7 +401,7 @@ public class ApacheSshdSftpSessionFactory
 
     protected ClientSession createClientSession() throws Exception {
         String hostname = ValidateUtils.checkNotNullAndNotEmpty(getHost(), 
"Host must not be empty");
-        String username = ValidateUtils.checkNotNullAndNotEmpty(getUser(), 
"User must not be empty");
+        String username = ValidateUtils.checkNotNullAndNotEmpty(getUsername(), 
"User must not be empty");
         String passwordIdentity = getPassword();
         KeyPair kp = getPrivateKeyPair();
         ValidateUtils.checkState(GenericUtils.isNotEmpty(passwordIdentity) || 
(kp != null),

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c24635d5/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
----------------------------------------------------------------------
diff --git 
a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
 
b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
index 75d7195..4b85c20 100644
--- 
a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
+++ 
b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
@@ -388,7 +388,7 @@ public class ApacheSshdSftpSessionFactoryTest extends 
BaseTestSupport {
         ApacheSshdSftpSessionFactory factory = new 
ApacheSshdSftpSessionFactory(sharedSession);
         factory.setHost(TEST_LOCALHOST);
         factory.setPort(port);
-        factory.setUser(getCurrentTestName());
+        factory.setUsername(getCurrentTestName());
         factory.setPassword(getCurrentTestName());
         factory.setSshClient(client);
         factory.setConnectTimeout(TimeUnit.SECONDS.toMillis(7L));

Reply via email to