GUACAMOLE-936: Merge ensure LDAP connections for followed referrals are always closed

diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
index 7e88f8a..67ed4de 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
@@ -40,8 +40,6 @@
 import org.apache.guacamole.GuacamoleUnsupportedException;
 import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.auth.ldap.conf.EncryptionMethod;
-import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
-import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -123,7 +121,8 @@
      *     bound.
      *
      * @throws GuacamoleException
-     *     If an error occurs while binding to the LDAP server.
+     *     If the configuration details relevant to binding to the LDAP server
+     *     cannot be read.
      */
     public LdapNetworkConnection bindAs(Dn userDN, String password)
             throws GuacamoleException {
@@ -165,46 +164,49 @@
     }
     
     /**
-     * Generate a new LdapNetworkConnection object for following a referral
-     * with the given LdapUrl, and copy the username and password
-     * from the original connection.
+     * Establishes a new network connection to the LDAP server indicated by the
+     * given LDAP referral URL. The credentials used to bind with the referred
+     * LDAP server will be the same as those used to bind with the original
+     * connection.
      * 
-     * @param referralUrl
-     *     The LDAP URL to follow.
-     * 
-     * @param ldapConfig
-     *     The connection configuration to use to retrieve username and
-     *     password.
-     * 
-     * @param hop
-     *     The current hop number of this referral - once the configured
-     *     limit is reached, this method will throw an exception.
-     * 
+     * @param ldapConnection
+     *     The LDAP connection that bind credentials should be copied from.
+     *
+     * @param url
+     *     The URL of the referred LDAP server to which a new network
+     *     connection should be established.
+     *
      * @return
-     *     A LdapNetworkConnection object that points at the location
-     *     specified in the referralUrl.
-     *     
-     * @throws GuacamoleException
-     *     If an error occurs parsing out the LdapUrl object or the
-     *     maximum number of referral hops is reached.
+     *     A LdapNetworkConnection representing a network connection to the
+     *     LDAP server specified in the URL, or null if the specified URL is
+     *     invalid.
      */
-    public LdapNetworkConnection getReferralConnection(LdapUrl referralUrl,
-            LdapConnectionConfig ldapConfig, int hop)
-            throws GuacamoleException {
+    public LdapNetworkConnection getReferralConnection(
+            LdapNetworkConnection ldapConnection, String url) {
        
-        if (hop >= confService.getMaxReferralHops())
-            throw new GuacamoleServerException("Maximum number of referrals reached.");
-        
+        LdapConnectionConfig ldapConfig = ldapConnection.getConfig();
         LdapConnectionConfig referralConfig = new LdapConnectionConfig();
         
         // Copy bind name and password from original config
         referralConfig.setName(ldapConfig.getName());
         referralConfig.setCredentials(ldapConfig.getCredentials());        
-        
+
+        LdapUrl referralUrl;
+        try {
+            referralUrl = new LdapUrl(url);
+        }
+        catch (LdapException e) {
+            logger.debug("Referral URL \"{}\" is invalid.", url, e);
+            return null;
+        }
+
         // Look for host - if not there, bail out.
         String host = referralUrl.getHost();
-        if (host == null || host.isEmpty())
-            throw new GuacamoleServerException("Referral URL contains no host.");
+        if (host == null || host.isEmpty()) {
+            logger.debug("Referral URL \"{}\" is invalid as it contains "
+                    + "no hostname.", url );
+            return null;
+        }
        
         referralConfig.setLdapHost(host);
        
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
index e1fa2bb..fcae4d6 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
@@ -23,6 +23,7 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -38,7 +39,6 @@
 import org.apache.directory.api.ldap.model.filter.ExprNode;
 import org.apache.directory.api.ldap.model.filter.OrNode;
 import org.apache.directory.api.ldap.model.filter.PresenceNode;
-import org.apache.directory.api.ldap.model.message.Referral;
 import org.apache.directory.api.ldap.model.message.SearchRequest;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.url.LdapUrl;
@@ -46,6 +46,8 @@
 import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
+import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
 import org.apache.guacamole.net.auth.Identifiable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -71,6 +73,12 @@
     private LDAPConnectionService ldapService;
 
     /**
+     * Service for retrieving LDAP server configuration information.
+     */
+    @Inject
+    private ConfigurationService confService;
+
+    /**
      * Returns the identifier of the object represented by the given LDAP
      * entry. Multiple attributes may be declared as containing the identifier
      * of the object when present on an LDAP entry. If multiple such attributes
@@ -204,13 +212,21 @@
     public List<Entry> search(LdapNetworkConnection ldapConnection,
             Dn baseDN, ExprNode query, int searchHop) throws GuacamoleException {
 
+        // Refuse to follow referrals if limit has been reached
+        int maxHops = confService.getMaxReferralHops();
+        if (searchHop >= maxHops) {
+            logger.debug("Refusing to follow further referrals as the maximum "
+                    + "number of referral hops ({}) has been reached. LDAP "
+                    + "search results may be incomplete. If further referrals "
+                    + "should be followed, consider setting the \"{}\" "
+                    + "property to a larger value.", maxHops, LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS.getName());
+            return Collections.emptyList();
+        }
+
         logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query);
 
-        LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
-            
         // Search within subtree of given base DN
-        SearchRequest request = ldapService.getSearchRequest(baseDN,
-                query);
+        SearchRequest request = ldapService.getSearchRequest(baseDN, query);
             
         // Produce list of all entries in the search result, automatically
         // following referrals if configured to do so
@@ -219,22 +235,48 @@
         try (SearchCursor results = ldapConnection.search(request)) {
             while (results.next()) {
 
-                if (results.isEntry()) {
+                // Add entry directly if no referral is involved
+                if (results.isEntry())
                     entries.add(results.getEntry());
-                }
-                else if (results.isReferral() && request.isFollowReferrals()) {
 
-                    Referral referral = results.getReferral();
-                    for (String url : referral.getLdapUrls()) {
-                        LdapNetworkConnection referralConnection =
-                                ldapService.getReferralConnection(
-                                        new LdapUrl(url),
-                                        ldapConnectionConfig, searchHop++
-                                );
-                        entries.addAll(search(referralConnection, baseDN, query,
-                                searchHop));
+                // If a referral must be followed to obtain further results,
+                // retrieval of those results depends on whether such referral
+                // following is enabled
+                else if (results.isReferral()) {
+
+                    // Follow received referrals only if configured to do so
+                    if (request.isFollowReferrals()) {
+                        for (String url : results.getReferral().getLdapUrls()) {
+
+                            // Connect to referred LDAP server to retrieve further results, ensuring the network
+                            // connection is always closed when it will no longer be used
+                            try (LdapNetworkConnection referralConnection = ldapService.getReferralConnection(ldapConnection, url)) {
+                                if (referralConnection != null) {
+                                    logger.debug("Following referral to \"{}\"...", url);
+                                    entries.addAll(search(referralConnection, baseDN, query, searchHop + 1));
+                                }
+                                else
+                                    logger.debug("Could not follow referral to "
+                                            + "\"{}\" as the URL is invalid.", url);
+                            }
+                            catch (GuacamoleException e) {
+                                logger.warn("Referral to \"{}\" could not be followed: {}", url, e.getMessage());
+                                logger.debug("Failed to follow LDAP referral.", e);
+                            }
+
+                        }
                     }
 
+                    // Log if referrals may be applicable but they aren't being
+                    // followed
+                    else
+                        logger.debug("Referrals to one or more other LDAP "
+                                + "servers were received but are being "
+                                + "ignored because following of referrals is "
+                                + "not enabled. If referrals must be "
+                                + "followed, consider setting the \"{}\" "
+                                + "property to \"true\".", LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS.getName());
+
                 }
 
             }
@@ -244,7 +286,7 @@
         }
         catch (CursorException | IOException | LdapException e) {
             throw new GuacamoleServerException("Unable to query list of "
-                    + "objects from LDAP directory.", e);
+                    + "objects from LDAP directory: " + e.getMessage(), e);
         }
 
     }