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