Return Timeout Error Code for timeout during LDAP operations. With test
and special test conf.

Change-Id: Id661ea7df66fdef317934b416ffe58d8c9b9f30f
diff --git a/Changes b/Changes
index e5dd0b4..ed17895 100644
--- a/Changes
+++ b/Changes
@@ -10,6 +10,8 @@
 - Remove query reference test data from the database migration (close #811)
 - Remove OAuth2 clients and access tokens from the DB migration (close #809)
 - Removed db/test folder (close #379)
+- Return Timeout Error Code for timeout during LDAP operations. 
+  With test and special test conf (bodmo)
 
 # version 0.78.2
 
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java b/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java
index 79a9462..72c12b5 100644
--- a/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java
+++ b/src/main/java/de/ids_mannheim/korap/authentication/LDAPConfig.java
@@ -6,6 +6,10 @@
 import java.util.Map;
 import java.util.Properties;
 
+/*
+ * adding ldapTimeout for controlling timeout handling during LDAP operations - 23.06.25/FB
+ */
+
 public class LDAPConfig {
     public final boolean useSSL;
     public final String host;
@@ -21,7 +25,11 @@
     public final String ldif;
     public final String authFilter;
     public final String userNotBlockedFilter;
-
+    public final int 	ldapTimeout; // sets LDAP operation timeout [ms]. should probably be < 10s (= network timeout).
+    
+    // default timeout for LDAP operations in [ms]. Should be < than default network timeout of 10s:
+    public static final String LDAP_DEFAULT_TIMEOUT = "9000"; 
+    
     public LDAPConfig (String ldapConfigFilename)
             throws LdapConfigurationException {
         Map<String, String> ldapConfig = null;
@@ -52,6 +60,7 @@
                 ldapConfig.getOrDefault("useEmbeddedServer", "false"));
         emailAttribute = ldapConfig.getOrDefault("emailAttribute", "mail");
         ldif = ldapConfig.getOrDefault("ldifFile", null);
+        ldapTimeout = Integer.parseInt(ldapConfig.getOrDefault("ldapTimeout",  LDAP_DEFAULT_TIMEOUT)); 
     }
 
     static HashMap<String, String> typeCastConvert (Properties prop) {
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java b/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
index 37a7233..cfa2157 100644
--- a/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
+++ b/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
@@ -11,6 +11,8 @@
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.net.ssl.SSLSocketFactory;
 
@@ -41,14 +43,23 @@
  */
 public class LdapAuth3 {
 
-    public static final int LDAP_AUTH_ROK = 0;
-    public static final int LDAP_AUTH_RCONNECT = 1; // cannot connect to LDAP Server
-    public static final int LDAP_AUTH_RINTERR = 2; // internal error: cannot verify User+Pwd.
-    public static final int LDAP_AUTH_RUNKNOWN = 3; // User Account or Pwd unknown;
-    public static final int LDAP_AUTH_RLOCKED = 4; // User Account locked;
-    public static final int LDAP_AUTH_RNOTREG = 5; // User known, but has not registered to KorAP/C2 Service yet;
-    public static final int LDAP_AUTH_RNOEMAIL = 6; // cannot obtain email for sUserDN
-    public static final int LDAP_AUTH_RNAUTH = 7; // User Account or Pwd unknown, or not authorized
+	// return codes:
+    public static final int LDAP_AUTH_ROK 		= 0;
+    public static final int LDAP_AUTH_RCONNECT 	= 1; 	// cannot connect to LDAP Server
+    public static final int LDAP_AUTH_RINTERR 	= 2; 	// internal error: cannot verify User+Pwd.
+    public static final int LDAP_AUTH_RUNKNOWN 	= 3; 	// User Account or Pwd unknown;
+    public static final int LDAP_AUTH_RLOCKED 	= 4; 	// User Account locked;
+    public static final int LDAP_AUTH_RNOTREG 	= 5; 	// User known, but has not registered to KorAP/C2 Service yet;
+    public static final int LDAP_AUTH_RNOEMAIL 	= 6; 	// cannot obtain email for sUserDN
+    public static final int LDAP_AUTH_RNAUTH 	= 7; 	// User Account or Pwd unknown, or not authorized
+    public static final int LDAP_AUTH_RTIMEOUT 	= 100; 	// could not reach LDAP server due to timeout (connect, search).
+    
+    // other constants:
+    private static final String PATT_TIMEOUT_MESS  = "Unable to establish a connection.* within the configured timeout";
+    													// pattern of message returned by the Cause() of a LDAPException in case of a timeout.
+    private static final String PATT_TIMEOUT_MESS2 = "SocketTimeoutException";
+    													// another hint on a connection timeout.
+    
     final static Boolean DEBUGLOG = false;        // log debug output.
 
     private static Logger jlog = LogManager.getLogger(LdapAuth3.class);
@@ -76,8 +87,47 @@
         }
     }
 
+    /* LDAP Exception handling
+     * 
+     * isTimeout()
+     * - somehow a dirty implementation, but documentation about timeouts is not very explicit.
+     * - the INT value of TIMEOUT is currently 85.
+     * - timeout return codes encountered with unboundid LDAP are 85 and 91.
+     * Returns true in case of a timeout -> caller should return LDAP_AUTH_TIME.
+     * 18.06.25/FB
+     */
+    
+    private static boolean isTimeout(LDAPException e)
+    
+    {
+    	if( e.getResultCode() == e.getResultCode().TIMEOUT )
+	    	{
+	    	return true; // LDAP_AUTH_TIMEOUT;	
+	    	}
+    	else if( e.getResultCode().intValue() == 91 || e.getResultCode().intValue() == 85 )
+    		{
+	    	if( e.getCause() != null )
+	    		{
+	    		String
+	    			patterns = String.format("(%s|%s)",  PATT_TIMEOUT_MESS, PATT_TIMEOUT_MESS2);
+	    		Pattern 
+	    			pat = Pattern.compile(patterns, Pattern.CASE_INSENSITIVE);
+	    		Matcher
+	    			mat = pat.matcher(e.getCause().toString());
+	    		
+	    		boolean matched = mat.find();
+	    		return matched;
+	    		}
+	    	}
+
+   		return false;
+    }
+    
+    // login
+    
     public static int login (String login, String password,
             String ldapConfigFilename) throws LDAPException {
+    	
         LDAPConfig ldapConfig = new LDAPConfig(ldapConfigFilename);
 
         login = Filter.encodeValue(login);
@@ -98,9 +148,11 @@
         SearchResult srchRes = ldapAuth3Result.getSearchResultValue();
 
         if (ldapAuth3Result.getErrorCode() != 0 || srchRes == null
-                || srchRes.getEntryCount() == 0) {
-            if (DEBUGLOG)
-                System.out.printf("Finding '%s': no entry found!\n", login);
+                || srchRes.getEntryCount() == 0) 
+        {
+            jlog.debug("Searching for '{}': ErrorCode={}, EntryCount={}, no entry found!\n", 
+            		login, ldapAuth3Result.getErrorCode(), srchRes != null ? srchRes.getEntryCount() : 0);
+            
             return ldapAuth3Result.getErrorCode();
         }
 
@@ -111,6 +163,7 @@
     public static LdapAuth3Result search (String login, String password,
             LDAPConfig ldapConfig, boolean bindWithFoundDN,
             boolean applyExtraFilters) {
+    	
         Map<String, String> valuesMap = new HashMap<>();
         valuesMap.put("login", login);
         valuesMap.put("password", password);
@@ -122,14 +175,6 @@
         sub = new StringSubstitutor(valuesMap);
         String insensitiveSearchFilter = sub.replace(ldapConfig.searchFilter);
 
-        if (DEBUGLOG) {
-            //System.out.printf("LDAP Version      = %d.\n", LDAPConnection.LDAP_V3);
-            System.out.printf("LDAP Host & Port  = '%s':%d.\n", ldapConfig.host,
-                    ldapConfig.port);
-            System.out.printf("Login User = '%s'\n", login);
-            System.out.println("LDAPS " + ldapConfig.useSSL);
-        }
-
         LDAPConnection lc;
 
         if (ldapConfig.useSSL) {
@@ -153,9 +198,7 @@
             }
             catch (GeneralSecurityException e) {
                 //jlog.error(
-            	jlog.error(
-                        "Error: login: Connecting to LDAPS Server: failed: '%s'!\n",
-                        e);
+            	jlog.error("login user '{}': Connecting to LDAPS Server: failed: {}!", login, e.toString());
                 ldapTerminate(null);
                 return new LdapAuth3Result(null, LDAP_AUTH_RCONNECT);
             }
@@ -163,25 +206,23 @@
         else {
             lc = new LDAPConnection();
         }
+        
         try {
-            lc.connect(ldapConfig.host, ldapConfig.port);
-            if (DEBUGLOG && ldapConfig.useSSL)
-                System.out.println("LDAPS Connection = OK\n");
-            if (DEBUGLOG && !ldapConfig.useSSL)
-                System.out.println("LDAP Connection = OK\n");
-        }
+        	// timeout - 18.06.25/FB
+            lc.connect(ldapConfig.host, ldapConfig.port, ldapConfig.ldapTimeout);
+            
+            jlog.debug("{}: connect: successfull.", ldapConfig.useSSL ? "LDAPS" : "LDAP");
+            }
         catch (LDAPException e) {
             String fullStackTrace = org.apache.commons.lang.exception.ExceptionUtils
                     .getFullStackTrace(e);
-            jlog.error(
-                    "Error: login: Connecting to LDAP Server: failed: '%s'!\n",
-                    fullStackTrace);
+            jlog.error("Connecting to LDAP Server: failed: '{}'!\n", fullStackTrace);
+            
             ldapTerminate(lc);
-            return new LdapAuth3Result(null, LDAP_AUTH_RCONNECT);
+            return new LdapAuth3Result(null, isTimeout(e) ? LDAP_AUTH_RTIMEOUT : LDAP_AUTH_RCONNECT);
         }
-        if (DEBUGLOG)
-            System.out.printf("Debug: isConnected=%d\n",
-                    lc.isConnected() ? 1 : 0);
+        
+        jlog.debug("isConnected={}.\n", lc.isConnected() ? "yes" : "no");
 
         try {
             // bind to server:
@@ -189,45 +230,43 @@
                 System.out.printf("Binding with '%s' ...\n",
                         ldapConfig.sLoginDN);
             lc.bind(ldapConfig.sLoginDN, ldapConfig.sPwd);
+            
             if (DEBUGLOG)
                 System.out.print("Binding: OK.\n");
         }
         catch (LDAPException e) {
-//            jlog.error("Error: login: Binding failed: '%s'!\n", e);
-            String error = String.format("Error: login: Binding failed: "
-                    + "'%s'!\n", e);
-            jlog.error(error);
+
+        	jlog.error("login user '{}': binding failed: {}.", login, e.toString());
             ldapTerminate(lc);
             return new LdapAuth3Result(null, LDAP_AUTH_RINTERR);
         }
 
-        if (DEBUGLOG)
-            System.out.printf("Debug: isConnected=%d\n",
-                    lc.isConnected() ? 1 : 0);
-
-        if (DEBUGLOG)
-            System.out.printf("Finding user '%s'...\n", login);
+        jlog.debug("login: isConnected={}.", lc.isConnected() ? "yes" : "no");
 
         SearchResult srchRes = null;
         try {
-            if (DEBUGLOG)
-                System.out.printf("Searching with searchFilter: '%s'.\n",
-                        insensitiveSearchFilter);
+        	jlog.debug("Searching with searchFilter: '{}'.", insensitiveSearchFilter);
 
             srchRes = lc.search(ldapConfig.searchBase, SearchScope.SUB,
                     searchFilterInstance);
 
-            if (DEBUGLOG)
-                System.out.printf("Found '%s': %d entries.\n", login,
-                        srchRes.getEntryCount());
+            jlog.debug("Found '{}': {} entries.", login, srchRes.getEntryCount());
         }
         catch (LDAPSearchException e) {
-            jlog.error("Error: Search for User failed: '%s'!\n", e);
+        	
+        	if( isTimeout(e) )
+        		{
+        		jlog.error("login user '{}': timeout reached: {}", login, e.toString());
+	            ldapTerminate(lc);
+	            return new LdapAuth3Result(null, LDAP_AUTH_RTIMEOUT);
+        		}
+        	else
+        		jlog.error("login user '{}': no results!", login);
         }
 
         if (srchRes == null || srchRes.getEntryCount() == 0) {
-            if (DEBUGLOG)
-                System.out.printf("Finding '%s': no entry found!\n", login);
+
+            jlog.error("login user '{}': no entry found!", login);
             ldapTerminate(lc);
             return new LdapAuth3Result(null, LDAP_AUTH_RUNKNOWN);
         }
@@ -250,12 +289,10 @@
                 }
             }
             catch (LDAPException e) {
-//                jlog.error("Error: login: Binding failed: '%s'!\n", e);
-                String error = String.format("Error: login: Binding failed: "
-                        + "'%s'!\n", e);
-                jlog.error(error);
+
+                jlog.error("login user '{}': binding with DN failed: {}. ", login, e.toString());
                 ldapTerminate(lc);
-                return new LdapAuth3Result(null, LDAP_AUTH_RUNKNOWN);
+                return new LdapAuth3Result(null, isTimeout(e) ? LDAP_AUTH_RTIMEOUT : LDAP_AUTH_RUNKNOWN);
             }
         }
 
diff --git a/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java b/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java
index 0580bba..dfdeb1c 100644
--- a/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java
+++ b/src/test/java/de/ids_mannheim/korap/authentication/LdapAuth3Test.java
@@ -25,6 +25,8 @@
 
     public static final String TEST_LDAPS_CONF = "src/test/resources/test-ldaps.conf";
 
+    public static final String TEST_LDAP_TIMEOUT_CONF = "src/test/resources/test-ldap-timeout.conf";
+
     public static final String TEST_LDAPS_TS_CONF = "src/test/resources/test-ldaps-with-truststore.conf";
 
     public static final String TEST_LDAP_USERS_LDIF = "src/test/resources/test-ldap-users.ldif";
@@ -69,6 +71,17 @@
     }
 
     @Test
+    public void loginWithTimeout () throws LDAPException 
+    {
+    	// To trigger a timeout inside login(), we load TEST_LDAP_TIMEOUT_CONF which:
+    	// - sets a timeout for LDAP operations to the lowest value possible = 1ms;
+    	// - sets the host to be on the network, not localhost, to obtain a response time > 1ms.
+    	
+    	assertEquals(LDAP_AUTH_RTIMEOUT,
+                LdapAuth3.login("testuser123", "password", TEST_LDAP_TIMEOUT_CONF));
+    }
+    
+    @Test
     public void loginWithExtraProfileNameWorks () throws LDAPException {
         assertEquals(LDAP_AUTH_ROK,
                 LdapAuth3.login("testuser123", "password", TEST_LDAP_CONF));
diff --git a/src/test/resources/test-ldap-timeout.conf b/src/test/resources/test-ldap-timeout.conf
new file mode 100644
index 0000000..9fa23c6
--- /dev/null
+++ b/src/test/resources/test-ldap-timeout.conf
@@ -0,0 +1,9 @@
+host=korap.ids-mannheim.de
+port=3268
+searchBase=dc=example,dc=com
+sLoginDN=cn=admin,dc=example,dc=com
+pwd=adminpassword
+searchFilter=(&(|(uid=${login})(mail=${login})(extraProfile=${login}))(|(userPassword=${password})(extraPassword=${password})))
+authFilter=(registered=TRUE)
+userNotBlockedFilter=(|(userStatus=0)(userStatus=1)(!(userStatus=*)))
+ldapTimeout=1