xref: /OpenGrok/plugins/src/main/java/opengrok/auth/plugin/ldap/LdapServer.java (revision c6f0939b1c668e9f8e1e276424439c3106b3a029)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved.
22  */
23 package opengrok.auth.plugin.ldap;
24 
25 import java.io.IOException;
26 import java.io.Serializable;
27 import java.net.InetAddress;
28 import java.net.InetSocketAddress;
29 import java.net.Socket;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.net.UnknownHostException;
33 import java.util.Hashtable;
34 import java.util.logging.Level;
35 import java.util.logging.Logger;
36 import javax.naming.CommunicationException;
37 import javax.naming.Context;
38 import javax.naming.NamingEnumeration;
39 import javax.naming.NamingException;
40 import javax.naming.directory.SearchControls;
41 import javax.naming.directory.SearchResult;
42 import javax.naming.ldap.InitialLdapContext;
43 import javax.naming.ldap.LdapContext;
44 
45 public class LdapServer implements Serializable {
46 
47     private static final long serialVersionUID = -1;
48 
49     private static final Logger LOGGER = Logger.getLogger(LdapServer.class.getName());
50 
51     private static final String LDAP_CONNECT_TIMEOUT_PARAMETER = "com.sun.jndi.ldap.connect.timeout";
52     private static final String LDAP_READ_TIMEOUT_PARAMETER = "com.sun.jndi.ldap.read.timeout";
53     private static final String LDAP_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
54 
55     // default connectTimeout value in milliseconds
56     private static final int LDAP_CONNECT_TIMEOUT = 5000;
57     // default readTimeout value in milliseconds
58     private static final int LDAP_READ_TIMEOUT = 3000;
59 
60     private String url;
61     private String username;
62     private String password;
63     private int connectTimeout;
64     private int readTimeout;
65     private int interval = 10 * 1000;
66 
67     private final Hashtable<String, String> env;
68     private LdapContext ctx;
69     private long errorTimestamp = 0;
70 
LdapServer()71     public LdapServer() {
72         this(prepareEnv());
73     }
74 
LdapServer(String server)75     public LdapServer(String server) {
76         this(prepareEnv());
77         setName(server);
78     }
79 
LdapServer(String server, String username, String password)80     public LdapServer(String server, String username, String password) {
81         this(prepareEnv());
82         setName(server);
83         this.username = username;
84         this.password = password;
85     }
86 
LdapServer(Hashtable<String, String> env)87     public LdapServer(Hashtable<String, String> env) {
88         this.env = env;
89     }
90 
getUrl()91     public String getUrl() {
92         return url;
93     }
94 
setName(String name)95     public LdapServer setName(String name) {
96         this.url = name;
97         return this;
98     }
99 
getUsername()100     public String getUsername() {
101         return username;
102     }
103 
setUsername(String username)104     public LdapServer setUsername(String username) {
105         this.username = username;
106         return this;
107     }
108 
getPassword()109     public String getPassword() {
110         return password;
111     }
112 
setPassword(String password)113     public LdapServer setPassword(String password) {
114         this.password = password;
115         return this;
116     }
117 
getConnectTimeout()118     public int getConnectTimeout() {
119         return connectTimeout;
120     }
121 
setConnectTimeout(int connectTimeout)122     public LdapServer setConnectTimeout(int connectTimeout) {
123         this.connectTimeout = connectTimeout;
124         return this;
125     }
126 
getReadTimeout()127     public int getReadTimeout() {
128         return readTimeout;
129     }
130 
setReadTimeout(int readTimeout)131     public LdapServer setReadTimeout(int readTimeout) {
132         this.readTimeout = readTimeout;
133         return this;
134     }
135 
getInterval()136     public int getInterval() {
137         return interval;
138     }
139 
setInterval(int interval)140     public void setInterval(int interval) {
141         this.interval = interval;
142     }
143 
urlToHostname(String urlStr)144     private String urlToHostname(String urlStr) throws URISyntaxException {
145         URI uri = new URI(urlStr);
146         return uri.getHost();
147     }
148 
149     /**
150      * This method converts the scheme from URI to port number.
151      * It is limited to the ldap/ldaps schemes.
152      * The method could be static however then it cannot be easily mocked in testing.
153      * @return port number or -1 if the scheme in given URI is not known
154      * @throws URISyntaxException if the URI is not valid
155      */
getPort()156     public int getPort() throws URISyntaxException {
157         URI uri = new URI(getUrl());
158         switch (uri.getScheme()) {
159             case "ldaps":
160                 return 636;
161             case "ldap":
162                 return 389;
163         }
164 
165         return -1;
166     }
167 
isReachable(InetAddress addr, int port, int timeOutMillis)168     private boolean isReachable(InetAddress addr, int port, int timeOutMillis) {
169         try {
170             try (Socket soc = new Socket()) {
171                 soc.connect(new InetSocketAddress(addr, port), timeOutMillis);
172             }
173             return true;
174         } catch (IOException e) {
175             return false;
176         }
177     }
178 
179     /**
180      * Wraps InetAddress.getAllByName() so that it can be mocked in testing.
181      * (mocking static methods is not really possible with Mockito)
182      * @param hostname hostname string
183      * @return array of InetAddress objects
184      * @throws UnknownHostException if the host cannot be resolved to any IP address
185      */
getAddresses(String hostname)186     public InetAddress[] getAddresses(String hostname) throws UnknownHostException {
187         return InetAddress.getAllByName(hostname);
188     }
189 
190     /**
191      * Go through all IP addresses and find out if they are reachable.
192      * @return true if all IP addresses are reachable, false otherwise
193      */
isReachable()194     public boolean isReachable() {
195         try {
196             InetAddress[] addresses = getAddresses(urlToHostname(getUrl()));
197             if (addresses.length == 0) {
198                 LOGGER.log(Level.WARNING, "LDAP server {0} does not resolve to any IP address", this);
199                 return false;
200             }
201 
202             for (InetAddress addr : addresses) {
203                 // InetAddr.isReachable() is not sufficient as it can only check ICMP and TCP echo.
204                 int port = getPort();
205                 if (!isReachable(addr, port, getConnectTimeout())) {
206                     LOGGER.log(Level.WARNING, "LDAP server {0} is not reachable on {1}:{2}",
207                             new Object[]{this, addr, Integer.toString(port)});
208                     return false;
209                 }
210             }
211         } catch (UnknownHostException e) {
212             LOGGER.log(Level.SEVERE, String.format("cannot get IP addresses for LDAP server %s", this), e);
213             return false;
214         } catch (URISyntaxException e) {
215             LOGGER.log(Level.SEVERE, String.format("not a valid URI: %s", getUrl()), e);
216             return false;
217         }
218 
219         return true;
220     }
221 
222     /**
223      * The LDAP server is working only when it is reachable and its connection is not null.
224      * This method tries to establish the connection if it is not established already.
225      *
226      * @return true if it is working
227      */
isWorking()228     public synchronized boolean isWorking() {
229         if (ctx == null) {
230             if (!isReachable()) {
231                 return false;
232             }
233 
234             ctx = connect();
235         }
236         return ctx != null;
237     }
238 
239     /**
240      * Connects to the LDAP server.
241      *
242      * @return the new connection or null
243      */
connect()244     private synchronized LdapContext connect() {
245         LOGGER.log(Level.INFO, "Connecting to LDAP server {0} ", this);
246 
247         if (errorTimestamp > 0 && errorTimestamp + interval > System.currentTimeMillis()) {
248             LOGGER.log(Level.WARNING, "LDAP server {0} is down", this.url);
249             close();
250             return null;
251         }
252 
253         if (ctx == null) {
254             env.put(Context.PROVIDER_URL, this.url);
255 
256             if (this.username != null) {
257                 env.put(Context.SECURITY_PRINCIPAL, this.username);
258             }
259             if (this.password != null) {
260                 env.put(Context.SECURITY_CREDENTIALS, this.password);
261             }
262             if (this.connectTimeout > 0) {
263                 env.put(LDAP_CONNECT_TIMEOUT_PARAMETER, Integer.toString(this.connectTimeout));
264             }
265             if (this.readTimeout > 0) {
266                 env.put(LDAP_READ_TIMEOUT_PARAMETER, Integer.toString(this.readTimeout));
267             }
268 
269             try {
270                 ctx = new InitialLdapContext(env, null);
271                 ctx.setRequestControls(null);
272                 LOGGER.log(Level.INFO, "Connected to LDAP server {0}", this);
273                 errorTimestamp = 0;
274             } catch (NamingException ex) {
275                 LOGGER.log(Level.WARNING, "LDAP server {0} is not responding", env.get(Context.PROVIDER_URL));
276                 errorTimestamp = System.currentTimeMillis();
277                 close();
278                 return ctx = null;
279             }
280         }
281 
282         return ctx;
283     }
284 
285     /**
286      * Lookups the LDAP server.
287      *
288      * @param name base dn for the search
289      * @param filter LDAP filter
290      * @param cons controls for the LDAP request
291      * @return LDAP enumeration with the results
292      *
293      * @throws NamingException naming exception
294      */
search(String name, String filter, SearchControls cons)295     public NamingEnumeration<SearchResult> search(String name, String filter, SearchControls cons) throws NamingException {
296         return search(name, filter, cons, false);
297     }
298 
299     /**
300      * Perform LDAP search.
301      *
302      * @param name base dn for the search
303      * @param filter LDAP filter
304      * @param controls controls for the LDAP request
305      * @param reconnected flag if the request has failed previously
306      * @return LDAP enumeration with the results
307      *
308      * @throws NamingException naming exception
309      */
search(String name, String filter, SearchControls controls, boolean reconnected)310     public NamingEnumeration<SearchResult> search(String name, String filter, SearchControls controls, boolean reconnected)
311             throws NamingException {
312 
313         if (!isWorking()) {
314             close();
315             throw new CommunicationException(String.format("LDAP server \"%s\" is down",
316                     env.get(Context.PROVIDER_URL)));
317         }
318 
319         if (reconnected) {
320             LOGGER.log(Level.INFO, "LDAP server {0} reconnect", env.get(Context.PROVIDER_URL));
321             close();
322             if ((ctx = connect()) == null) {
323                 throw new CommunicationException(String.format("LDAP server \"%s\" cannot reconnect",
324                         env.get(Context.PROVIDER_URL)));
325             }
326         }
327 
328         try {
329             synchronized (this) {
330                 return ctx.search(name, filter, controls);
331             }
332         } catch (CommunicationException ex) {
333             if (reconnected) {
334                 throw ex;
335             }
336             return search(name, filter, controls, true);
337         }
338     }
339 
340     /**
341      * Closes the server context.
342      */
close()343     public synchronized void close() {
344         if (ctx != null) {
345             try {
346                 ctx.close();
347             } catch (NamingException ex) {
348                 LOGGER.log(Level.WARNING, "cannot close LDAP server {0}", getUrl());
349             }
350             ctx = null;
351         }
352     }
353 
prepareEnv()354     private static Hashtable<String, String> prepareEnv() {
355         Hashtable<String, String> e = new Hashtable<>();
356 
357         e.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CONTEXT_FACTORY);
358         e.put(LDAP_CONNECT_TIMEOUT_PARAMETER, Integer.toString(LDAP_CONNECT_TIMEOUT));
359         e.put(LDAP_READ_TIMEOUT_PARAMETER, Integer.toString(LDAP_READ_TIMEOUT));
360 
361         return e;
362     }
363 
364     @Override
toString()365     public String toString() {
366         StringBuilder sb = new StringBuilder();
367 
368         sb.append(getUrl());
369 
370         if (getConnectTimeout() > 0) {
371             sb.append(", connect timeout: ");
372             sb.append(getConnectTimeout());
373         }
374         if (getReadTimeout() > 0) {
375             sb.append(", read timeout: ");
376             sb.append(getReadTimeout());
377         }
378 
379         if (getUsername() != null && !getUsername().isEmpty()) {
380             sb.append(", username: ");
381             sb.append(getUsername());
382         }
383 
384         return sb.toString();
385     }
386 }
387