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/19 09:58:53 UTC

[1/4] mina-sshd git commit: [SSHD-607] Provide PasswordAuthenticator that uses LDAP

Repository: mina-sshd
Updated Branches:
  refs/heads/master 458c38fa5 -> c66c6d421


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
new file mode 100644
index 0000000..1e4ff23
--- /dev/null
+++ b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
@@ -0,0 +1,494 @@
+/*
+ * 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.util.net;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * Uses the <A HREF="http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html">
+ * LDAP Naming Service Provider for the Java Naming and Directory Interface (JNDI)</A>
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LdapNetworkConnector extends NetworkConnector {
+    public static final String DEFAULT_LDAP_PROTOCOL = "ldap";
+    public static final int DEFAULT_LDAP_PORT = 389;
+
+    /**
+     * Property used to override the default LDAP context factory class
+     */
+    public static final String DEFAULT_LDAP_FACTORY_PROPNAME = "javax.naming.ldap.factory";
+    /**
+     * Default LDAP context factory class - unless overridden via the {@link #DEFAULT_LDAP_FACTORY_PROPNAME} property
+     */
+    public static final String DEFAULT_LDAP_FACTORY_PROPVAL = "com.sun.jndi.ldap.LdapCtxFactory";
+    public static final int DEFAULT_LDAP_SEARCH_SCOPE = SearchControls.SUBTREE_SCOPE;
+    public static final long DEFAULT_LDAP_TIME_LIMIT = TimeUnit.SECONDS.toMillis(15L);
+    public static final String DEFAULT_LDAP_REFERRAL_MODE = "ignore";
+    public static final long DEFAULT_LDAP_COUNT_LIMIT = 1L;
+    public static final boolean DEFAULT_LDAP_DEREF_ENABLED = false;
+    /**
+     * A special value used to indicate that all attributes are required
+     */
+    public static final String ALL_LDAP_ATTRIBUTES = "*";
+    public static final boolean DEFAULT_LDAP_RETURN_OBJVALUE = false;
+    public static final boolean DEFAULT_LDAP_ACCUMULATE_MULTIVALUES = false;
+    public static final String DEFAULT_LDAP_BIND_DN_PATTERN = "{0}";
+    public static final String DEFAULT_LDAP_BIND_PASSWORD_PATTERN = "{1}";
+    /**
+     * A list of known binary attributes
+     * @see <A HREF="http://docs.oracle.com/javase/jndi/tutorial/ldap/misc/attrs.html">LDAP Attributes</A>
+     */
+    public static final String DEFAULT_BINARY_ATTRIBUTES =
+             "photo,personalSignature,audio,jpegPhoto,javaSerializedData,thumbnailPhoto,thumbnailLogo"
+           + ",userPassword,userCertificate,cACertificate,authorityRevocationList,certificateRevocationList"
+           + ",crossCertificatePair,x500UniqueIdentifier";
+
+    protected final SearchControls searchControls = new SearchControls();
+    protected final Map<String, Object> ldapEnv = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
+    protected MessageFormat bindDNPattern = new MessageFormat(DEFAULT_LDAP_BIND_DN_PATTERN);
+    protected MessageFormat bindPasswordPattern = new MessageFormat(DEFAULT_LDAP_BIND_PASSWORD_PATTERN);
+    protected MessageFormat searchFilterPattern;
+    protected MessageFormat baseDNPattern;
+
+    private boolean accumulateMultiValues = DEFAULT_LDAP_ACCUMULATE_MULTIVALUES;
+
+    public LdapNetworkConnector() {
+        setProtocol(DEFAULT_LDAP_PROTOCOL);
+        setPort(DEFAULT_LDAP_PORT);
+        setSearchScope(DEFAULT_LDAP_SEARCH_SCOPE);
+        setLdapFactory(DEFAULT_LDAP_FACTORY_PROPVAL);
+        setTimeLimit(DEFAULT_LDAP_TIME_LIMIT);
+        setCountLimit(DEFAULT_LDAP_COUNT_LIMIT);
+        setDerefLink(DEFAULT_LDAP_DEREF_ENABLED);
+        setReturningObjFlag(DEFAULT_LDAP_RETURN_OBJVALUE);
+        setReferralMode(DEFAULT_LDAP_REFERRAL_MODE);
+        setBinaryAttributes(DEFAULT_BINARY_ATTRIBUTES);
+    }
+
+    public String getLdapFactory() {
+        return Objects.toString(ldapEnv.get(Context.INITIAL_CONTEXT_FACTORY), null);
+    }
+
+    /**
+     * @param factory The LDAP context factory
+     */
+    public void setLdapFactory(String factory) {
+        ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, ValidateUtils.checkNotNullAndNotEmpty(factory, "No LDAP factory"));
+    }
+
+    public String getBaseDN() {
+        return baseDNPattern.toPattern();
+    }
+
+    /**
+     * @param p The base DN pattern - the arguments to the pattern depend on the actual usage
+     * @see MessageFormat#format(String, Object...)
+     */
+    public void setBaseDN(String p) {
+        baseDNPattern = new MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No base DN pattern"));
+    }
+
+    public String getBindDNPattern() {
+        return bindDNPattern.toPattern();
+    }
+
+    public void setBindDNPattern(String p) {
+        bindDNPattern = new MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No bind DN pattern"));
+    }
+
+    public String getBindPasswordPattern() {
+        return bindPasswordPattern.toPattern();
+    }
+
+    public void setBindPasswordPattern(String p) {
+        bindPasswordPattern = new MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No bind password pattern"));
+    }
+
+    public String getSearchFilterPattern() {
+        return searchFilterPattern.toPattern();
+    }
+
+    public void setSearchFilterPattern(String p) {
+        searchFilterPattern = new MessageFormat(ValidateUtils.checkNotNullAndNotEmpty(p, "No seatch filter pattern"));
+    }
+
+    /**
+     * @return The search scope
+     * @see SearchControls#OBJECT_SCOPE
+     * @see SearchControls#ONELEVEL_SCOPE
+     * @see SearchControls#SUBTREE_SCOPE
+     */
+    public int getSearchScope() {
+        return searchControls.getSearchScope();
+    }
+
+    /**
+     * @param scope The search scope
+     * @see SearchControls#OBJECT_SCOPE
+     * @see SearchControls#ONELEVEL_SCOPE
+     * @see SearchControls#SUBTREE_SCOPE
+     */
+    public void setSearchScope(int scope) {
+        searchControls.setSearchScope(scope);
+    }
+
+    /**
+     * @return Time limit (millis) to wait for result - zero means forever
+     */
+    public long getTimeLimit() {
+        return searchControls.getTimeLimit();
+    }
+
+    public void setTimeLimit(long limit) {
+        ValidateUtils.checkTrue(limit >= 0L, "Negative time limit: %d", limit);
+        searchControls.setTimeLimit((int) limit);
+    }
+
+    /**
+     * @return Maximum number of entries to be returned in a query
+     */
+    public long getCountLimit() {
+        return searchControls.getCountLimit();
+    }
+
+    public void setCountLimit(long count) {
+        ValidateUtils.checkTrue(count >= 0L, "Bad count limit: %d", count);
+        searchControls.setCountLimit(count);
+    }
+
+    /**
+     * @return {@code true} whether links should be de-referenced
+     * @see SearchControls#getDerefLinkFlag()
+     */
+    public boolean isDerefLink() {
+        return searchControls.getDerefLinkFlag();
+    }
+
+    public void setDerefLink(boolean enabled) {
+        searchControls.setDerefLinkFlag(enabled);
+    }
+
+    /**
+     * @return Comma separated list of attributes to retrieve
+     */
+    public String getRetrievedAttributes() {
+        String[] attrs = searchControls.getReturningAttributes();
+        if (attrs == null) {
+            return "*";
+        } else if (attrs.length == 0) {
+            return "";
+        } else if (attrs.length == 1) {
+            return attrs[0];
+        } else {
+            return GenericUtils.join(attrs, ',');
+        }
+    }
+
+    /**
+     * @param attrs Comma separated list of attributes to retrieve - if
+     * {@code null}/empty then no attributes are retrieved
+     * @see SearchControls#setReturningAttributes(String[])
+     */
+    public void setRetrievedAttributes(String attrs) {
+        if (GenericUtils.isEmpty(attrs)) {
+            searchControls.setReturningAttributes(GenericUtils.EMPTY_STRING_ARRAY);
+        } else if (ALL_LDAP_ATTRIBUTES.equals(attrs)) {
+            searchControls.setReturningAttributes(null);
+        } else {
+            searchControls.setReturningAttributes(GenericUtils.split(attrs, ','));
+        }
+    }
+
+    public boolean isAccumulateMultiValues() {
+        return accumulateMultiValues;
+    }
+
+    public void setAccumulateMultiValues(boolean enabled) {
+        accumulateMultiValues = enabled;
+    }
+
+    /**
+     * @return {@code true} if objects are returned as result of the query
+     * @see SearchControls#getReturningObjFlag()
+     */
+    public boolean isReturningObjFlag() {
+        return searchControls.getReturningObjFlag();
+    }
+
+    public void setReturningObjFlag(boolean enabled) {
+        searchControls.setReturningObjFlag(enabled);
+    }
+
+    /**
+     * @return Authentication mode to use: &qout;none&quot;, &quot;simple&quot;, etc.
+     * @see Context#SECURITY_AUTHENTICATION
+     */
+    public String getAuthenticationMode() {
+        return Objects.toString(ldapEnv.get(Context.SECURITY_AUTHENTICATION), null);
+    }
+
+    public void setAuthenticationMode(String mode) {
+        ldapEnv.put(Context.SECURITY_AUTHENTICATION, ValidateUtils.checkNotNull(mode, "No authentication mode"));
+    }
+
+    /**
+     * @return How referrals encountered by the service provider are to be processed
+     * @see Context#REFERRAL
+     */
+    public String getReferralMode() {
+        return Objects.toString(ldapEnv.get(Context.REFERRAL), null);
+    }
+
+    public void setReferralMode(String mode) {
+        ldapEnv.put(Context.REFERRAL, ValidateUtils.checkNotNullAndNotEmpty(mode, "No referral mode"));
+    }
+
+    /**
+     * @return The specified protocol version - non-positive if default provider version used
+     */
+    public int getProtocolVersion() {
+        Object value = ldapEnv.get("java.naming.ldap.version");
+        return (value != null) ? ((Number) value).intValue() : -1;
+    }
+
+    public void setProtocolVersion(int value) {
+        ValidateUtils.checkTrue(value > 0, "Non-positive protocol value: %d", value);
+        ldapEnv.put("java.naming.ldap.version", value);
+    }
+
+    /**
+     * @return Comma separated list of attributes known to be binary
+     * so that they are returned as {@code byte[]} value rather than strings
+     */
+    public String getBinaryAttributes() {
+        return Objects.toString(ldapEnv.get("java.naming.ldap.attributes.binary"), "").replace(' ', ',');
+    }
+
+    /**
+     * @param value Comma separated list of attributes known to be binary
+     * so that they are returned as {@code byte[]} value rather than strings
+     * @see <A HREF="http://docs.oracle.com/javase/jndi/tutorial/ldap/misc/attrs.html">LDAP Attributes</A>
+     */
+    public void setBinaryAttributes(String value) {
+        value = ValidateUtils.checkNotNullAndNotEmpty(value, "No attributes").replace(',', ' ');
+        ldapEnv.put("java.naming.ldap.attributes.binary", value);
+    }
+
+    /**
+     * @param username Username to be used either to access the LDAP or retrieve the user's attributes -
+     *                 may be {@code null}/empty if not required for the specific query
+     * @param password Password Password to be used if necessary - may be {@code null}/empty if not
+     *                 required for the specific query
+     * @param queryContext User specific query context - relevant only for derived classes that want
+     *                 to override some of query processing methods
+     * @return A {@link Map} of the retrieved attributes - <B>Note:</B> if {@link #isAccumulateMultiValues()}
+     * is {@code true} and multiple values are encountered for an attribute then a {@link List} of them is
+     * mapped as its value
+     * @throws NamingException If failed to executed the LDAP query
+     */
+    public Map<String, Object> resolveAttributes(String username, String password, Object queryContext) throws NamingException {
+        DirContext context = initializeDirContext(queryContext, ldapEnv, username, password);
+        try {
+            Map<?, ?> ldapConfig = context.getEnvironment();
+            String baseDN = resolveBaseDN(queryContext, ldapConfig, username, password);
+            String filter = resolveSearchFilter(queryContext, ldapConfig, username, password);
+            NamingEnumeration<? extends SearchResult> result =
+                    context.search(ValidateUtils.checkNotNullAndNotEmpty(baseDN, "No base DN"),
+                                   ValidateUtils.checkNotNullAndNotEmpty(filter, "No filter"),
+                                   searchControls);
+            try {
+                Map<String, Object> attrsMap = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
+                String referralMode = Objects.toString(ldapConfig.get(Context.REFERRAL), null);
+                for (int index = 0;; index++) {
+                    if (!result.hasMore()) {
+                        break;
+                    }
+
+                    processSearchResult(queryContext, ldapConfig, attrsMap, index, result.next());
+
+                    // if not following referrals stop at the 1st result regardless if there are others
+                    if ("ignore".equals(referralMode)) {
+                        break;
+                    }
+                }
+
+                return attrsMap;
+            } finally {
+                result.close();
+            }
+        } finally {
+            context.close();
+        }
+    }
+
+    protected DirContext initializeDirContext(Object queryContext, Map<String, ?> ldapConfig, String username, String password) throws NamingException {
+        Map<String, Object> env;
+        synchronized (ldapConfig) { // create a copy so we can change it
+            env = new HashMap<String, Object>(ldapConfig);
+        }
+
+        if (!env.containsKey(Context.PROVIDER_URL)) {
+            int port = getPort();
+            ValidateUtils.checkTrue(port > 0, "No port configured");
+            String url = ValidateUtils.checkNotNullAndNotEmpty(getProtocol(), "No protocol")
+                       + "://" + ValidateUtils.checkNotNullAndNotEmpty(getHost(), "No host")
+                       + ":" + port;
+            env.put(Context.PROVIDER_URL, url);
+        }
+
+        String mode = Objects.toString(env.get(Context.SECURITY_AUTHENTICATION), null);
+        boolean anonymous = GenericUtils.isEmpty(mode) || "none".equalsIgnoreCase(mode);
+        if (!anonymous) {
+            Object[] bindParams = {username, password};
+            if (!env.containsKey(Context.SECURITY_PRINCIPAL)) {
+                String bindDN = ValidateUtils.checkNotNull(bindDNPattern, "No bind DN pattern").format(bindParams);
+                env.put(Context.SECURITY_PRINCIPAL, ValidateUtils.checkNotNullAndNotEmpty(bindDN, "No bind DN"));
+            }
+
+            if (!env.containsKey(Context.SECURITY_CREDENTIALS)) {
+                String bindPassword = ValidateUtils.checkNotNull(bindPasswordPattern, "No bind password pattern").format(bindParams);
+                env.put(Context.SECURITY_CREDENTIALS, ValidateUtils.checkNotNullAndNotEmpty(bindPassword, "No bind password"));
+            }
+        }
+
+        return new InitialDirContext(new Hashtable<String, Object>(env));
+    }
+
+    protected String resolveBaseDN(Object queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+        Object[] bindParams = {username, password};
+        return ValidateUtils.checkNotNull(baseDNPattern, "No base DN pattern").format(bindParams);
+    }
+
+    protected String resolveSearchFilter(Object queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+        Object[] bindParams = {username, password};
+        return ValidateUtils.checkNotNull(searchFilterPattern, "No search filter pattern").format(bindParams);
+    }
+
+    protected void processSearchResult(Object queryContext, Map<?, ?> ldapConfig, Map<String, Object> attrsMap,
+            int resultIndex, SearchResult result)
+                    throws NamingException {
+        String dn = result.getName();
+        accumulateAttributeValue(queryContext, attrsMap, Context.AUTHORITATIVE, dn);
+
+        Attributes attrs = result.getAttributes();
+        NamingEnumeration<? extends Attribute>  attrVals = attrs.getAll();
+        try {
+            while (attrVals.hasMore()) {
+                processResultAttributeValue(queryContext, ldapConfig, dn, resultIndex, attrsMap, attrVals.next());
+            }
+        } finally {
+            attrVals.close();
+        }
+    }
+
+    // returns the most up-to-date value mapped for the attribute
+    protected Object processResultAttributeValue(Object queryContext, Map<?, ?> ldapConfig,
+            String dn, int resultIndex, Map<String, Object> attrsMap, Attribute a)
+                    throws NamingException {
+        String attrID = a.getID();
+        int numValues = a.size();
+        for (int index = 0; index < numValues; index++) {
+            Object attrVal = a.get(index);
+
+            if (attrVal != null) {
+                Object  prev = accumulateAttributeValue(queryContext, attrsMap, attrID, attrVal);
+                if (log.isTraceEnabled()) {
+                    if (prev != null) {
+                        log.trace("processResultAttributeValue({})[{}] multiple values: {} / {}",
+                                  dn, attrID, toString(prev), toString(attrVal));
+                    } else {
+                        log.trace("processResultAttributeValue({}) {} = {}", dn, attrID, toString(attrVal));
+                    }
+                }
+            } else {
+                if (log.isTraceEnabled()) {
+                    log.trace("processResultAttributeValue({}) skip null attribute: {}", dn, attrID);
+                }
+            }
+
+            if ((numValues > 1) && (!isAccumulateMultiValues())) {
+                if (log.isTraceEnabled()) {
+                    log.trace("processResultAttributeValue({})[{}] skip remaining {} values",
+                              dn, attrID, numValues - 1);
+                }
+
+                break;
+            }
+        }
+
+        return attrsMap.get(attrID);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected Object accumulateAttributeValue(Object queryContext, Map<String, Object> attrsMap, String attrID, Object attrVal) {
+        Object prev = attrsMap.put(attrID, attrVal);
+        if (prev == null) {
+            return null;    // debug breakpoint
+        }
+
+        List<Object> values = null;
+        if (prev instanceof List<?>) {
+            values = (List<Object>) prev;
+        } else {
+            values = new ArrayList<Object>();
+            values.add(prev);
+            attrsMap.put(attrID, values);
+        }
+
+        values.add(attrVal);
+        return values.get(values.size() - 2);
+    }
+
+    public static String toString(Object attrVal) {
+        if (attrVal == null) {
+            return null;
+        }
+
+        Class<?> attrType = attrVal.getClass();
+        if (attrType.isArray()) {
+            return (attrVal instanceof byte[]) ? BufferUtils.printHex((byte[]) attrVal) : Arrays.toString((Object[]) attrVal);
+        }
+
+        return attrVal.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
new file mode 100644
index 0000000..251a3a8
--- /dev/null
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
@@ -0,0 +1,71 @@
+/*
+ * 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.server.auth.password;
+
+import java.util.Map;
+
+import javax.naming.NamingException;
+
+import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LdapPasswordAuthenticator extends LdapNetworkConnector implements PasswordAuthenticator {
+    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+    public static final String DEFAULT_PASSWORD_ATTR_NAME = "userPassword";
+
+    public static final String DEFAULT_SEARCH_FILTER_PATTERN =
+            "(&(" + DEFAULT_USERNAME_ATTR_NAME + "={0})(" + DEFAULT_PASSWORD_ATTR_NAME + "={1}))";
+    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+
+    public LdapPasswordAuthenticator() {
+        setRetrievedAttributes(null);
+        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
+        setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
+    }
+
+    @Override
+    public boolean authenticate(String username, String password, ServerSession session) throws PasswordChangeRequiredException {
+        try {
+            Map<String, ?> attrs = resolveAttributes(username, password, session);
+            return authenticate(username, password, session, attrs);
+        } catch (NamingException | RuntimeException e) {
+            log.warn("authenticate({}@{}) failed ({}) to query: {}",
+                      username, session, e.getClass().getSimpleName(), e.getMessage());
+
+            if (log.isDebugEnabled()) {
+                log.debug("authenticate(" + username + "@" + session + ") query failure details", e);
+            }
+
+            return false;
+        }
+    }
+
+    protected boolean authenticate(String username, String password, ServerSession session, Map<String, ?> attrs) {
+        /*
+         * By default we assume that the user + password are the same for
+         * accessing the LDAP as the user's account, so the very LDAP query
+         * success is enough
+         */
+        return true;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/main/resources/.gitignore
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/resources/.gitignore b/sshd-ldap/src/main/resources/.gitignore
new file mode 100644
index 0000000..e69de29

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
new file mode 100644
index 0000000..8c0fa47
--- /dev/null
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.server.auth;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.directory.server.constants.ServerDNConstants;
+import org.apache.directory.server.core.CoreSession;
+import org.apache.directory.server.core.DefaultDirectoryService;
+import org.apache.directory.server.core.DirectoryService;
+import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
+import org.apache.directory.server.core.partition.ldif.LdifPartition;
+import org.apache.directory.server.core.schema.SchemaPartition;
+import org.apache.directory.server.core.schema.SchemaService;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+import org.apache.directory.server.protocol.shared.transport.Transport;
+import org.apache.directory.shared.ldap.entry.Entry;
+import org.apache.directory.shared.ldap.entry.EntryAttribute;
+import org.apache.directory.shared.ldap.ldif.ChangeType;
+import org.apache.directory.shared.ldap.ldif.LdifEntry;
+import org.apache.directory.shared.ldap.ldif.LdifReader;
+import org.apache.directory.shared.ldap.message.AddRequestImpl;
+import org.apache.directory.shared.ldap.message.internal.InternalAddRequest;
+import org.apache.directory.shared.ldap.schema.SchemaManager;
+import org.apache.directory.shared.ldap.schema.ldif.extractor.SchemaLdifExtractor;
+import org.apache.directory.shared.ldap.schema.ldif.extractor.impl.DefaultSchemaLdifExtractor;
+import org.apache.directory.shared.ldap.schema.loader.ldif.LdifSchemaLoader;
+import org.apache.directory.shared.ldap.schema.manager.impl.DefaultSchemaManager;
+import org.apache.directory.shared.ldap.schema.registries.SchemaLoader;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class BaseAuthenticatorTest extends BaseTestSupport {
+    public static final int PORT = Integer.parseInt(System.getProperty("org.apache.sshd.test.ldap.port", "11389"));
+    public static String BASE_DN_TEST = "ou=People,dc=sshd,dc=apache,dc=org";
+
+    protected BaseAuthenticatorTest() {
+        super();
+    }
+
+    public static int getPort(Pair<LdapServer, DirectoryService> context) {
+        return getPort((context == null) ? null : context.getFirst());
+    }
+
+    public static int getPort(LdapServer ldapServer) {
+        return getPort((ldapServer == null) ? null : ldapServer.getTransports());
+    }
+
+    public static int getPort(Transport ... transports) {
+        return GenericUtils.isEmpty(transports) ? -1 : transports[0].getPort();
+    }
+
+    // see http://javlog.cacek.cz/2014/09/speed-up-apacheds-ldap-server.html
+    // see https://cwiki.apache.org/confluence/display/DIRxSRVx11/4.1.+Embedding+ApacheDS+into+an+application
+    // see http://stackoverflow.com/questions/1560230/running-apache-ds-embedded-in-my-application
+    public static Pair<LdapServer, DirectoryService> startApacheDs(Class<?> anchor) throws Exception {
+        Logger log = LoggerFactory.getLogger(anchor);
+        File targetFolder = ValidateUtils.checkNotNull(Utils.detectTargetFolder(anchor), "Failed to detect target folder");
+        File workingDirectory = assertHierarchyTargetFolderExists(Utils.deleteRecursive(Utils.resolve(targetFolder, anchor.getSimpleName(), "apacheds-work")));
+
+        DirectoryService directoryService = new DefaultDirectoryService();
+        directoryService.setWorkingDirectory(workingDirectory);
+
+        SchemaService schemaService = directoryService.getSchemaService();
+        SchemaPartition schemaPartition = schemaService.getSchemaPartition();
+        LdifPartition ldifPartition = new LdifPartition();
+        // see DefaultSchemaLdifExtractor#SCHEMA...
+        File schemaRepository = assertHierarchyTargetFolderExists(new File(workingDirectory, "schema"));
+        ldifPartition.setWorkingDirectory(schemaRepository.getAbsolutePath());
+
+        SchemaLdifExtractor extractor = new DefaultSchemaLdifExtractor(workingDirectory);
+        extractor.extractOrCopy(true);
+        schemaPartition.setWrappedPartition(ldifPartition);
+
+        SchemaLoader loader = new LdifSchemaLoader(schemaRepository);
+        SchemaManager schemaManager = new DefaultSchemaManager(loader);
+        directoryService.setSchemaManager(schemaManager);
+
+        schemaManager.loadAllEnabled();
+
+        schemaPartition.setSchemaManager(schemaManager);
+
+        List<Throwable> errors = schemaManager.getErrors();
+        if (GenericUtils.size(errors) > 0) {
+            log.error("Schema management loading errors found");
+            for (Throwable t : errors) {
+                log.error(t.getClass().getSimpleName() + ": " + t.getMessage(), t);
+            }
+            throw new Exception("Schema load failed");
+        }
+
+        {
+            JdbmPartition systemPartition = new JdbmPartition();
+            systemPartition.setId("system");
+            systemPartition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new File(workingDirectory, systemPartition.getId()))));
+            systemPartition.setSuffix(ServerDNConstants.SYSTEM_DN);
+            systemPartition.setSchemaManager(schemaManager);
+            directoryService.setSystemPartition(systemPartition);
+        }
+
+        // Create a new partition for the users
+        {
+            JdbmPartition partition = new JdbmPartition();
+            partition.setId("users");
+            partition.setSuffix(BASE_DN_TEST);
+            partition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new File(workingDirectory, partition.getId()))));
+            directoryService.addPartition(partition);
+        }
+
+        directoryService.setShutdownHookEnabled(true);
+        directoryService.getChangeLog().setEnabled(false);
+
+        LdapServer ldapServer = new LdapServer();
+        ldapServer.setTransports(new TcpTransport(PORT));
+        ldapServer.setDirectoryService(directoryService);
+
+        log.info("Starting directory service ...");
+        directoryService.startup();
+        log.info("Directory service started");
+
+        log.info("Starting LDAP server on port=" + getPort(ldapServer) + " ...");
+        try {
+            ldapServer.start();
+            log.info("LDAP server started");
+        } catch(Exception e) {
+            log.error("Failed (" + e.getClass().getSimpleName() + ") to start LDAP server: " + e.getMessage(), e);
+            e.printStackTrace(System.err);
+            stopApacheDs(directoryService);
+            throw e;
+        }
+
+        return new Pair<LdapServer, DirectoryService>(ldapServer, directoryService);
+    }
+
+    // see http://users.directory.apache.narkive.com/GkyqAkot/how-to-import-ldif-file-programmatically
+    public static Map<String, String> populateUsers(DirectoryService service, Class<?> anchor, String credentialName) throws Exception {
+        Logger log = LoggerFactory.getLogger(anchor);
+        CoreSession session = ValidateUtils.checkNotNull(service.getAdminSession(), "No core session");
+        Map<String, String> usersMap = new HashMap<>();
+        try (LdifReader reader = new LdifReader(ValidateUtils.checkNotNull(anchor.getResourceAsStream("/auth-users.ldif"), "No users ldif"))) {
+            int id = 1;
+            for (LdifEntry entry : reader) {
+                if (log.isDebugEnabled()) {
+                    log.debug("Add LDIF entry={}", entry);
+                }
+
+                ChangeType changeType = entry.getChangeType();
+                assertEquals("Mismatched change type in users ldif entry=" + entry, ChangeType.Add, changeType);
+
+                Entry data = entry.getEntry();
+                EntryAttribute userAttr = data.get("uid");
+                EntryAttribute passAttr = data.get(credentialName);
+                if ((userAttr != null) && (passAttr != null)) {
+                    String username = userAttr.getString();
+                    ValidateUtils.checkTrue(usersMap.put(username, passAttr.getString()) == null, "Multiple entries for user=%s", username);
+                }
+
+                InternalAddRequest addRequest = new AddRequestImpl(id++);
+                addRequest.setEntry(data);
+                try {
+                    session.add(addRequest);
+                } catch (Exception e) {
+                    log.error("Failed (" + e.getClass().getSimpleName() + ") to add entry=" + entry + ": " + e.getMessage(), e);
+                    throw e;
+                }
+            }
+        }
+
+        return usersMap;
+    }
+
+    public static void stopApacheDs(Pair<LdapServer, DirectoryService> context) throws Exception {
+        stopApacheDs((context == null) ? null : context.getFirst());
+        stopApacheDs((context == null) ? null : context.getSecond());
+    }
+
+    public static void stopApacheDs(LdapServer ldapServer) throws Exception {
+        if ((ldapServer == null) || (!ldapServer.isStarted())) {
+            return;
+        }
+
+        Logger log = LoggerFactory.getLogger(BaseAuthenticatorTest.class);
+        log.info("Stopping LDAP server...");
+        ldapServer.stop();
+        log.info("LDAP server stopped");
+    }
+
+    public static void stopApacheDs(DirectoryService directoryService) throws Exception {
+        if ((directoryService == null) || (!directoryService.isStarted())) {
+            return;
+        }
+
+        Logger log = LoggerFactory.getLogger(BaseAuthenticatorTest.class);
+        File workDir = directoryService.getWorkingDirectory();
+
+        log.info("Shutdown directory service ...");
+        directoryService.shutdown();
+        log.info("Directory service shut down");
+
+        log.info("Deleting " + workDir.getAbsolutePath());
+        Utils.deleteRecursive(workDir);
+        log.info(workDir.getAbsolutePath() + " deleted");
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
new file mode 100644
index 0000000..2a88342
--- /dev/null
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.server.auth.password;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.directory.server.core.DirectoryService;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.server.auth.BaseAuthenticatorTest;
+import org.apache.sshd.server.session.ServerSession;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LdapPasswordAuthenticatorTest extends BaseAuthenticatorTest {
+    private static final AtomicReference<Pair<LdapServer, DirectoryService>> ldapContextHolder = new AtomicReference<>();
+    private static Map<String, String> usersMap;
+
+    public LdapPasswordAuthenticatorTest() {
+        super();
+    }
+
+    @BeforeClass
+    public static void startApacheDs() throws Exception {
+        ldapContextHolder.set(startApacheDs(LdapPasswordAuthenticatorTest.class));
+        usersMap = populateUsers(ldapContextHolder.get().getSecond(), LdapPasswordAuthenticatorTest.class, LdapPasswordAuthenticator.DEFAULT_PASSWORD_ATTR_NAME);
+    }
+
+    @AfterClass
+    public static void stopApacheDs() throws Exception {
+        stopApacheDs(ldapContextHolder.getAndSet(null));
+    }
+
+    @Test   // the user's password is compared with the LDAP stored one
+    public void testPasswordComparison() throws Exception {
+        LdapPasswordAuthenticator auth = new LdapPasswordAuthenticator();
+        auth.setBaseDN(BASE_DN_TEST);
+        auth.setPort(getPort(ldapContextHolder.get()));
+
+        ServerSession session = Mockito.mock(ServerSession.class);
+        for (Map.Entry<String, String> ue : usersMap.entrySet()) {
+            String username = ue.getKey();
+            String password = ue.getValue();
+            outputDebugMessage("Authenticate: user=%s, password=%s", username, password);
+            assertTrue("Failed to authenticate " + username, auth.authenticate(username, password, session));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/resources/auth-users.ldif
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/auth-users.ldif b/sshd-ldap/src/test/resources/auth-users.ldif
new file mode 100644
index 0000000..4792099
--- /dev/null
+++ b/sshd-ldap/src/test/resources/auth-users.ldif
@@ -0,0 +1,31 @@
+version: 1
+
+dn: ou=People,dc=sshd,dc=apache,dc=org
+ou: People
+objectClass: top
+objectClass: organizationalUnit
+description: Parent object of all users accounts
+
+dn: cn=Guillaume Nodet,ou=People,dc=sshd,dc=apache,dc=org
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Guillaume Nodet
+givenName: Guillaume
+sn: Nodet
+uid: gnodet
+userpassword: gnodet
+mail: gnodet@apache.org
+
+dn: cn=Lyor Goldstein,ou=People,dc=sshd,dc=apache,dc=org
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Lyor Goldstein
+givenName: Lyor
+sn: Goldstein
+uid: lgoldstein
+userpassword: lgoldstein
+mail: lgoldstein@apache.org

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/resources/hostkey.pem
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/hostkey.pem b/sshd-ldap/src/test/resources/hostkey.pem
new file mode 100644
index 0000000..18d68ac
--- /dev/null
+++ b/sshd-ldap/src/test/resources/hostkey.pem
@@ -0,0 +1,30 @@
+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.
+
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDdfIWeSV4o68dRrKSzFd/Bk51E65UTmmSrmW0O1ohtzi6HzsDP
+jXgCtlTt3FqTcfFfI92IlTr4JWqC9UK1QT1ZTeng0MkPQmv68hDANHbt5CpETZHj
+W5q4OOgWhVvj5IyOC2NZHtKlJBkdsMAa15ouOOJLzBvAvbqOR/yUROsEiQIDAQAB
+AoGBANG3JDW6NoP8rF/zXoeLgLCj+tfVUPSczhGFVrQkAk4mWfyRkhN0WlwHFOec
+K89MpkV1ij/XPVzU4MNbQ2yod1KiDylzvweYv+EaEhASCmYNs6LS03punml42SL9
+97tOmWfVJXxlQoLiY6jHPU97vTc65k8gL+gmmrpchsW0aqmZAkEA/c8zfmKvY37T
+cxcLLwzwsqqH7g2KZGTf9aRmx2ebdW+QKviJJhbdluDgl1TNNFj5vCLznFDRHiqJ
+wq0wkZ39cwJBAN9l5v3kdXj21UrurNPdlV0n2GZBt2vblooQC37XHF97r2zM7Ou+
+Lg6MyfJClyguhWL9dxnGbf3btQ0l3KDstxMCQCRaiEqjAfIjWVATzeNIXDWLHXso
+b1kf5cA+cwY+vdKdTy4IeUR+Y/DXdvPWDqpf0C11aCVMohdLCn5a5ikFUycCQDhV
+K/BuAallJNfmY7JxN87r00fF3ojWMJnT/fIYMFFrkQrwifXQWTDWE76BSDibsosJ
+u1TGksnm8zrDh2UVC/0CQFrHTiSl/3DHvWAbOJawGKg46cnlDcAhSyV8Frs8/dlP
+7YGG3eqkw++lsghqmFO6mRUTKsBmiiB2wgLGhL5pyYY=
+-----END RSA PRIVATE KEY-----

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/log4j.properties b/sshd-ldap/src/test/resources/log4j.properties
new file mode 100644
index 0000000..590c257
--- /dev/null
+++ b/sshd-ldap/src/test/resources/log4j.properties
@@ -0,0 +1,38 @@
+#
+# 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.
+#
+#
+
+#
+# The logging properties used during tests..
+#
+log4j.rootLogger=INFO, stdout, logfile
+#log4j.logger.org.apache.sshd=TRACE
+#log4j.logger.org.apache.sshd.common.channel.Window=DEBUG
+
+# CONSOLE appender
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} - %m%n
+
+# File appender
+log4j.appender.logfile=org.apache.log4j.FileAppender
+log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
+log4j.appender.logfile.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} - %m%n
+log4j.appender.logfile.file=target/sshd-ldap-tests.log
+log4j.appender.logfile.append=true


[2/4] mina-sshd git commit: [SSHD-607] Provide PasswordAuthenticator that uses LDAP

Posted by lg...@apache.org.
[SSHD-607] Provide PasswordAuthenticator that uses LDAP


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

Branch: refs/heads/master
Commit: f374e72c59a63de8a65537111c3c10b487ed62c6
Parents: 458c38f
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue Jan 19 10:50:20 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue Jan 19 10:50:20 2016 +0200

----------------------------------------------------------------------
 pom.xml                                         |  12 +-
 .../java/org/apache/sshd/client/SshClient.java  |   2 +-
 .../java/org/apache/sshd/client/SshKeyScan.java |   2 +-
 .../auth/hostbased/UserAuthHostBased.java       |   2 +-
 .../sshd/client/channel/ChannelDirectTcpip.java |   2 +-
 .../client/session/AbstractClientSession.java   |   2 +-
 .../sshd/client/session/ClientSession.java      |   2 +-
 .../apache/sshd/common/SshdSocketAddress.java   | 415 ----------------
 .../sshd/common/config/SshConfigFileReader.java |   2 +-
 .../common/forward/DefaultTcpipForwarder.java   |   2 +-
 .../common/forward/LocalForwardingEntry.java    |   2 +-
 .../apache/sshd/common/forward/SocksProxy.java  |   2 +-
 .../sshd/common/forward/TcpipClientChannel.java |   2 +-
 .../sshd/common/forward/TcpipForwarder.java     |   2 +-
 .../sshd/common/util/net/NetworkConnector.java  |  90 ++++
 .../sshd/common/util/net/SshdSocketAddress.java | 415 ++++++++++++++++
 .../sshd/server/forward/ForwardingFilter.java   |   2 +-
 .../forward/StaticDecisionForwardingFilter.java |   2 +-
 .../sshd/server/forward/TcpipServerChannel.java |   2 +-
 .../global/CancelTcpipForwardHandler.java       |   2 +-
 .../sshd/server/global/TcpipForwardHandler.java |   2 +-
 .../sshd/server/x11/X11ForwardSupport.java      |   2 +-
 .../test/java/org/apache/sshd/ProxyTest.java    |   2 +-
 .../hosts/HostConfigEntryResolverTest.java      |   2 +-
 .../sshd/common/ForwardingFilterTest.java       |   1 +
 .../sshd/common/auth/AuthenticationTest.java    |   2 +-
 .../sshd/common/forward/PortForwardingTest.java |   2 +-
 .../apache/sshd/util/test/BaseTestSupport.java  |   2 +-
 .../java/org/apache/sshd/util/test/Utils.java   |  19 +
 .../apache/sshd/git/pgm/GitPgmCommandTest.java  |   2 +-
 sshd-ldap/pom.xml                               | 139 ++++++
 .../common/util/net/LdapNetworkConnector.java   | 494 +++++++++++++++++++
 .../password/LdapPasswordAuthenticator.java     |  71 +++
 sshd-ldap/src/main/resources/.gitignore         |   0
 .../sshd/server/auth/BaseAuthenticatorTest.java | 233 +++++++++
 .../password/LdapPasswordAuthenticatorTest.java |  74 +++
 sshd-ldap/src/test/resources/auth-users.ldif    |  31 ++
 sshd-ldap/src/test/resources/hostkey.pem        |  30 ++
 sshd-ldap/src/test/resources/log4j.properties   |  38 ++
 39 files changed, 1660 insertions(+), 450 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index c883bfc..bf0d8db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -961,20 +961,10 @@
         </repository>
     </distributionManagement>
 
-    <profiles>
-        <profile>
-            <id>experimental</id>
-            <modules>
-                <module>sshd-pam</module>
-                <module>sshd-sftp</module>
-            </modules>
-        </profile>
-    </profiles>
-
     <modules>
         <module>sshd-core</module>
+        <module>sshd-ldap</module>
         <module>sshd-git</module>
         <module>assembly</module>
     </modules>
-
 </project>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index f5f50b0..8c31468 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -85,7 +85,6 @@ import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.ServiceFactory;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.Channel;
 import org.apache.sshd.common.config.SshConfigFileReader;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
@@ -105,6 +104,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
 import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * <P>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java b/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
index ca9e4e2..27ba3ce 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshKeyScan.java
@@ -58,7 +58,6 @@ import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.cipher.ECCurves;
 import org.apache.sshd.common.config.SshConfigFileReader;
 import org.apache.sshd.common.config.keys.BuiltinIdentities;
@@ -78,6 +77,7 @@ import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
 import org.apache.sshd.common.util.logging.AbstractSimplifiedLog;
 import org.apache.sshd.common.util.logging.LoggingUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * A naive implementation of <A HREF="https://www.freebsd.org/cgi/man.cgi?query=ssh-keyscan&sektion=1">ssh-keyscan(1)</A>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
index a8313f3..28f2781 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/auth/hostbased/UserAuthHostBased.java
@@ -30,7 +30,6 @@ import org.apache.sshd.client.auth.AbstractUserAuth;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.SshConstants;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.signature.SignatureFactoriesManager;
@@ -41,6 +40,7 @@ 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.common.util.net.SshdSocketAddress;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelDirectTcpip.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelDirectTcpip.java b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelDirectTcpip.java
index 4f1aa99..945d431 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelDirectTcpip.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/channel/ChannelDirectTcpip.java
@@ -26,7 +26,6 @@ import org.apache.sshd.client.future.DefaultOpenFuture;
 import org.apache.sshd.client.future.OpenFuture;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.ChannelAsyncInputStream;
 import org.apache.sshd.common.channel.ChannelAsyncOutputStream;
 import org.apache.sshd.common.channel.ChannelOutputStream;
@@ -34,6 +33,7 @@ import org.apache.sshd.common.channel.ChannelPipedInputStream;
 import org.apache.sshd.common.channel.ChannelPipedOutputStream;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * TODO Add javadoc

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index 3976971..936866f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -45,7 +45,6 @@ import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.Channel;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.forward.TcpipForwarder;
@@ -55,6 +54,7 @@ import org.apache.sshd.common.session.AbstractSession;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index bcb95ee..9d7acbd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -35,10 +35,10 @@ import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.scp.ScpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpClient;
 import org.apache.sshd.client.subsystem.sftp.SftpVersionSelector;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.future.KeyExchangeFuture;
 import org.apache.sshd.common.scp.ScpTransferEventListener;
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * <P>An authenticated session to a given SSH server</P>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/SshdSocketAddress.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/SshdSocketAddress.java b/sshd-core/src/main/java/org/apache/sshd/common/SshdSocketAddress.java
deleted file mode 100644
index 8b8a507..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/SshdSocketAddress.java
+++ /dev/null
@@ -1,415 +0,0 @@
-/*
- * 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;
-
-import java.net.Inet4Address;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.NetworkInterface;
-import java.net.SocketAddress;
-import java.net.SocketException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Objects;
-
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.NumberUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * <P>A simple socket address holding the host name and port number. The reason
- * it does not extend {@link InetSocketAddress} is twofold:</P>
- * <OL>
- * <LI><P>
- * The {@link InetSocketAddress} performs a DNS resolution on the
- * provided host name - which we don't want do use until we want to
- * create a connection using this address (thus the {@link #toInetSocketAddress()}
- * call which executes this query
- * </P></LI>
- *
- * <LI><P>
- * If empty host name is provided we replace it with the <I>any</I>
- * address of 0.0.0.0
- * </P></LI>
- * </OL>
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class SshdSocketAddress extends SocketAddress {
-    public static final String LOCALHOST_NAME = "localhost";
-    public static final String LOCALHOST_IP = "127.0.0.1";
-    public static final String IP_ANYADDR = "0.0.0.0";
-
-    // 10.0.0.0 - 10.255.255.255
-    public static final String PRIVATE_CLASS_A_PREFIX = "10.";
-    // 172.16.0.0 - 172.31.255.255
-    public static final String PRIVATE_CLASS_B_PREFIX = "172.";
-    // 192.168.0.0 - 192.168.255.255
-    public static final String PRIVATE_CLASS_C_PREFIX = "192.168.";
-    // 100.64.0.0 - 100.127.255.255
-    public static final String CARRIER_GRADE_NAT_PREFIX = "100.";
-    // The IPv4 broadcast address
-    public static final String BROADCAST_ADDRESS = "255.255.255.255";
-
-    /**
-     * A dummy placeholder that can be used instead of {@code null}s
-     */
-    public static final SshdSocketAddress LOCALHOST_ADDRESS = new SshdSocketAddress(LOCALHOST_IP, 0);
-
-    /**
-     * Compares {@link InetAddress}-es according to their {@link InetAddress#getHostAddress()}
-     * value case <U>insensitive</U>
-     */
-    public static final Comparator<InetAddress> BY_HOST_ADDRESS = new Comparator<InetAddress>() {
-            @Override
-            public int compare(InetAddress a1, InetAddress a2) {
-                String n1 = GenericUtils.trimToEmpty(toAddressString(a1));
-                String n2 = GenericUtils.trimToEmpty(toAddressString(a2));
-                return String.CASE_INSENSITIVE_ORDER.compare(n1, n2);
-            }
-        };
-
-    private static final long serialVersionUID = 6461645947151952729L;
-
-    private final String hostName;
-    private final int port;
-
-    public SshdSocketAddress(int port) {
-        this(IP_ANYADDR, port);
-    }
-
-    public SshdSocketAddress(String hostName, int port) {
-        ValidateUtils.checkNotNull(hostName, "Host name may not be null");
-        this.hostName = GenericUtils.isEmpty(hostName) ? IP_ANYADDR : hostName;
-
-        ValidateUtils.checkTrue(port >= 0, "Port must be >= 0: %d", port);
-        this.port = port;
-    }
-
-    public String getHostName() {
-        return hostName;
-    }
-
-    public int getPort() {
-        return port;
-    }
-
-    public InetSocketAddress toInetSocketAddress() {
-        return new InetSocketAddress(getHostName(), getPort());
-    }
-
-    @Override
-    public String toString() {
-        return getHostName() + ":" + getPort();
-    }
-
-    protected boolean isEquivalent(SshdSocketAddress that) {
-        if (that == null) {
-            return false;
-        } else if (that == this) {
-            return true;
-        } else {
-            return (this.getPort() == that.getPort())
-                    && Objects.equals(this.getHostName(), that.getHostName());
-        }
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (o == null) {
-            return false;
-        }
-        if (getClass() != o.getClass()) {
-            return false;
-        }
-        return isEquivalent((SshdSocketAddress) o);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hashCode(getHostName()) + getPort();
-    }
-
-
-    /**
-     * Returns the first external network address assigned to this
-     * machine or null if one is not found.
-     * @return Inet4Address associated with an external interface
-     * DevNote:  We actually return InetAddress here, as Inet4Addresses are final and cannot be mocked.
-     */
-    public static InetAddress getFirstExternalNetwork4Address() {
-        List<? extends InetAddress> addresses = getExternalNetwork4Addresses();
-        return (GenericUtils.size(addresses) > 0) ? addresses.get(0) : null;
-    }
-
-    /**
-     * @return a {@link List} of local network addresses which are not multicast
-     * or localhost sorted according to {@link #BY_HOST_ADDRESS}
-     */
-    public static List<InetAddress> getExternalNetwork4Addresses() {
-        List<InetAddress> addresses = new ArrayList<InetAddress>();
-        try {
-            for (Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); (nets != null) && nets.hasMoreElements();) {
-                NetworkInterface netint = nets.nextElement();
-                /* TODO - uncomment when 1.5 compatibility no longer required
-                if (!netint.isUp()) {
-                    continue;    // ignore non-running interfaces
-                }
-                */
-
-                for (Enumeration<InetAddress> inetAddresses = netint.getInetAddresses(); (inetAddresses != null) && inetAddresses.hasMoreElements();) {
-                    InetAddress inetAddress = inetAddresses.nextElement();
-                    if (isValidHostAddress(inetAddress)) {
-                        addresses.add(inetAddress);
-                    }
-                }
-            }
-        } catch (SocketException e) {
-            // swallow
-        }
-
-        if (GenericUtils.size(addresses) > 1) {
-            Collections.sort(addresses, BY_HOST_ADDRESS);
-        }
-
-        return addresses;
-    }
-
-    /**
-     * @param addr The {@link InetAddress} to be verified
-     * @return <P><code>true</code> if the address is:</P></BR>
-     * <UL>
-     *         <LI>Not <code>null</code></LI>
-     *         <LI>An {@link Inet4Address}</LI>
-     *         <LI>Not link local</LI>
-     *         <LI>Not a multicast</LI>
-     *         <LI>Not a loopback</LI>
-     * </UL>
-     * @see InetAddress#isLinkLocalAddress()
-     * @see InetAddress#isMulticastAddress()
-     * @see InetAddress#isMulticastAddress()
-     */
-    public static boolean isValidHostAddress(InetAddress addr) {
-        if (addr == null) {
-            return false;
-        }
-
-        if (addr.isLinkLocalAddress()) {
-            return false;
-        }
-
-        if (addr.isMulticastAddress()) {
-            return false;
-        }
-
-        if (!(addr instanceof Inet4Address)) {
-            return false;
-        }
-
-        if (isLoopback(addr)) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * @param addr The {@link InetAddress} to be considered
-     * @return <code>true</code> if the address is a loopback one.
-     * <B>Note:</B> if {@link InetAddress#isLoopbackAddress()}
-     * returns <code>false</code> the address <U>string</U> is checked
-     * @see #toAddressString(InetAddress)
-     * @see #isLoopback(String)
-     */
-    public static boolean isLoopback(InetAddress addr) {
-        if (addr == null) {
-            return false;
-        }
-
-        if (addr.isLoopbackAddress()) {
-            return true;
-        }
-
-        String ip = toAddressString(addr);
-        return isLoopback(ip);
-    }
-
-    /**
-     * @param ip IP value to be tested
-     * @return <code>true</code> if the IP is &quot;localhost&quot; or
-     * &quot;127.x.x.x&quot;.
-     */
-    public static boolean isLoopback(String ip) {
-        if (GenericUtils.isEmpty(ip)) {
-            return false;
-        }
-
-        if (LOCALHOST_NAME.equals(ip) || LOCALHOST_IP.equals(ip)) {
-            return true;
-        }
-
-        String[] values = GenericUtils.split(ip, '.');
-        if (GenericUtils.length(values) != 4) {
-            return false;
-        }
-
-        for (int index = 0; index < values.length; index++) {
-            String val = values[index];
-            if (!isValidIPv4AddressComponent(val)) {
-                return false;
-            }
-
-            if (index == 0) {
-                int number = Integer.parseInt(val);
-                if (number != 127) {
-                    return false;
-                }
-            }
-        }
-
-        return true;
-    }
-
-    public static String toAddressString(InetAddress addr) {
-        String ip = (addr == null) ? null : addr.toString();
-        if (GenericUtils.isEmpty(ip)) {
-            return null;
-        } else {
-            return ip.replaceAll(".*/", "");
-        }
-    }
-
-    public static final boolean isIPv4Address(String addr) {
-        if (GenericUtils.isEmpty(addr)) {
-            return false;
-        }
-
-        String[] comps = GenericUtils.split(addr, '.');
-        if (GenericUtils.length(comps) != 4) {
-            return false;
-        }
-
-        for (String c : comps) {
-            if (!isValidIPv4AddressComponent(c)) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Checks if the address is one of the allocated private blocks
-     * @param addr The address string
-     * @return {@code true} if this is one of the allocated private
-     * blocks. <B>Note:</B> it assumes that the address string is
-     * indeed an IPv4 address
-     * @see #isIPv4Address(String)
-     * @see #PRIVATE_CLASS_A_PREFIX
-     * @see #PRIVATE_CLASS_B_PREFIX
-     * @see #PRIVATE_CLASS_C_PREFIX
-     * @see <A HREF="http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces">Wiki page</A>
-     */
-    public static final boolean isPrivateIPv4Address(String addr) {
-        if (GenericUtils.isEmpty(addr)) {
-            return false;
-        }
-
-        if (addr.startsWith(PRIVATE_CLASS_A_PREFIX) || addr.startsWith(PRIVATE_CLASS_C_PREFIX)) {
-            return true;
-        }
-
-        // for 172.x.x.x we need further checks
-        if (!addr.startsWith(PRIVATE_CLASS_B_PREFIX)) {
-            return false;
-        }
-
-        int nextCompPos = addr.indexOf('.', PRIVATE_CLASS_B_PREFIX.length());
-        if (nextCompPos <= PRIVATE_CLASS_B_PREFIX.length()) {
-            return false;
-        }
-
-        String value = addr.substring(PRIVATE_CLASS_B_PREFIX.length(), nextCompPos);
-        if (!isValidIPv4AddressComponent(value)) {
-            return false;
-        }
-
-        int v = Integer.parseInt(value);
-        return (v >= 16) && (v <= 31);
-    }
-
-    /**
-     * @param addr The address to be checked
-     * @return {@code true} if the address is in the 100.64.0.0/10 range
-     * @see <A HREF="http://tools.ietf.org/html/rfc6598">RFC6598</A>
-     */
-    public static final boolean isCarrierGradeNatIPv4Address(String addr) {
-        if (GenericUtils.isEmpty(addr)) {
-            return false;
-        }
-
-        if (!addr.startsWith(CARRIER_GRADE_NAT_PREFIX)) {
-            return false;
-        }
-
-        int nextCompPos = addr.indexOf('.', CARRIER_GRADE_NAT_PREFIX.length());
-        if (nextCompPos <= CARRIER_GRADE_NAT_PREFIX.length()) {
-            return false;
-        }
-
-        String value = addr.substring(CARRIER_GRADE_NAT_PREFIX.length(), nextCompPos);
-        if (!isValidIPv4AddressComponent(value)) {
-            return false;
-        }
-
-        int v = Integer.parseInt(value);
-        return (v >= 64) && (v <= 127);
-    }
-
-    /**
-     * <P>Checks if the provided argument is a valid IPv4 address component:</P></BR>
-     * <UL>
-     *     <LI>Not {@code null}/empty</LI>
-     *     <LI>Has at most 3 <U>digits</U></LI>
-     *     <LI>Its value is &le; 255</LI>
-     * </UL>
-     * @param c The {@link CharSequence} to be validate
-     * @return {@code true} if valid IPv4 address component
-     */
-    public static final boolean isValidIPv4AddressComponent(CharSequence c) {
-        if (GenericUtils.isEmpty(c) || (c.length() > 3)) {
-            return false;
-        }
-
-        char ch = c.charAt(0);
-        if ((ch < '0') || (ch > '9')) {
-            return false;
-        }
-
-        if (!NumberUtils.isIntegerNumber(c)) {
-            return false;
-        }
-
-        int v = Integer.parseInt(c.toString());
-        return (v >= 0) && (v <= 255);
-    }
-}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java b/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
index dc31ad6..b89a4ec 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/config/SshConfigFileReader.java
@@ -43,7 +43,6 @@ import org.apache.sshd.client.SshClient;
 import org.apache.sshd.common.AbstractFactoryManager;
 import org.apache.sshd.common.BuiltinFactory;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.cipher.BuiltinCiphers;
 import org.apache.sshd.common.cipher.Cipher;
 import org.apache.sshd.common.compression.BuiltinCompressions;
@@ -63,6 +62,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.IoUtils;
 import org.apache.sshd.common.util.io.NoCloseInputStream;
 import org.apache.sshd.common.util.io.NoCloseReader;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.ServerBuilder;
 import org.apache.sshd.server.SshServer;
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
index 3115e80..69dc6db 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultTcpipForwarder.java
@@ -37,7 +37,6 @@ import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoHandler;
@@ -53,6 +52,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.closeable.AbstractInnerCloseable;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.forward.ForwardingFilter;
 
 /**

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
index 6be6fab..b45562e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
@@ -22,9 +22,9 @@ package org.apache.sshd.common.forward;
 import java.util.Collection;
 import java.util.Objects;
 
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
index 36b664b..9ab9c10 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/SocksProxy.java
@@ -25,7 +25,6 @@ import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.sshd.client.future.OpenFuture;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.future.SshFutureListener;
 import org.apache.sshd.common.io.IoHandler;
 import org.apache.sshd.common.io.IoSession;
@@ -33,6 +32,7 @@ import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.closeable.AbstractCloseable;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * SOCKS proxy server, supporting simple socks4/5 protocols.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
index 06c7f88..44856bd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
@@ -28,12 +28,12 @@ import org.apache.sshd.client.future.OpenFuture;
 import org.apache.sshd.common.Closeable;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.ChannelOutputStream;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * TODO Add javadoc

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipForwarder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipForwarder.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipForwarder.java
index e19da1e..1eb8c8d 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipForwarder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipForwarder.java
@@ -22,7 +22,7 @@ package org.apache.sshd.common.forward;
 import java.io.IOException;
 
 import org.apache.sshd.common.Closeable;
-import org.apache.sshd.common.SshdSocketAddress;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 public interface TcpipForwarder extends Closeable {
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java b/sshd-core/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java
new file mode 100644
index 0000000..4d9e547
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/net/NetworkConnector.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.util.net;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class NetworkConnector extends AbstractLoggingBean {
+    public static final String DEFAULT_HOST = SshdSocketAddress.LOCALHOST_IP;
+    public static final long DEFAULT_CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
+    public static final long DEFAULT_READ_TIMEOUT = TimeUnit.SECONDS.toMillis(15L);
+
+    private String protocol;
+    private String host = DEFAULT_HOST;
+    private int port;
+    private long connectTimeout = DEFAULT_CONNECT_TIMEOUT;
+    private long readTimeout = DEFAULT_READ_TIMEOUT;
+
+    public NetworkConnector() {
+        super();
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+
+    public void setProtocol(String protocol) {
+        this.protocol = protocol;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public long getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    public void setConnectTimeout(long connectTimeout) {
+        this.connectTimeout = connectTimeout;
+    }
+
+    public long getReadTimeout() {
+        return readTimeout;
+    }
+
+    public void setReadTimeout(long readTimeout) {
+        this.readTimeout = readTimeout;
+    }
+
+    @Override
+    public String toString() {
+        return getProtocol() + "://" + getHost() + ":" + getPort()
+             + ";connect=" + getConnectTimeout()
+             + ";read=" + getReadTimeout();
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java b/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
new file mode 100644
index 0000000..7ea3806
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
@@ -0,0 +1,415 @@
+/*
+ * 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.util.net;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * <P>A simple socket address holding the host name and port number. The reason
+ * it does not extend {@link InetSocketAddress} is twofold:</P>
+ * <OL>
+ * <LI><P>
+ * The {@link InetSocketAddress} performs a DNS resolution on the
+ * provided host name - which we don't want do use until we want to
+ * create a connection using this address (thus the {@link #toInetSocketAddress()}
+ * call which executes this query
+ * </P></LI>
+ *
+ * <LI><P>
+ * If empty host name is provided we replace it with the <I>any</I>
+ * address of 0.0.0.0
+ * </P></LI>
+ * </OL>
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SshdSocketAddress extends SocketAddress {
+    public static final String LOCALHOST_NAME = "localhost";
+    public static final String LOCALHOST_IP = "127.0.0.1";
+    public static final String IP_ANYADDR = "0.0.0.0";
+
+    // 10.0.0.0 - 10.255.255.255
+    public static final String PRIVATE_CLASS_A_PREFIX = "10.";
+    // 172.16.0.0 - 172.31.255.255
+    public static final String PRIVATE_CLASS_B_PREFIX = "172.";
+    // 192.168.0.0 - 192.168.255.255
+    public static final String PRIVATE_CLASS_C_PREFIX = "192.168.";
+    // 100.64.0.0 - 100.127.255.255
+    public static final String CARRIER_GRADE_NAT_PREFIX = "100.";
+    // The IPv4 broadcast address
+    public static final String BROADCAST_ADDRESS = "255.255.255.255";
+
+    /**
+     * A dummy placeholder that can be used instead of {@code null}s
+     */
+    public static final SshdSocketAddress LOCALHOST_ADDRESS = new SshdSocketAddress(LOCALHOST_IP, 0);
+
+    /**
+     * Compares {@link InetAddress}-es according to their {@link InetAddress#getHostAddress()}
+     * value case <U>insensitive</U>
+     */
+    public static final Comparator<InetAddress> BY_HOST_ADDRESS = new Comparator<InetAddress>() {
+            @Override
+            public int compare(InetAddress a1, InetAddress a2) {
+                String n1 = GenericUtils.trimToEmpty(toAddressString(a1));
+                String n2 = GenericUtils.trimToEmpty(toAddressString(a2));
+                return String.CASE_INSENSITIVE_ORDER.compare(n1, n2);
+            }
+        };
+
+    private static final long serialVersionUID = 6461645947151952729L;
+
+    private final String hostName;
+    private final int port;
+
+    public SshdSocketAddress(int port) {
+        this(IP_ANYADDR, port);
+    }
+
+    public SshdSocketAddress(String hostName, int port) {
+        ValidateUtils.checkNotNull(hostName, "Host name may not be null");
+        this.hostName = GenericUtils.isEmpty(hostName) ? IP_ANYADDR : hostName;
+
+        ValidateUtils.checkTrue(port >= 0, "Port must be >= 0: %d", port);
+        this.port = port;
+    }
+
+    public String getHostName() {
+        return hostName;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public InetSocketAddress toInetSocketAddress() {
+        return new InetSocketAddress(getHostName(), getPort());
+    }
+
+    @Override
+    public String toString() {
+        return getHostName() + ":" + getPort();
+    }
+
+    protected boolean isEquivalent(SshdSocketAddress that) {
+        if (that == null) {
+            return false;
+        } else if (that == this) {
+            return true;
+        } else {
+            return (this.getPort() == that.getPort())
+                    && Objects.equals(this.getHostName(), that.getHostName());
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) {
+            return false;
+        }
+        if (getClass() != o.getClass()) {
+            return false;
+        }
+        return isEquivalent((SshdSocketAddress) o);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(getHostName()) + getPort();
+    }
+
+
+    /**
+     * Returns the first external network address assigned to this
+     * machine or null if one is not found.
+     * @return Inet4Address associated with an external interface
+     * DevNote:  We actually return InetAddress here, as Inet4Addresses are final and cannot be mocked.
+     */
+    public static InetAddress getFirstExternalNetwork4Address() {
+        List<? extends InetAddress> addresses = getExternalNetwork4Addresses();
+        return (GenericUtils.size(addresses) > 0) ? addresses.get(0) : null;
+    }
+
+    /**
+     * @return a {@link List} of local network addresses which are not multicast
+     * or localhost sorted according to {@link #BY_HOST_ADDRESS}
+     */
+    public static List<InetAddress> getExternalNetwork4Addresses() {
+        List<InetAddress> addresses = new ArrayList<InetAddress>();
+        try {
+            for (Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces(); (nets != null) && nets.hasMoreElements();) {
+                NetworkInterface netint = nets.nextElement();
+                /* TODO - uncomment when 1.5 compatibility no longer required
+                if (!netint.isUp()) {
+                    continue;    // ignore non-running interfaces
+                }
+                */
+
+                for (Enumeration<InetAddress> inetAddresses = netint.getInetAddresses(); (inetAddresses != null) && inetAddresses.hasMoreElements();) {
+                    InetAddress inetAddress = inetAddresses.nextElement();
+                    if (isValidHostAddress(inetAddress)) {
+                        addresses.add(inetAddress);
+                    }
+                }
+            }
+        } catch (SocketException e) {
+            // swallow
+        }
+
+        if (GenericUtils.size(addresses) > 1) {
+            Collections.sort(addresses, BY_HOST_ADDRESS);
+        }
+
+        return addresses;
+    }
+
+    /**
+     * @param addr The {@link InetAddress} to be verified
+     * @return <P><code>true</code> if the address is:</P></BR>
+     * <UL>
+     *         <LI>Not <code>null</code></LI>
+     *         <LI>An {@link Inet4Address}</LI>
+     *         <LI>Not link local</LI>
+     *         <LI>Not a multicast</LI>
+     *         <LI>Not a loopback</LI>
+     * </UL>
+     * @see InetAddress#isLinkLocalAddress()
+     * @see InetAddress#isMulticastAddress()
+     * @see InetAddress#isMulticastAddress()
+     */
+    public static boolean isValidHostAddress(InetAddress addr) {
+        if (addr == null) {
+            return false;
+        }
+
+        if (addr.isLinkLocalAddress()) {
+            return false;
+        }
+
+        if (addr.isMulticastAddress()) {
+            return false;
+        }
+
+        if (!(addr instanceof Inet4Address)) {
+            return false;
+        }
+
+        if (isLoopback(addr)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @param addr The {@link InetAddress} to be considered
+     * @return <code>true</code> if the address is a loopback one.
+     * <B>Note:</B> if {@link InetAddress#isLoopbackAddress()}
+     * returns <code>false</code> the address <U>string</U> is checked
+     * @see #toAddressString(InetAddress)
+     * @see #isLoopback(String)
+     */
+    public static boolean isLoopback(InetAddress addr) {
+        if (addr == null) {
+            return false;
+        }
+
+        if (addr.isLoopbackAddress()) {
+            return true;
+        }
+
+        String ip = toAddressString(addr);
+        return isLoopback(ip);
+    }
+
+    /**
+     * @param ip IP value to be tested
+     * @return <code>true</code> if the IP is &quot;localhost&quot; or
+     * &quot;127.x.x.x&quot;.
+     */
+    public static boolean isLoopback(String ip) {
+        if (GenericUtils.isEmpty(ip)) {
+            return false;
+        }
+
+        if (LOCALHOST_NAME.equals(ip) || LOCALHOST_IP.equals(ip)) {
+            return true;
+        }
+
+        String[] values = GenericUtils.split(ip, '.');
+        if (GenericUtils.length(values) != 4) {
+            return false;
+        }
+
+        for (int index = 0; index < values.length; index++) {
+            String val = values[index];
+            if (!isValidIPv4AddressComponent(val)) {
+                return false;
+            }
+
+            if (index == 0) {
+                int number = Integer.parseInt(val);
+                if (number != 127) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public static String toAddressString(InetAddress addr) {
+        String ip = (addr == null) ? null : addr.toString();
+        if (GenericUtils.isEmpty(ip)) {
+            return null;
+        } else {
+            return ip.replaceAll(".*/", "");
+        }
+    }
+
+    public static final boolean isIPv4Address(String addr) {
+        if (GenericUtils.isEmpty(addr)) {
+            return false;
+        }
+
+        String[] comps = GenericUtils.split(addr, '.');
+        if (GenericUtils.length(comps) != 4) {
+            return false;
+        }
+
+        for (String c : comps) {
+            if (!isValidIPv4AddressComponent(c)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if the address is one of the allocated private blocks
+     * @param addr The address string
+     * @return {@code true} if this is one of the allocated private
+     * blocks. <B>Note:</B> it assumes that the address string is
+     * indeed an IPv4 address
+     * @see #isIPv4Address(String)
+     * @see #PRIVATE_CLASS_A_PREFIX
+     * @see #PRIVATE_CLASS_B_PREFIX
+     * @see #PRIVATE_CLASS_C_PREFIX
+     * @see <A HREF="http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces">Wiki page</A>
+     */
+    public static final boolean isPrivateIPv4Address(String addr) {
+        if (GenericUtils.isEmpty(addr)) {
+            return false;
+        }
+
+        if (addr.startsWith(PRIVATE_CLASS_A_PREFIX) || addr.startsWith(PRIVATE_CLASS_C_PREFIX)) {
+            return true;
+        }
+
+        // for 172.x.x.x we need further checks
+        if (!addr.startsWith(PRIVATE_CLASS_B_PREFIX)) {
+            return false;
+        }
+
+        int nextCompPos = addr.indexOf('.', PRIVATE_CLASS_B_PREFIX.length());
+        if (nextCompPos <= PRIVATE_CLASS_B_PREFIX.length()) {
+            return false;
+        }
+
+        String value = addr.substring(PRIVATE_CLASS_B_PREFIX.length(), nextCompPos);
+        if (!isValidIPv4AddressComponent(value)) {
+            return false;
+        }
+
+        int v = Integer.parseInt(value);
+        return (v >= 16) && (v <= 31);
+    }
+
+    /**
+     * @param addr The address to be checked
+     * @return {@code true} if the address is in the 100.64.0.0/10 range
+     * @see <A HREF="http://tools.ietf.org/html/rfc6598">RFC6598</A>
+     */
+    public static final boolean isCarrierGradeNatIPv4Address(String addr) {
+        if (GenericUtils.isEmpty(addr)) {
+            return false;
+        }
+
+        if (!addr.startsWith(CARRIER_GRADE_NAT_PREFIX)) {
+            return false;
+        }
+
+        int nextCompPos = addr.indexOf('.', CARRIER_GRADE_NAT_PREFIX.length());
+        if (nextCompPos <= CARRIER_GRADE_NAT_PREFIX.length()) {
+            return false;
+        }
+
+        String value = addr.substring(CARRIER_GRADE_NAT_PREFIX.length(), nextCompPos);
+        if (!isValidIPv4AddressComponent(value)) {
+            return false;
+        }
+
+        int v = Integer.parseInt(value);
+        return (v >= 64) && (v <= 127);
+    }
+
+    /**
+     * <P>Checks if the provided argument is a valid IPv4 address component:</P></BR>
+     * <UL>
+     *     <LI>Not {@code null}/empty</LI>
+     *     <LI>Has at most 3 <U>digits</U></LI>
+     *     <LI>Its value is &le; 255</LI>
+     * </UL>
+     * @param c The {@link CharSequence} to be validate
+     * @return {@code true} if valid IPv4 address component
+     */
+    public static final boolean isValidIPv4AddressComponent(CharSequence c) {
+        if (GenericUtils.isEmpty(c) || (c.length() > 3)) {
+            return false;
+        }
+
+        char ch = c.charAt(0);
+        if ((ch < '0') || (ch > '9')) {
+            return false;
+        }
+
+        if (!NumberUtils.isIntegerNumber(c)) {
+            return false;
+        }
+
+        int v = Integer.parseInt(c.toString());
+        return (v >= 0) && (v <= 255);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/server/forward/ForwardingFilter.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/forward/ForwardingFilter.java b/sshd-core/src/main/java/org/apache/sshd/server/forward/ForwardingFilter.java
index 0a9f0a4..23e2f14 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/forward/ForwardingFilter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/forward/ForwardingFilter.java
@@ -23,9 +23,9 @@ import java.util.EnumSet;
 import java.util.Set;
 
 import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * Determines if a forwarding request will be permitted.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/server/forward/StaticDecisionForwardingFilter.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/forward/StaticDecisionForwardingFilter.java b/sshd-core/src/main/java/org/apache/sshd/server/forward/StaticDecisionForwardingFilter.java
index 6b82b70..a4a46fb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/forward/StaticDecisionForwardingFilter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/forward/StaticDecisionForwardingFilter.java
@@ -18,9 +18,9 @@
  */
 package org.apache.sshd.server.forward;
 
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * A {@link ForwardingFilter} implementation that returns the same &quot;static&quot;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/server/forward/TcpipServerChannel.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/forward/TcpipServerChannel.java b/sshd-core/src/main/java/org/apache/sshd/server/forward/TcpipServerChannel.java
index bf97181..ced89cb 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/forward/TcpipServerChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/forward/TcpipServerChannel.java
@@ -28,7 +28,6 @@ import org.apache.sshd.client.future.DefaultOpenFuture;
 import org.apache.sshd.client.future.OpenFuture;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.SshConstants;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.Channel;
 import org.apache.sshd.common.channel.ChannelFactory;
 import org.apache.sshd.common.channel.ChannelListener;
@@ -45,6 +44,7 @@ import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.Readable;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.channel.AbstractServerChannel;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
index 23724b5..bba75e4 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/CancelTcpipForwardHandler.java
@@ -19,7 +19,6 @@
 package org.apache.sshd.server.global;
 
 import org.apache.sshd.common.SshConstants;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.forward.TcpipForwarder;
 import org.apache.sshd.common.session.AbstractConnectionServiceRequestHandler;
 import org.apache.sshd.common.session.ConnectionService;
@@ -27,6 +26,7 @@ import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.Int2IntFunction;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * Handler for cancel-tcpip-forward global request.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java b/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
index 3b4618b..b9a44e8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/global/TcpipForwardHandler.java
@@ -19,7 +19,6 @@
 package org.apache.sshd.server.global;
 
 import org.apache.sshd.common.SshConstants;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.forward.TcpipForwarder;
 import org.apache.sshd.common.session.AbstractConnectionServiceRequestHandler;
 import org.apache.sshd.common.session.ConnectionService;
@@ -27,6 +26,7 @@ import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.Int2IntFunction;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * Handler for tcpip-forward global request.

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java b/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
index d44e589..a73bb71 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/x11/X11ForwardSupport.java
@@ -33,7 +33,6 @@ import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.PropertyResolverUtils;
 import org.apache.sshd.common.SshConstants;
 import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.channel.ChannelOutputStream;
 import org.apache.sshd.common.io.IoAcceptor;
 import org.apache.sshd.common.io.IoHandler;
@@ -46,6 +45,7 @@ import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.closeable.AbstractInnerCloseable;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java b/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
index 31befd4..92e4965 100644
--- a/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/ProxyTest.java
@@ -36,7 +36,7 @@ import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.SshdSocketAddress;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
 import org.apache.sshd.util.test.BaseTestSupport;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index 5c1e160..9ff1dc1 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -38,7 +38,6 @@ import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.config.keys.ClientIdentityLoader;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.io.IoSession;
@@ -46,6 +45,7 @@ import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
 import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/common/ForwardingFilterTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/ForwardingFilterTest.java b/sshd-core/src/test/java/org/apache/sshd/common/ForwardingFilterTest.java
index 1403ba1..2c3531a 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/ForwardingFilterTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/ForwardingFilterTest.java
@@ -20,6 +20,7 @@
 package org.apache.sshd.common;
 
 import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
 import org.apache.sshd.server.forward.ForwardingFilter;
 import org.apache.sshd.server.forward.RejectAllForwardingFilter;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
index afaa8a5..b58072a 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/auth/AuthenticationTest.java
@@ -44,7 +44,6 @@ import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.io.IoSession;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
@@ -55,6 +54,7 @@ import org.apache.sshd.common.signature.Signature;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.ServerFactoryManager;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.auth.hostbased.HostBasedAuthenticator;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
index 0cd71e0..7d6cf8d 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
@@ -46,12 +46,12 @@ import org.apache.sshd.client.channel.ChannelDirectTcpip;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.forward.TcpipForwarder;
 import org.apache.sshd.common.forward.TcpipForwarderFactory;
 import org.apache.sshd.common.session.ConnectionService;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
 import org.apache.sshd.server.global.CancelTcpipForwardHandler;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
index 410f2d1..d1478e8 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
@@ -47,10 +47,10 @@ import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.sshd.client.SshClient;
-import org.apache.sshd.common.SshdSocketAddress;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.SshServer;
 import org.junit.Assert;
 import org.junit.Rule;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java b/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
index 4bb8584..1dcfd12 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
@@ -203,6 +203,25 @@ public class Utils {
         return path;
     }
 
+    public static File resolve(File root, String... children) {
+        if (GenericUtils.isEmpty(children)) {
+            return root;
+        } else {
+            return resolve(root, Arrays.asList(children));
+        }
+    }
+
+    public static File resolve(File root, Collection<String> children) {
+        File path = root;
+        if (!GenericUtils.isEmpty(children)) {
+            for (String child : children) {
+                path = new File(path, child);
+            }
+        }
+
+        return path;
+    }
+
     /**
      * Removes the specified file - if it is a directory, then its children
      * are deleted recursively and then the directory itself. <B>Note:</B>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java b/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
index 6d0f308..33e6f61 100644
--- a/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
+++ b/sshd-git/src/test/java/org/apache/sshd/git/pgm/GitPgmCommandTest.java
@@ -30,7 +30,7 @@ import org.apache.sshd.client.channel.ChannelExec;
 import org.apache.sshd.client.channel.ClientChannel;
 import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.SshdSocketAddress;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/f374e72c/sshd-ldap/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-ldap/pom.xml b/sshd-ldap/pom.xml
new file mode 100644
index 0000000..b692860
--- /dev/null
+++ b/sshd-ldap/pom.xml
@@ -0,0 +1,139 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+
+    <!--
+
+        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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd</artifactId>
+        <version>1.1.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sshd-ldap</artifactId>
+    <name>Apache Mina SSHD :: Git</name>
+    <packaging>jar</packaging>
+    <inceptionYear>2008</inceptionYear>
+
+    <properties>
+        <projectRoot>${basedir}/..</projectRoot>
+        <apacheds.version>1.5.7</apacheds.version>
+        <apache.shared.version>0.9.19</apache.shared.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.directory.shared</groupId>
+                <artifactId>shared-dsml-parser</artifactId>
+                <version>${apache.shared.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>xml-apis</groupId>
+                        <artifactId>xml-apis</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>dom4j</groupId>
+                        <artifactId>dom4j</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>xpp3</groupId>
+                        <artifactId>xpp3</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency> 
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+            <!--  Test dependencies -->
+       <dependency>
+            <groupId>org.apache.directory.server</groupId>
+            <artifactId>apacheds-test-framework</artifactId>
+            <version>${apacheds.version}</version>
+            <scope>test</scope>
+        </dependency> 
+        <dependency>
+            <groupId>org.apache.directory.server</groupId>
+            <artifactId>apacheds-server-integ</artifactId>
+            <version>${apacheds.version}</version>
+            <scope>test</scope>
+        </dependency> 
+        <dependency>
+            <groupId>org.apache.directory.server</groupId>
+            <artifactId>apacheds-core-integ</artifactId>
+            <version>${apacheds.version}</version>
+            <scope>test</scope>
+        </dependency> 
+
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <additionalparam>-Xdoclint:none</additionalparam>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>


[4/4] mina-sshd git commit: Added a few improvements + documentation for the LDAP based authenticators

Posted by lg...@apache.org.
Added a few improvements + documentation for the LDAP based authenticators


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

Branch: refs/heads/master
Commit: c66c6d421b185a86ad2851424cc84197eb8545ca
Parents: e6991a7
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue Jan 19 10:58:37 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue Jan 19 10:58:37 2016 +0200

----------------------------------------------------------------------
 .../common/util/net/LdapNetworkConnector.java   | 124 +++++++++++------
 .../sshd/server/auth/LdapAuthenticator.java     |  37 +++++
 .../password/LdapPasswordAuthenticator.java     |  28 +++-
 .../auth/pubkey/LdapPublickeyAuthenticator.java | 136 +++++++++++++++++--
 .../sshd/server/auth/BaseAuthenticatorTest.java |  23 ++--
 .../password/LdapPasswordAuthenticatorTest.java |   5 +-
 .../pubkey/LdapPublickeyAuthenticatorTest.java  |   5 +-
 7 files changed, 291 insertions(+), 67 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
index 1e4ff23..3bb626d 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/common/util/net/LdapNetworkConnector.java
@@ -47,9 +47,11 @@ import org.apache.sshd.common.util.buffer.BufferUtils;
 /**
  * Uses the <A HREF="http://docs.oracle.com/javase/7/docs/technotes/guides/jndi/jndi-ldap.html">
  * LDAP Naming Service Provider for the Java Naming and Directory Interface (JNDI)</A>
+ *
+ * @param <C> Type of context being passed to {@link #resolveAttributes(String, String, Object)}
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class LdapNetworkConnector extends NetworkConnector {
+public class LdapNetworkConnector<C> extends NetworkConnector {
     public static final String DEFAULT_LDAP_PROTOCOL = "ldap";
     public static final int DEFAULT_LDAP_PORT = 389;
 
@@ -105,6 +107,22 @@ public class LdapNetworkConnector extends NetworkConnector {
         setBinaryAttributes(DEFAULT_BINARY_ATTRIBUTES);
     }
 
+    @Override
+    public void setConnectTimeout(long connectTimeout) {
+        // value must fit in an integer
+        ValidateUtils.checkTrue((connectTimeout >= Integer.MIN_VALUE) && (connectTimeout <= Integer.MAX_VALUE), "Invalid connect timeout: %d", connectTimeout);
+        ldapEnv.put("com.sun.jndi.ldap.connect.timeout", Long.toString(connectTimeout));
+        super.setConnectTimeout(connectTimeout);
+    }
+
+    @Override
+    public void setReadTimeout(long readTimeout) {
+        // value must fit in an integer
+        ValidateUtils.checkTrue((readTimeout >= Integer.MIN_VALUE) && (readTimeout <= Integer.MAX_VALUE), "Invalid read timeout: %d", readTimeout);
+        super.setReadTimeout(readTimeout);
+        ldapEnv.put("com.sun.jndi.ldap.read.timeout", Long.toString(readTimeout));
+    }
+
     public String getLdapFactory() {
         return Objects.toString(ldapEnv.get(Context.INITIAL_CONTEXT_FACTORY), null);
     }
@@ -319,54 +337,82 @@ public class LdapNetworkConnector extends NetworkConnector {
      *                 may be {@code null}/empty if not required for the specific query
      * @param password Password Password to be used if necessary - may be {@code null}/empty if not
      *                 required for the specific query
-     * @param queryContext User specific query context - relevant only for derived classes that want
+     * @param queryContext User specific query context - relevant for derived classes that want
      *                 to override some of query processing methods
      * @return A {@link Map} of the retrieved attributes - <B>Note:</B> if {@link #isAccumulateMultiValues()}
      * is {@code true} and multiple values are encountered for an attribute then a {@link List} of them is
      * mapped as its value
      * @throws NamingException If failed to executed the LDAP query
+     * @see #queryAttributes(Object, DirContext, Map, String, String)
      */
-    public Map<String, Object> resolveAttributes(String username, String password, Object queryContext) throws NamingException {
-        DirContext context = initializeDirContext(queryContext, ldapEnv, username, password);
+    public Map<String, Object> resolveAttributes(String username, String password, C queryContext) throws NamingException {
+        // create a copy of the original environment so we can change it
+        DirContext context = initializeDirContext(queryContext, new HashMap<String, Object>(ldapEnv), username, password);
         try {
-            Map<?, ?> ldapConfig = context.getEnvironment();
-            String baseDN = resolveBaseDN(queryContext, ldapConfig, username, password);
-            String filter = resolveSearchFilter(queryContext, ldapConfig, username, password);
-            NamingEnumeration<? extends SearchResult> result =
-                    context.search(ValidateUtils.checkNotNullAndNotEmpty(baseDN, "No base DN"),
-                                   ValidateUtils.checkNotNullAndNotEmpty(filter, "No filter"),
-                                   searchControls);
-            try {
-                Map<String, Object> attrsMap = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
-                String referralMode = Objects.toString(ldapConfig.get(Context.REFERRAL), null);
-                for (int index = 0;; index++) {
-                    if (!result.hasMore()) {
-                        break;
-                    }
-
-                    processSearchResult(queryContext, ldapConfig, attrsMap, index, result.next());
+            return queryAttributes(queryContext, context, context.getEnvironment(), username, password);
+        } finally {
+            context.close();
+        }
+    }
 
-                    // if not following referrals stop at the 1st result regardless if there are others
-                    if ("ignore".equals(referralMode)) {
-                        break;
-                    }
+    /**
+     * @param queryContext The user-specific query context
+     * @param context The initialized {@link DirContext}
+     * @param ldapConfig The LDAP environment setup
+     * @param username The username
+     * @param password The password
+     * @return A {@link Map} of the retrieved attributes - <B>Note:</B> if {@link #isAccumulateMultiValues()}
+     * is {@code true} and multiple values are encountered for an attribute then a {@link List} of them is
+     * mapped as its value
+     * @throws NamingException If failed to executed the LDAP query
+     */
+    protected Map<String, Object> queryAttributes(C queryContext, DirContext context, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+        String baseDN = resolveBaseDN(queryContext, ldapConfig, username, password);
+        String filter = resolveSearchFilter(queryContext, ldapConfig, username, password);
+        NamingEnumeration<? extends SearchResult> result =
+                context.search(ValidateUtils.checkNotNullAndNotEmpty(baseDN, "No base DN"),
+                               ValidateUtils.checkNotNullAndNotEmpty(filter, "No filter"),
+                               searchControls);
+        try {
+            Map<String, Object> attrsMap = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
+            String referralMode = Objects.toString(ldapConfig.get(Context.REFERRAL), null);
+            for (int index = 0;; index++) {
+                if (!result.hasMore()) {
+                    break;
                 }
 
-                return attrsMap;
-            } finally {
-                result.close();
+                processSearchResult(queryContext, ldapConfig, attrsMap, index, result.next());
+
+                // if not following referrals stop at the 1st result regardless if there are others
+                if ("ignore".equals(referralMode)) {
+                    break;
+                }
             }
+
+            return attrsMap;
         } finally {
-            context.close();
+            result.close();
         }
     }
 
-    protected DirContext initializeDirContext(Object queryContext, Map<String, ?> ldapConfig, String username, String password) throws NamingException {
-        Map<String, Object> env;
-        synchronized (ldapConfig) { // create a copy so we can change it
-            env = new HashMap<String, Object>(ldapConfig);
-        }
+    protected DirContext initializeDirContext(C queryContext, Map<String, Object> env, String username, String password) throws NamingException {
+        Map<String, ?> ldapConfig = setupDirContextEnvironment(queryContext, env, username, password);
+        return new InitialDirContext(new Hashtable<String, Object>(ldapConfig));
+    }
 
+    /**
+     * Called in order to set up the environment configuration passed to the
+     * {@link InitialDirContext#InitialDirContext(Hashtable)} constructor
+     *
+     * @param queryContext The caller-specific query context
+     * @param env The current environment setup
+     * @param username The username - may be {@code null}/empty
+     * @param password The password  - may be {@code null}/empty
+     * @return An updated environment configuration - can be a <U>new</U> instance
+     * or just the original one with some changes in it
+     * @throws NamingException If failed to set up the environment
+     */
+    protected Map<String, Object> setupDirContextEnvironment(C queryContext, Map<String, Object> env, String username, String password) throws NamingException {
         if (!env.containsKey(Context.PROVIDER_URL)) {
             int port = getPort();
             ValidateUtils.checkTrue(port > 0, "No port configured");
@@ -391,20 +437,20 @@ public class LdapNetworkConnector extends NetworkConnector {
             }
         }
 
-        return new InitialDirContext(new Hashtable<String, Object>(env));
+        return env;
     }
 
-    protected String resolveBaseDN(Object queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+    protected String resolveBaseDN(C queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
         Object[] bindParams = {username, password};
         return ValidateUtils.checkNotNull(baseDNPattern, "No base DN pattern").format(bindParams);
     }
 
-    protected String resolveSearchFilter(Object queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
+    protected String resolveSearchFilter(C queryContext, Map<?, ?> ldapConfig, String username, String password) throws NamingException {
         Object[] bindParams = {username, password};
         return ValidateUtils.checkNotNull(searchFilterPattern, "No search filter pattern").format(bindParams);
     }
 
-    protected void processSearchResult(Object queryContext, Map<?, ?> ldapConfig, Map<String, Object> attrsMap,
+    protected void processSearchResult(C queryContext, Map<?, ?> ldapConfig, Map<String, Object> attrsMap,
             int resultIndex, SearchResult result)
                     throws NamingException {
         String dn = result.getName();
@@ -422,7 +468,7 @@ public class LdapNetworkConnector extends NetworkConnector {
     }
 
     // returns the most up-to-date value mapped for the attribute
-    protected Object processResultAttributeValue(Object queryContext, Map<?, ?> ldapConfig,
+    protected Object processResultAttributeValue(C queryContext, Map<?, ?> ldapConfig,
             String dn, int resultIndex, Map<String, Object> attrsMap, Attribute a)
                     throws NamingException {
         String attrID = a.getID();
@@ -460,7 +506,7 @@ public class LdapNetworkConnector extends NetworkConnector {
     }
 
     @SuppressWarnings("unchecked")
-    protected Object accumulateAttributeValue(Object queryContext, Map<String, Object> attrsMap, String attrID, Object attrVal) {
+    protected Object accumulateAttributeValue(C queryContext, Map<String, Object> attrsMap, String attrID, Object attrVal) {
         Object prev = attrsMap.put(attrID, attrVal);
         if (prev == null) {
             return null;    // debug breakpoint

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java
new file mode 100644
index 0000000..2544f35
--- /dev/null
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/LdapAuthenticator.java
@@ -0,0 +1,37 @@
+/*
+ * 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.server.auth;
+
+import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Serves as the base class for password and public key authenticators.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LdapAuthenticator extends LdapNetworkConnector<ServerSession> {
+    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+
+    public LdapAuthenticator() {
+        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
index 251a3a8..83165e7 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticator.java
@@ -23,23 +23,41 @@ import java.util.Map;
 
 import javax.naming.NamingException;
 
-import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.auth.LdapAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
 
 /**
+ * Uses LDAP to authenticate a user and password. By default it can achieve this using 2 ways:
+ * <OL>
+ *      <P><LI>
+ *      Comparing the provided password with the one stored in LDAP. In this case,
+ *      the bind DN and password patterns can be either empty (if anonymous access
+ *      allowed) or can contain the administrative username / password required to
+ *      run the LDAP query. The search filter pattern should be set to require a
+ *      match for <U>both</U> the username and password - e.g., <code>&quot;(&(user={0})(password={1}))&quot;</code>.
+ *      The set default ({@link #DEFAULT_SEARCH_FILTER_PATTERN}) uses the most
+ *      commonly encountered attributes names for this purpose.
+ *      </LI></P>
+ *
+ *      <P><LI>
+ *      Using the original username + password to access LDAP - in which case the very
+ *      success of retrieving anything can be considered a successful authentication.
+ *      In this case, the bind DN and password patterns should be set up to generate
+ *      the correct credentials - the default is to &quot;echo&quot; the provided
+ *      username and password as-is. E.g., if the username is always the alias part
+ *      of a known e-mail, the bind DN should be set to <code>&quot;{0}@my.domain.com&quot;</code>.
+ *      </LI></P>
+ * </OL>
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class LdapPasswordAuthenticator extends LdapNetworkConnector implements PasswordAuthenticator {
-    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+public class LdapPasswordAuthenticator extends LdapAuthenticator implements PasswordAuthenticator {
     public static final String DEFAULT_PASSWORD_ATTR_NAME = "userPassword";
 
     public static final String DEFAULT_SEARCH_FILTER_PATTERN =
             "(&(" + DEFAULT_USERNAME_ATTR_NAME + "={0})(" + DEFAULT_PASSWORD_ATTR_NAME + "={1}))";
-    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
 
     public LdapPasswordAuthenticator() {
         setRetrievedAttributes(null);
-        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
         setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
index fa3e929..ff8f5ce 100644
--- a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
@@ -22,6 +22,10 @@ package org.apache.sshd.server.auth.pubkey;
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -29,30 +33,37 @@ import javax.naming.NamingException;
 
 import org.apache.sshd.common.config.keys.KeyUtils;
 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.auth.LdapAuthenticator;
 import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
 import org.apache.sshd.server.session.ServerSession;
 
 /**
+ * Uses LDAP to retrieve a user's registered public key and compare it with
+ * the provided one. The default search pattern attempts to retrieve the user's
+ * SSH public key value which is assumed to be in {@code OpenSSH} format. The
+ * default assumes that the value resides in the {@link #DEFAULT_PUBKEY_ATTR_NAME}
+ * attribute and can be either a single or a multi-valued one
+ *
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public class LdapPublickeyAuthenticator extends LdapNetworkConnector implements PublickeyAuthenticator {
-    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
-    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+public class LdapPublickeyAuthenticator extends LdapAuthenticator implements PublickeyAuthenticator {
     public static final String DEFAULT_SEARCH_FILTER_PATTERN = DEFAULT_USERNAME_ATTR_NAME + "={0}";
+    // this seems to be the most commonly used attribute name
     public static final String DEFAULT_PUBKEY_ATTR_NAME = "sshPublicKey";
 
     private String keyAttributeName = DEFAULT_PUBKEY_ATTR_NAME;
 
     public LdapPublickeyAuthenticator() {
-        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
         setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
         setRetrievedAttributes(DEFAULT_PUBKEY_ATTR_NAME);
+        setAccumulateMultiValues(true); // in case multiple keys registered
     }
 
     /**
-     * @return The LDAP attribute name containing the public key in {@code OpenSSH} format
+     * @return The LDAP attribute name containing the public key - assumed
+     * by default to be in {@code OpenSSH} format
      */
     public String getKeyAttributeName() {
         return keyAttributeName;
@@ -79,17 +90,120 @@ public class LdapPublickeyAuthenticator extends LdapNetworkConnector implements
         }
     }
 
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @return {@code true} whether to accept the presented public key
+     * @throws GeneralSecurityException If failed to recover the public key(s)
+     * @throws IOException If failed to parse the public key(s) data
+     * @see #recoverPublicKeys(String, PublicKey, ServerSession, Map, Object)
+     * @see #authenticate(String, PublicKey, ServerSession, Map, Collection)
+     */
     protected boolean authenticate(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs)
             throws GeneralSecurityException, IOException {
         String attrName = getKeyAttributeName();
-        Object keyData = ValidateUtils.checkNotNull(attrs.get(attrName), "No data for attribute=%s", attrName);
-        PublicKey actual = recoverPublicKey(username, expected, session, keyData);
-        return KeyUtils.compareKeys(expected, actual);
+        Collection<PublicKey> keys = recoverPublicKeys(username, expected, session, attrs, attrs.get(attrName));
+        return authenticate(username, expected, session, attrs, keys);
+    }
+
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @param keys The {@link Collection} of recovered {@link PublicKey}s - may be {@code null}/empty
+     * @return {@code true} whether to accept the presented public key
+     */
+    protected boolean authenticate(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs, Collection<? extends PublicKey> keys) {
+        if (GenericUtils.isEmpty(keys)) {
+            if (log.isDebugEnabled()) {
+                log.debug("authenticate({}@{}) no registered keys", username, session);
+            }
+            return false;
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("authenticate({}@{}) check {} registered keys", username, session, keys.size());
+        }
+
+        for (PublicKey actual : keys) {
+            if (log.isTraceEnabled()) {
+                log.trace("authenticate({}@{}) expected={}-{}, actual={}-{}",
+                          username, session,
+                          KeyUtils.getKeyType(expected), KeyUtils.getFingerPrint(expected),
+                          KeyUtils.getKeyType(actual), KeyUtils.getFingerPrint(actual));
+            }
+
+            if (KeyUtils.compareKeys(expected, actual)) {
+                return true;
+            }
+        }
+
+        if (log.isDebugEnabled()) {
+            log.debug("authenticate({}@{}) no matching keys", username, session);
+        }
+
+        return false;
+    }
+
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @param keyData The value of the {@link #getKeyAttributeName()} attribute - may be {@code null},
+     * a single object or a collection of such (if multi-valued attribute)
+     * @return A {@link List} of the recovered {@link PublicKey}s - may be {@code null}/empty
+     * @throws GeneralSecurityException If failed to recover the public key(s)
+     * @throws IOException If failed to parse the public key(s) data
+     * @see #parsePublicKeyValue(String, PublicKey, ServerSession, Map, Object)
+     */
+    protected List<PublicKey> recoverPublicKeys(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs, Object keyData)
+            throws GeneralSecurityException, IOException {
+        // handle case of multi-valued attribute
+        if (keyData instanceof Collection<?>) {
+            Collection<?> values = (Collection<?>) keyData;
+            List<PublicKey> keys = new ArrayList<PublicKey>(values.size());
+            for (Object v : values) {
+                PublicKey k = parsePublicKeyValue(username, expected, session, attrs, v);
+                if (k == null) {
+                    continue;   // debug breakpoint
+                }
+
+                keys.add(k);
+            }
+
+            return keys;
+        }
+
+        PublicKey k = parsePublicKeyValue(username, expected, session, attrs, keyData);
+        return (k == null) ? Collections.<PublicKey>emptyList() : Collections.singletonList(k);
     }
 
-    protected PublicKey recoverPublicKey(String username, PublicKey expected, ServerSession session, Object keyData)
+    /**
+     * @param username The SSH username attempting to authenticate
+     * @param expected The provided {@link PublicKey}
+     * @param session The {@link ServerSession}
+     * @param attrs The extracted LDAP attributes {@link Map}
+     * @param keyData One of the values (if multi-valued attribute) - may be {@code null}
+     * @return The extracted {@link PublicKey} or {@code null} if none available
+     * @throws GeneralSecurityException If failed to recover the public key
+     * @throws IOException If failed to parse the public key data
+     */
+    protected PublicKey parsePublicKeyValue(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs, Object keyData)
             throws GeneralSecurityException, IOException {
+        if (keyData == null) {
+            return null;
+        }
+
         AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(Objects.toString(keyData, null));
-        return ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING);
+        PublicKey key = ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING);
+        if (log.isTraceEnabled()) {
+            log.trace("parsePublicKeyValue({}@{}) {}-{}",
+                      username, session, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+        }
+        return key;
     }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
index 2b60fb6..eb115ea 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
@@ -67,6 +67,18 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
         super();
     }
 
+    public static String getHost(Pair<LdapServer, DirectoryService> context) {
+        return getHost((context == null) ? null : context.getFirst());
+    }
+
+    public static String getHost(LdapServer ldapServer) {
+        return getHost((ldapServer == null) ? null : ldapServer.getTransports());
+    }
+
+    public static String getHost(Transport ... transports) {
+        return GenericUtils.isEmpty(transports) ? null : transports[0].getAddress();
+    }
+
     public static int getPort(Pair<LdapServer, DirectoryService> context) {
         return getPort((context == null) ? null : context.getFirst());
     }
@@ -127,15 +139,6 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
             directoryService.setSystemPartition(systemPartition);
         }
 
-        // Create a new partition for the special extra attributes
-        {
-            JdbmPartition partition = new JdbmPartition();
-            partition.setId("openssh-lpk");
-            partition.setSuffix("cn=openssh-lpk,cn=schema,cn=config");
-            partition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new File(workingDirectory, partition.getId()))));
-            directoryService.addPartition(partition);
-        }
-
         // Create a new partition for the users
         {
             JdbmPartition partition = new JdbmPartition();
@@ -149,7 +152,7 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
         directoryService.getChangeLog().setEnabled(false);
 
         LdapServer ldapServer = new LdapServer();
-        ldapServer.setTransports(new TcpTransport(PORT));
+        ldapServer.setTransports(new TcpTransport(TEST_LOCALHOST, PORT));
         ldapServer.setDirectoryService(directoryService);
 
         log.info("Starting directory service ...");

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
index aa862ca..b7c2f2b 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
@@ -61,11 +61,14 @@ public class LdapPasswordAuthenticatorTest extends BaseAuthenticatorTest {
 
     @Test   // the user's password is compared with the LDAP stored one
     public void testPasswordComparison() throws Exception {
+        Pair<LdapServer, DirectoryService> ldapContext = ldapContextHolder.get();
         LdapPasswordAuthenticator auth = new LdapPasswordAuthenticator();
+        auth.setHost(getHost(ldapContext));
+        auth.setPort(getPort(ldapContext));
         auth.setBaseDN(BASE_DN_TEST);
-        auth.setPort(getPort(ldapContextHolder.get()));
 
         ServerSession session = Mockito.mock(ServerSession.class);
+        outputDebugMessage("%s: %s", getCurrentTestName(), auth);
         for (Map.Entry<String, String> ue : usersMap.entrySet()) {
             String username = ue.getKey();
             String password = ue.getValue();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/c66c6d42/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
index c697a56..7bf273f 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
@@ -77,13 +77,16 @@ public class LdapPublickeyAuthenticatorTest extends BaseAuthenticatorTest {
 
     @Test
     public void testPublicKeyComparison() throws Exception {
+        Pair<LdapServer, DirectoryService> ldapContext = ldapContextHolder.get();
         LdapPublickeyAuthenticator auth = new LdapPublickeyAuthenticator();
+        auth.setHost(getHost(ldapContext));
+        auth.setPort(getPort(ldapContext));
         auth.setBaseDN(BASE_DN_TEST);
-        auth.setPort(getPort(ldapContextHolder.get()));
         auth.setKeyAttributeName(TEST_ATTR_NAME);
         auth.setRetrievedAttributes(TEST_ATTR_NAME);
 
         ServerSession session = Mockito.mock(ServerSession.class);
+        outputDebugMessage("%s: %s", getCurrentTestName(), auth);
         for (Map.Entry<String, PublicKey> ke : keysMap.entrySet()) {
             String username = ke.getKey();
             PublicKey key = ke.getValue();


[3/4] mina-sshd git commit: [SSHD-608] Provide PublicKeyAuthenticator that uses LDAP

Posted by lg...@apache.org.
[SSHD-608] Provide PublicKeyAuthenticator that uses LDAP


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

Branch: refs/heads/master
Commit: e6991a73bd7829c327f1d678b3f2e403d673a418
Parents: f374e72
Author: Lyor Goldstein <lg...@vmware.com>
Authored: Tue Jan 19 10:56:55 2016 +0200
Committer: Lyor Goldstein <lg...@vmware.com>
Committed: Tue Jan 19 10:56:55 2016 +0200

----------------------------------------------------------------------
 sshd-ldap/pom.xml                               |  4 +-
 .../auth/pubkey/LdapPublickeyAuthenticator.java | 95 ++++++++++++++++++++
 .../sshd/server/auth/BaseAuthenticatorTest.java | 29 ++++--
 .../password/LdapPasswordAuthenticatorTest.java |  2 +
 .../pubkey/LdapPublickeyAuthenticatorTest.java  | 95 ++++++++++++++++++++
 sshd-ldap/src/test/resources/auth-users.ldif    |  8 +-
 6 files changed, 222 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-ldap/pom.xml b/sshd-ldap/pom.xml
index b692860..5921ccb 100644
--- a/sshd-ldap/pom.xml
+++ b/sshd-ldap/pom.xml
@@ -28,9 +28,9 @@
     </parent>
 
     <artifactId>sshd-ldap</artifactId>
-    <name>Apache Mina SSHD :: Git</name>
+    <name>Apache Mina SSHD :: LDAP</name>
     <packaging>jar</packaging>
-    <inceptionYear>2008</inceptionYear>
+    <inceptionYear>2016</inceptionYear>
 
     <properties>
         <projectRoot>${basedir}/..</projectRoot>

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
new file mode 100644
index 0000000..fa3e929
--- /dev/null
+++ b/sshd-ldap/src/main/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticator.java
@@ -0,0 +1,95 @@
+/*
+ * 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.server.auth.pubkey;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.naming.NamingException;
+
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.net.LdapNetworkConnector;
+import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LdapPublickeyAuthenticator extends LdapNetworkConnector implements PublickeyAuthenticator {
+    public static final String DEFAULT_USERNAME_ATTR_NAME = "uid";
+    public static final String DEFAULT_AUTHENTICATION_MODE = "none";
+    public static final String DEFAULT_SEARCH_FILTER_PATTERN = DEFAULT_USERNAME_ATTR_NAME + "={0}";
+    public static final String DEFAULT_PUBKEY_ATTR_NAME = "sshPublicKey";
+
+    private String keyAttributeName = DEFAULT_PUBKEY_ATTR_NAME;
+
+    public LdapPublickeyAuthenticator() {
+        setAuthenticationMode(DEFAULT_AUTHENTICATION_MODE);
+        setSearchFilterPattern(DEFAULT_SEARCH_FILTER_PATTERN);
+        setRetrievedAttributes(DEFAULT_PUBKEY_ATTR_NAME);
+    }
+
+    /**
+     * @return The LDAP attribute name containing the public key in {@code OpenSSH} format
+     */
+    public String getKeyAttributeName() {
+        return keyAttributeName;
+    }
+
+    public void setKeyAttributeName(String keyAttributeName) {
+        this.keyAttributeName = ValidateUtils.checkNotNullAndNotEmpty(keyAttributeName, "No attribute name");
+    }
+
+    @Override
+    public boolean authenticate(String username, PublicKey key, ServerSession session) {
+        try {
+            Map<String, ?> attrs = resolveAttributes(username, null, session);
+            return authenticate(username, key, session, attrs);
+        } catch (NamingException | GeneralSecurityException | IOException | RuntimeException e) {
+            log.warn("authenticate({}@{}) failed ({}) to query: {}",
+                      username, session, e.getClass().getSimpleName(), e.getMessage());
+
+            if (log.isDebugEnabled()) {
+                log.debug("authenticate(" + username + "@" + session + ") query failure details", e);
+            }
+
+            return false;
+        }
+    }
+
+    protected boolean authenticate(String username, PublicKey expected, ServerSession session, Map<String, ?> attrs)
+            throws GeneralSecurityException, IOException {
+        String attrName = getKeyAttributeName();
+        Object keyData = ValidateUtils.checkNotNull(attrs.get(attrName), "No data for attribute=%s", attrName);
+        PublicKey actual = recoverPublicKey(username, expected, session, keyData);
+        return KeyUtils.compareKeys(expected, actual);
+    }
+
+    protected PublicKey recoverPublicKey(String username, PublicKey expected, ServerSession session, Object keyData)
+            throws GeneralSecurityException, IOException {
+        AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(Objects.toString(keyData, null));
+        return ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
index 8c0fa47..2b60fb6 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/BaseAuthenticatorTest.java
@@ -127,6 +127,15 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
             directoryService.setSystemPartition(systemPartition);
         }
 
+        // Create a new partition for the special extra attributes
+        {
+            JdbmPartition partition = new JdbmPartition();
+            partition.setId("openssh-lpk");
+            partition.setSuffix("cn=openssh-lpk,cn=schema,cn=config");
+            partition.setPartitionDir(assertHierarchyTargetFolderExists(Utils.deleteRecursive(new File(workingDirectory, partition.getId()))));
+            directoryService.addPartition(partition);
+        }
+
         // Create a new partition for the users
         {
             JdbmPartition partition = new JdbmPartition();
@@ -170,12 +179,9 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
             int id = 1;
             for (LdifEntry entry : reader) {
                 if (log.isDebugEnabled()) {
-                    log.debug("Add LDIF entry={}", entry);
+                    log.debug("Process LDIF entry={}", entry);
                 }
 
-                ChangeType changeType = entry.getChangeType();
-                assertEquals("Mismatched change type in users ldif entry=" + entry, ChangeType.Add, changeType);
-
                 Entry data = entry.getEntry();
                 EntryAttribute userAttr = data.get("uid");
                 EntryAttribute passAttr = data.get(credentialName);
@@ -184,10 +190,19 @@ public abstract class BaseAuthenticatorTest extends BaseTestSupport {
                     ValidateUtils.checkTrue(usersMap.put(username, passAttr.getString()) == null, "Multiple entries for user=%s", username);
                 }
 
-                InternalAddRequest addRequest = new AddRequestImpl(id++);
-                addRequest.setEntry(data);
+                ChangeType changeType = entry.getChangeType();
                 try {
-                    session.add(addRequest);
+                    switch (changeType) {
+                        case Add: {
+                            InternalAddRequest addRequest = new AddRequestImpl(id++);
+                            addRequest.setEntry(data);
+                            session.add(addRequest);
+                            break;
+                        }
+
+                        default:
+                            throw new UnsupportedOperationException("Unsupported change type (" + changeType + ") for entry=" + entry);
+                    }
                 } catch (Exception e) {
                     log.error("Failed (" + e.getClass().getSimpleName() + ") to add entry=" + entry + ": " + e.getMessage(), e);
                     throw e;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
index 2a88342..aa862ca 100644
--- a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/password/LdapPasswordAuthenticatorTest.java
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import org.apache.directory.server.core.DirectoryService;
 import org.apache.directory.server.ldap.LdapServer;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.Pair;
 import org.apache.sshd.server.auth.BaseAuthenticatorTest;
 import org.apache.sshd.server.session.ServerSession;
@@ -50,6 +51,7 @@ public class LdapPasswordAuthenticatorTest extends BaseAuthenticatorTest {
     public static void startApacheDs() throws Exception {
         ldapContextHolder.set(startApacheDs(LdapPasswordAuthenticatorTest.class));
         usersMap = populateUsers(ldapContextHolder.get().getSecond(), LdapPasswordAuthenticatorTest.class, LdapPasswordAuthenticator.DEFAULT_PASSWORD_ATTR_NAME);
+        assertFalse("No users retrieved", GenericUtils.isEmpty(usersMap));
     }
 
     @AfterClass

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
new file mode 100644
index 0000000..c697a56
--- /dev/null
+++ b/sshd-ldap/src/test/java/org/apache/sshd/server/auth/pubkey/LdapPublickeyAuthenticatorTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.server.auth.pubkey;
+
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.directory.server.core.DirectoryService;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.Pair;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.server.auth.BaseAuthenticatorTest;
+import org.apache.sshd.server.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.server.session.ServerSession;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.mockito.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class LdapPublickeyAuthenticatorTest extends BaseAuthenticatorTest {
+    private static final AtomicReference<Pair<LdapServer, DirectoryService>> ldapContextHolder = new AtomicReference<>();
+    private static final Map<String, PublicKey> keysMap = new HashMap<>();
+    // we use this instead of the default since the default requires some extra LDIF manipulation which we don't need
+    private static final String TEST_ATTR_NAME = "description";
+
+    public LdapPublickeyAuthenticatorTest() {
+        super();
+    }
+
+    @BeforeClass
+    public static void startApacheDs() throws Exception {
+        ldapContextHolder.set(startApacheDs(LdapPublickeyAuthenticatorTest.class));
+        Map<String, String> credentials =
+                populateUsers(ldapContextHolder.get().getSecond(), LdapPublickeyAuthenticatorTest.class, TEST_ATTR_NAME);
+        assertFalse("No keys retrieved", GenericUtils.isEmpty(credentials));
+
+        for (Map.Entry<String, String> ce : credentials.entrySet()) {
+            String username = ce.getKey();
+            AuthorizedKeyEntry entry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(ce.getValue());
+            PublicKey key = ValidateUtils.checkNotNull(entry, "No key extracted").resolvePublicKey(PublicKeyEntryResolver.FAILING);
+            keysMap.put(username, key);
+        }
+    }
+
+    @AfterClass
+    public static void stopApacheDs() throws Exception {
+        stopApacheDs(ldapContextHolder.getAndSet(null));
+    }
+
+    @Test
+    public void testPublicKeyComparison() throws Exception {
+        LdapPublickeyAuthenticator auth = new LdapPublickeyAuthenticator();
+        auth.setBaseDN(BASE_DN_TEST);
+        auth.setPort(getPort(ldapContextHolder.get()));
+        auth.setKeyAttributeName(TEST_ATTR_NAME);
+        auth.setRetrievedAttributes(TEST_ATTR_NAME);
+
+        ServerSession session = Mockito.mock(ServerSession.class);
+        for (Map.Entry<String, PublicKey> ke : keysMap.entrySet()) {
+            String username = ke.getKey();
+            PublicKey key = ke.getValue();
+            outputDebugMessage("Authenticate: user=%s, key-type=%s, fingerprint=%s",
+                               username, KeyUtils.getKeyType(key), KeyUtils.getFingerPrint(key));
+            assertTrue("Failed to authenticate user=" + username, auth.authenticate(username, key, session));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e6991a73/sshd-ldap/src/test/resources/auth-users.ldif
----------------------------------------------------------------------
diff --git a/sshd-ldap/src/test/resources/auth-users.ldif b/sshd-ldap/src/test/resources/auth-users.ldif
index 4792099..0fc3b18 100644
--- a/sshd-ldap/src/test/resources/auth-users.ldif
+++ b/sshd-ldap/src/test/resources/auth-users.ldif
@@ -1,31 +1,35 @@
 version: 1
 
+## Add People group definition
 dn: ou=People,dc=sshd,dc=apache,dc=org
 ou: People
 objectClass: top
 objectClass: organizationalUnit
 description: Parent object of all users accounts
 
+## Add users
 dn: cn=Guillaume Nodet,ou=People,dc=sshd,dc=apache,dc=org
 objectClass: top
 objectClass: person
 objectClass: organizationalPerson
-objectclass: inetOrgPerson
+objectClass: inetOrgPerson
 cn: Guillaume Nodet
 givenName: Guillaume
 sn: Nodet
 uid: gnodet
 userpassword: gnodet
 mail: gnodet@apache.org
+description: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDEAGEwosEIS7koPfYFf2qXAqV+gGiuf3wizjYCz8UdYtmSBINRQhWvVtiWAiLOcHiI/nYSTXctIFyzbWJ74BqvgoZS8eujaPUyBO+asF3enfyrnug9uvqrhsGqnsqFSaAYSQG3p4paKThRhxGX1jqMy6wJaTED1d9SxHs9gnGCZLk1yAr0w/mDkF59YmPoAibmyz1aaXGkjPBWa4ac+RBTEuOxt0hz+ykA7rNua5QPMdNx3sXrv80ECDEs78poOiZgkRvBEnA4qCvSK5FBB5DkVn+3cUhd36tEhKz79u8mLAsrUrgi/JKcrdAZSCyS4oPRt9NEm9YZxcYzqDH98RDp sshd@apache.org
 
 dn: cn=Lyor Goldstein,ou=People,dc=sshd,dc=apache,dc=org
 objectClass: top
 objectClass: person
 objectClass: organizationalPerson
-objectclass: inetOrgPerson
+objectClass: inetOrgPerson
 cn: Lyor Goldstein
 givenName: Lyor
 sn: Goldstein
 uid: lgoldstein
 userpassword: lgoldstein
 mail: lgoldstein@apache.org
+description: ssh-dss AAAAB3NzaC1kc3MAAACBAJ6tabphvozXBCnOe2j48OF6pSGmV7+7mRrPxiyPY5mHcHGJXwsE4Rvu81epbTghX3A/s1sme1QtmVVOU3Y3NIxmDBK5UQmSIYS+chXe45GBwC6uJawOn4lPIYw15PJg2ZfjxC1QU5fa6HSQ3vX2MZfYFhEovlBd4mGo2+XY0DqHAAAAFQDeDO2UFNZRBFNN1jEBojpeALh7oQAAAIAI/wYgC1kTjsIFaVvgK3RcGb+lYW0EiuiuG0q37+BzcqtEgjVckvxoCTeghcyf6vhB+ZbrM9we2jH3JvmF4gmtmzjW8UtgYCB2ovRYmd21cSPQW2F9U+sIl2+2sI9rsyrAdyhW1qZ4HVIrKH0RdWkAkjQ+8EGKHKXKo6aEV/HdpAAAAIAv80m0MZoocokStZV7EclLwt3ihvN7/Tjf5aHS9fY8b1ev2Z7eIVm3UHirggEeJMQgIw3MBE/T1ttYyqwnVdOatBJVcVLXYUPuXrq6f9/TYEpYxLqowjOwPaAkZAnecVYo0eU52VSkR38onu7juk4BNzHtLILTvtZppQ6l3loOpA== sshd@apache.org